Skip to content

Commit 5cd67b8

Browse files
committed
Support custom preparationBatchSize defined via SourceKit's options
1 parent 515260f commit 5cd67b8

File tree

10 files changed

+295
-35
lines changed

10 files changed

+295
-35
lines changed

Documentation/Configuration File.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ The structure of the file is currently not guaranteed to be stable. Options may
6060
- `sourcekitdRequestTimeout: number`: The maximum duration that a sourcekitd request should be allowed to execute before being declared as timed out. In general, editors should cancel requests that they are no longer interested in, but in case editors don't cancel requests, this ensures that a long-running non-cancelled request is not blocking sourcekitd and thus most semantic functionality. In particular, VS Code does not cancel the semantic tokens request, which can cause a long-running AST build that blocks sourcekitd.
6161
- `semanticServiceRestartTimeout: number`: If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it.
6262
- `buildServerWorkspaceRequestsTimeout: number`: Duration how long to wait for responses to `workspace/buildTargets` or `buildTarget/sources` request by the build server before defaulting to an empty response.
63+
- `preparationBatchingStrategy: object`: Defines the batch size for target preparation. If nil, defaults to preparing 1 target at a time.
64+
- This is a tagged union discriminated by the `strategy` field. Each case has the following structure:
65+
- `strategy: "target"`: Prepare a fixed number of targets in a single batch. `batchSize`: The number of targets to prepare in each batch.
66+
- `batchSize: integer`: The number of targets to prepare in each batch.

