From 3c725681afaaa951b6a08f4fb616f07384409552 Mon Sep 17 00:00:00 2001 From: Steffeeen Date: Sun, 24 May 2026 16:26:19 +0200 Subject: [PATCH] Fix RegistrationOptions encoding to correctly merge objects The two members of the different `RegistrationOptions` should be encoded into a single object which contains the members of each of the two members. --- .../SupportTypes/LSPAny+Coding.swift | 18 +- .../SupportTypes/RegistrationOptions.swift | 72 ++++- .../CodingTests.swift | 255 ++++++++++++++++++ 3 files changed, 342 insertions(+), 3 deletions(-) diff --git a/Sources/LanguageServerProtocol/SupportTypes/LSPAny+Coding.swift b/Sources/LanguageServerProtocol/SupportTypes/LSPAny+Coding.swift index 0b2158408..d8c6892ea 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/LSPAny+Coding.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/LSPAny+Coding.swift @@ -80,7 +80,14 @@ private final class LSPAnyEncoder: Encoder { // MARK: - .keyed func prepareKeyed() { - storage = .keyed([:]) + switch storage { + case nil: + storage = .keyed([:]) + case .keyed?: + break + case .single?, .unkeyed?: + preconditionFailure("cannot create keyed container after encoding a non-keyed value") + } } func set(key: String, value: LSPAnyReference) { guard case .keyed(var dictionary)? = storage else { @@ -94,7 +101,14 @@ private final class LSPAnyEncoder: Encoder { // MARK: - .unkeyed func prepareUnkeyed() { - storage = .unkeyed([]) + switch storage { + case nil: + storage = .unkeyed([]) + case .unkeyed?: + break + case .single?, .keyed?: + preconditionFailure("cannot create unkeyed container after encoding a non-unkeyed value") + } } func append(value: LSPAnyReference) { guard case .unkeyed(var array)? = storage else { diff --git a/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift b/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift index c8cfe181e..daa64ea22 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift @@ -34,7 +34,7 @@ public protocol TextDocumentRegistrationOptionsProtocol { var textDocumentRegistrationOptions: TextDocumentRegistrationOptions { get } } -/// Code completiion registration options. +/// Code completion registration options. public struct CompletionRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol, Hashable { public var textDocumentRegistrationOptions: TextDocumentRegistrationOptions public var completionOptions: CompletionOptions @@ -44,6 +44,16 @@ public struct CompletionRegistrationOptions: RegistrationOptions, TextDocumentRe TextDocumentRegistrationOptions(documentSelector: documentSelector) self.completionOptions = completionOptions } + + public init(from decoder: Decoder) throws { + self.textDocumentRegistrationOptions = try TextDocumentRegistrationOptions(from: decoder) + self.completionOptions = try CompletionOptions(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try textDocumentRegistrationOptions.encode(to: encoder) + try completionOptions.encode(to: encoder) + } } /// Signature help registration options. @@ -56,6 +66,16 @@ public struct SignatureHelpRegistrationOptions: RegistrationOptions, TextDocumen TextDocumentRegistrationOptions(documentSelector: documentSelector) self.signatureHelpOptions = signatureHelpOptions } + + public init(from decoder: Decoder) throws { + self.textDocumentRegistrationOptions = try TextDocumentRegistrationOptions(from: decoder) + self.signatureHelpOptions = try SignatureHelpOptions(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try textDocumentRegistrationOptions.encode(to: encoder) + try signatureHelpOptions.encode(to: encoder) + } } /// Folding range registration options. @@ -68,6 +88,16 @@ public struct FoldingRangeRegistrationOptions: RegistrationOptions, TextDocument TextDocumentRegistrationOptions(documentSelector: documentSelector) self.foldingRangeOptions = foldingRangeOptions } + + public init(from decoder: Decoder) throws { + self.textDocumentRegistrationOptions = try TextDocumentRegistrationOptions(from: decoder) + self.foldingRangeOptions = try FoldingRangeOptions(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try textDocumentRegistrationOptions.encode(to: encoder) + try foldingRangeOptions.encode(to: encoder) + } } public struct SemanticTokensRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol, Hashable @@ -84,6 +114,16 @@ public struct SemanticTokensRegistrationOptions: RegistrationOptions, TextDocume TextDocumentRegistrationOptions(documentSelector: documentSelector) self.semanticTokenOptions = semanticTokenOptions } + + public init(from decoder: Decoder) throws { + self.textDocumentRegistrationOptions = try TextDocumentRegistrationOptions(from: decoder) + self.semanticTokenOptions = try SemanticTokensOptions(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try textDocumentRegistrationOptions.encode(to: encoder) + try semanticTokenOptions.encode(to: encoder) + } } public struct InlayHintRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol, Hashable { @@ -97,6 +137,16 @@ public struct InlayHintRegistrationOptions: RegistrationOptions, TextDocumentReg textDocumentRegistrationOptions = TextDocumentRegistrationOptions(documentSelector: documentSelector) self.inlayHintOptions = inlayHintOptions } + + public init(from decoder: Decoder) throws { + self.textDocumentRegistrationOptions = try TextDocumentRegistrationOptions(from: decoder) + self.inlayHintOptions = try InlayHintOptions(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try textDocumentRegistrationOptions.encode(to: encoder) + try inlayHintOptions.encode(to: encoder) + } } /// Describe options to be used when registering for pull diagnostics. Since LSP 3.17.0 @@ -111,6 +161,16 @@ public struct DiagnosticRegistrationOptions: RegistrationOptions, TextDocumentRe textDocumentRegistrationOptions = TextDocumentRegistrationOptions(documentSelector: documentSelector) self.diagnosticOptions = diagnosticOptions } + + public init(from decoder: Decoder) throws { + self.textDocumentRegistrationOptions = try TextDocumentRegistrationOptions(from: decoder) + self.diagnosticOptions = try DiagnosticOptions(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try textDocumentRegistrationOptions.encode(to: encoder) + try diagnosticOptions.encode(to: encoder) + } } /// Describe options to be used when registering for code lenses. @@ -125,6 +185,16 @@ public struct CodeLensRegistrationOptions: RegistrationOptions, TextDocumentRegi textDocumentRegistrationOptions = TextDocumentRegistrationOptions(documentSelector: documentSelector) self.codeLensOptions = codeLensOptions } + + public init(from decoder: Decoder) throws { + self.textDocumentRegistrationOptions = try TextDocumentRegistrationOptions(from: decoder) + self.codeLensOptions = try CodeLensOptions(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try textDocumentRegistrationOptions.encode(to: encoder) + try codeLensOptions.encode(to: encoder) + } } /// Describe options to be used when registering for file system change events. diff --git a/Tests/LanguageServerProtocolTests/CodingTests.swift b/Tests/LanguageServerProtocolTests/CodingTests.swift index 7137a8af6..0835e23b6 100644 --- a/Tests/LanguageServerProtocolTests/CodingTests.swift +++ b/Tests/LanguageServerProtocolTests/CodingTests.swift @@ -1392,6 +1392,261 @@ final class CodingTests: XCTestCase { func testShutdownResponse() { checkCoding(ShutdownRequest.Response(), json: "null") } + + func testRegistrationOptions() { + // Include the actual CapabilityRegistration here to ensure LSPAny encoding can merge the options correctly + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: CompletionRegistrationOptions( + documentSelector: [DocumentFilter(language: "swift")], + completionOptions: CompletionOptions(resolveProvider: true, triggerCharacters: [".", "("]) + ).encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "documentSelector" : [ + { + "language" : "swift" + } + ], + "resolveProvider" : true, + "triggerCharacters" : [ + ".", + "(" + ] + } + } + """ + ) + + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: SignatureHelpRegistrationOptions( + documentSelector: [DocumentFilter(language: "swift")], + signatureHelpOptions: SignatureHelpOptions(triggerCharacters: ["(", "["], retriggerCharacters: [",", ":"]) + ).encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "documentSelector" : [ + { + "language" : "swift" + } + ], + "retriggerCharacters" : [ + ",", + ":" + ], + "triggerCharacters" : [ + "(", + "[" + ] + } + } + """ + ) + + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: + FoldingRangeRegistrationOptions( + documentSelector: [DocumentFilter(language: "swift")], + foldingRangeOptions: FoldingRangeOptions() + ).encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "documentSelector" : [ + { + "language" : "swift" + } + ] + } + } + """ + ) + + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: + SemanticTokensRegistrationOptions( + documentSelector: [DocumentFilter(language: "swift")], + semanticTokenOptions: SemanticTokensOptions( + legend: SemanticTokensLegend( + tokenTypes: ["class", "function"], + tokenModifiers: ["declaration", "static"] + ), + range: .bool(true), + full: .bool(true) + ) + ).encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "documentSelector" : [ + { + "language" : "swift" + } + ], + "full" : true, + "legend" : { + "tokenModifiers" : [ + "declaration", + "static" + ], + "tokenTypes" : [ + "class", + "function" + ] + }, + "range" : true + } + } + """ + ) + + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: + InlayHintRegistrationOptions( + documentSelector: [DocumentFilter(language: "swift")], + inlayHintOptions: InlayHintOptions(resolveProvider: true) + ).encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "documentSelector" : [ + { + "language" : "swift" + } + ], + "resolveProvider" : true + } + } + """ + ) + + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: + DiagnosticRegistrationOptions( + documentSelector: [DocumentFilter(language: "swift")], + diagnosticOptions: DiagnosticOptions(interFileDependencies: true, workspaceDiagnostics: false) + ).encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "documentSelector" : [ + { + "language" : "swift" + } + ], + "interFileDependencies" : true, + "workspaceDiagnostics" : false + } + } + """ + ) + + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: + CodeLensRegistrationOptions( + documentSelector: [DocumentFilter(language: "swift")], + codeLensOptions: CodeLensOptions(resolveProvider: true) + ).encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "documentSelector" : [ + { + "language" : "swift" + } + ], + "resolveProvider" : true + } + } + """ + ) + + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: + DidChangeWatchedFilesRegistrationOptions(watchers: [FileSystemWatcher(globPattern: "**/*.swift")]) + .encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "watchers" : [ + { + "globPattern" : "**/*.swift" + } + ] + } + } + """ + ) + + checkCoding( + CapabilityRegistration( + id: "registration-id", + method: "test/method", + registerOptions: + ExecuteCommandRegistrationOptions(commands: ["sourcekit-lsp.test"]).encodeToLSPAny() + ), + json: """ + { + "id" : "registration-id", + "method" : "test/method", + "registerOptions" : { + "commands" : [ + "sourcekit-lsp.test" + ] + } + } + """ + ) + } } func with(_ value: T, mutate: (inout T) -> Void) -> T {