From a4ac6e0edafcfb30d6639f2e15ed293b673b9b7a Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Mon, 14 Jul 2025 16:35:03 +0200 Subject: [PATCH] Allow clients to pass experimental capabilities they want to enable as a dictionary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The motivation for this is that I noticed that the `workspace/peekDocuments` request only allows the specification of a document to peek but not the location to peek within it. The fix is to change the `location` parameter from `DocumentUri` to `Location`, but this is a protocol-breaking change, so the client needs to communicate that it can support locations in the `workspace/peekDocuments` request. The client currently communicates that it supports peeking documents by passing `workspace/peekDocuments: true` in the experimental client capabilities. We could just add another option to the experimental client capabilities like `workspace/peekDocuments.supportsLocations` but this seems a little hacky. Instead, what I propose is that we 1. Allow enabling experimental capabilities as `"": { "supported": true }` 2. Switch the VS Code Swift extension to enable client capabilities using a dictionary if it discovers that it uses SourceKit-LSP ≥6.3 (older SourceKit-LSP don’t recogize a dictionary as enabling a capability) 3. Expanding the options to `"workspace/peekDocuments": { "supported" true, "supportsLocation": true }` to communicate that location-based peeking is supported This pattern will also support experimental capability changes like this in the future. Use cases that might lack because of this are: 1. Using an old Swift 6.3 toolchain that doesn’t support dictionary-based capabilities with the VS Code Swift extension. In that case macro expansion will work in the fallback mode that we have for other editors and active editor tracking (which cancels target preparation when a file is switched) is disabled. I think this is acceptable. 2. If there are other editors that support these experimental capabilities, they will continue to work just fine by passing the boolen value to enable the option. If they want to support one of the options (eg. location-based peeking), they will need to switch to dictinary-based enabling and thus check the SourceKit-LSP version prior to startup. This might be mildly annoying on their side but since the number of client that support these capabilities should be very small (I am not aware of any) and they need to explicitly opt-in to the new behavior, I think this is also acceptable. --- Contributor Documentation/LSP Extensions.md | 8 ++++++++ Sources/SourceKitLSP/CapabilityRegistry.swift | 13 ++++++++++++- Sources/SourceKitLSP/SourceKitLSPServer.swift | 14 ++++++++------ Sources/SourceKitLSP/Swift/OpenInterface.swift | 4 +--- Tests/SourceKitLSPTests/ExpandMacroTests.swift | 12 ++++++------ Tests/SourceKitLSPTests/SwiftInterfaceTests.swift | 8 ++++---- Tests/SourceKitLSPTests/WorkspaceTests.swift | 2 +- 7 files changed, 40 insertions(+), 21 deletions(-) diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md index bbc53fca7..27a37158d 100644 --- a/Contributor Documentation/LSP Extensions.md +++ b/Contributor Documentation/LSP Extensions.md @@ -2,6 +2,14 @@ SourceKit-LSP extends the LSP protocol in the following ways. +To enable some of these extensions, the client needs to communicate that it can support them. To do so, it should pass a dictionary for the `capabilities.experimental` field in the `initialize` request. For each capability to enable, it should pass an entry as follows. + +```json +"": { + "supported": true +} +``` + ## `PublishDiagnosticsClientCapabilities` Added field (this is an extension from clangd that SourceKit-LSP re-exposes): diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index 4b44e1a20..118758c1e 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -103,7 +103,18 @@ package final actor CapabilityRegistry { guard case .dictionary(let experimentalCapabilities) = clientCapabilities.experimental else { return false } - return experimentalCapabilities[name] == .bool(true) + // Before Swift 6.3 we expected experimental client capabilities to be passed as `"capabilityName": true`. + // This proved to be insufficient for experimental capabilities that evolved over time. Since 6.3 we encourage + // clients to pass experimental capabilities as `"capabilityName": { "supported": true }`, which allows the addition + // of more configuration parameters to the capability. + switch experimentalCapabilities[name] { + case .bool(true): + return true + case .dictionary(let dict): + return dict["supported"] == .bool(true) + default: + return false + } } // MARK: Initializer diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 1dd4aee1b..a684d6d38 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -938,12 +938,14 @@ extension SourceKitLSPServer { guard let experimentalCapability = initializationOptions[capabilityName] else { continue } - if case .dictionary(var experimentalCapabilities) = clientCapabilities.experimental { - experimentalCapabilities[capabilityName] = experimentalCapability - clientCapabilities.experimental = .dictionary(experimentalCapabilities) - } else { - clientCapabilities.experimental = .dictionary([capabilityName: experimentalCapability]) - } + var experimentalCapabilities: [String: LSPAny] = + if case .dictionary(let experimentalCapabilities) = clientCapabilities.experimental { + experimentalCapabilities + } else { + [:] + } + experimentalCapabilities[capabilityName] = experimentalCapability + clientCapabilities.experimental = .dictionary(experimentalCapabilities) } // The client announces what CodeLenses it supports, and the LSP will only return diff --git a/Sources/SourceKitLSP/Swift/OpenInterface.swift b/Sources/SourceKitLSP/Swift/OpenInterface.swift index 945c322b2..973643712 100644 --- a/Sources/SourceKitLSP/Swift/OpenInterface.swift +++ b/Sources/SourceKitLSP/Swift/OpenInterface.swift @@ -36,9 +36,7 @@ extension SwiftLanguageService { nil } - if case .dictionary(let experimentalCapabilities) = self.capabilityRegistry.clientCapabilities.experimental, - case .bool(true) = experimentalCapabilities["workspace/getReferenceDocument"] - { + if self.capabilityRegistry.clientHasExperimentalCapability(GetReferenceDocumentRequest.method) { return GeneratedInterfaceDetails(uri: try urlData.uri, position: position) } let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent(urlData.displayName) diff --git a/Tests/SourceKitLSPTests/ExpandMacroTests.swift b/Tests/SourceKitLSPTests/ExpandMacroTests.swift index 429c88b77..1679f1b57 100644 --- a/Tests/SourceKitLSPTests/ExpandMacroTests.swift +++ b/Tests/SourceKitLSPTests/ExpandMacroTests.swift @@ -79,8 +79,8 @@ final class ExpandMacroTests: XCTestCase { files: files, manifest: SwiftPMTestProject.macroPackageManifest, capabilities: ClientCapabilities(experimental: [ - "workspace/peekDocuments": .bool(peekDocuments), - "workspace/getReferenceDocument": .bool(getReferenceDocument), + PeekDocumentsRequest.method: .dictionary(["supported": .bool(peekDocuments)]), + GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(getReferenceDocument)]), ]), options: SourceKitLSPOptions.testDefault(), enableBackgroundIndexing: true @@ -268,8 +268,8 @@ final class ExpandMacroTests: XCTestCase { files: files, manifest: SwiftPMTestProject.macroPackageManifest, capabilities: ClientCapabilities(experimental: [ - PeekDocumentsRequest.method: .bool(peekDocuments), - GetReferenceDocumentRequest.method: .bool(getReferenceDocument), + PeekDocumentsRequest.method: .dictionary(["supported": .bool(peekDocuments)]), + GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(getReferenceDocument)]), ]), options: SourceKitLSPOptions.testDefault(), enableBackgroundIndexing: true @@ -464,8 +464,8 @@ final class ExpandMacroTests: XCTestCase { files: files, manifest: SwiftPMTestProject.macroPackageManifest, capabilities: ClientCapabilities(experimental: [ - "workspace/peekDocuments": .bool(true), - "workspace/getReferenceDocument": .bool(true), + PeekDocumentsRequest.method: .dictionary(["supported": .bool(true)]), + GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(true)]), ]), options: SourceKitLSPOptions.testDefault(), enableBackgroundIndexing: true diff --git a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift index e0eba1ce2..4997c5942 100644 --- a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift +++ b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift @@ -45,7 +45,7 @@ final class SwiftInterfaceTests: XCTestCase { func testSystemModuleInterfaceReferenceDocument() async throws { let testClient = try await TestSourceKitLSPClient( capabilities: ClientCapabilities(experimental: [ - "workspace/getReferenceDocument": .bool(true) + GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(true)]) ]) ) let uri = DocumentURI(for: .swift) @@ -117,7 +117,7 @@ final class SwiftInterfaceTests: XCTestCase { } """, capabilities: ClientCapabilities(experimental: [ - "workspace/getReferenceDocument": .bool(true) + GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(true)]) ]), indexSystemModules: true ) @@ -210,7 +210,7 @@ final class SwiftInterfaceTests: XCTestCase { ) """, capabilities: ClientCapabilities(experimental: [ - "workspace/getReferenceDocument": .bool(true) + GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(true)]) ]), enableBackgroundIndexing: true ) @@ -292,7 +292,7 @@ final class SwiftInterfaceTests: XCTestCase { func testNoDiagnosticsInGeneratedInterface() async throws { let testClient = try await TestSourceKitLSPClient( capabilities: ClientCapabilities(experimental: [ - "workspace/getReferenceDocument": .bool(true) + GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(true)]) ]) ) let uri = DocumentURI(for: .swift) diff --git a/Tests/SourceKitLSPTests/WorkspaceTests.swift b/Tests/SourceKitLSPTests/WorkspaceTests.swift index c2c76e562..d3d72f60c 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTests.swift @@ -1125,7 +1125,7 @@ final class WorkspaceTests: XCTestCase { ) """, capabilities: ClientCapabilities(experimental: [ - DidChangeActiveDocumentNotification.method: .bool(true) + DidChangeActiveDocumentNotification.method: .dictionary(["supported": .bool(true)]) ]), hooks: Hooks( indexHooks: IndexHooks(preparationTaskDidStart: { task in