diff --git a/Documentation/Configuration File.md b/Documentation/Configuration File.md index e27dd770..7c3b6a4a 100644 --- a/Documentation/Configuration File.md +++ b/Documentation/Configuration File.md @@ -4,6 +4,7 @@ - `~/.sourcekit-lsp/config.json` - On macOS: `~/Library/Application Support/org.swift.sourcekit-lsp/config.json` from the various `Library` folders on the system - If the `XDG_CONFIG_HOME` environment variable is set: `$XDG_CONFIG_HOME/org.swift.sourcekit-lsp/config.json` +- Initialization options passed in the initialize request - A `.sourcekit-lsp/config.json` file in a workspace’s root The structure of the file is currently not guaranteed to be stable. Options may be removed or renamed. diff --git a/Sources/SKCore/SourceKitLSPOptions.swift b/Sources/SKCore/SourceKitLSPOptions.swift index cba4e327..0f9da813 100644 --- a/Sources/SKCore/SourceKitLSPOptions.swift +++ b/Sources/SKCore/SourceKitLSPOptions.swift @@ -12,7 +12,7 @@ import Foundation import LSPLogging -import SKCore +import LanguageServerProtocol import SKSupport import struct TSCBasic.AbsolutePath @@ -250,6 +250,21 @@ public struct SourceKitLSPOptions: Sendable, Codable { self.workDoneProgressDebounceDuration = workDoneProgressDebounceDuration } + public init?(fromLSPAny lspAny: LSPAny?) throws { + guard let lspAny else { + return nil + } + let jsonEncoded = try JSONEncoder().encode(lspAny) + self = try JSONDecoder().decode(Self.self, from: jsonEncoded) + } + + public var asLSPAny: LSPAny { + get throws { + let jsonEncoded = try JSONEncoder().encode(self) + return try JSONDecoder().decode(LSPAny.self, from: jsonEncoded) + } + } + public init?(path: URL?) { guard let path, let contents = try? String(contentsOf: path, encoding: .utf8) else { return nil diff --git a/Sources/SKTestSupport/MultiFileTestProject.swift b/Sources/SKTestSupport/MultiFileTestProject.swift index b25fa27f..5303cf46 100644 --- a/Sources/SKTestSupport/MultiFileTestProject.swift +++ b/Sources/SKTestSupport/MultiFileTestProject.swift @@ -80,6 +80,7 @@ public class MultiFileTestProject { public init( files: [RelativeFileLocation: String], workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] }, + initializationOptions: LSPAny? = nil, capabilities: ClientCapabilities = ClientCapabilities(), options: SourceKitLSPOptions = .testDefault(), testHooks: TestHooks = TestHooks(), @@ -118,6 +119,7 @@ public class MultiFileTestProject { self.testClient = try await TestSourceKitLSPClient( options: options, testHooks: testHooks, + initializationOptions: initializationOptions, capabilities: capabilities, usePullDiagnostics: usePullDiagnostics, enableBackgroundIndexing: enableBackgroundIndexing, diff --git a/Sources/SKTestSupport/SwiftPMTestProject.swift b/Sources/SKTestSupport/SwiftPMTestProject.swift index d481cf63..5ffdb410 100644 --- a/Sources/SKTestSupport/SwiftPMTestProject.swift +++ b/Sources/SKTestSupport/SwiftPMTestProject.swift @@ -150,6 +150,7 @@ public class SwiftPMTestProject: MultiFileTestProject { files: [RelativeFileLocation: String], manifest: String = SwiftPMTestProject.defaultPackageManifest, workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] }, + initializationOptions: LSPAny? = nil, capabilities: ClientCapabilities = ClientCapabilities(), options: SourceKitLSPOptions = .testDefault(), testHooks: TestHooks = TestHooks(), @@ -190,6 +191,7 @@ public class SwiftPMTestProject: MultiFileTestProject { try await super.init( files: filesByPath, workspaces: workspaces, + initializationOptions: initializationOptions, capabilities: capabilities, options: options, testHooks: testHooks, diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 2e218002..3c8148bb 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -104,7 +104,7 @@ public actor SourceKitLSPServer { /// This ensures that we only inform the user about background indexing not being supported for these projects once. private var didSendBackgroundIndexingNotSupportedNotification = false - let options: SourceKitLSPOptions + var options: SourceKitLSPOptions let testHooks: TestHooks @@ -959,6 +959,10 @@ extension SourceKitLSPServer { func initialize(_ req: InitializeRequest) async throws -> InitializeResult { capabilityRegistry = CapabilityRegistry(clientCapabilities: req.capabilities) + self.options = SourceKitLSPOptions.merging( + base: self.options, + override: orLog("Parsing SourceKitLSPOptions", { try SourceKitLSPOptions(fromLSPAny: req.initializationOptions) }) + ) await workspaceQueue.async { [testHooks] in if let workspaceFolders = req.workspaceFolders { @@ -983,12 +987,13 @@ extension SourceKitLSPServer { if self.workspaces.isEmpty { logger.error("No workspace found") + let options = self.options let workspace = await Workspace( documentManager: self.documentManager, rootUri: req.rootURI, capabilityRegistry: self.capabilityRegistry!, toolchainRegistry: self.toolchainRegistry, - options: self.options, + options: options, testHooks: testHooks, underlyingBuildSystem: nil, index: nil, diff --git a/Tests/SourceKitLSPTests/WorkspaceTests.swift b/Tests/SourceKitLSPTests/WorkspaceTests.swift index f5e3c3c6..5542c44f 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTests.swift @@ -861,4 +861,68 @@ final class WorkspaceTests: XCTestCase { } XCTAssertEqual(diagnostics.items.map(\.message), ["Cannot convert value of type 'Int' to specified type 'String'"]) } + + func testOptionsInInitializeRequest() async throws { + let project = try await SwiftPMTestProject( + files: [ + "Test.swift": """ + func test() { + #if TEST + let x: String = 1 + #endif + } + """ + ], + initializationOptions: SourceKitLSPOptions( + swiftPM: SourceKitLSPOptions.SwiftPMOptions(swiftCompilerFlags: ["-D", "TEST"]) + ).asLSPAny + ) + + let (uri, _) = try project.openDocument("Test.swift") + let diagnostics = try await project.testClient.send( + DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri)) + ) + guard case .full(let diagnostics) = diagnostics else { + XCTFail("Expected full diagnostics") + return + } + XCTAssertEqual(diagnostics.items.map(\.message), ["Cannot convert value of type 'Int' to specified type 'String'"]) + } + + func testWorkspaceOptionsOverrideGlobalOptions() async throws { + let project = try await SwiftPMTestProject( + files: [ + "/.sourcekit-lsp/config.json": """ + { + "swiftPM": { + "swiftCompilerFlags": ["-D", "TEST"] + } + } + """, + "Test.swift": """ + func test() { + #if TEST + let x: String = 1 + #endif + #if OTHER + let x: String = 1.0 + #endif + } + """, + ], + initializationOptions: SourceKitLSPOptions( + swiftPM: SourceKitLSPOptions.SwiftPMOptions(swiftCompilerFlags: ["-D", "OTHER"]) + ).asLSPAny + ) + + let (uri, _) = try project.openDocument("Test.swift") + let diagnostics = try await project.testClient.send( + DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri)) + ) + guard case .full(let diagnostics) = diagnostics else { + XCTFail("Expected full diagnostics") + return + } + XCTAssertEqual(diagnostics.items.map(\.message), ["Cannot convert value of type 'Int' to specified type 'String'"]) + } }