diff --git a/Package.resolved b/Package.resolved index 4679d48..cbb1f1e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,70 +1,67 @@ { - "object": { - "pins": [ - { - "package": "FSEventsWrapper", - "repositoryURL": "https://github.com/Frizlab/FSEventsWrapper", - "state": { - "branch": null, - "revision": "70bbea4b108221fcabfce8dbced8502831c0ae04", - "version": "2.1.0" - } - }, - { - "package": "GlobPattern", - "repositoryURL": "https://github.com/ChimeHQ/GlobPattern", - "state": { - "branch": null, - "revision": "4ebb9e89e07cc475efa74f87dc6d21f4a9e060f8", - "version": "0.1.1" - } - }, - { - "package": "JSONRPC", - "repositoryURL": "https://github.com/ChimeHQ/JSONRPC", - "state": { - "branch": null, - "revision": "5f48cfdc1c4ce70cd50d996a95ee60d26aa72fee", - "version": "0.8.0" - } - }, - { - "package": "LanguageServerProtocol", - "repositoryURL": "https://github.com/ChimeHQ/LanguageServerProtocol", - "state": { - "branch": null, - "revision": "a244efe43ac7a42577b4afd9ccc43b5223c7f18d", - "version": "0.10.0" - } - }, - { - "package": "ProcessEnv", - "repositoryURL": "https://github.com/ChimeHQ/ProcessEnv", - "state": { - "branch": null, - "revision": "83f1ebc9dd6fb1db0bd89a3fcae00488a0f3fdd9", - "version": "1.0.0" - } - }, - { - "package": "Queue", - "repositoryURL": "https://github.com/mattmassicotte/Queue", - "state": { - "branch": null, - "revision": "8d6f936097888f97011610ced40313655dc5948d", - "version": "0.1.4" - } - }, - { - "package": "Semaphore", - "repositoryURL": "https://github.com/groue/Semaphore", - "state": { - "branch": null, - "revision": "f1c4a0acabeb591068dea6cffdd39660b86dec28", - "version": "0.0.8" - } + "pins" : [ + { + "identity" : "fseventswrapper", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Frizlab/FSEventsWrapper", + "state" : { + "revision" : "70bbea4b108221fcabfce8dbced8502831c0ae04", + "version" : "2.1.0" } - ] - }, - "version": 1 + }, + { + "identity" : "globpattern", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/GlobPattern", + "state" : { + "revision" : "4ebb9e89e07cc475efa74f87dc6d21f4a9e060f8", + "version" : "0.1.1" + } + }, + { + "identity" : "jsonrpc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/JSONRPC", + "state" : { + "revision" : "c6ec759d41a76ac88fe7327c41a77d9033943374", + "version" : "0.9.0" + } + }, + { + "identity" : "languageserverprotocol", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", + "state" : { + "revision" : "a355005e6c00775bb567e1d5927a4d57423474c9" + } + }, + { + "identity" : "processenv", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/ProcessEnv", + "state" : { + "revision" : "83f1ebc9dd6fb1db0bd89a3fcae00488a0f3fdd9", + "version" : "1.0.0" + } + }, + { + "identity" : "queue", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattmassicotte/Queue", + "state" : { + "revision" : "8d6f936097888f97011610ced40313655dc5948d", + "version" : "0.1.4" + } + }, + { + "identity" : "semaphore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/Semaphore", + "state" : { + "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", + "version" : "0.0.8" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index ae90855..78f71d2 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.9 import PackageDescription @@ -15,10 +15,10 @@ let package = Package( targets: ["LanguageClient"]), ], dependencies: [ - .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", from: "0.10.0"), + .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", from: "0.11.0"), .package(url: "https://github.com/Frizlab/FSEventsWrapper", from: "2.1.0"), .package(url: "https://github.com/ChimeHQ/GlobPattern", from: "0.1.1"), - .package(url: "https://github.com/ChimeHQ/JSONRPC", from: "0.8.0"), + .package(url: "https://github.com/ChimeHQ/JSONRPC", from: "0.9.0"), .package(url: "https://github.com/ChimeHQ/ProcessEnv", from: "1.0.0"), .package(url: "https://github.com/groue/Semaphore", from: "0.0.8"), .package(url: "https://github.com/mattmassicotte/Queue", from: "0.1.4"), @@ -30,7 +30,8 @@ let package = Package( .product(name: "FSEventsWrapper", package: "FSEventsWrapper", condition: .when(platforms: [.macOS])), .product(name: "GlobPattern", package: "GlobPattern", condition: .when(platforms: [.macOS])), "JSONRPC", - "LanguageServerProtocol", + .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), + .product(name: "LSPClient", package: "LanguageServerProtocol"), .product(name: "ProcessEnv", package: "ProcessEnv", condition: .when(platforms: [.macOS])), "Queue", "Semaphore", diff --git a/README.md b/README.md index fbeefd1..a695fee 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,13 @@ let server = JSONRPCServer(dataChannel: channel) ```swift import LanguageClient import LanguageServerProtocol +import LSPClient +import Foundation let executionParams = Process.ExecutionParameters(path: "/usr/bin/sourcekit-lsp", environment: ProcessInfo.processInfo.userEnvironment) let channel = DataChannel.localProcessChannel(parameters: executionParams, terminationHandler: { print("terminated") }) -let localServer = JSONRPCServer(dataChannel: channel) +let localServer = JSONRPCServerConnection(dataChannel: channel) let provider: InitializingServer.InitializeParamsProvider = { // you may need to fill in more of the textDocument field for completions @@ -45,14 +47,14 @@ let provider: InitializingServer.InitializeParamsProvider = { window: nil, general: nil, experimental: nil) - + // pay careful attention to rootPath/rootURI/workspaceFolders, as different servers will // have different expectations/requirements here - + return InitializeParams(processId: Int(ProcessInfo.processInfo.processIdentifier), locale: nil, rootPath: nil, - rootURI: projectURL.absoluteString, + rootUri: projectURL.absoluteString, initializationOptions: nil, capabilities: capabilities, trace: nil, @@ -72,7 +74,7 @@ Task { text: docContent) let docParams = DidOpenTextDocumentParams(textDocument: doc) - try await server.didOpenTextDocument(params: docParams) + try await server.textDocumentDidOpen(params: docParams) // make sure to pick a reasonable position within your test document let pos = Position(line: 5, character: 25) diff --git a/Sources/LanguageClient/AsyncStreamTap.swift b/Sources/LanguageClient/AsyncStreamTap.swift index 1f158c3..33b4e23 100644 --- a/Sources/LanguageClient/AsyncStreamTap.swift +++ b/Sources/LanguageClient/AsyncStreamTap.swift @@ -1,6 +1,5 @@ import Foundation -#if compiler(>=5.9) /// Maintains a consistent external `AsyncStream` as interal source streams are changed. public actor AsyncStreamTap { public typealias Stream = AsyncStream @@ -30,4 +29,3 @@ public actor AsyncStreamTap { } } } -#endif diff --git a/Sources/LanguageClient/DataChannel+LocalProcess.swift b/Sources/LanguageClient/DataChannel+LocalProcess.swift index 744b4a2..10c8279 100644 --- a/Sources/LanguageClient/DataChannel+LocalProcess.swift +++ b/Sources/LanguageClient/DataChannel+LocalProcess.swift @@ -5,28 +5,6 @@ import JSONRPC #if canImport(ProcessEnv) import ProcessEnv -#if compiler(>=5.9) - -extension FileHandle { - public var dataStream: AsyncStream { - let (stream, continuation) = AsyncStream.makeStream() - - readabilityHandler = { handle in - let data = handle.availableData - - if data.isEmpty { - handle.readabilityHandler = nil - continuation.finish() - return - } - - continuation.yield(data) - } - - return stream - } -} - extension DataChannel { @available(macOS 12.0, *) public static func localProcessChannel( @@ -54,10 +32,8 @@ extension DataChannel { Task { let dataStream = stdoutPipe.fileHandleForReading.dataStream - let byteStream = AsyncByteSequence(base: dataStream) - let framedData = AsyncMessageFramingSequence(base: byteStream) - for try await data in framedData { + for try await data in dataStream { continuation.yield(data) } @@ -85,6 +61,5 @@ extension DataChannel { return DataChannel(writeHandler: handler, dataSequence: stream) } } -#endif #endif diff --git a/Sources/LanguageClient/DataChannel+UserScript.swift b/Sources/LanguageClient/DataChannel+UserScript.swift index 2163acb..10d866c 100644 --- a/Sources/LanguageClient/DataChannel+UserScript.swift +++ b/Sources/LanguageClient/DataChannel+UserScript.swift @@ -2,19 +2,16 @@ import Foundation import LanguageServerProtocol import JSONRPC - -#if canImport(ProcessEnv) -import ProcessEnv - -#if compiler(>=5.9) - +#if os(macOS) /// The user script directory for this app. /// @available(macOS 12.0, *) -private let userScriptDirectory = try? FileManager.default.url(for: .applicationScriptsDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: false) +private let userScriptDirectory = try? FileManager.default.url( + for: .applicationScriptsDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false +) extension DataChannel { @@ -26,11 +23,11 @@ extension DataChannel { /// - terminationHandler: Termination handler to invoke when the user script terminates. /// @available(macOS 12.0, *) - public static func userScriptChannel(scriptPath: String, - arguments: [String] = [], - terminationHandler: @escaping @Sendable () -> Void) - throws -> DataChannel - { + public static func userScriptChannel( + scriptPath: String, + arguments: [String] = [], + terminationHandler: @escaping @Sendable () -> Void + ) throws -> DataChannel { guard let scriptURL = userScriptDirectory?.appendingPathComponent(scriptPath) else { throw CocoaError(.fileNoSuchFile) } @@ -45,10 +42,8 @@ extension DataChannel { // Forward stdout to the data channel Task { let dataStream = stdoutPipe.fileHandleForReading.dataStream - let byteStream = AsyncByteSequence(base: dataStream) - let framedData = AsyncMessageFramingSequence(base: byteStream) - for try await data in framedData { + for try await data in dataStream { continuation.yield(data) } @@ -91,5 +86,3 @@ extension DataChannel { } #endif - -#endif diff --git a/Sources/LanguageClient/FileEventAsyncSequence.swift b/Sources/LanguageClient/FileEventAsyncSequence.swift index 7aaae69..cb7fde2 100644 --- a/Sources/LanguageClient/FileEventAsyncSequence.swift +++ b/Sources/LanguageClient/FileEventAsyncSequence.swift @@ -61,9 +61,9 @@ extension FileChangeType { } } +#if compiler(>=5.9) public struct FileEventAsyncSequence: AsyncSequence { public typealias Element = FileEvent - public struct FileEventAsyncIterator: AsyncIteratorProtocol { private let stream: AsyncCompactMapSequence private var internalIterator: AsyncCompactMapSequence.Iterator @@ -111,6 +111,8 @@ public struct FileEventAsyncSequence: AsyncSequence { public func makeAsyncIterator() -> FileEventAsyncIterator { FileEventAsyncIterator(root: root.path, kind: kind, pattern: pattern, filterInProcessChanges: filterInProcessChanges) } + } +#endif #endif diff --git a/Sources/LanguageClient/FileHandle+DataStream.swift b/Sources/LanguageClient/FileHandle+DataStream.swift new file mode 100644 index 0000000..fe4fb36 --- /dev/null +++ b/Sources/LanguageClient/FileHandle+DataStream.swift @@ -0,0 +1,21 @@ +import Foundation + +extension FileHandle { + public var dataStream: AsyncStream { + let (stream, continuation) = AsyncStream.makeStream() + + readabilityHandler = { handle in + let data = handle.availableData + + if data.isEmpty { + handle.readabilityHandler = nil + continuation.finish() + return + } + + continuation.yield(data) + } + + return stream + } +} diff --git a/Sources/LanguageClient/InitializingServer.swift b/Sources/LanguageClient/InitializingServer.swift index c6d5c3d..b2f1e06 100644 --- a/Sources/LanguageClient/InitializingServer.swift +++ b/Sources/LanguageClient/InitializingServer.swift @@ -5,6 +5,7 @@ import os.log import Semaphore import LanguageServerProtocol +import LSPClient enum InitializingServerError: Error { case noStateProvider @@ -12,7 +13,6 @@ enum InitializingServerError: Error { case stateInvalid } -#if compiler(>=5.9) /// Server implementation that lazily initializes another Server on first message. /// /// Provides special handling for `shutdown` and `exit` messages. @@ -50,20 +50,18 @@ public actor InitializingServer { } } - private let channel: Server + private let channel: ServerConnection private var state = State.uninitialized private let semaphore = AsyncSemaphore(value: 1) - private let requestStreamTap = AsyncStreamTap() + private let eventStreamTap = AsyncStreamTap() private let initializeParamsProvider: InitializeParamsProvider private let capabilitiesContinuation: StatefulServer.CapabilitiesSequence.Continuation - public let notificationSequence: NotificationSequence public let capabilitiesSequence: CapabilitiesSequence - public init(server: Server, initializeParamsProvider: @escaping InitializeParamsProvider) { + public init(server: ServerConnection, initializeParamsProvider: @escaping InitializeParamsProvider) { self.channel = server self.initializeParamsProvider = initializeParamsProvider - self.notificationSequence = channel.notificationSequence (self.capabilitiesSequence, self.capabilitiesContinuation) = CapabilitiesSequence.makeStream() Task { @@ -76,8 +74,8 @@ public actor InitializingServer { } private func startMonitoringServer() async { - await requestStreamTap.setInputStream(channel.requestSequence) { [weak self] in - await self?.handleRequest($0) + await eventStreamTap.setInputStream(channel.eventSequence) { [weak self] in + await self?.handleEvent($0) } } @@ -123,8 +121,8 @@ extension InitializingServer: StatefulServer { self.state = .uninitialized } - public nonisolated var requestSequence: RequestSequence { - requestStreamTap.stream + public nonisolated var eventSequence: EventSequence { + eventStreamTap.stream } public func sendNotification(_ notif: LanguageServerProtocol.ClientNotification) async throws { @@ -185,6 +183,15 @@ extension InitializingServer { return caps } + private func handleEvent(_ event: ServerEvent) { + switch event { + case let .request(_, request): + handleRequest(request) + default: + break + } + } + private func handleRequest(_ request: ServerRequest) { guard case .initialized(let caps) = self.state else { fatalError("received a request without being initialized") @@ -212,4 +219,3 @@ extension InitializingServer { } } } -#endif diff --git a/Sources/LanguageClient/RestartingServer.swift b/Sources/LanguageClient/RestartingServer.swift index 9191698..986a326 100644 --- a/Sources/LanguageClient/RestartingServer.swift +++ b/Sources/LanguageClient/RestartingServer.swift @@ -10,17 +10,17 @@ let NSEC_PER_SEC: UInt64 = 1000000000 import Semaphore import LanguageServerProtocol +import LSPClient -enum RestartingServerError: Error { +public enum RestartingServerError: Error { case noProvider case serverStopped case noURIMatch(DocumentUri) case noTextDocumentForURI(DocumentUri) } -#if compiler(>=5.9) /// A `Server` wrapper that provides transparent server-side state restoration should the underlying process crash. -public actor RestartingServer { +public actor RestartingServer { public typealias ServerProvider = @Sendable () async throws -> WrappedServer public typealias TextDocumentItemProvider = @Sendable (DocumentUri) async throws -> TextDocumentItem public typealias InitializeParamsProvider = InitializingServer.InitializeParamsProvider @@ -63,8 +63,7 @@ public actor RestartingServer { private let logger = Logger(subsystem: "com.chimehq.LanguageClient", category: "RestartingServer") #endif - private let requestStreamTap = AsyncStreamTap() - private let notificationStreamTap = AsyncStreamTap() + private let eventStreamTap = AsyncStreamTap() private let capabilitiesStreamTap = AsyncStreamTap() public init(configuration: Configuration) { @@ -128,7 +127,7 @@ public actor RestartingServer { let params = DidOpenTextDocumentParams(textDocument: item) - try await server.didOpenTextDocument(params: params) + try await server.textDocumentDidOpen(params: params) } catch { #if canImport(OSLog) logger.error("Failed to reopen document \(uri, privacy: .public): \(error, privacy: .public)") @@ -140,8 +139,7 @@ public actor RestartingServer { } private func startMonitoringServer(_ server: InitializingServer) async { - await requestStreamTap.setInputStream(server.requestSequence) - await notificationStreamTap.setInputStream(server.notificationSequence) + await eventStreamTap.setInputStream(server.eventSequence) await capabilitiesStreamTap.setInputStream(server.capabilitiesSequence) } @@ -204,9 +202,9 @@ public actor RestartingServer { private func processOutboundNotification(_ notification: ClientNotification) { switch notification { - case .didOpenTextDocument(let params): + case .textDocumentDidOpen(let params): self.handleDidOpen(params) - case .didCloseTextDocument(let params): + case .textDocumentDidClose(let params): self.handleDidClose(params) default: break @@ -244,12 +242,8 @@ extension RestartingServer: StatefulServer { self.state = .notStarted } - public nonisolated var notificationSequence: NotificationSequence { - notificationStreamTap.stream - } - - public nonisolated var requestSequence: RequestSequence { - requestStreamTap.stream + public nonisolated var eventSequence: EventSequence { + eventStreamTap.stream } public nonisolated var capabilitiesSequence: CapabilitiesSequence { @@ -279,4 +273,3 @@ extension RestartingServer: StatefulServer { return try await server.sendRequest(request) } } -#endif diff --git a/Sources/LanguageClient/Server+Shutdown.swift b/Sources/LanguageClient/Server+Shutdown.swift index 15beb73..ce4e7c6 100644 --- a/Sources/LanguageClient/Server+Shutdown.swift +++ b/Sources/LanguageClient/Server+Shutdown.swift @@ -1,8 +1,9 @@ import Foundation import LanguageServerProtocol +import LSPClient -extension Server { +extension ServerConnection { /// This function will always attempt to decode "null". /// /// We don't know the generic type of the return. So, we have to emulate. diff --git a/Sources/LanguageClient/StatefulServer.swift b/Sources/LanguageClient/StatefulServer.swift index 7e20348..8f0d28f 100644 --- a/Sources/LanguageClient/StatefulServer.swift +++ b/Sources/LanguageClient/StatefulServer.swift @@ -1,14 +1,15 @@ import Foundation import LanguageServerProtocol +import LSPClient -extension Server { +extension ServerConnection { public typealias CapabilitiesSequence = AsyncStream } /// An extension of `Server` that provides access to server state. -protocol StatefulServer: Server { - var capabilitiesSequence: Server.CapabilitiesSequence { get } +protocol StatefulServer: ServerConnection { + var capabilitiesSequence: ServerConnection.CapabilitiesSequence { get } func shutdownAndExit() async throws func connectionInvalidated() async diff --git a/Tests/LanguageClientTests/ServerTests.swift b/Tests/LanguageClientTests/ServerTests.swift index c3716a5..88913bd 100644 --- a/Tests/LanguageClientTests/ServerTests.swift +++ b/Tests/LanguageClientTests/ServerTests.swift @@ -1,5 +1,6 @@ import XCTest import LanguageServerProtocol +import LSPClient import LanguageClient enum ServerTestError: Error { @@ -43,9 +44,9 @@ final class ServerTests: XCTestCase { let messages = await mockChannel.finishSession() XCTAssertEqual(messages, [ - .request(.initialize(Self.initParams)), + .request(.initialize(Self.initParams, ClientRequest.NullHandler)), .notification(.initialized(InitializedParams())), - .request(.hover(params)), + .request(.hover(params, ClientRequest.NullHandler)), ]) XCTAssertEqual(response?.range, LSPRange(startPair: (0, 0), endPair: (0, 1)))