Skip to content

Commit fe72dc2

Browse files
committed
Use batch soucekit request
1 parent b2c95cd commit fe72dc2

4 files changed

Lines changed: 222 additions & 37 deletions

File tree

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,8 @@ package struct sourcekitd_api_requests {
886886
package let collectExpressionType: sourcekitd_api_uid_t
887887
/// `source.request.variable.type`
888888
package let collectVariableType: sourcekitd_api_uid_t
889+
/// `source.request.declaration.usr`
890+
package let collectDeclarationUSR: sourcekitd_api_uid_t
889891
/// `source.request.configuration.global`
890892
package let globalConfiguration: sourcekitd_api_uid_t
891893
/// `source.request.dependency_updated`
@@ -955,6 +957,7 @@ package struct sourcekitd_api_requests {
955957
testNotification = api.uid_get_from_cstr("source.request.test_notification")!
956958
collectExpressionType = api.uid_get_from_cstr("source.request.expression.type")!
957959
collectVariableType = api.uid_get_from_cstr("source.request.variable.type")!
960+
collectDeclarationUSR = api.uid_get_from_cstr("source.request.declaration.usr")!
958961
globalConfiguration = api.uid_get_from_cstr("source.request.configuration.global")!
959962
dependencyUpdated = api.uid_get_from_cstr("source.request.dependency_updated")!
960963
diagnostics = api.uid_get_from_cstr("source.request.diagnostics")!

Sources/SwiftLanguageService/SwiftCodeLensScanner.swift

Lines changed: 158 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ internal import IndexStoreDB
1616
@_spi(SourceKitLSP) import LanguageServerProtocol
1717
@_spi(SourceKitLSP) import SKLogging
1818
import SemanticIndex
19+
import SourceKitD
1920
import SourceKitLSP
2021
import SwiftSyntax
2122
import ToolchainRegistry
2223

24+
/// Scans a source file for classes or structs annotated with `@main` and returns a code lens for them.
2325
/// Scans a source file for code lenses including `@main` run/debug actions,
2426
/// symbol reference counts, and playground entries.
2527
final class SwiftCodeLensScanner: SyntaxVisitor {
@@ -32,7 +34,8 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
3234
/// The display name of the build target containing this document, if available.
3335
private let targetName: String?
3436

35-
/// The language service used to resolve cursor info for symbols.
37+
/// The map of supported commands and their client side command names
38+
/// The language service used to resolve symbol metadata for code lenses.
3639
private let languageService: SwiftLanguageService
3740

3841
/// The map of supported commands and their client side command names.
@@ -46,6 +49,7 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
4649
private init(
4750
snapshot: DocumentSnapshot,
4851
targetName: String?,
52+
supportedCommands: [SupportedCodeLensCommand: String]
4953
supportedCommands: [SupportedCodeLensCommand: String],
5054
workspace: Workspace?,
5155
languageService: SwiftLanguageService
@@ -58,6 +62,8 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
5862
super.init(viewMode: .fixedUp)
5963
}
6064

65+
/// Public entry point. Scans the syntax tree of the given snapshot for an `@main` annotation
66+
/// and returns CodeLens's with Commands to run/debug the application.
6167
/// Public entry point. Scans the syntax tree of the given snapshot and returns
6268
/// all applicable code lenses including `@main` run/debug actions, reference counts,
6369
/// and playground entries.
@@ -66,15 +72,34 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
6672
workspace: Workspace?,
6773
syntaxTreeManager: SyntaxTreeManager,
6874
supportedCommands: [SupportedCodeLensCommand: String],
75+
toolchain: Toolchain
6976
toolchain: Toolchain,
7077
languageService: SwiftLanguageService
7178
) async -> [CodeLens] {
7279
guard !supportedCommands.isEmpty else {
7380
return []
7481
}
7582

83+
var targetDisplayName: String? = nil
84+
if let workspace,
85+
let target = await workspace.buildServerManager.canonicalTarget(for: snapshot.uri),
86+
let buildTarget = await workspace.buildServerManager.buildTarget(named: target)
87+
{
88+
targetDisplayName = buildTarget.displayName
89+
}
7690
let targetDisplayName = await resolveTargetDisplayName(for: snapshot, workspace: workspace)
7791

92+
var codeLenses: [CodeLens] = []
93+
if snapshot.text.contains("@main") {
94+
let visitor = SwiftCodeLensScanner(
95+
snapshot: snapshot,
96+
targetName: targetDisplayName,
97+
supportedCommands: supportedCommands
98+
)
99+
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
100+
visitor.walk(syntaxTree)
101+
codeLenses += visitor.result
102+
}
78103
// Process @main annotations and symbol references
79104
let visitor = SwiftCodeLensScanner(
80105
snapshot: snapshot,
@@ -86,10 +111,28 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
86111
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
87112
visitor.walk(syntaxTree)
88113

89-
// Process collected symbols asynchronously for reference counts
90-
for (nameToken, displayRange) in visitor.symbolsToProcess {
91-
await visitor.captureReferenceLens(for: nameToken, at: displayRange)
114+
// "swift.play" CodeLens should be ignored if "swift-play" is not in the toolchain as the client has no way of running
115+
if toolchain.swiftPlay != nil,
116+
let workspace,
117+
let playCommand = supportedCommands[SupportedCodeLensCommand.play]
118+
{
119+
let playgrounds = await SwiftPlaygroundsScanner.findDocumentPlaygrounds(
120+
for: snapshot,
121+
workspace: workspace,
122+
syntaxTreeManager: syntaxTreeManager
123+
)
124+
codeLenses += playgrounds.map({
125+
CodeLens(
126+
range: $0.range,
127+
command: Command(
128+
title: "Play \"\($0.label ?? $0.id)\"",
129+
command: playCommand,
130+
arguments: [$0.encodeToLSPAny()]
131+
)
132+
)
133+
})
92134
}
135+
await visitor.captureReferenceLenses()
93136

94137
var codeLenses = visitor.result
95138

@@ -106,6 +149,8 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
106149
}
107150

108151
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
152+
node.attributes.forEach(self.captureLensFromAttribute)
153+
return .skipChildren
109154
node.attributes.forEach(captureMainAttributeLens)
110155
symbolsToProcess.append((nameToken: node.name, displayRange: node.trimmedRange))
111156
return .visitChildren
@@ -123,6 +168,18 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
123168
}
124169

125170
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
171+
node.attributes.forEach(self.captureLensFromAttribute)
172+
return .skipChildren
173+
}
174+
175+
private func captureLensFromAttribute(attribute: AttributeListSyntax.Element) {
176+
if attribute.trimmedDescription == "@main" {
177+
let range = self.snapshot.absolutePositionRange(of: attribute.trimmedRange)
178+
var targetNameToAppend: String = ""
179+
var arguments: [LSPAny] = []
180+
if let targetName {
181+
targetNameToAppend = " \(targetName)"
182+
arguments.append(.string(targetName))
126183
node.attributes.forEach(captureMainAttributeLens)
127184
symbolsToProcess.append((nameToken: node.name, displayRange: node.trimmedRange))
128185
return .visitChildren
@@ -197,48 +254,66 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
197254
}
198255
}
199256

200-
/// Queries the index for the number of references to a symbol and appends a code lens with the count.
201-
private func captureReferenceLens(for nameToken: TokenSyntax, at displayRange: Range<AbsolutePosition>) async {
202-
guard let referencesCommand = supportedCommands[.references] else { return }
203-
204-
let lensRange = snapshot.absolutePositionRange(of: displayRange)
205-
let nameRange = snapshot.absolutePositionRange(of: nameToken.trimmedRange)
257+
/// Queries sourcekitd once for declaration USRs, then looks up reference counts in the index.
258+
private func captureReferenceLenses() async {
259+
guard let referencesCommand = supportedCommands[.references],
260+
let index = await workspace?.index(checkedFor: .deletedFiles)
261+
else {
262+
return
263+
}
206264

207265
do {
208-
let cursorInfoResults = try await languageService.cursorInfo(
266+
let declarationUsrs = try await languageService.declarationUSRs(
209267
snapshot,
210-
compileCommand: await languageService.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false),
211-
nameRange
268+
compileCommand: await languageService.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)
269+
)
270+
let usrsByOffset = Dictionary(
271+
declarationUsrs.map { ($0.offset, $0.usr) },
272+
uniquingKeysWith: { first, _ in first }
212273
)
213-
.cursorInfo
214-
215-
guard let cursorInfo = cursorInfoResults.first,
216-
let usr = cursorInfo.symbolInfo.usr,
217-
let index = await workspace?.index(checkedFor: .deletedFiles)
218-
else { return }
219-
220-
var referenceCount = 0
221-
index.forEachSymbolOccurrence(byUSR: usr, roles: .reference) { _ in
222-
referenceCount += 1
223-
return true
224-
}
225274

226-
let title = "\(referenceCount) reference\(referenceCount == 1 ? "" : "s")"
227-
result.append(
228-
CodeLens(
229-
range: lensRange,
230-
command: Command(
231-
title: title,
232-
command: referencesCommand,
233-
arguments: [.string(snapshot.uri.stringValue), nameRange.lowerBound.encodeToLSPAny()]
275+
for (nameToken, displayRange) in symbolsToProcess {
276+
guard let usr = usrsByOffset[nameToken.trimmedRange.lowerBound.utf8Offset] else {
277+
continue
278+
}
279+
280+
if let runCommand = supportedCommands[SupportedCodeLensCommand.run] {
281+
// Return commands for running/debugging the executable.
282+
// These command names must be recognized by the client and so should not be chosen arbitrarily.
283+
self.result.append(
284+
var referenceCount = 0
285+
try index.forEachSymbolOccurrence(byUSR: usr, roles: .reference) { _ in
286+
referenceCount += 1
287+
return true
288+
}
289+
290+
let lensRange = snapshot.absolutePositionRange(of: displayRange)
291+
let nameRange = snapshot.absolutePositionRange(of: nameToken.trimmedRange)
292+
let title = "\(referenceCount) reference\(referenceCount == 1 ? "" : "s")"
293+
result.append(
294+
CodeLens(
295+
range: range,
296+
command: Command(title: "Run" + targetNameToAppend, command: runCommand, arguments: arguments)
297+
range: lensRange,
298+
command: Command(
299+
title: title,
300+
command: referencesCommand,
301+
arguments: [.string(snapshot.uri.stringValue), nameRange.lowerBound.encodeToLSPAny()]
302+
)
234303
)
235304
)
236-
)
305+
}
237306
} catch {
238-
logger.info("Failed to get cursor info for reference count: \(error.forLogging, privacy: .public)")
307+
logger.info("Failed to get declaration USRs for reference count: \(error.forLogging, privacy: .public)")
239308
}
240309
}
241310

311+
if let debugCommand = supportedCommands[SupportedCodeLensCommand.debug] {
312+
self.result.append(
313+
CodeLens(
314+
range: range,
315+
command: Command(title: "Debug" + targetNameToAppend, command: debugCommand, arguments: arguments)
316+
)
242317
/// Resolves the display name of the build target containing the given document.
243318
private static func resolveTargetDisplayName(for snapshot: DocumentSnapshot, workspace: Workspace?) async -> String? {
244319
guard let workspace,
@@ -284,3 +359,51 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
284359
}
285360
}
286361
}
362+
363+
private struct DeclarationUSRInfo {
364+
let offset: Int
365+
let usr: String
366+
}
367+
368+
extension SwiftLanguageService {
369+
fileprivate func declarationUSRs(
370+
_ snapshot: DocumentSnapshot,
371+
compileCommand: SwiftCompileCommand?,
372+
_ range: Range<Position>? = nil
373+
) async throws -> [DeclarationUSRInfo] {
374+
let skreq = sourcekitd.dictionary([
375+
keys.cancelOnSubsequentRequest: 0,
376+
keys.filePath: snapshot.uri.sourcekitdSourceFile,
377+
keys.compilerArgs: compileCommand?.compilerArgs as [any SKDRequestValue]?,
378+
])
379+
380+
if let range {
381+
let start = snapshot.utf8Offset(of: range.lowerBound)
382+
let end = snapshot.utf8Offset(of: range.upperBound)
383+
skreq.set(keys.offset, to: start)
384+
skreq.set(keys.length, to: end - start)
385+
}
386+
387+
let dict = try await send(sourcekitdRequest: \.collectDeclarationUSR, skreq, snapshot: snapshot)
388+
guard let declarations: SKDResponseArray = dict[keys.declarations] else {
389+
return []
390+
}
391+
392+
var result: [DeclarationUSRInfo] = []
393+
result.reserveCapacity(declarations.count)
394+
395+
// swift-format-ignore: ReplaceForEachWithForLoop
396+
declarations.forEach { (_, declaration) -> Bool in
397+
guard let offset: Int = declaration[keys.offset],
398+
let usr: String = declaration[keys.usr]
399+
else {
400+
assertionFailure("DeclarationUSRInfo failed to deserialize")
401+
return true
402+
}
403+
result.append(DeclarationUSRInfo(offset: offset, usr: usr))
404+
return true
405+
}
406+
407+
return result
408+
}
409+
}

Sources/SwiftLanguageService/SwiftLanguageService.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import SemanticIndex
2525
package import SourceKitD
2626
package import SourceKitLSP
2727
import SwiftExtensions
28-
import SwiftParser
28+
@_spi(ExperimentalLanguageFeatures) public import SwiftParser
2929
import SwiftParserDiagnostics
3030
package import SwiftSyntax
3131
package import ToolchainRegistry
@@ -95,6 +95,28 @@ package struct SwiftCompileCommand: Sendable, Equatable, Hashable {
9595
}
9696
self.isFallback = settings.isFallback
9797
}
98+
99+
/// Extract the `Parser.ExperimentalFeatures` from the compiler arguments.
100+
///
101+
/// This scans the compiler arguments for `-enable-experimental-feature <name>` flags and maps them
102+
/// to `Parser.ExperimentalFeatures` values so that the SwiftParser can parse the file correctly
103+
/// with the same experimental features that the compiler would use.
104+
package var experimentalFeatures: Parser.ExperimentalFeatures {
105+
var features: Parser.ExperimentalFeatures = []
106+
var iterator = compilerArgs.makeIterator()
107+
while let arg = iterator.next() {
108+
if arg == "-enable-experimental-feature", let featureName = iterator.next() {
109+
// The feature name from the compiler flag may include a colon-separated
110+
// availability suffix (e.g. "FeatureName:adoption"). Strip it before
111+
// looking up the parser feature.
112+
let baseName = featureName.firstIndex(of: ":").map { String(featureName[..<$0]) } ?? featureName
113+
if let feature = Parser.ExperimentalFeatures(name: baseName) {
114+
features.insert(feature)
115+
}
116+
}
117+
}
118+
return features
119+
}
98120
}
99121

100122
package actor SwiftLanguageService: LanguageService, Sendable {
@@ -418,7 +440,8 @@ extension SwiftLanguageService {
418440
diagnosticProvider: DiagnosticOptions(
419441
interFileDependencies: true,
420442
workspaceDiagnostics: false
421-
)
443+
),
444+
selectionRangeProvider: .bool(true)
422445
)
423446
)
424447
}
@@ -476,6 +499,10 @@ extension SwiftLanguageService {
476499
compileCommand: buildSettings
477500
)
478501
self.buildSettingsForOpenFiles[snapshot.uri] = buildSettings
502+
await self.syntaxTreeManager.setExperimentalFeatures(
503+
buildSettings?.experimentalFeatures ?? [],
504+
for: snapshot.uri
505+
)
479506
_ = await orLog("Re-opening document") {
480507
try await self.send(sourcekitdRequest: \.editorOpen, openReq, snapshot: snapshot)
481508
}
@@ -559,6 +586,10 @@ extension SwiftLanguageService {
559586

560587
let buildSettings = await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)
561588
buildSettingsForOpenFiles[snapshot.uri] = buildSettings
589+
await syntaxTreeManager.setExperimentalFeatures(
590+
buildSettings?.experimentalFeatures ?? [],
591+
for: snapshot.uri
592+
)
562593

563594
let req = openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: buildSettings)
564595
await orLog("Opening sourcekitd document") {
@@ -574,6 +605,7 @@ extension SwiftLanguageService {
574605
await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri)
575606
buildSettingsForOpenFiles[notification.textDocument.uri] = nil
576607
await syntaxTreeManager.clearSyntaxTrees(for: notification.textDocument.uri)
608+
await syntaxTreeManager.clearExperimentalFeatures(for: notification.textDocument.uri)
577609
switch try? ReferenceDocumentURL(from: notification.textDocument.uri) {
578610
case .macroExpansion:
579611
break
@@ -949,6 +981,10 @@ extension SwiftLanguageService {
949981
}.flatMap { $0 }
950982
}
951983

984+
package func codeActionResolve(_ req: CodeActionResolveRequest) async throws -> CodeAction {
985+
return req.codeAction
986+
}
987+
952988
func retrieveRefactorCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {
953989
let additionalCursorInfoParameters: ((SKDRequestDictionary) -> Void) = { skreq in
954990
skreq.set(self.keys.retrieveRefactorActions, to: 1)
@@ -1044,6 +1080,7 @@ extension SwiftLanguageService {
10441080
workspace: workspace,
10451081
syntaxTreeManager: self.syntaxTreeManager,
10461082
supportedCommands: self.capabilityRegistry.supportedCodeLensCommands,
1083+
toolchain: toolchain
10471084
toolchain: toolchain,
10481085
languageService: self
10491086
)

0 commit comments

Comments
 (0)