Skip to content

Commit 1acc2f5

Browse files
authored
Merge pull request #1556 from plemarquand/codelens
Add Run/Debug CodeLens Support
2 parents bda4ade + 960317b commit 1acc2f5

12 files changed

+344
-12
lines changed

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ add_library(LanguageServerProtocol STATIC
125125
SupportTypes/SemanticTokenTypes.swift
126126
SupportTypes/ServerCapabilities.swift
127127
SupportTypes/StringOrMarkupContent.swift
128+
SupportTypes/SupportedCodeLensCommand.swift
128129
SupportTypes/SymbolKind.swift
129130
SupportTypes/TestItem.swift
130131
SupportTypes/TextDocumentContentChangeEvent.swift

Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,25 @@ public struct TextDocumentClientCapabilities: Hashable, Codable, Sendable {
465465
}
466466
}
467467

468+
public struct CodeLens: Hashable, Codable, Sendable {
469+
470+
/// Whether the client supports dynamic registration of this request.
471+
public var dynamicRegistration: Bool?
472+
473+
/// Dictionary of supported commands announced by the client.
474+
/// The key is the CodeLens name recognized by SourceKit-LSP and the
475+
/// value is the command as recognized by the client.
476+
public var supportedCommands: [SupportedCodeLensCommand: String]?
477+
478+
public init(
479+
dynamicRegistration: Bool? = nil,
480+
supportedCommands: [SupportedCodeLensCommand: String] = [:]
481+
) {
482+
self.dynamicRegistration = dynamicRegistration
483+
self.supportedCommands = supportedCommands
484+
}
485+
}
486+
468487
/// Capabilities specific to `textDocument/rename`.
469488
public struct Rename: Hashable, Codable, Sendable {
470489

@@ -666,7 +685,7 @@ public struct TextDocumentClientCapabilities: Hashable, Codable, Sendable {
666685

667686
public var codeAction: CodeAction? = nil
668687

669-
public var codeLens: DynamicRegistrationCapability? = nil
688+
public var codeLens: CodeLens? = nil
670689

671690
public var documentLink: DynamicRegistrationCapability? = nil
672691

@@ -715,7 +734,7 @@ public struct TextDocumentClientCapabilities: Hashable, Codable, Sendable {
715734
documentHighlight: DynamicRegistrationCapability? = nil,
716735
documentSymbol: DocumentSymbol? = nil,
717736
codeAction: CodeAction? = nil,
718-
codeLens: DynamicRegistrationCapability? = nil,
737+
codeLens: CodeLens? = nil,
719738
documentLink: DynamicRegistrationCapability? = nil,
720739
colorProvider: DynamicRegistrationCapability? = nil,
721740
formatting: DynamicRegistrationCapability? = nil,

Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,48 @@ public struct DiagnosticRegistrationOptions: RegistrationOptions, TextDocumentRe
242242
}
243243
}
244244

245+
/// Describe options to be used when registering for code lenses.
246+
public struct CodeLensRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol {
247+
public var textDocumentRegistrationOptions: TextDocumentRegistrationOptions
248+
public var codeLensOptions: CodeLensOptions
249+
250+
public init(
251+
documentSelector: DocumentSelector? = nil,
252+
codeLensOptions: CodeLensOptions
253+
) {
254+
textDocumentRegistrationOptions = TextDocumentRegistrationOptions(documentSelector: documentSelector)
255+
self.codeLensOptions = codeLensOptions
256+
}
257+
258+
public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
259+
self.codeLensOptions = CodeLensOptions()
260+
261+
if case .bool(let resolveProvider) = dictionary["resolveProvider"] {
262+
self.codeLensOptions.resolveProvider = resolveProvider
263+
}
264+
265+
guard let textDocumentRegistrationOptions = TextDocumentRegistrationOptions(fromLSPDictionary: dictionary) else {
266+
return nil
267+
}
268+
269+
self.textDocumentRegistrationOptions = textDocumentRegistrationOptions
270+
}
271+
272+
public func encodeToLSPAny() -> LSPAny {
273+
var dict: [String: LSPAny] = [:]
274+
275+
if let resolveProvider = codeLensOptions.resolveProvider {
276+
dict["resolveProvider"] = .bool(resolveProvider)
277+
}
278+
279+
if case .dictionary(let dictionary) = textDocumentRegistrationOptions.encodeToLSPAny() {
280+
dict.merge(dictionary) { (current, _) in current }
281+
}
282+
283+
return .dictionary(dict)
284+
}
285+
}
286+
245287
/// Describe options to be used when registering for file system change events.
246288
public struct DidChangeWatchedFilesRegistrationOptions: RegistrationOptions {
247289
/// The watchers to register.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
/// Code lenses that LSP can annotate code with.
14+
///
15+
/// Clients provide these as keys to the `supportedCommands` dictionary supplied
16+
/// in the client's `InitializeRequest`.
17+
public struct SupportedCodeLensCommand: Codable, Hashable, RawRepresentable, Sendable {
18+
public var rawValue: String
19+
20+
public init(rawValue: String) {
21+
self.rawValue = rawValue
22+
}
23+
24+
/// Lens to run the application
25+
public static let run: Self = Self(rawValue: "swift.run")
26+
27+
/// Lens to debug the application
28+
public static let debug: Self = Self(rawValue: "swift.debug")
29+
}

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ target_sources(SourceKitLSP PRIVATE
5757
Swift/SemanticRefactoring.swift
5858
Swift/SemanticTokens.swift
5959
Swift/SourceKitD+ResponseError.swift
60+
Swift/SwiftCodeLensScanner.swift
6061
Swift/SwiftCommand.swift
6162
Swift/SwiftLanguageService.swift
6263
Swift/SwiftTestingScanner.swift

Sources/SourceKitLSP/CapabilityRegistry.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ package final actor CapabilityRegistry {
8080
clientCapabilities.textDocument?.publishDiagnostics?.codeDescriptionSupport == true
8181
}
8282

83+
public var supportedCodeLensCommands: [SupportedCodeLensCommand: String] {
84+
clientCapabilities.textDocument?.codeLens?.supportedCommands ?? [:]
85+
}
86+
8387
/// Since LSP 3.17.0, diagnostics can be reported through pull-based requests in addition to the existing push-based
8488
/// publish notifications.
8589
///
@@ -279,6 +283,7 @@ package final actor CapabilityRegistry {
279283
server: SourceKitLSPServer
280284
) async {
281285
guard clientHasDynamicInlayHintRegistration else { return }
286+
282287
await registerLanguageSpecificCapability(
283288
options: InlayHintRegistrationOptions(
284289
documentSelector: DocumentSelector(for: languages),
@@ -345,7 +350,7 @@ package final actor CapabilityRegistry {
345350
}
346351

347352
fileprivate extension DocumentSelector {
348-
init(for languages: [Language]) {
349-
self.init(languages.map { DocumentFilter(language: $0.rawValue) })
353+
init(for languages: [Language], scheme: String? = nil) {
354+
self.init(languages.map { DocumentFilter(language: $0.rawValue, scheme: scheme) })
350355
}
351356
}

Sources/SourceKitLSP/Clang/ClangLanguageService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,10 @@ extension ClangLanguageService {
632632
return try await forwardRequestToClangd(req)
633633
}
634634

635+
func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] {
636+
return try await forwardRequestToClangd(req) ?? []
637+
}
638+
635639
func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? {
636640
guard self.capabilities?.foldingRangeProvider?.isSupported ?? false else {
637641
return nil

Sources/SourceKitLSP/LanguageService.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ package protocol LanguageService: AnyObject, Sendable {
197197
func colorPresentation(_ req: ColorPresentationRequest) async throws -> [ColorPresentation]
198198
func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse?
199199
func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint]
200+
func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens]
200201
func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport
201202
func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]?
202203

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,8 @@ extension SourceKitLSPServer: MessageHandler {
700700
await self.handleRequest(for: request, requestHandler: self.prepareCallHierarchy)
701701
case let request as RequestAndReply<CodeActionRequest>:
702702
await self.handleRequest(for: request, requestHandler: self.codeAction)
703+
case let request as RequestAndReply<CodeLensRequest>:
704+
await self.handleRequest(for: request, requestHandler: self.codeLens)
703705
case let request as RequestAndReply<ColorPresentationRequest>:
704706
await self.handleRequest(for: request, requestHandler: self.colorPresentation)
705707
case let request as RequestAndReply<CompletionRequest>:
@@ -961,14 +963,30 @@ extension SourceKitLSPServer {
961963
// The below is a workaround for the vscode-swift extension since it cannot set client capabilities.
962964
// It passes "workspace/peekDocuments" through the `initializationOptions`.
963965
var clientCapabilities = req.capabilities
964-
if case .dictionary(let initializationOptions) = req.initializationOptions,
965-
let peekDocuments = initializationOptions["workspace/peekDocuments"]
966-
{
967-
if case .dictionary(var experimentalCapabilities) = clientCapabilities.experimental {
968-
experimentalCapabilities["workspace/peekDocuments"] = peekDocuments
969-
clientCapabilities.experimental = .dictionary(experimentalCapabilities)
970-
} else {
971-
clientCapabilities.experimental = .dictionary(["workspace/peekDocuments": peekDocuments])
966+
if case .dictionary(let initializationOptions) = req.initializationOptions {
967+
if let peekDocuments = initializationOptions["workspace/peekDocuments"] {
968+
if case .dictionary(var experimentalCapabilities) = clientCapabilities.experimental {
969+
experimentalCapabilities["workspace/peekDocuments"] = peekDocuments
970+
clientCapabilities.experimental = .dictionary(experimentalCapabilities)
971+
} else {
972+
clientCapabilities.experimental = .dictionary(["workspace/peekDocuments": peekDocuments])
973+
}
974+
}
975+
976+
// The client announces what CodeLenses it supports, and the LSP will only return
977+
// ones found in the supportedCommands dictionary.
978+
if let codeLens = initializationOptions["textDocument/codeLens"],
979+
case let .dictionary(codeLensConfig) = codeLens,
980+
case let .dictionary(supportedCommands) = codeLensConfig["supportedCommands"]
981+
{
982+
let commandMap = supportedCommands.compactMap { (key, value) in
983+
if case let .string(clientCommand) = value {
984+
return (SupportedCodeLensCommand(rawValue: key), clientCommand)
985+
}
986+
return nil
987+
}
988+
989+
clientCapabilities.textDocument?.codeLens?.supportedCommands = Dictionary(uniqueKeysWithValues: commandMap)
972990
}
973991
}
974992

@@ -1100,6 +1118,7 @@ extension SourceKitLSPServer {
11001118
supportsCodeActions: true
11011119
)
11021120
),
1121+
codeLensProvider: CodeLensOptions(),
11031122
documentFormattingProvider: .value(DocumentFormattingOptions(workDoneProgress: false)),
11041123
renameProvider: .value(RenameOptions(prepareProvider: true)),
11051124
colorProvider: .bool(true),
@@ -1642,6 +1661,14 @@ extension SourceKitLSPServer {
16421661
return req.injectMetadata(toResponse: response)
16431662
}
16441663

1664+
func codeLens(
1665+
_ req: CodeLensRequest,
1666+
workspace: Workspace,
1667+
languageService: LanguageService
1668+
) async throws -> [CodeLens] {
1669+
return try await languageService.codeLens(req)
1670+
}
1671+
16451672
func inlayHint(
16461673
_ req: InlayHintRequest,
16471674
workspace: Workspace,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 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+
import LanguageServerProtocol
14+
import SwiftSyntax
15+
16+
/// Scans a source file for classes or structs annotated with `@main` and returns a code lens for them.
17+
final class SwiftCodeLensScanner: SyntaxVisitor {
18+
/// The document snapshot of the syntax tree that is being walked.
19+
private let snapshot: DocumentSnapshot
20+
21+
/// The collection of CodeLenses found in the document.
22+
private var result: [CodeLens] = []
23+
24+
/// The map of supported commands and their client side command names
25+
private let supportedCommands: [SupportedCodeLensCommand: String]
26+
27+
private init(snapshot: DocumentSnapshot, supportedCommands: [SupportedCodeLensCommand: String]) {
28+
self.snapshot = snapshot
29+
self.supportedCommands = supportedCommands
30+
super.init(viewMode: .fixedUp)
31+
}
32+
33+
/// Public entry point. Scans the syntax tree of the given snapshot for an `@main` annotation
34+
/// and returns CodeLens's with Commands to run/debug the application.
35+
public static func findCodeLenses(
36+
in snapshot: DocumentSnapshot,
37+
syntaxTreeManager: SyntaxTreeManager,
38+
supportedCommands: [SupportedCodeLensCommand: String]
39+
) async -> [CodeLens] {
40+
guard snapshot.text.contains("@main") && !supportedCommands.isEmpty else {
41+
// This is intended to filter out files that obviously do not contain an entry point.
42+
return []
43+
}
44+
45+
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
46+
let visitor = SwiftCodeLensScanner(snapshot: snapshot, supportedCommands: supportedCommands)
47+
visitor.walk(syntaxTree)
48+
return visitor.result
49+
}
50+
51+
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
52+
node.attributes.forEach(self.captureLensFromAttribute)
53+
return .skipChildren
54+
}
55+
56+
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
57+
node.attributes.forEach(self.captureLensFromAttribute)
58+
return .skipChildren
59+
}
60+
61+
private func captureLensFromAttribute(attribute: AttributeListSyntax.Element) {
62+
if attribute.trimmedDescription == "@main" {
63+
let range = self.snapshot.absolutePositionRange(of: attribute.trimmedRange)
64+
65+
if let runCommand = supportedCommands[SupportedCodeLensCommand.run] {
66+
// Return commands for running/debugging the executable.
67+
// These command names must be recognized by the client and so should not be chosen arbitrarily.
68+
self.result.append(
69+
CodeLens(
70+
range: range,
71+
command: Command(title: "Run", command: runCommand, arguments: nil)
72+
)
73+
)
74+
}
75+
76+
if let debugCommand = supportedCommands[SupportedCodeLensCommand.debug] {
77+
self.result.append(
78+
CodeLens(
79+
range: range,
80+
command: Command(title: "Debug", command: debugCommand, arguments: nil)
81+
)
82+
)
83+
}
84+
}
85+
}
86+
}

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ extension SwiftLanguageService {
317317
supportsCodeActions: true
318318
)
319319
),
320+
codeLensProvider: CodeLensOptions(),
320321
colorProvider: .bool(true),
321322
foldingRangeProvider: .bool(true),
322323
executeCommandProvider: ExecuteCommandOptions(
@@ -921,6 +922,15 @@ extension SwiftLanguageService {
921922
return Array(hints)
922923
}
923924

925+
package func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] {
926+
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
927+
return await SwiftCodeLensScanner.findCodeLenses(
928+
in: snapshot,
929+
syntaxTreeManager: self.syntaxTreeManager,
930+
supportedCommands: self.capabilityRegistry.supportedCodeLensCommands
931+
)
932+
}
933+
924934
package func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport {
925935
do {
926936
await semanticIndexManager?.prepareFileForEditorFunctionality(req.textDocument.uri)

0 commit comments

Comments
 (0)