SourceKitLSPDevUtils/Sources/ConfigSchemaGen/JSONSchema.swift

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ struct JSONSchema: Encodable {
3838
case additionalProperties
3939
case markdownDescription
4040
case markdownEnumDescriptions
41+
case oneOf
42+
case const
4143
}
4244
var _schema: String?
4345
var id: String?
@@ -59,6 +61,9 @@ struct JSONSchema: Encodable {
5961
/// VSCode extension: Markdown formatted descriptions for rich hover for enum values
6062
/// https://github.com/microsoft/vscode-wiki/blob/main/Setting-Descriptions.md
6163
var markdownEnumDescriptions: [String]?
64+
65+
var oneOf: [JSONSchema]?
66+
var const: String?
6267

6368
func encode(to encoder: any Encoder) throws {
6469
// Manually implement encoding to use `encodeIfPresent` for HeapBox-ed fields
@@ -82,6 +87,10 @@ struct JSONSchema: Encodable {
8287
if let markdownEnumDescriptions {
8388
try container.encode(markdownEnumDescriptions, forKey: .markdownEnumDescriptions)
8489
}
90+
if let oneOf = oneOf, !oneOf.isEmpty {
91+
try container.encode(oneOf, forKey: .oneOf)
92+
}
93+
try container.encodeIfPresent(const, forKey: .const)
8594
}
8695
}
8796

@@ -126,13 +135,53 @@ struct JSONSchemaBuilder {
126135
schema.properties = properties
127136
schema.required = required
128137
case .enum(let enumInfo):
129-
schema.type = "string"
130-
schema.enum = enumInfo.cases.map(\.name)
131-
// Set `markdownEnumDescriptions` for better rendering in VSCode rich hover
132-
// Unlike `description`, `enumDescriptions` field is not a part of JSON Schema spec,
133-
// so we only set `markdownEnumDescriptions` here.
134-
if enumInfo.cases.contains(where: { $0.description != nil }) {
135-
schema.markdownEnumDescriptions = enumInfo.cases.map { $0.description ?? "" }
138+
let hasAssociatedTypes = enumInfo.cases.contains { $0.associatedProperties != nil && !$0.associatedProperties!.isEmpty }
139+
140+
if hasAssociatedTypes {
141+
let discriminatorFieldName = enumInfo.discriminatorFieldName ?? "type"
142+
var oneOfSchemas: [JSONSchema] = []
143+
144+
for caseInfo in enumInfo.cases {
145+
var caseSchema = JSONSchema()
146+
caseSchema.type = "object"
147+
caseSchema.description = caseInfo.description
148+
caseSchema.markdownDescription = caseInfo.description
149+
150+
var caseProperties: [String: JSONSchema] = [:]
151+
var caseRequired: [String] = [discriminatorFieldName]
152+
153+
var discriminatorSchema = JSONSchema()
154+
discriminatorSchema.const = caseInfo.name
155+
caseProperties[discriminatorFieldName] = discriminatorSchema
156+
157+
if let associatedProperties = caseInfo.associatedProperties {
158+
for property in associatedProperties {
159+
let propertyType = property.type
160+
var propertySchema = try buildJSONSchema(from: propertyType)
161+
propertySchema.description = property.description
162+
propertySchema.markdownDescription = property.description
163+
caseProperties[property.name] = propertySchema
164+
if !propertyType.isOptional {
165+
caseRequired.append(property.name)
166+
}
167+
}
168+
}
169+
170+
caseSchema.properties = caseProperties
171+
caseSchema.required = caseRequired
172+
oneOfSchemas.append(caseSchema)
173+
}
174+
175+
schema.oneOf = oneOfSchemas
176+
} else {
177+
schema.type = "string"
178+
schema.enum = enumInfo.cases.map(\.name)
179+
// Set `markdownEnumDescriptions` for better rendering in VSCode rich hover
180+
// Unlike `description`, `enumDescriptions` field is not a part of JSON Schema spec,
181+
// so we only set `markdownEnumDescriptions` here.
182+
if enumInfo.cases.contains(where: { $0.description != nil }) {
183+
schema.markdownEnumDescriptions = enumInfo.cases.map { $0.description ?? "" }
184+
}
136185
}
137186
}
138187
return schema

SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionDocument.swift

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,34 @@ struct OptionDocumentBuilder {
6666
try appendProperty(property, indentLevel: indentLevel + 1)
6767
}
6868
case .enum(let schema):
69-
for caseInfo in schema.cases {
70-
// Add detailed description for each case if available
71-
guard let description = caseInfo.description else {
72-
continue
69+
let hasAssociatedTypes = schema.cases.contains { $0.associatedProperties != nil && !$0.associatedProperties!.isEmpty }
70+
71+
if hasAssociatedTypes {
72+
let discriminatorFieldName = schema.discriminatorFieldName ?? "type"
73+
doc += "\(indent) - This is a tagged union discriminated by the `\(discriminatorFieldName)` field. Each case has the following structure:\n"
74+
75+
for caseInfo in schema.cases {
76+
doc += "\(indent) - `\(discriminatorFieldName): \"\(caseInfo.name)\"`"
77+
if let description = caseInfo.description {
78+
doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ")
79+
}
80+
doc += "\n"
81+
82+
if let associatedProperties = caseInfo.associatedProperties {
83+
for assocProp in associatedProperties {
84+
try appendProperty(assocProp, indentLevel: indentLevel + 2)
85+
}
86+
}
87+
}
88+
} else {
89+
for caseInfo in schema.cases {
90+
guard let description = caseInfo.description else {
91+
continue
92+
}
93+
doc += "\(indent) - `\(caseInfo.name)`"
94+
doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ")
95+
doc += "\n"
7396
}
74-
doc += "\(indent) - `\(caseInfo.name)`"
75-
doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ")
76-
doc += "\n"
7797
}
7898
default: break
7999
}
@@ -100,8 +120,13 @@ struct OptionDocumentBuilder {
100120
case .struct(let structInfo):
101121
return structInfo.name
102122
case .enum(let enumInfo):
103-
let cases = enumInfo.cases.map { "\"\($0.name)\"" }.joined(separator: "|")
104-
return shouldWrap ? "(\(cases))" : cases
123+
let hasAssociatedTypes = enumInfo.cases.contains { $0.associatedProperties != nil && !$0.associatedProperties!.isEmpty }
124+
if hasAssociatedTypes {
125+
return "object"
126+
} else {
127+
let cases = enumInfo.cases.map { "\"\($0.name)\"" }.joined(separator: "|")
128+
return shouldWrap ? "(\(cases))" : cases
129+
}
105130
}
106131
}
107132
}

SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ struct OptionTypeSchama {
3131
struct Case {
3232
var name: String
3333
var description: String?
34+
var associatedProperties: [Property]?
3435
}
3536

3637
struct Enum {
3738
var name: String
3839
var cases: [Case]
40+
var discriminatorFieldName: String?
3941
}
4042

4143
enum Kind {
@@ -146,14 +148,13 @@ struct OptionSchemaContext {
146148
}
147149

148150
private func buildEnumCases(_ node: EnumDeclSyntax) throws -> OptionTypeSchama.Enum {
151+
let discriminatorFieldName = Self.extractDiscriminatorFieldName(node.leadingTrivia)
152+
149153
let cases = try node.memberBlock.members.flatMap { member -> [OptionTypeSchama.Case] in
150154
guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
151155
return []
152156
}
153157
return try caseDecl.elements.compactMap {
154-
guard $0.parameterClause == nil else {
155-
throw ConfigSchemaGenError("Associated values in enum cases are not supported: \(caseDecl)")
156-
}
157158
let name: String
158159
if let rawValue = $0.rawValue?.value {
159160
if let stringLiteral = rawValue.as(StringLiteralExprSyntax.self),
@@ -172,11 +173,44 @@ struct OptionSchemaContext {
172173
if description?.contains("- Note: Internal option") ?? false {
173174
return nil
174175
}
175-
return OptionTypeSchama.Case(name: name, description: description)
176+
177+
var associatedProperties: [OptionTypeSchama.Property]? = nil
178+
if let parameterClause = $0.parameterClause {
179+
let caseDescription = description
180+
associatedProperties = try parameterClause.parameters.map { param in
181+
let propertyName: String
182+
if let firstName = param.firstName, firstName.tokenKind != .wildcard {
183+
propertyName = firstName.text
184+
} else if let secondName = param.secondName {
185+
propertyName = secondName.text
186+
} else {
187+
propertyName = name
188+
}
189+
190+
let propertyType = try resolveType(param.type)
191+
let propertyDescription = Self.extractParameterDescription(
192+
from: caseDescription,
193+
parameterName: propertyName
194+
) ?? Self.extractDocComment(param.leadingTrivia)
195+
196+
return OptionTypeSchama.Property(
197+
name: propertyName,
198+
type: propertyType,
199+
description: propertyDescription,
200+
defaultValue: nil
201+
)
202+
}
203+
}
204+
205+
return OptionTypeSchama.Case(
206+
name: name,
207+
description: description,
208+
associatedProperties: associatedProperties
209+
)
176210
}
177211
}
178212
let typeName = node.name.text
179-
return .init(name: typeName, cases: cases)
213+
return .init(name: typeName, cases: cases, discriminatorFieldName: discriminatorFieldName)
180214
}
181215

182216
private func buildStructProperties(_ node: StructDeclSyntax) throws -> OptionTypeSchama.Struct {
@@ -234,4 +268,52 @@ struct OptionSchemaContext {
234268
}
235269
return docLines.joined(separator: " ")
236270
}
271+
272+
private static func extractDiscriminatorFieldName(_ trivia: Trivia) -> String? {
273+
let docLines = trivia.flatMap { piece -> [Substring] in
274+
switch piece {
275+
case .docBlockComment(let text):
276+
assert(text.hasPrefix("/**") && text.hasSuffix("*/"), "Unexpected doc block comment format: \(text)")
277+
return text.dropFirst(3).dropLast(2).split { $0.isNewline }
278+
case .docLineComment(let text):
279+
assert(text.hasPrefix("///"), "Unexpected doc line comment format: \(text)")
280+
let text = text.dropFirst(3)
281+
return [text]
282+
default:
283+
return []
284+
}
285+
}
286+
287+
for line in docLines {
288+
var trimmed = line
289+
while trimmed.first?.isWhitespace == true {
290+
trimmed = trimmed.dropFirst()
291+
}
292+
if trimmed.hasPrefix("- discriminator:") {
293+
let fieldName = trimmed.dropFirst("- discriminator:".count).trimmingCharacters(in: .whitespaces)
294+
return fieldName.isEmpty ? nil : fieldName
295+
}
296+
}
297+
return nil
298+
}
299+
300+
private static func extractParameterDescription(from docComment: String?, parameterName: String) -> String? {
301+
guard let docComment = docComment else {
302+
return nil
303+
}
304+
305+
let pattern = "`\(parameterName)`:"
306+
guard let range = docComment.range(of: pattern) else {
307+
return nil
308+
}
309+
310+
let afterPattern = docComment[range.upperBound...]
311+
let lines = afterPattern.split(separator: "\n", maxSplits: 1, omittingEmptySubsequences: false)
312+
guard let firstLine = lines.first else {
313+
return nil
314+
}
315+
316+
let description = firstLine.trimmingCharacters(in: .whitespaces)
317+
return description.isEmpty ? nil : description
318+
}
237319
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Defines the batch size for target preparation.
14+
///
15+
/// If nil, SourceKit-LSP will default to preparing 1 target at a time.
16+
///
17+
/// - discriminator: strategy
18+
public enum PreparationBatchingStrategy: Sendable, Equatable {
19+
/// Prepare a fixed number of targets in a single batch.
20+
///
21+
/// `batchSize`: The number of targets to prepare in each batch.
22+
case target(batchSize: Int)
23+
}
24+
25+
extension PreparationBatchingStrategy: Codable {
26+
private enum CodingKeys: String, CodingKey {
27+
case strategy
28+
case batchSize
29+
}
30+
31+
private enum StrategyValue: String, Codable {
32+
case target
33+
}
34+
35+
public init(from decoder: Decoder) throws {
36+
let container = try decoder.container(keyedBy: CodingKeys.self)
37+
let strategy = try container.decode(StrategyValue.self, forKey: .strategy)
38+
39+
switch strategy {
40+
case .target:
41+
let batchSize = try container.decode(Int.self, forKey: .batchSize)
42+
self = .target(batchSize: batchSize)
43+
}
44+
}
45+
46+
public func encode(to encoder: Encoder) throws {
47+
var container = encoder.container(keyedBy: CodingKeys.self)
48+
switch self {
49+
case .target(let batchSize):
50+
try container.encode(StrategyValue.target, forKey: .strategy)
51+
try container.encode(batchSize, forKey: .batchSize)
52+
}
53+
}
54+
}

Sources/SKOptions/SourceKitLSPOptions.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,14 +441,13 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
441441
if let buildServerWorkspaceRequestsTimeout {
442442
return .seconds(buildServerWorkspaceRequestsTimeout)
443443
}
444-
// The default value needs to strike a balance: If the build server is slow to respond, we don't want to constantly
445-
// run into this timeout, which causes somewhat expensive computations because we trigger the `buildTargetsChanged`
446-
// chain.
447-
// At the same time, we do want to provide functionality based on fallback settings after some time.
448-
// 15s seems like it should strike a balance here but there is no data backing this value up.
449444
return .seconds(15)
450445
}
451446

447+
/// Defines the batch size for target preparation.
448+
/// If nil, defaults to preparing 1 target at a time.
449+
public var preparationBatchingStrategy: PreparationBatchingStrategy?
450+
452451
public init(
453452
swiftPM: SwiftPMOptions? = .init(),
454453
fallbackBuildSystem: FallbackBuildSystemOptions? = .init(),
@@ -462,6 +461,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
462461
generatedFilesPath: String? = nil,
463462
backgroundIndexing: Bool? = nil,
464463
backgroundPreparationMode: BackgroundPreparationMode? = nil,
464+
preparationBatchingStrategy: PreparationBatchingStrategy? = nil,
465465
cancelTextDocumentRequestsOnEditAndClose: Bool? = nil,
466466
experimentalFeatures: Set<ExperimentalFeature>? = nil,
467467
swiftPublishDiagnosticsDebounceDuration: Double? = nil,
@@ -482,6 +482,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
482482
self.defaultWorkspaceType = defaultWorkspaceType
483483
self.backgroundIndexing = backgroundIndexing
484484
self.backgroundPreparationMode = backgroundPreparationMode
485+
self.preparationBatchingStrategy = preparationBatchingStrategy
485486
self.cancelTextDocumentRequestsOnEditAndClose = cancelTextDocumentRequestsOnEditAndClose
486487
self.experimentalFeatures = experimentalFeatures
487488
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
@@ -545,6 +546,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
545546
generatedFilesPath: override?.generatedFilesPath ?? base.generatedFilesPath,
546547
backgroundIndexing: override?.backgroundIndexing ?? base.backgroundIndexing,
547548
backgroundPreparationMode: override?.backgroundPreparationMode ?? base.backgroundPreparationMode,
549+
preparationBatchingStrategy: override?.preparationBatchingStrategy ?? base.preparationBatchingStrategy,
548550
cancelTextDocumentRequestsOnEditAndClose: override?.cancelTextDocumentRequestsOnEditAndClose
549551
?? base.cancelTextDocumentRequestsOnEditAndClose,
550552
experimentalFeatures: override?.experimentalFeatures ?? base.experimentalFeatures,

0 commit comments

Comments
 (0)