Skip to content

Commit dc49728

Browse files
committed
Support custom preparationBatchSize defined via SourceKit's options
1 parent 0e061c5 commit dc49728

File tree

11 files changed

+390
-30
lines changed

11 files changed

+390
-30
lines changed

Documentation/Configuration File.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,7 @@ The structure of the file is currently not guaranteed to be stable. Options may
6262
- `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.
6363
- `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.
6464
- `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.
65+
- `preparationBatchingStrategy: object`: Defines the batch size for target preparation. If nil, defaults to preparing 1 target at a time.
66+
- This is a tagged union discriminated by the `strategy` field. Each case has the following structure:
67+
- `strategy: "fixedTargetBatchSize"`: Prepare a fixed number of targets in a single batch. `batchSize`: The number of targets to prepare in each batch.
68+
- `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?
@@ -60,6 +62,9 @@ struct JSONSchema: Encodable {
6062
/// https://github.com/microsoft/vscode-wiki/blob/main/Setting-Descriptions.md
6163
var markdownEnumDescriptions: [String]?
6264

65+
var oneOf: [JSONSchema]?
66+
var const: String?
67+
6368
func encode(to encoder: any Encoder) throws {
6469
// Manually implement encoding to use `encodeIfPresent` for HeapBox-ed fields
6570
var container = encoder.container(keyedBy: CodingKeys.self)
@@ -82,6 +87,10 @@ struct JSONSchema: Encodable {
8287
if let markdownEnumDescriptions {
8388
try container.encode(markdownEnumDescriptions, forKey: .markdownEnumDescriptions)
8489
}
90+
if let 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?.isEmpty ?? true) }
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: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,39 @@ struct OptionDocumentBuilder {
6868
try appendProperty(property, indentLevel: indentLevel + 1)
6969
}
7070
case .enum(let schema):
71-
for caseInfo in schema.cases {
72-
// Add detailed description for each case if available
73-
guard let description = caseInfo.description else {
74-
continue
71+
let hasAssociatedTypes = schema.cases.contains {
72+
$0.associatedProperties != nil && !$0.associatedProperties!.isEmpty
73+
}
74+
75+
if hasAssociatedTypes {
76+
let discriminatorFieldName = schema.discriminatorFieldName ?? "type"
77+
doc +=
78+
"\(indent) - This is a tagged union discriminated by the `\(discriminatorFieldName)` field. Each case has the following structure:\n"
79+
80+
for caseInfo in schema.cases {
81+
doc += """
82+
\(indent) - `\(discriminatorFieldName): "\(caseInfo.name)"`
83+
"""
84+
if let description = caseInfo.description {
85+
doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ")
86+
}
87+
doc += "\n"
88+
89+
if let associatedProperties = caseInfo.associatedProperties {
90+
for assocProp in associatedProperties {
91+
try appendProperty(assocProp, indentLevel: indentLevel + 2)
92+
}
93+
}
94+
}
95+
} else {
96+
for caseInfo in schema.cases {
97+
guard let description = caseInfo.description else {
98+
continue
99+
}
100+
doc += "\(indent) - `\(caseInfo.name)`"
101+
doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ")
102+
doc += "\n"
75103
}
76-
doc += "\(indent) - `\(caseInfo.name)`"
77-
doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ")
78-
doc += "\n"
79104
}
80105
default: break
81106
}
@@ -102,8 +127,15 @@ struct OptionDocumentBuilder {
102127
case .struct(let structInfo):
103128
return structInfo.name
104129
case .enum(let enumInfo):
105-
let cases = enumInfo.cases.map { "\"\($0.name)\"" }.joined(separator: "|")
106-
return shouldWrap ? "(\(cases))" : cases
130+
let hasAssociatedTypes = enumInfo.cases.contains {
131+
$0.associatedProperties != nil && !$0.associatedProperties!.isEmpty
132+
}
133+
if hasAssociatedTypes {
134+
return "object"
135+
} else {
136+
let cases = enumInfo.cases.map { "\"\($0.name)\"" }.joined(separator: "|")
137+
return shouldWrap ? "(\(cases))" : cases
138+
}
107139
}
108140
}
109141
}

SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift

Lines changed: 88 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,45 @@ 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 =
192+
Self.extractParameterDescription(
193+
from: caseDescription,
194+
parameterName: propertyName
195+
) ?? Self.extractDocComment(param.leadingTrivia)
196+
197+
return OptionTypeSchama.Property(
198+
name: propertyName,
199+
type: propertyType,
200+
description: propertyDescription,
201+
defaultValue: nil
202+
)
203+
}
204+
}
205+
206+
return OptionTypeSchama.Case(
207+
name: name,
208+
description: description,
209+
associatedProperties: associatedProperties
210+
)
176211
}
177212
}
178213
let typeName = node.name.text
179-
return .init(name: typeName, cases: cases)
214+
return .init(name: typeName, cases: cases, discriminatorFieldName: discriminatorFieldName)
180215
}
181216

