@@ -16,10 +16,12 @@ internal import IndexStoreDB
1616@_spi ( SourceKitLSP) import LanguageServerProtocol
1717@_spi ( SourceKitLSP) import SKLogging
1818import SemanticIndex
19+ import SourceKitD
1920import SourceKitLSP
2021import SwiftSyntax
2122import 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.
2527final 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+ }
0 commit comments