182217
private func buildStructProperties(_ node: StructDeclSyntax) throws -> OptionTypeSchama.Struct {
@@ -234,4 +269,52 @@ struct OptionSchemaContext {
234269
}
235270
return docLines.joined(separator: " ")
236271
}
272+
273+
private static func extractDiscriminatorFieldName(_ trivia: Trivia) -> String? {
274+
let docLines = trivia.flatMap { piece -> [Substring] in
275+
switch piece {
276+
case .docBlockComment(let text):
277+
assert(text.hasPrefix("/**") && text.hasSuffix("*/"), "Unexpected doc block comment format: \(text)")
278+
return text.dropFirst(3).dropLast(2).split { $0.isNewline }
279+
case .docLineComment(let text):
280+
assert(text.hasPrefix("///"), "Unexpected doc line comment format: \(text)")
281+
let text = text.dropFirst(3)
282+
return [text]
283+
default:
284+
return []
285+
}
286+
}
287+
288+
for line in docLines {
289+
var trimmed = line
290+
while trimmed.first?.isWhitespace == true {
291+
trimmed = trimmed.dropFirst()
292+
}
293+
if trimmed.hasPrefix("- discriminator:") {
294+
let fieldName = trimmed.dropFirst("- discriminator:".count).trimmingCharacters(in: .whitespaces)
295+
return fieldName.isEmpty ? nil : fieldName
296+
}
297+
}
298+
return nil
299+
}
300+
301+
private static func extractParameterDescription(from docComment: String?, parameterName: String) -> String? {
302+
guard let docComment = docComment else {
303+
return nil
304+
}
305+
306+
let pattern = "`\(parameterName)`:"
307+
guard let range = docComment.range(of: pattern) else {
308+
return nil
309+
}
310+
311+
let afterPattern = docComment[range.upperBound...]
312+
let lines = afterPattern.split(separator: "\n", maxSplits: 1, omittingEmptySubsequences: false)
313+
guard let firstLine = lines.first else {
314+
return nil
315+
}
316+
317+
let description = firstLine.trimmingCharacters(in: .whitespaces)
318+
return description.isEmpty ? nil : description
319+
}
237320
}
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 fixedTargetBatchSize(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 fixedTargetBatchSize
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 .fixedTargetBatchSize:
41+
let batchSize = try container.decode(Int.self, forKey: .batchSize)
42+
self = .fixedTargetBatchSize(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 .fixedTargetBatchSize(let batchSize):
50+
try container.encode(StrategyValue.fixedTargetBatchSize, forKey: .strategy)
51+
try container.encode(batchSize, forKey: .batchSize)
52+
}
53+
}
54+
}

Sources/SKOptions/SourceKitLSPOptions.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,10 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
449449
return .seconds(15)
450450
}
451451

452+
/// Defines the batch size for target preparation.
453+
/// If nil, defaults to preparing 1 target at a time.
454+
public var preparationBatchingStrategy: PreparationBatchingStrategy?
455+
452456
public init(
453457
swiftPM: SwiftPMOptions? = .init(),
454458
fallbackBuildSystem: FallbackBuildSystemOptions? = .init(),
@@ -462,6 +466,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
462466
generatedFilesPath: String? = nil,
463467
backgroundIndexing: Bool? = nil,
464468
backgroundPreparationMode: BackgroundPreparationMode? = nil,
469+
preparationBatchingStrategy: PreparationBatchingStrategy? = nil,
465470
cancelTextDocumentRequestsOnEditAndClose: Bool? = nil,
466471
experimentalFeatures: Set<ExperimentalFeature>? = nil,
467472
swiftPublishDiagnosticsDebounceDuration: Double? = nil,
@@ -482,6 +487,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
482487
self.defaultWorkspaceType = defaultWorkspaceType
483488
self.backgroundIndexing = backgroundIndexing
484489
self.backgroundPreparationMode = backgroundPreparationMode
490+
self.preparationBatchingStrategy = preparationBatchingStrategy
485491
self.cancelTextDocumentRequestsOnEditAndClose = cancelTextDocumentRequestsOnEditAndClose
486492
self.experimentalFeatures = experimentalFeatures
487493
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
@@ -545,6 +551,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
545551
generatedFilesPath: override?.generatedFilesPath ?? base.generatedFilesPath,
546552
backgroundIndexing: override?.backgroundIndexing ?? base.backgroundIndexing,
547553
backgroundPreparationMode: override?.backgroundPreparationMode ?? base.backgroundPreparationMode,
554+
preparationBatchingStrategy: override?.preparationBatchingStrategy ?? base.preparationBatchingStrategy,
548555
cancelTextDocumentRequestsOnEditAndClose: override?.cancelTextDocumentRequestsOnEditAndClose
549556
?? base.cancelTextDocumentRequestsOnEditAndClose,
550557
experimentalFeatures: override?.experimentalFeatures ?? base.experimentalFeatures,

0 commit comments

Comments
 (0)