diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7edf22a..342ade1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Test platform ${{ matrix.destination }} - run: set -o pipefail && xcodebuild -scheme LanguageServerProtocol -destination "${{ matrix.destination }}" test | xcpretty + run: set -o pipefail && xcodebuild -scheme LanguageServerProtocol-Package -destination "${{ matrix.destination }}" test | xcpretty linux_test: name: Test Linux @@ -42,4 +42,4 @@ jobs: - name: Install the latest Swift toolchain run: swiftly install latest - name: Test - run: swift test \ No newline at end of file + run: swift test diff --git a/Package.resolved b/Package.resolved index 0fb1466..0423246 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,16 +1,14 @@ { - "object": { - "pins": [ - { - "package": "JSONRPC", - "repositoryURL": "https://github.com/ChimeHQ/JSONRPC", - "state": { - "branch": null, - "revision": "1ae27fceaec3208f62b3ef23d2d565d4222c3ac6", - "version": "0.8.1" - } + "pins" : [ + { + "identity" : "jsonrpc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/JSONRPC", + "state" : { + "revision" : "c6ec759d41a76ac88fe7327c41a77d9033943374", + "version" : "0.9.0" } - ] - }, - "version": 1 + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 663ad81..63dfa2b 100644 --- a/Package.swift +++ b/Package.swift @@ -9,17 +9,33 @@ let package = Package( .library( name: "LanguageServerProtocol", targets: ["LanguageServerProtocol"]), + + .library( + name: "LSPClient", + targets: ["LSPClient"]), + + .library( + name: "LSPServer", + targets: ["LSPServer"]), ], dependencies: [ - .package(url: "https://github.com/ChimeHQ/JSONRPC", "0.8.0"..<"0.9.0"), + .package(url: "https://github.com/ChimeHQ/JSONRPC", from: "0.9.0"), + ], targets: [ .target( name: "LanguageServerProtocol", dependencies: ["JSONRPC"]), + .target( + name: "LSPClient", + dependencies: ["LanguageServerProtocol"]), + .target( + name: "LSPServer", + dependencies: ["LanguageServerProtocol"]), + .testTarget( name: "LanguageServerProtocolTests", - dependencies: ["LanguageServerProtocol"]), + dependencies: ["LanguageServerProtocol", "LSPClient"]), ] ) diff --git a/Sources/LanguageServerProtocol/Additions/JSONRPCServer.swift b/Sources/LSPClient/Client.JSONRPCServerConnection.swift similarity index 64% rename from Sources/LanguageServerProtocol/Additions/JSONRPCServer.swift rename to Sources/LSPClient/Client.JSONRPCServerConnection.swift index b5270ff..679d14c 100644 --- a/Sources/LanguageServerProtocol/Additions/JSONRPCServer.swift +++ b/Sources/LSPClient/Client.JSONRPCServerConnection.swift @@ -1,79 +1,49 @@ import Foundation import JSONRPC +import LanguageServerProtocol -enum ServerError: Error { - case unrecognizedMethod(String) - case missingParams - case unhandledRegisterationMethod(String) - case missingReply -} - -public actor JSONRPCServer: Server { - public let notificationSequence: NotificationSequence - public let requestSequence: RequestSequence - private let notificationContinuation: NotificationSequence.Continuation - private let requestContinuation: RequestSequence.Continuation +public actor JSONRPCServerConnection: ServerConnection { + public let eventSequence: EventSequence + private let eventContinuation: EventSequence.Continuation + private var eventTask: Task? private let session: JSONRPCSession - private var notificationTask: Task? - private var requestTask: Task? + /// NOTE: The channel will wrapped with message framing public init(dataChannel: DataChannel) { - self.session = JSONRPCSession(channel: dataChannel) - - // this is annoying, but temporary -#if compiler(>=5.9) - (self.notificationSequence, self.notificationContinuation) = NotificationSequence.makeStream() - (self.requestSequence, self.requestContinuation) = RequestSequence.makeStream() -#else - var escapedNoteContinuation: NotificationSequence.Continuation? - - self.notificationSequence = NotificationSequence { escapedNoteContinuation = $0 } - self.notificationContinuation = escapedNoteContinuation! + self.session = JSONRPCSession(channel: dataChannel.withMessageFraming()) - var escapedRequestContinuation: RequestSequence.Continuation? + (self.eventSequence, self.eventContinuation) = EventSequence.makeStream() - self.requestSequence = RequestSequence { escapedRequestContinuation = $0 } - self.requestContinuation = escapedRequestContinuation! -#endif Task { await startMonitoringSession() } } deinit { - notificationTask?.cancel() - notificationContinuation.finish() - - requestTask?.cancel() - requestContinuation.finish() + eventTask?.cancel() + eventContinuation.finish() } private func startMonitoringSession() async { - let noteSequence = await session.notificationSequence - - self.notificationTask = Task { [weak self] in - for await (notification, data) in noteSequence { - guard let self = self else { break } + let seq = await session.eventSequence - await self.handleNotification(notification, data: data) - } + for await event in seq { - self?.notificationContinuation.finish() - } - let reqSequence = await session.requestSequence - - self.requestTask = Task { [weak self] in - for await (request, handler, data) in reqSequence { - guard let self = self else { break } - - await self.handleRequest(request, data: data, handler: handler) + switch event { + case let .notification(notification, data): + self.handleNotification(notification, data: data) + case let .request(request, handler, data): + self.handleRequest(request, data: data, handler: handler) + case .error: + break // TODO? } - self?.requestContinuation.finish() } + + eventContinuation.finish() } public func sendNotification(_ notif: ClientNotification) async throws { @@ -119,101 +89,101 @@ public actor JSONRPCServer: Server { let method = request.method.rawValue switch request { - case .initialize(let params): + case .initialize(let params, _): return try await session.response(to: method, params: params) case .shutdown: return try await session.response(to: method) - case .workspaceExecuteCommand(let params): + case .workspaceExecuteCommand(let params, _): return try await session.response(to: method, params: params) case .workspaceInlayHintRefresh: return try await session.response(to: method) - case .workspaceWillCreateFiles(let params): + case .workspaceWillCreateFiles(let params, _): return try await session.response(to: method, params: params) - case .workspaceWillRenameFiles(let params): + case .workspaceWillRenameFiles(let params, _): return try await session.response(to: method, params: params) - case .workspaceWillDeleteFiles(let params): + case .workspaceWillDeleteFiles(let params, _): return try await session.response(to: method, params: params) - case .workspaceSymbol(let params): + case .workspaceSymbol(let params, _): return try await session.response(to: method, params: params) - case .workspaceSymbolResolve(let params): + case .workspaceSymbolResolve(let params, _): return try await session.response(to: method, params: params) - case .textDocumentWillSaveWaitUntil(let params): + case .textDocumentWillSaveWaitUntil(let params, _): return try await session.response(to: method, params: params) - case .completion(let params): + case .completion(let params, _): return try await session.response(to: method, params: params) - case .completionItemResolve(let params): + case .completionItemResolve(let params, _): return try await session.response(to: method, params: params) - case .hover(let params): + case .hover(let params, _): return try await session.response(to: method, params: params) - case .signatureHelp(let params): + case .signatureHelp(let params, _): return try await session.response(to: method, params: params) - case .declaration(let params): + case .declaration(let params, _): return try await session.response(to: method, params: params) - case .definition(let params): + case .definition(let params, _): return try await session.response(to: method, params: params) - case .typeDefinition(let params): + case .typeDefinition(let params, _): return try await session.response(to: method, params: params) - case .implementation(let params): + case .implementation(let params, _): return try await session.response(to: method, params: params) - case .documentHighlight(let params): + case .documentHighlight(let params, _): return try await session.response(to: method, params: params) - case .documentSymbol(let params): + case .documentSymbol(let params, _): return try await session.response(to: method, params: params) - case .codeAction(let params): + case .codeAction(let params, _): return try await session.response(to: method, params: params) - case .codeActionResolve(let params): + case .codeActionResolve(let params, _): return try await session.response(to: method, params: params) - case .codeLens(let params): + case .codeLens(let params, _): return try await session.response(to: method, params: params) - case .codeLensResolve(let params): + case .codeLensResolve(let params, _): return try await session.response(to: method, params: params) - case .selectionRange(let params): + case .selectionRange(let params, _): return try await session.response(to: method, params: params) - case .linkedEditingRange(let params): + case .linkedEditingRange(let params, _): return try await session.response(to: method, params: params) - case .moniker(let params): + case .prepareCallHierarchy(let params, _): return try await session.response(to: method, params: params) - case .prepareCallHierarchy(let params): + case .prepareRename(let params, _): return try await session.response(to: method, params: params) - case .prepareRename(let params): + case .rename(let params, _): return try await session.response(to: method, params: params) - case .rename(let params): + case .inlayHint(let params, _): return try await session.response(to: method, params: params) - case .inlayHint(let params): + case .inlayHintResolve(let params, _): return try await session.response(to: method, params: params) - case .inlayHintResolve(let params): + case .diagnostics(let params, _): return try await session.response(to: method, params: params) - case .diagnostics(let params): + case .documentLink(let params, _): return try await session.response(to: method, params: params) - case .documentLink(let params): + case .documentLinkResolve(let params, _): return try await session.response(to: method, params: params) - case .documentLinkResolve(let params): + case .documentColor(let params, _): return try await session.response(to: method, params: params) - case .documentColor(let params): + case .colorPresentation(let params, _): return try await session.response(to: method, params: params) - case .colorPresentation(let params): + case .formatting(let params, _): return try await session.response(to: method, params: params) - case .formatting(let params): + case .rangeFormatting(let params, _): return try await session.response(to: method, params: params) - case .rangeFormatting(let params): + case .onTypeFormatting(let params, _): return try await session.response(to: method, params: params) - case .onTypeFormatting(let params): + case .references(let params, _): return try await session.response(to: method, params: params) - case .references(let params): + case .foldingRange(let params, _): return try await session.response(to: method, params: params) - case .foldingRange(let params): + case .moniker(let params, _): return try await session.response(to: method, params: params) - case .semanticTokensFull(let params): + case .semanticTokensFull(let params, _): return try await session.response(to: method, params: params) - case .semanticTokensFullDelta(let params): + case .semanticTokensFullDelta(let params, _): return try await session.response(to: method, params: params) - case .semanticTokensRange(let params): + case .semanticTokensRange(let params, _): return try await session.response(to: method, params: params) - case .callHierarchyIncomingCalls(let params): + case .callHierarchyIncomingCalls(let params, _): return try await session.response(to: method, params: params) - case .callHierarchyOutgoingCalls(let params): + case .callHierarchyOutgoingCalls(let params, _): return try await session.response(to: method, params: params) - case let .custom(method, params): + case let .custom(method, params, _): return try await session.response(to: method, params: params) } } @@ -228,6 +198,14 @@ public actor JSONRPCServer: Server { return params } + private func yield(_ notification: ServerNotification) { + eventContinuation.yield(.notification(notification)) + } + + private func yield(id: JSONId, request: ServerRequest) { + eventContinuation.yield(.request(id: id, request: request)) + } + private func handleNotification(_ anyNotification: AnyJSONRPCNotification, data: Data) { let methodName = anyNotification.method @@ -240,31 +218,31 @@ public actor JSONRPCServer: Server { case .windowLogMessage: let params = try decodeNotificationParams(LogMessageParams.self, from: data) - notificationContinuation.yield(.windowLogMessage(params)) + yield(.windowLogMessage(params)) case .windowShowMessage: let params = try decodeNotificationParams(ShowMessageParams.self, from: data) - notificationContinuation.yield(.windowShowMessage(params)) + yield(.windowShowMessage(params)) case .textDocumentPublishDiagnostics: let params = try decodeNotificationParams(PublishDiagnosticsParams.self, from: data) - notificationContinuation.yield(.textDocumentPublishDiagnostics(params)) + yield(.textDocumentPublishDiagnostics(params)) case .telemetryEvent: let params = anyNotification.params ?? .null - notificationContinuation.yield(.telemetryEvent(params)) + yield(.telemetryEvent(params)) case .protocolCancelRequest: let params = try decodeNotificationParams(CancelParams.self, from: data) - notificationContinuation.yield(.protocolCancelRequest(params)) + yield(.protocolCancelRequest(params)) case .protocolProgress: let params = try decodeNotificationParams(ProgressParams.self, from: data) - notificationContinuation.yield(.protocolProgress(params)) + yield(.protocolProgress(params)) case .protocolLogTrace: let params = try decodeNotificationParams(LogTraceParams.self, from: data) - notificationContinuation.yield(.protocolLogTrace(params)) + yield(.protocolLogTrace(params)) } } catch { // should we backchannel this to the client somehow? @@ -282,7 +260,7 @@ public actor JSONRPCServer: Server { return params } - private nonisolated func makeErrorOnlyHandler(_ handler: @escaping JSONRPCSession.RequestHandler) -> ServerRequest.ErrorOnlyHandler { + private nonisolated func makeErrorOnlyHandler(_ handler: @escaping JSONRPCEvent.RequestHandler) -> ServerRequest.ErrorOnlyHandler { return { if let error = $0 { await handler(.failure(error)) @@ -292,7 +270,7 @@ public actor JSONRPCServer: Server { } } - private nonisolated func makeHandler(_ handler: @escaping JSONRPCSession.RequestHandler) -> ServerRequest.Handler { + private nonisolated func makeHandler(_ handler: @escaping JSONRPCEvent.RequestHandler) -> ServerRequest.Handler { return { let loweredResult = $0.map({ $0 as Encodable & Sendable }) @@ -300,8 +278,9 @@ public actor JSONRPCServer: Server { } } - private func handleRequest(_ anyRequest: AnyJSONRPCRequest, data: Data, handler: @escaping JSONRPCSession.RequestHandler) { + private func handleRequest(_ anyRequest: AnyJSONRPCRequest, data: Data, handler: @escaping JSONRPCEvent.RequestHandler) { let methodName = anyRequest.method + let id = anyRequest.id do { guard let method = ServerRequest.Method(rawValue: methodName) else { @@ -313,49 +292,49 @@ public actor JSONRPCServer: Server { let params = try decodeRequestParams(ConfigurationParams.self, from: data) let reqHandler: ServerRequest.Handler<[LSPAny]> = makeHandler(handler) - requestContinuation.yield(ServerRequest.workspaceConfiguration(params, reqHandler)) + yield(id: id, request: ServerRequest.workspaceConfiguration(params, reqHandler)) case .workspaceFolders: let reqHandler: ServerRequest.Handler = makeHandler(handler) - requestContinuation.yield(ServerRequest.workspaceFolders(reqHandler)) + yield(id: id, request: ServerRequest.workspaceFolders(reqHandler)) case .workspaceApplyEdit: let params = try decodeRequestParams(ApplyWorkspaceEditParams.self, from: data) let reqHandler: ServerRequest.Handler = makeHandler(handler) - requestContinuation.yield(ServerRequest.workspaceApplyEdit(params, reqHandler)) + yield(id: id, request: ServerRequest.workspaceApplyEdit(params, reqHandler)) case .clientRegisterCapability: let params = try decodeRequestParams(RegistrationParams.self, from: data) let reqHandler = makeErrorOnlyHandler(handler) - requestContinuation.yield(ServerRequest.clientRegisterCapability(params, reqHandler)) + yield(id: id, request: ServerRequest.clientRegisterCapability(params, reqHandler)) case .clientUnregisterCapability: let params = try decodeRequestParams(UnregistrationParams.self, from: data) let reqHandler = makeErrorOnlyHandler(handler) - requestContinuation.yield(ServerRequest.clientUnregisterCapability(params, reqHandler)) + yield(id: id, request: ServerRequest.clientUnregisterCapability(params, reqHandler)) case .workspaceCodeLensRefresh: let reqHandler = makeErrorOnlyHandler(handler) - requestContinuation.yield(ServerRequest.workspaceCodeLensRefresh(reqHandler)) + yield(id: id, request: ServerRequest.workspaceCodeLensRefresh(reqHandler)) case .workspaceSemanticTokenRefresh: let reqHandler = makeErrorOnlyHandler(handler) - requestContinuation.yield(ServerRequest.workspaceSemanticTokenRefresh(reqHandler)) + yield(id: id, request: ServerRequest.workspaceSemanticTokenRefresh(reqHandler)) case .windowShowMessageRequest: let params = try decodeRequestParams(ShowMessageRequestParams.self, from: data) let reqHandler: ServerRequest.Handler = makeHandler(handler) - requestContinuation.yield(ServerRequest.windowShowMessageRequest(params, reqHandler)) + yield(id: id, request: ServerRequest.windowShowMessageRequest(params, reqHandler)) case .windowShowDocument: let params = try decodeRequestParams(ShowDocumentParams.self, from: data) let reqHandler: ServerRequest.Handler = makeHandler(handler) - requestContinuation.yield(ServerRequest.windowShowDocument(params, reqHandler)) + yield(id: id, request: ServerRequest.windowShowDocument(params, reqHandler)) case .windowWorkDoneProgressCreate: let params = try decodeRequestParams(WorkDoneProgressCreateParams.self, from: data) let reqHandler = makeErrorOnlyHandler(handler) - requestContinuation.yield(ServerRequest.windowWorkDoneProgressCreate(params, reqHandler)) + yield(id: id, request: ServerRequest.windowWorkDoneProgressCreate(params, reqHandler)) } diff --git a/Sources/LanguageServerProtocol/Additions/MockServer.swift b/Sources/LSPClient/Client.MockServer.swift similarity index 60% rename from Sources/LanguageServerProtocol/Additions/MockServer.swift rename to Sources/LSPClient/Client.MockServer.swift index 5de5c1b..87a0f36 100644 --- a/Sources/LanguageServerProtocol/Additions/MockServer.swift +++ b/Sources/LSPClient/Client.MockServer.swift @@ -1,4 +1,5 @@ import Foundation +import LanguageServerProtocol extension AsyncSequence { func collect() async rethrows -> [Element] { @@ -7,8 +8,8 @@ extension AsyncSequence { } /// Simulate LSP communication. -public actor MockServer: Server { - public enum ClientMessage: Sendable, Hashable { +public actor MockServer: ServerConnection { + public enum ClientMessage: Sendable { case notification(ClientNotification) case request(ClientRequest) } @@ -16,11 +17,8 @@ public actor MockServer: Server { public typealias ClientMessageSequence = AsyncStream private typealias ResponseDataSequence = AsyncStream - public let notificationSequence: NotificationSequence - public let requestSequence: RequestSequence - - private let notificationContinuation: NotificationSequence.Continuation - private let requestContinuation: RequestSequence.Continuation + public let eventSequence: EventSequence + private let eventContinuation: EventSequence.Continuation private var mockResponses = [Data]() @@ -28,33 +26,13 @@ public actor MockServer: Server { private let sentMessageContinuation: ClientMessageSequence.Continuation public init() { - // this is annoying, but temporary -#if compiler(>=5.9) - (self.notificationSequence, self.notificationContinuation) = NotificationSequence.makeStream() - (self.requestSequence, self.requestContinuation) = RequestSequence.makeStream() + (self.eventSequence, self.eventContinuation) = EventSequence.makeStream() (self.sentMessageSequence, self.sentMessageContinuation) = ClientMessageSequence.makeStream() -#else - var escapedNoteContinuation: NotificationSequence.Continuation? - - self.notificationSequence = NotificationSequence { escapedNoteContinuation = $0 } - self.notificationContinuation = escapedNoteContinuation! - - var escapedRequestContinuation: RequestSequence.Continuation? - - self.requestSequence = RequestSequence { escapedRequestContinuation = $0 } - self.requestContinuation = escapedRequestContinuation! - - var escapedSentContinuation: ClientMessageSequence.Continuation? - - self.sentMessageSequence = ClientMessageSequence { escapedSentContinuation = $0 } - self.sentMessageContinuation = escapedSentContinuation! -#endif } deinit { sentMessageContinuation.finish() - notificationContinuation.finish() - requestContinuation.finish() + eventContinuation.finish() } public func sendNotification(_ notif: ClientNotification) async throws { @@ -78,8 +56,7 @@ extension MockServer { /// Returns an array of sent messages. public func finishSession() async -> [ClientMessage] { sentMessageContinuation.finish() - notificationContinuation.finish() - requestContinuation.finish() + eventContinuation.finish() return await sentMessageSequence.collect() } @@ -103,11 +80,11 @@ extension MockServer { /// Simulate a server request. public func sendMockRequest(_ request: ServerRequest) { - requestContinuation.yield(request) + eventContinuation.yield(.request(id: .numericId(0) , request: request)) } /// Simulate a server notification. public func sendMockNotification(_ note: ServerNotification) { - notificationContinuation.yield(note) + eventContinuation.yield(.notification(note)) } } diff --git a/Sources/LanguageServerProtocol/Additions/Server.swift b/Sources/LSPClient/Client.ServerConnection.swift similarity index 64% rename from Sources/LanguageServerProtocol/Additions/Server.swift rename to Sources/LSPClient/Client.ServerConnection.swift index 0bbab32..1dec8cc 100644 --- a/Sources/LanguageServerProtocol/Additions/Server.swift +++ b/Sources/LSPClient/Client.ServerConnection.swift @@ -1,28 +1,27 @@ import Foundation +import LanguageServerProtocol /// Handles the communication between an LSP client and server. /// /// This protocol defines all the messages that can be sent between an LSP client and server. It does **not** enforce correct ordering of those messages. -public protocol Server { - typealias NotificationSequence = AsyncStream - typealias RequestSequence = AsyncStream +public protocol ServerConnection { + typealias EventSequence = AsyncStream - var notificationSequence: NotificationSequence { get } - var requestSequence: RequestSequence { get } + var eventSequence: EventSequence { get } func sendNotification(_ notif: ClientNotification) async throws func sendRequest(_ request: ClientRequest) async throws -> Response } -extension Server { +extension ServerConnection { func sendRequestWithErrorOnlyResult(_ request: ClientRequest) async throws { let _: UnusedResult = try await sendRequest(request) } } -public extension Server { +public extension ServerConnection { func initialize(params: InitializeParams) async throws -> InitializationResponse { - return try await sendRequest(.initialize(params)) + return try await sendRequest(.initialize(params, ClientRequest.NullHandler)) } func initialized(params: InitializedParams) async throws { @@ -30,7 +29,7 @@ public extension Server { } func shutdown() async throws { - try await sendRequestWithErrorOnlyResult(.shutdown) + try await sendRequestWithErrorOnlyResult(.shutdown(ClientRequest.NullHandler)) } func exit() async throws { @@ -62,7 +61,7 @@ public extension Server { } func textDocumentWillSaveWaitUntil(params: WillSaveTextDocumentParams) async throws -> WillSaveWaitUntilResponse { - try await sendRequest(.textDocumentWillSaveWaitUntil(params)) + try await sendRequest(.textDocumentWillSaveWaitUntil(params, ClientRequest.NullHandler)) } func textDocumentDidSave(params: DidSaveTextDocumentParams) async throws { @@ -74,97 +73,97 @@ public extension Server { } func callHierarchyIncomingCalls(params: CallHierarchyIncomingCallsParams) async throws -> CallHierarchyIncomingCallsResponse { - try await sendRequest(.callHierarchyIncomingCalls(params)) + try await sendRequest(.callHierarchyIncomingCalls(params, ClientRequest.NullHandler)) } func callHierarchyOutgoingCalls(params: CallHierarchyOutgoingCallsParams) async throws -> CallHierarchyOutgoingCallsResponse { - try await sendRequest(.callHierarchyOutgoingCalls(params)) + try await sendRequest(.callHierarchyOutgoingCalls(params, ClientRequest.NullHandler)) } func completion(params: CompletionParams) async throws -> CompletionResponse { - try await sendRequest(.completion(params)) + try await sendRequest(.completion(params, ClientRequest.NullHandler)) } func hover(params: TextDocumentPositionParams) async throws -> HoverResponse { - try await sendRequest(.hover(params)) + try await sendRequest(.hover(params, ClientRequest.NullHandler)) } func signatureHelp(params: TextDocumentPositionParams) async throws -> SignatureHelpResponse { - try await sendRequest(.signatureHelp(params)) + try await sendRequest(.signatureHelp(params, ClientRequest.NullHandler)) } func declaration(params: TextDocumentPositionParams) async throws -> DeclarationResponse { - try await sendRequest(.declaration(params)) + try await sendRequest(.declaration(params, ClientRequest.NullHandler)) } func definition(params: TextDocumentPositionParams) async throws -> DefinitionResponse { - try await sendRequest(.definition(params)) + try await sendRequest(.definition(params, ClientRequest.NullHandler)) } func typeDefinition(params: TextDocumentPositionParams) async throws -> TypeDefinitionResponse { - try await sendRequest(.typeDefinition(params)) + try await sendRequest(.typeDefinition(params, ClientRequest.NullHandler)) } func implementation(params: TextDocumentPositionParams) async throws -> ImplementationResponse { - try await sendRequest(.implementation(params)) + try await sendRequest(.implementation(params, ClientRequest.NullHandler)) } func documentSymbol(params: DocumentSymbolParams) async throws -> DocumentSymbolResponse { - try await sendRequest(.documentSymbol(params)) + try await sendRequest(.documentSymbol(params, ClientRequest.NullHandler)) } func prepareCallHierarchy(params: CallHierarchyPrepareParams) async throws -> CallHierarchyPrepareResponse { - try await sendRequest(.prepareCallHierarchy(params)) + try await sendRequest(.prepareCallHierarchy(params, ClientRequest.NullHandler)) } func prepareRename(params: PrepareRenameParams) async throws -> PrepareRenameResponse { - try await sendRequest(.prepareRename(params)) + try await sendRequest(.prepareRename(params, ClientRequest.NullHandler)) } func rename(params: RenameParams) async throws -> RenameResponse { - try await sendRequest(.rename(params)) + try await sendRequest(.rename(params, ClientRequest.NullHandler)) } func formatting(params: DocumentFormattingParams) async throws -> FormattingResult { - try await sendRequest(.formatting(params)) + try await sendRequest(.formatting(params, ClientRequest.NullHandler)) } func rangeFormatting(params: DocumentRangeFormattingParams) async throws -> FormattingResult { - try await sendRequest(.rangeFormatting(params)) + try await sendRequest(.rangeFormatting(params, ClientRequest.NullHandler)) } func onTypeFormatting(params: DocumentOnTypeFormattingParams) async throws -> FormattingResult { - try await sendRequest(.onTypeFormatting(params)) + try await sendRequest(.onTypeFormatting(params, ClientRequest.NullHandler)) } func references(params: ReferenceParams) async throws -> ReferenceResponse { - try await sendRequest(.references(params)) + try await sendRequest(.references(params, ClientRequest.NullHandler)) } func foldingRange(params: FoldingRangeParams) async throws -> FoldingRangeResponse { - try await sendRequest(.foldingRange(params)) + try await sendRequest(.foldingRange(params, ClientRequest.NullHandler)) } func semanticTokensFull(params: SemanticTokensParams) async throws -> SemanticTokensResponse { - try await sendRequest(.semanticTokensFull(params)) + try await sendRequest(.semanticTokensFull(params, ClientRequest.NullHandler)) } func semanticTokensFullDelta(params: SemanticTokensDeltaParams) async throws -> SemanticTokensDeltaResponse { - try await sendRequest(.semanticTokensFullDelta(params)) + try await sendRequest(.semanticTokensFullDelta(params, ClientRequest.NullHandler)) } func semanticTokensRange(params: SemanticTokensRangeParams) async throws -> SemanticTokensResponse { - try await sendRequest(.semanticTokensRange(params)) + try await sendRequest(.semanticTokensRange(params, ClientRequest.NullHandler)) } func customRequest(method: String, params: LSPAny) async throws -> Response { - try await sendRequest(.custom(method, params)) + try await sendRequest(.custom(method, params, ClientRequest.NullHandler)) } } // Workspace notifications -public extension Server { +public extension ServerConnection { func didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) async throws { try await sendNotification(.workspaceDidChangeWorkspaceFolders(params)) } @@ -187,87 +186,87 @@ public extension Server { } // Workspace Requests -public extension Server { +public extension ServerConnection { func inlayHintRefresh() async throws { - try await sendRequestWithErrorOnlyResult(.workspaceInlayHintRefresh) + try await sendRequestWithErrorOnlyResult(.workspaceInlayHintRefresh(ClientRequest.NullHandler)) } func willCreateFiles(params: CreateFilesParams) async throws -> WorkspaceWillCreateFilesResponse { - try await sendRequest(.workspaceWillCreateFiles(params)) + try await sendRequest(.workspaceWillCreateFiles(params, ClientRequest.NullHandler)) } func willRenameFiles(params: RenameFilesParams) async throws -> WorkspaceWillRenameFilesResponse { - try await sendRequest(.workspaceWillRenameFiles(params)) + try await sendRequest(.workspaceWillRenameFiles(params, ClientRequest.NullHandler)) } func willDeleteFiles(params: DeleteFilesParams) async throws -> WorkspaceWillDeleteFilesResponse { - try await sendRequest(.workspaceWillDeleteFiles(params)) + try await sendRequest(.workspaceWillDeleteFiles(params, ClientRequest.NullHandler)) } func executeCommand(params: ExecuteCommandParams) async throws -> ExecuteCommandResponse { - try await sendRequest(.workspaceExecuteCommand(params)) + try await sendRequest(.workspaceExecuteCommand(params, ClientRequest.NullHandler)) } func workspaceSymbol(params: WorkspaceSymbolParams) async throws -> WorkspaceSymbolResponse { - try await sendRequest(.workspaceSymbol(params)) + try await sendRequest(.workspaceSymbol(params, ClientRequest.NullHandler)) } func workspaceSymbolResolve(params: WorkspaceSymbol) async throws -> WorkspaceSymbolResponse { - try await sendRequest(.workspaceSymbolResolve(params)) + try await sendRequest(.workspaceSymbolResolve(params, ClientRequest.NullHandler)) } } // Language Features -public extension Server { +public extension ServerConnection { func documentHighlight(params: DocumentHighlightParams) async throws -> DocumentHighlightResponse { - try await sendRequest(.documentHighlight(params)) + try await sendRequest(.documentHighlight(params, ClientRequest.NullHandler)) } func codeAction(params: CodeActionParams) async throws -> CodeActionResponse { - try await sendRequest(.codeAction(params)) + try await sendRequest(.codeAction(params, ClientRequest.NullHandler)) } func codeActionResolve(params: CodeAction) async throws -> CodeAction { - try await sendRequest(.codeActionResolve(params)) + try await sendRequest(.codeActionResolve(params, ClientRequest.NullHandler)) } func codeLens(params: CodeLensParams) async throws -> CodeLensResponse { - try await sendRequest(.codeLens(params)) + try await sendRequest(.codeLens(params, ClientRequest.NullHandler)) } func codeLensResolve(params: CodeLens) async throws -> CodeLensResolveResponse { - try await sendRequest(.codeLensResolve(params)) + try await sendRequest(.codeLensResolve(params, ClientRequest.NullHandler)) } func diagnostics(params: DocumentDiagnosticParams) async throws -> DocumentDiagnosticReport { - try await sendRequest(.diagnostics(params)) + try await sendRequest(.diagnostics(params, ClientRequest.NullHandler)) } func selectionRange(params: SelectionRangeParams) async throws -> SelectionRangeResponse { - try await sendRequest(.selectionRange(params)) + try await sendRequest(.selectionRange(params, ClientRequest.NullHandler)) } func documentLink(params: DocumentLinkParams) async throws -> DocumentLinkResponse { - try await sendRequest(.documentLink(params)) + try await sendRequest(.documentLink(params, ClientRequest.NullHandler)) } func documentLinkResolve(params: DocumentLink) async throws -> DocumentLink { - try await sendRequest(.documentLinkResolve(params)) + try await sendRequest(.documentLinkResolve(params, ClientRequest.NullHandler)) } func documentColor(params: DocumentColorParams) async throws -> DocumentColorResponse { - try await sendRequest(.documentColor(params)) + try await sendRequest(.documentColor(params, ClientRequest.NullHandler)) } func colorPresentation(params: ColorPresentationParams) async throws -> ColorPresentationResponse { - try await sendRequest(.colorPresentation(params)) + try await sendRequest(.colorPresentation(params, ClientRequest.NullHandler)) } func inlayHint(params: InlayHintParams) async throws -> InlayHintResponse { - try await sendRequest(.inlayHint(params)) + try await sendRequest(.inlayHint(params, ClientRequest.NullHandler)) } func inlayHintResolve(params: InlayHint) async throws -> InlayHint { - try await sendRequest(.inlayHintResolve(params)) + try await sendRequest(.inlayHintResolve(params, ClientRequest.NullHandler)) } } diff --git a/Sources/LSPServer/Server.ClientConnection.swift b/Sources/LSPServer/Server.ClientConnection.swift new file mode 100644 index 0000000..1e5ad9d --- /dev/null +++ b/Sources/LSPServer/Server.ClientConnection.swift @@ -0,0 +1,12 @@ +import JSONRPC +import Foundation +import LanguageServerProtocol + +public protocol ClientConnection { + typealias EventSequence = AsyncStream + + var eventSequence: EventSequence { get } + + func sendNotification(_ notif: ServerNotification) async throws + func sendRequest(_ request: ServerRequest) async throws -> Response +} diff --git a/Sources/LSPServer/Server.ErrorHandler.swift b/Sources/LSPServer/Server.ErrorHandler.swift new file mode 100644 index 0000000..9e74c89 --- /dev/null +++ b/Sources/LSPServer/Server.ErrorHandler.swift @@ -0,0 +1,3 @@ +public protocol ErrorHandler { + func internalError(_ error: Error) async +} diff --git a/Sources/LSPServer/Server.EventDispatcher.swift b/Sources/LSPServer/Server.EventDispatcher.swift new file mode 100644 index 0000000..19bd8f1 --- /dev/null +++ b/Sources/LSPServer/Server.EventDispatcher.swift @@ -0,0 +1,39 @@ +import LanguageServerProtocol + +public struct EventDispatcher { + private let connection: JSONRPCClientConnection + private let requestHandler: RequestHandler + private let notificationHandler: NotificationHandler + private let errorHandler: ErrorHandler + + public init(connection: JSONRPCClientConnection, requestHandler: RequestHandler, notificationHandler: NotificationHandler, errorHandler: ErrorHandler) { + self.connection = connection + self.requestHandler = requestHandler + self.notificationHandler = notificationHandler + self.errorHandler = errorHandler + } + + public init(connection: JSONRPCClientConnection, eventHandler: EventHandler) { + self.connection = connection + self.requestHandler = eventHandler + self.notificationHandler = eventHandler + self.errorHandler = eventHandler + } + + public func run() async { + await monitorEvents() + } + + private func monitorEvents() async { + for await event in connection.eventSequence { + switch event { + case let .notification(notification): + await notificationHandler.handleNotification(notification) + case let .request(id, request): + await requestHandler.handleRequest(id: id, request: request) + case let .error(error): + await errorHandler.internalError(error) + } + } + } +} diff --git a/Sources/LSPServer/Server.EventHandler.swift b/Sources/LSPServer/Server.EventHandler.swift new file mode 100644 index 0000000..d5bebd1 --- /dev/null +++ b/Sources/LSPServer/Server.EventHandler.swift @@ -0,0 +1,2 @@ +public protocol EventHandler : NotificationHandler, RequestHandler, ErrorHandler { +} diff --git a/Sources/LSPServer/Server.JSONRPCClientConnection.swift b/Sources/LSPServer/Server.JSONRPCClientConnection.swift new file mode 100644 index 0000000..9888370 --- /dev/null +++ b/Sources/LSPServer/Server.JSONRPCClientConnection.swift @@ -0,0 +1,328 @@ +import Foundation +import JSONRPC +import LanguageServerProtocol + +public actor JSONRPCClientConnection : ClientConnection { + public let eventSequence: EventSequence + private let eventContinuation: EventSequence.Continuation + + private let session: JSONRPCSession + + /// NOTE: The channel will wrapped with message framing + public init(_ dataChannel: DataChannel) { + self.session = JSONRPCSession(channel: dataChannel.withMessageFraming()) + + (self.eventSequence, self.eventContinuation) = EventSequence.makeStream() + + Task { + await startMonitoringSession() + } + } + + deinit { + eventContinuation.finish() + } + + + private func startMonitoringSession() async { + let seq = await session.eventSequence + + for await event in seq { + + switch event { + case let .notification(notification, data): + self.handleNotification(notification, data: data) + case let .request(request, handler, data): + self.handleRequest(request, data: data, handler: handler) + case let .error(error): + self.handleError(error) + } + } + + eventContinuation.finish() + } + + public func stop() { + eventContinuation.finish() + } + + + private func decodeNotificationParams(_ type: Params.Type, from data: Data) throws -> Params where Params : Decodable { + let note = try JSONDecoder().decode(JSONRPCNotification.self, from: data) + + guard let params = note.params else { + throw ClientError.missingParams + } + + return params + } + + private func yield(_ notification: ClientNotification) { + eventContinuation.yield(.notification(notification)) + } + + private func yield(id: JSONId, request: ClientRequest) { + eventContinuation.yield(.request(id: id, request: request)) + } + + private func handleNotification(_ anyNotification: AnyJSONRPCNotification, data: Data) { + let methodName = anyNotification.method + + do { + guard let method = ClientNotification.Method(rawValue: methodName) else { + throw ClientError.unrecognizedMethod(methodName) + } + + switch method { + case .initialized: + let params = try decodeNotificationParams(InitializedParams.self, from: data) + yield(.initialized(params)) + case .exit: + yield(.exit) + case .textDocumentDidOpen: + let params = try decodeNotificationParams(DidOpenTextDocumentParams.self, from: data) + yield(.textDocumentDidOpen(params)) + case .textDocumentDidChange: + let params = try decodeNotificationParams(DidChangeTextDocumentParams.self, from: data) + yield(.textDocumentDidChange(params)) + case .textDocumentDidClose: + let params = try decodeNotificationParams(DidCloseTextDocumentParams.self, from: data) + yield(.textDocumentDidClose(params)) + case .textDocumentWillSave: + let params = try decodeNotificationParams(WillSaveTextDocumentParams.self, from: data) + yield(.textDocumentWillSave(params)) + case .textDocumentDidSave: + let params = try decodeNotificationParams(DidSaveTextDocumentParams.self, from: data) + yield(.textDocumentDidSave(params)) + case .protocolCancelRequest: + let params = try decodeNotificationParams(CancelParams.self, from: data) + yield(.protocolCancelRequest(params)) + case .protocolSetTrace: + let params = try decodeNotificationParams(SetTraceParams.self, from: data) + yield(.protocolSetTrace(params)) + case .workspaceDidChangeWatchedFiles: + let params = try decodeNotificationParams(DidChangeWatchedFilesParams.self, from: data) + yield(.workspaceDidChangeWatchedFiles(params)) + case .windowWorkDoneProgressCancel: + let params = try decodeNotificationParams(WorkDoneProgressCancelParams.self, from: data) + yield(.windowWorkDoneProgressCancel(params)) + case .workspaceDidChangeWorkspaceFolders: + let params = try decodeNotificationParams(DidChangeWorkspaceFoldersParams.self, from: data) + yield(.workspaceDidChangeWorkspaceFolders(params)) + case .workspaceDidChangeConfiguration: + let params = try decodeNotificationParams(DidChangeConfigurationParams.self, from: data) + yield(.workspaceDidChangeConfiguration(params)) + case .workspaceDidCreateFiles: + let params = try decodeNotificationParams(CreateFilesParams.self, from: data) + yield(.workspaceDidCreateFiles(params)) + case .workspaceDidRenameFiles: + let params = try decodeNotificationParams(RenameFilesParams.self, from: data) + yield(.workspaceDidRenameFiles(params)) + case .workspaceDidDeleteFiles: + let params = try decodeNotificationParams(DeleteFilesParams.self, from: data) + yield(.workspaceDidDeleteFiles(params)) + } + } catch { + // should we backchannel this to the client somehow? + print("failed to relay notification: \(error)") + } + } + + + private func decodeRequestParams(_ data: Data) throws -> Params where Params : Decodable { + let req = try JSONDecoder().decode(JSONRPCRequest.self, from: data) + + guard let params = req.params else { + throw ClientError.missingParams + } + + return params + } + + private func decodeRequestParams(_ type: Params.Type, from data: Data) throws -> Params where Params : Decodable { + let req = try JSONDecoder().decode(JSONRPCRequest.self, from: data) + + guard let params = req.params else { + throw ClientError.missingParams + } + + return params + } + + + private nonisolated func makeHandler(_ handler: @escaping JSONRPCEvent.RequestHandler) -> ServerRequest.Handler { + return { + let loweredResult = $0.map({ $0 as Encodable & Sendable }) + + await handler(loweredResult) + } + } + + private func handleRequest(_ anyRequest: AnyJSONRPCRequest, data: Data, handler: @escaping JSONRPCEvent.RequestHandler) { + let methodName = anyRequest.method + let id = anyRequest.id + + do { + guard let method = ClientRequest.Method(rawValue: methodName) else { + throw ClientError.unrecognizedMethod(methodName) + } + + switch method { + case .initialize: + yield(id: id, request: ClientRequest.initialize(try decodeRequestParams(data), makeHandler(handler))) + case .shutdown: + yield(id: id, request: ClientRequest.shutdown(makeHandler(handler))) + case .workspaceInlayHintRefresh: + yield(id: id, request: ClientRequest.workspaceInlayHintRefresh(makeHandler(handler))) + case .workspaceExecuteCommand: + yield(id: id, request: ClientRequest.workspaceExecuteCommand(try decodeRequestParams(data), makeHandler(handler))) + case .workspaceWillCreateFiles: + yield(id: id, request: ClientRequest.workspaceWillCreateFiles(try decodeRequestParams(data), makeHandler(handler))) + case .workspaceWillRenameFiles: + yield(id: id, request: ClientRequest.workspaceWillRenameFiles(try decodeRequestParams(data), makeHandler(handler))) + case .workspaceWillDeleteFiles: + yield(id: id, request: ClientRequest.workspaceWillDeleteFiles(try decodeRequestParams(data), makeHandler(handler))) + case .workspaceSymbol: + yield(id: id, request: ClientRequest.workspaceSymbol(try decodeRequestParams(data), makeHandler(handler))) + case .workspaceSymbolResolve: + yield(id: id, request: ClientRequest.workspaceSymbolResolve(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentWillSaveWaitUntil: + yield(id: id, request: ClientRequest.textDocumentWillSaveWaitUntil(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentCompletion: + yield(id: id, request: ClientRequest.completion(try decodeRequestParams(data), makeHandler(handler))) + case .completionItemResolve: + yield(id: id, request: ClientRequest.completionItemResolve(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentHover: + yield(id: id, request: ClientRequest.hover(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentSignatureHelp: + yield(id: id, request: ClientRequest.signatureHelp(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentDeclaration: + yield(id: id, request: ClientRequest.declaration(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentDefinition: + yield(id: id, request: ClientRequest.definition(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentTypeDefinition: + yield(id: id, request: ClientRequest.typeDefinition(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentImplementation: + yield(id: id, request: ClientRequest.implementation(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentDiagnostic: + yield(id: id, request: ClientRequest.diagnostics(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentDocumentHighlight: + yield(id: id, request: ClientRequest.documentHighlight(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentDocumentSymbol: + yield(id: id, request: ClientRequest.documentSymbol(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentCodeAction: + yield(id: id, request: ClientRequest.codeAction(try decodeRequestParams(data), makeHandler(handler))) + case .codeActionResolve: + yield(id: id, request: ClientRequest.codeActionResolve(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentCodeLens: + yield(id: id, request: ClientRequest.codeLens(try decodeRequestParams(data), makeHandler(handler))) + case .codeLensResolve: + yield(id: id, request: ClientRequest.codeLensResolve(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentSelectionRange: + yield(id: id, request: ClientRequest.selectionRange(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentLinkedEditingRange: + yield(id: id, request: ClientRequest.linkedEditingRange(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentPrepareCallHierarchy: + yield(id: id, request: ClientRequest.prepareCallHierarchy(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentPrepareRename: + yield(id: id, request: ClientRequest.prepareRename(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentRename: + yield(id: id, request: ClientRequest.rename(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentInlayHint: + yield(id: id, request: ClientRequest.inlayHint(try decodeRequestParams(data), makeHandler(handler))) + case .inlayHintResolve: + yield(id: id, request: ClientRequest.inlayHintResolve(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentDocumentLink: + yield(id: id, request: ClientRequest.documentLink(try decodeRequestParams(data), makeHandler(handler))) + case .documentLinkResolve: + yield(id: id, request: ClientRequest.documentLinkResolve(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentDocumentColor: + yield(id: id, request: ClientRequest.documentColor(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentColorPresentation: + yield(id: id, request: ClientRequest.colorPresentation(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentFormatting: + yield(id: id, request: ClientRequest.formatting(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentRangeFormatting: + yield(id: id, request: ClientRequest.rangeFormatting(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentOnTypeFormatting: + yield(id: id, request: ClientRequest.onTypeFormatting(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentReferences: + yield(id: id, request: ClientRequest.references(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentFoldingRange: + yield(id: id, request: ClientRequest.foldingRange(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentMoniker: + yield(id: id, request: ClientRequest.moniker(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentSemanticTokens: + throw ClientError.unhandleRegistrationMethod(methodName) + case .textDocumentSemanticTokensFull: + yield(id: id, request: ClientRequest.semanticTokensFull(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentSemanticTokensFullDelta: + yield(id: id, request: ClientRequest.semanticTokensFullDelta(try decodeRequestParams(data), makeHandler(handler))) + case .textDocumentSemanticTokensRange: + yield(id: id, request: ClientRequest.semanticTokensRange(try decodeRequestParams(data), makeHandler(handler))) + case .callHierarchyIncomingCalls: + yield(id: id, request: ClientRequest.callHierarchyIncomingCalls(try decodeRequestParams(data), makeHandler(handler))) + case .callHierarchyOutgoingCalls: + yield(id: id, request: ClientRequest.callHierarchyOutgoingCalls(try decodeRequestParams(data), makeHandler(handler))) + case .custom: + yield(id: id, request: ClientRequest.custom(methodName, try decodeRequestParams(data), makeHandler(handler))) + } + + } catch { + eventContinuation.yield(.error(error)) + } + } + + private func handleError(_ anyError: Error) { + eventContinuation.yield(.error(anyError)) + } + + public func sendNotification(_ notif: ServerNotification) async throws { + let method = notif.method.rawValue + + switch notif { + case .windowLogMessage(let params): + try await session.sendNotification(params, method: method) + case .windowShowMessage(let params): + try await session.sendNotification(params, method: method) + case .textDocumentPublishDiagnostics(let params): + try await session.sendNotification(params, method: method) + case .telemetryEvent(let params): + try await session.sendNotification(params, method: method) + case .protocolCancelRequest(let params): + try await session.sendNotification(params, method: method) + case .protocolProgress(let params): + try await session.sendNotification(params, method: method) + case .protocolLogTrace(let params): + try await session.sendNotification(params, method: method) + } + } + + public func sendRequest(_ request: ServerRequest) async throws -> Response where Response : Decodable & Sendable { + let method = request.method.rawValue + + switch request { + + case .workspaceConfiguration(let params, _): + return try await session.response(to: method, params: params) + case .workspaceFolders: + return try await session.response(to: method) + case .workspaceApplyEdit(let params, _): + return try await session.response(to: method, params: params) + case .clientRegisterCapability(let params, _): + return try await session.response(to: method, params: params) + case .clientUnregisterCapability(let params, _): + return try await session.response(to: method, params: params) + case .workspaceCodeLensRefresh: + return try await session.response(to: method) + case .workspaceSemanticTokenRefresh: + return try await session.response(to: method) + case .windowShowMessageRequest(let params, _): + return try await session.response(to: method, params: params) + case .windowShowDocument(let params, _): + return try await session.response(to: method, params: params) + case .windowWorkDoneProgressCreate(let params, _): + return try await session.response(to: method, params: params) + } + } +} diff --git a/Sources/LSPServer/Server.NotificationHandler.swift b/Sources/LSPServer/Server.NotificationHandler.swift new file mode 100644 index 0000000..9eb2a9c --- /dev/null +++ b/Sources/LSPServer/Server.NotificationHandler.swift @@ -0,0 +1,138 @@ +import Foundation +import JSONRPC +import LanguageServerProtocol + +public protocol NotificationHandler : ErrorHandler { + func handleNotification(_ notification: ClientNotification) async + + func initialized(_ params: InitializedParams) async + func exit() async + func textDocumentDidOpen(_ params: DidOpenTextDocumentParams) async + func textDocumentDidChange(_ params: DidChangeTextDocumentParams) async + func textDocumentDidClose(_ params: DidCloseTextDocumentParams) async + func textDocumentWillSave(_ params: WillSaveTextDocumentParams) async + func textDocumentDidSave(_ params: DidSaveTextDocumentParams) async + func protocolCancelRequest(_ params: CancelParams) async + func protocolSetTrace(_ params: SetTraceParams) async + func workspaceDidChangeWatchedFiles(_ params: DidChangeWatchedFilesParams) async + func windowWorkDoneProgressCancel(_ params: WorkDoneProgressCancelParams) async + func workspaceDidChangeWorkspaceFolders(_ params: DidChangeWorkspaceFoldersParams) async + func workspaceDidChangeConfiguration(_ params: DidChangeConfigurationParams) async + func workspaceDidCreateFiles(_ params: CreateFilesParams) async + func workspaceDidRenameFiles(_ params: RenameFilesParams) async + func workspaceDidDeleteFiles(_ params: DeleteFilesParams) async +} + +public extension NotificationHandler { + func handleNotification(_ notification: ClientNotification) async { + await defaultNotificationDispatch(notification) + } + + func defaultNotificationDispatch(_ notification: ClientNotification) async { + + switch notification { + case let .initialized(params): + await initialized(params) + case .exit: + await exit() + case let .textDocumentDidOpen(params): + await textDocumentDidOpen(params) + case let .textDocumentDidChange(params): + await textDocumentDidChange(params) + case let .textDocumentDidClose(params): + await textDocumentDidClose(params) + case let .textDocumentWillSave(params): + await textDocumentWillSave(params) + case let .textDocumentDidSave(params): + await textDocumentDidSave(params) + case let .protocolCancelRequest(params): + await protocolCancelRequest(params) + case let .protocolSetTrace(params): + await protocolSetTrace(params) + case let .workspaceDidChangeWatchedFiles(params): + await workspaceDidChangeWatchedFiles(params) + case let .windowWorkDoneProgressCancel(params): + await windowWorkDoneProgressCancel(params) + case let .workspaceDidChangeWorkspaceFolders(params): + await workspaceDidChangeWorkspaceFolders(params) + case let .workspaceDidChangeConfiguration(params): + await workspaceDidChangeConfiguration(params) + case let .workspaceDidCreateFiles(params): + await workspaceDidCreateFiles(params) + case let .workspaceDidRenameFiles(params): + await workspaceDidRenameFiles(params) + case let .workspaceDidDeleteFiles(params): + await workspaceDidDeleteFiles(params) + } + } +} + +/// Provide default implementations for all protocol methods +/// We do this since the handler only need to support a subset, based on dynamically registered capabilities +public extension NotificationHandler { + + func initialized(_ params: InitializedParams) async { + await internalError(NotImplementedError) + } + + func exit() async { + await internalError(NotImplementedError) + } + + func textDocumentDidOpen(_ params: DidOpenTextDocumentParams) async { + await internalError(NotImplementedError) + } + + func textDocumentDidChange(_ params: DidChangeTextDocumentParams) async { + await internalError(NotImplementedError) + } + + func textDocumentDidClose(_ params: DidCloseTextDocumentParams) async { + await internalError(NotImplementedError) + } + + func textDocumentWillSave(_ params: WillSaveTextDocumentParams) async { + await internalError(NotImplementedError) + } + + func textDocumentDidSave(_ params: DidSaveTextDocumentParams) async { + await internalError(NotImplementedError) + } + + func protocolCancelRequest(_ params: CancelParams) async { + await internalError(NotImplementedError) + } + + func protocolSetTrace(_ params: SetTraceParams) async { + await internalError(NotImplementedError) + } + + func workspaceDidChangeWatchedFiles(_ params: DidChangeWatchedFilesParams) async { + await internalError(NotImplementedError) + } + + func windowWorkDoneProgressCancel(_ params: WorkDoneProgressCancelParams) async { + await internalError(NotImplementedError) + } + + func workspaceDidChangeWorkspaceFolders(_ params: DidChangeWorkspaceFoldersParams) async { + await internalError(NotImplementedError) + } + + func workspaceDidChangeConfiguration(_ params: DidChangeConfigurationParams) async { + await internalError(NotImplementedError) + } + + func workspaceDidCreateFiles(_ params: CreateFilesParams) async { + await internalError(NotImplementedError) + } + + func workspaceDidRenameFiles(_ params: RenameFilesParams) async { + await internalError(NotImplementedError) + } + + func workspaceDidDeleteFiles(_ params: DeleteFilesParams) async { + await internalError(NotImplementedError) + } + +} diff --git a/Sources/LSPServer/Server.RequestHandler.swift b/Sources/LSPServer/Server.RequestHandler.swift new file mode 100644 index 0000000..8d74851 --- /dev/null +++ b/Sources/LSPServer/Server.RequestHandler.swift @@ -0,0 +1,225 @@ +import Foundation +import JSONRPC +import LanguageServerProtocol + + +public protocol RequestHandler : ErrorHandler { + typealias Handler = ClientRequest.Handler; + + func handleRequest(id: JSONId, request: ClientRequest) async + + func initialize(id: JSONId, params: InitializeParams) async -> Result + func shutdown(id: JSONId) async + func workspaceInlayHintRefresh(id: JSONId) async + func workspaceExecuteCommand(id: JSONId, params: ExecuteCommandParams) async -> Result + func workspaceWillCreateFiles(id: JSONId, params: CreateFilesParams) async -> Result + func workspaceWillRenameFiles(id: JSONId, params: RenameFilesParams) async -> Result + func workspaceWillDeleteFiles(id: JSONId, params: DeleteFilesParams) async -> Result + func workspaceSymbol(id: JSONId, params: WorkspaceSymbolParams) async -> Result + func workspaceSymbolResolve(id: JSONId, params: WorkspaceSymbol) async -> Result + func textDocumentWillSaveWaitUntil(id: JSONId, params: WillSaveTextDocumentParams) async -> Result<[TextEdit]?, AnyJSONRPCResponseError> + func completion(id: JSONId, params: CompletionParams) async -> Result + func completionItemResolve(id: JSONId, params: CompletionItem) async -> Result + func hover(id: JSONId, params: TextDocumentPositionParams) async -> Result + func signatureHelp(id: JSONId, params: TextDocumentPositionParams) async -> Result + func declaration(id: JSONId, params: TextDocumentPositionParams) async -> Result + func definition(id: JSONId, params: TextDocumentPositionParams) async -> Result + func typeDefinition(id: JSONId, params: TextDocumentPositionParams) async -> Result + func implementation(id: JSONId, params: TextDocumentPositionParams) async -> Result + func diagnostics(id: JSONId, params: DocumentDiagnosticParams) async -> Result + func documentHighlight(id: JSONId, params: DocumentHighlightParams) async -> Result + func documentSymbol(id: JSONId, params: DocumentSymbolParams) async -> Result + func codeAction(id: JSONId, params: CodeActionParams) async -> Result + func codeActionResolve(id: JSONId, params: CodeAction) async -> Result + func codeLens(id: JSONId, params: CodeLensParams) async -> Result + func codeLensResolve(id: JSONId, params: CodeLens) async -> Result + func selectionRange(id: JSONId, params: SelectionRangeParams) async -> Result + func linkedEditingRange(id: JSONId, params: LinkedEditingRangeParams) async -> Result + func prepareCallHierarchy(id: JSONId, params: CallHierarchyPrepareParams) async -> Result + func prepareRename(id: JSONId, params: PrepareRenameParams) async -> Result + func rename(id: JSONId, params: RenameParams) async -> Result + func documentLink(id: JSONId, params: DocumentLinkParams) async -> Result + func documentLinkResolve(id: JSONId, params: DocumentLink) async -> Result + func documentColor(id: JSONId, params: DocumentColorParams) async -> Result + func colorPresentation(id: JSONId, params: ColorPresentationParams) async -> Result + func formatting(id: JSONId, params: DocumentFormattingParams) async -> Result + func rangeFormatting(id: JSONId, params: DocumentRangeFormattingParams) async -> Result + func onTypeFormatting(id: JSONId, params: DocumentOnTypeFormattingParams) async -> Result + func references(id: JSONId, params: ReferenceParams) async -> Result + func foldingRange(id: JSONId, params: FoldingRangeParams) async -> Result + func moniker(id: JSONId, params: MonikerParams) async -> Result + func semanticTokensFull(id: JSONId, params: SemanticTokensParams) async -> Result + func semanticTokensFullDelta(id: JSONId, params: SemanticTokensDeltaParams) async -> Result + func semanticTokensRange(id: JSONId, params: SemanticTokensRangeParams) async -> Result + func callHierarchyIncomingCalls(id: JSONId, params: CallHierarchyIncomingCallsParams) async -> Result + func callHierarchyOutgoingCalls(id: JSONId, params: CallHierarchyOutgoingCallsParams) async -> Result + func custom(id: JSONId, method: String, params: LSPAny) async -> Result +} + + +public extension RequestHandler { + func handleRequest(id: JSONId, request: ClientRequest) async { + await defaultRequestDispatch(id: id, request: request) + } + + func defaultRequestDispatch(id: JSONId, request: ClientRequest) async { + + switch request { + case let .initialize(params, handler): + await handler(await initialize(id: id, params: params)) + case let .shutdown(handler): + await shutdown(id: id) + await handler(.success(nil)) + case let .workspaceInlayHintRefresh(handler): + await workspaceInlayHintRefresh(id: id) + await handler(.success(nil)) + case let .workspaceExecuteCommand(params, handler): + await handler(await workspaceExecuteCommand(id: id, params: params)) + case let .workspaceWillCreateFiles(params, handler): + await handler(await workspaceWillCreateFiles(id: id, params: params)) + case let .workspaceWillRenameFiles(params, handler): + await handler(await workspaceWillRenameFiles(id: id, params: params)) + case let .workspaceWillDeleteFiles(params, handler): + await handler(await workspaceWillDeleteFiles(id: id, params: params)) + case let .workspaceSymbol(params, handler): + await handler(await workspaceSymbol(id: id, params: params)) + case let .workspaceSymbolResolve(params, handler): + await handler(await workspaceSymbolResolve(id: id, params: params)) + case let .textDocumentWillSaveWaitUntil(params, handler): + await handler(await textDocumentWillSaveWaitUntil(id: id, params: params)) + case let .completion(params, handler): + await handler(await completion(id: id, params: params)) + case let .completionItemResolve(params, handler): + await handler(await completionItemResolve(id: id, params: params)) + case let .hover(params, handler): + await handler(await hover(id: id, params: params)) + case let .signatureHelp(params, handler): + await handler(await signatureHelp(id: id, params: params)) + case let .declaration(params, handler): + await handler(await declaration(id: id, params: params)) + case let .definition(params, handler): + await handler(await definition(id: id, params: params)) + case let .typeDefinition(params, handler): + await handler(await typeDefinition(id: id, params: params)) + case let .implementation(params, handler): + await handler(await implementation(id: id, params: params)) + case let .diagnostics(params, handler): + await handler(await diagnostics(id: id, params: params)) + case let .documentHighlight(params, handler): + await handler(await documentHighlight(id: id, params: params)) + case let .documentSymbol(params, handler): + await handler(await documentSymbol(id: id, params: params)) + case let .codeAction(params, handler): + await handler(await codeAction(id: id, params: params)) + case let .codeActionResolve(params, handler): + await handler(await codeActionResolve(id: id, params: params)) + case let .codeLens(params, handler): + await handler(await codeLens(id: id, params: params)) + case let .codeLensResolve(params, handler): + await handler(await codeLensResolve(id: id, params: params)) + case let .selectionRange(params, handler): + await handler(await selectionRange(id: id, params: params)) + case let .linkedEditingRange(params, handler): + await handler(await linkedEditingRange(id: id, params: params)) + case let .prepareCallHierarchy(params, handler): + await handler(await prepareCallHierarchy(id: id, params: params)) + case let .prepareRename(params, handler): + await handler(await prepareRename(id: id, params: params)) + case let .rename(params, handler): + await handler(await rename(id: id, params: params)) + case let .inlayHint(params, handler): + await handler(await inlayHint(id: id, params: params)) + case let .inlayHintResolve(params, handler): + await handler(await inlayHintResolve(id: id, params: params)) + case let .documentLink(params, handler): + await handler(await documentLink(id: id, params: params)) + case let .documentLinkResolve(params, handler): + await handler(await documentLinkResolve(id: id, params: params)) + case let .documentColor(params, handler): + await handler(await documentColor(id: id, params: params)) + case let .colorPresentation(params, handler): + await handler(await colorPresentation(id: id, params: params)) + case let .formatting(params, handler): + await handler(await formatting(id: id, params: params)) + case let .rangeFormatting(params, handler): + await handler(await rangeFormatting(id: id, params: params)) + case let .onTypeFormatting(params, handler): + await handler(await onTypeFormatting(id: id, params: params)) + case let .references(params, handler): + await handler(await references(id: id, params: params)) + case let .foldingRange(params, handler): + await handler(await foldingRange(id: id, params: params)) + case let .moniker(params, handler): + await handler(await moniker(id: id, params: params)) + case let .semanticTokensFull(params, handler): + await handler(await semanticTokensFull(id: id, params: params)) + case let .semanticTokensFullDelta(params, handler): + await handler(await semanticTokensFullDelta(id: id, params: params)) + case let .semanticTokensRange(params, handler): + await handler(await semanticTokensRange(id: id, params: params)) + case let .callHierarchyIncomingCalls(params, handler): + await handler(await callHierarchyIncomingCalls(id: id, params: params)) + case let .callHierarchyOutgoingCalls(params, handler): + await handler(await callHierarchyOutgoingCalls(id: id, params: params)) + case let .custom(method, params, handler): + await handler(await custom(id: id, method: method, params: params)) + } + } +} + + +let NotImplementedError = AnyJSONRPCResponseError(code: ErrorCodes.InternalError, message: "Not implemented") + +/// Provide default implementations for all protocol methods +/// We do this since the handler only need to support a subset, based on dynamically registered capabilities +public extension RequestHandler { + + func initialize(id: JSONId, params: InitializeParams) async -> Result { .failure(NotImplementedError) } + func shutdown(id: JSONId) async { } + func workspaceInlayHintRefresh(id: JSONId) async { } + func workspaceExecuteCommand(id: JSONId, params: ExecuteCommandParams) async -> Result { .failure(NotImplementedError) } + func workspaceWillCreateFiles(id: JSONId, params: CreateFilesParams) async -> Result { .failure(NotImplementedError) } + func workspaceWillRenameFiles(id: JSONId, params: RenameFilesParams) async -> Result { .failure(NotImplementedError) } + func workspaceWillDeleteFiles(id: JSONId, params: DeleteFilesParams) async -> Result { .failure(NotImplementedError) } + func workspaceSymbol(id: JSONId, params: WorkspaceSymbolParams) async -> Result { .failure(NotImplementedError) } + func workspaceSymbolResolve(id: JSONId, params: WorkspaceSymbol) async -> Result { .failure(NotImplementedError) } + func textDocumentWillSaveWaitUntil(id: JSONId, params: WillSaveTextDocumentParams) async -> Result<[TextEdit]?, AnyJSONRPCResponseError> { .failure(NotImplementedError) } + func completion(id: JSONId, params: CompletionParams) async -> Result { .failure(NotImplementedError) } + func completionItemResolve(id: JSONId, params: CompletionItem) async -> Result { .failure(NotImplementedError) } + func hover(id: JSONId, params: TextDocumentPositionParams) async -> Result { .failure(NotImplementedError) } + func signatureHelp(id: JSONId, params: TextDocumentPositionParams) async -> Result { .failure(NotImplementedError) } + func declaration(id: JSONId, params: TextDocumentPositionParams) async -> Result { .failure(NotImplementedError) } + func definition(id: JSONId, params: TextDocumentPositionParams) async -> Result { .failure(NotImplementedError) } + func typeDefinition(id: JSONId, params: TextDocumentPositionParams) async -> Result { .failure(NotImplementedError) } + func implementation(id: JSONId, params: TextDocumentPositionParams) async -> Result { .failure(NotImplementedError) } + func diagnostics(id: JSONId, params: DocumentDiagnosticParams) async -> Result { .failure(NotImplementedError) } + func documentHighlight(id: JSONId, params: DocumentHighlightParams) async -> Result { .failure(NotImplementedError) } + func documentSymbol(id: JSONId, params: DocumentSymbolParams) async -> Result { .failure(NotImplementedError) } + func codeAction(id: JSONId, params: CodeActionParams) async -> Result { .failure(NotImplementedError) } + func codeActionResolve(id: JSONId, params: CodeAction) async -> Result { .failure(NotImplementedError) } + func codeLens(id: JSONId, params: CodeLensParams) async -> Result { .failure(NotImplementedError) } + func codeLensResolve(id: JSONId, params: CodeLens) async -> Result { .failure(NotImplementedError) } + func selectionRange(id: JSONId, params: SelectionRangeParams) async -> Result { .failure(NotImplementedError) } + func linkedEditingRange(id: JSONId, params: LinkedEditingRangeParams) async -> Result { .failure(NotImplementedError) } + func prepareCallHierarchy(id: JSONId, params: CallHierarchyPrepareParams) async -> Result { .failure(NotImplementedError) } + func prepareRename(id: JSONId, params: PrepareRenameParams) async -> Result { .failure(NotImplementedError) } + func rename(id: JSONId, params: RenameParams) async -> Result { .failure(NotImplementedError) } + func inlayHint(id: JSONId, params: InlayHintParams) async -> Result { .failure(NotImplementedError) } + func inlayHintResolve(id: JSONId, params: InlayHint) async -> Result { .failure(NotImplementedError) } + func documentLink(id: JSONId, params: DocumentLinkParams) async -> Result { .failure(NotImplementedError) } + func documentLinkResolve(id: JSONId, params: DocumentLink) async -> Result { .failure(NotImplementedError) } + func documentColor(id: JSONId, params: DocumentColorParams) async -> Result { .failure(NotImplementedError) } + func colorPresentation(id: JSONId, params: ColorPresentationParams) async -> Result { .failure(NotImplementedError) } + func formatting(id: JSONId, params: DocumentFormattingParams) async -> Result { .failure(NotImplementedError) } + func rangeFormatting(id: JSONId, params: DocumentRangeFormattingParams) async -> Result { .failure(NotImplementedError) } + func onTypeFormatting(id: JSONId, params: DocumentOnTypeFormattingParams) async -> Result { .failure(NotImplementedError) } + func references(id: JSONId, params: ReferenceParams) async -> Result { .failure(NotImplementedError) } + func foldingRange(id: JSONId, params: FoldingRangeParams) async -> Result { .failure(NotImplementedError) } + func moniker(id: JSONId, params: MonikerParams) async -> Result { .failure(NotImplementedError) } + func semanticTokensFull(id: JSONId, params: SemanticTokensParams) async -> Result { .failure(NotImplementedError) } + func semanticTokensFullDelta(id: JSONId, params: SemanticTokensDeltaParams) async -> Result { .failure(NotImplementedError) } + func semanticTokensRange(id: JSONId, params: SemanticTokensRangeParams) async -> Result { .failure(NotImplementedError) } + func callHierarchyIncomingCalls(id: JSONId, params: CallHierarchyIncomingCallsParams) async -> Result { .failure(NotImplementedError) } + func callHierarchyOutgoingCalls(id: JSONId, params: CallHierarchyOutgoingCallsParams) async -> Result { .failure(NotImplementedError) } + func custom(id: JSONId, method: String, params: LSPAny) async -> Result { .failure(NotImplementedError) } +} diff --git a/Sources/LanguageServerProtocol/AsyncStreamPolyfill.swift b/Sources/LanguageServerProtocol/AsyncStreamPolyfill.swift new file mode 100644 index 0000000..a23e762 --- /dev/null +++ b/Sources/LanguageServerProtocol/AsyncStreamPolyfill.swift @@ -0,0 +1,45 @@ +import Foundation +#if compiler(<5.9) + +// @available(SwiftStdlib 5.1, *) +extension AsyncStream { + /// Initializes a new ``AsyncStream`` and an ``AsyncStream/Continuation``. + /// + /// - Parameters: + /// - elementType: The element type of the stream. + /// - limit: The buffering policy that the stream should use. + /// - Returns: A tuple containing the stream and its continuation. The continuation should be passed to the + /// producer while the stream should be passed to the consumer. + // @backDeployed(before: SwiftStdlib 5.9) + public static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(bufferingPolicy: limit) { continuation = $0 } + return (stream: stream, continuation: continuation!) + } +} + +// @available(SwiftStdlib 5.1, *) +extension AsyncThrowingStream { + /// Initializes a new ``AsyncThrowingStream`` and an ``AsyncThrowingStream/Continuation``. + /// + /// - Parameters: + /// - elementType: The element type of the stream. + /// - failureType: The failure type of the stream. + /// - limit: The buffering policy that the stream should use. + /// - Returns: A tuple containing the stream and its continuation. The continuation should be passed to the + /// producer while the stream should be passed to the consumer. + // @backDeployed(before: SwiftStdlib 5.9) + public static func makeStream( + of elementType: Element.Type = Element.self, + throwing failureType: Failure.Type = Failure.self, + bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncThrowingStream, continuation: AsyncThrowingStream.Continuation) where Failure == Error { + var continuation: AsyncThrowingStream.Continuation! + let stream = AsyncThrowingStream(bufferingPolicy: limit) { continuation = $0 } + return (stream: stream, continuation: continuation!) + } +} +#endif diff --git a/Sources/LanguageServerProtocol/Client.swift b/Sources/LanguageServerProtocol/Client.swift index 7895d02..bfc6093 100644 --- a/Sources/LanguageServerProtocol/Client.swift +++ b/Sources/LanguageServerProtocol/Client.swift @@ -25,7 +25,7 @@ extension Registration { func decodeServerRegistration() throws -> ServerRegistration { guard let regMethod = ServerRegistration.Method(rawValue: method) else { - throw ServerError.unhandledRegisterationMethod(method) + throw ServerError.unhandleRegistrationMethod(method) } switch regMethod { diff --git a/Sources/LanguageServerProtocol/ErrorCodes.swift b/Sources/LanguageServerProtocol/ErrorCodes.swift new file mode 100644 index 0000000..c436032 --- /dev/null +++ b/Sources/LanguageServerProtocol/ErrorCodes.swift @@ -0,0 +1,91 @@ +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage +public enum ErrorCodes { + public static let ParseError = -32700 + public static let InvalidRequest = -32600 + public static let MethodNotFound = -32601 + public static let InvalidParams = -32602 + public static let InternalError = -32603 + + /** + * This is the start range of JSON-RPC reserved error codes. + * It doesn't denote a real error code. No LSP error codes should + * be defined between the start and end range. For backwards + * compatibility the `ServerNotInitialized` and the `UnknownErrorCode` + * are left in the range. + * + * @since 3.16.0 + */ + public static let jsonrpcReservedErrorRangeStart = -32099 + /** @deprecated use jsonrpcReservedErrorRangeStart */ + public static let serverErrorStart = jsonrpcReservedErrorRangeStart + + /** + * Error code indicating that a server received a notification or + * request before the server has received the `initialize` request. + */ + public static let ServerNotInitialized = -32002 + public static let UnknownErrorCode = -32001 + + /** + * This is the end range of JSON-RPC reserved error codes. + * It doesn't denote a real error code. + * + * @since 3.16.0 + */ + public static let jsonrpcReservedErrorRangeEnd = -32000 + /** @deprecated use jsonrpcReservedErrorRangeEnd */ + public static let serverErrorEnd = jsonrpcReservedErrorRangeEnd + + /** + * This is the start range of LSP reserved error codes. + * It doesn't denote a real error code. + * + * @since 3.16.0 + */ + public static let lspReservedErrorRangeStart = -32899 + + /** + * A request failed but it was syntactically correct, e.g the + * method name was known and the parameters were valid. The error + * message should contain human readable information about why + * the request failed. + * + * @since 3.17.0 + */ + public static let RequestFailed = -32803 + + /** + * The server cancelled the request. This error code should + * only be used for requests that explicitly support being + * server cancellable. + * + * @since 3.17.0 + */ + public static let ServerCancelled = -32802 + + /** + * The server detected that the content of a document got + * modified outside normal conditions. A server should + * NOT send this error code if it detects a content change + * in it unprocessed messages. The result even computed + * on an older state might still be useful for the client. + * + * If a client decides that a result is not of any use anymore + * the client should cancel the request. + */ + public static let ContentModified = -32801 + + /** + * The client has canceled a request and a server as detected + * the cancel. + */ + public static let RequestCancelled = -32800 + + /** + * This is the end range of LSP reserved error codes. + * It doesn't denote a real error code. + * + * @since 3.16.0 + */ + public static let lspReservedErrorRangeEnd = -32800 +} diff --git a/Sources/LanguageServerProtocol/LanguageServerProtocol.swift b/Sources/LanguageServerProtocol/LanguageServerProtocol.swift index 9df0555..139c2b5 100644 --- a/Sources/LanguageServerProtocol/LanguageServerProtocol.swift +++ b/Sources/LanguageServerProtocol/LanguageServerProtocol.swift @@ -1,7 +1,41 @@ import JSONRPC -typealias UnusedResult = String? -typealias UnusedParam = String? +public typealias UnusedResult = String? +public typealias UnusedParam = String? + +public enum ProtocolError: Error { + case unrecognizedMethod(String) + case missingParams + case unhandleRegistrationMethod(String) + case missingReply +} + +// NOTE: We should remove these and only use `ProtocolError`? +public typealias ServerError = ProtocolError +public typealias ClientError = ProtocolError + + +public enum ClientEvent: Sendable { + public typealias RequestResult = Result + public typealias RequestHandler = @Sendable (RequestResult) async -> Void + + case request(id: JSONId, request: ClientRequest) + case notification(ClientNotification) + case error(Error) + // case error(ClientError) +} + + +public enum ServerEvent: Sendable { + public typealias RequestResult = Result + public typealias RequestHandler = @Sendable (RequestResult) async -> Void + + case request(id: JSONId, request: ServerRequest) + case notification(ServerNotification) + case error(Error) + // case error(ServerError) +} + public enum ClientNotification: Sendable, Hashable { public enum Method: String, Hashable, Sendable { @@ -25,15 +59,15 @@ public enum ClientNotification: Sendable, Hashable { case initialized(InitializedParams) case exit - case textDocumentDidChange(DidChangeTextDocumentParams) case textDocumentDidOpen(DidOpenTextDocumentParams) + case textDocumentDidChange(DidChangeTextDocumentParams) case textDocumentDidClose(DidCloseTextDocumentParams) case textDocumentWillSave(WillSaveTextDocumentParams) case textDocumentDidSave(DidSaveTextDocumentParams) case protocolCancelRequest(CancelParams) case protocolSetTrace(SetTraceParams) - case windowWorkDoneProgressCancel(WorkDoneProgressCancelParams) case workspaceDidChangeWatchedFiles(DidChangeWatchedFilesParams) + case windowWorkDoneProgressCancel(WorkDoneProgressCancelParams) case workspaceDidChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams) case workspaceDidChangeConfiguration(DidChangeConfigurationParams) case workspaceDidCreateFiles(CreateFilesParams) @@ -78,8 +112,18 @@ public enum ClientNotification: Sendable, Hashable { } } -public enum ClientRequest: Sendable, Hashable { - public enum Method: String, Hashable, Sendable { +public enum ClientRequest: Sendable { + public typealias Handler = @Sendable (Result) async -> Void + public typealias ErrorOnlyHandler = @Sendable (AnyJSONRPCResponseError?) async -> Void + + // NOTE: The same `ClientRequest` type is used on the client side and the server side, only the server use the handler to send back the response, on the client side we use the `NullHandler`, which will never be called + @Sendable + public static func NullHandler(result: Result) async { + // throw NullHandlerError.notImplemented(result) + } + + + public enum Method: String { case initialize case shutdown case workspaceExecuteCommand = "workspace/executeCommand" @@ -131,54 +175,55 @@ public enum ClientRequest: Sendable, Hashable { case custom } - case initialize(InitializeParams) - case shutdown - case workspaceExecuteCommand(ExecuteCommandParams) - case workspaceInlayHintRefresh - case workspaceWillCreateFiles(CreateFilesParams) - case workspaceWillRenameFiles(RenameFilesParams) - case workspaceWillDeleteFiles(DeleteFilesParams) - case workspaceSymbol(WorkspaceSymbolParams) - case workspaceSymbolResolve(WorkspaceSymbol) - case textDocumentWillSaveWaitUntil(WillSaveTextDocumentParams) - case completion(CompletionParams) - case completionItemResolve(CompletionItem) - case hover(TextDocumentPositionParams) - case signatureHelp(TextDocumentPositionParams) - case declaration(TextDocumentPositionParams) - case definition(TextDocumentPositionParams) - case typeDefinition(TextDocumentPositionParams) - case implementation(TextDocumentPositionParams) - case diagnostics(DocumentDiagnosticParams) - case documentHighlight(DocumentHighlightParams) - case documentSymbol(DocumentSymbolParams) - case codeAction(CodeActionParams) - case codeActionResolve(CodeAction) - case codeLens(CodeLensParams) - case codeLensResolve(CodeLens) - case selectionRange(SelectionRangeParams) - case linkedEditingRange(LinkedEditingRangeParams) - case moniker(MonikerParams) - case prepareCallHierarchy(CallHierarchyPrepareParams) - case prepareRename(PrepareRenameParams) - case rename(RenameParams) - case inlayHint(InlayHintParams) - case inlayHintResolve(InlayHint) - case documentLink(DocumentLinkParams) - case documentLinkResolve(DocumentLink) - case documentColor(DocumentColorParams) - case colorPresentation(ColorPresentationParams) - case formatting(DocumentFormattingParams) - case rangeFormatting(DocumentRangeFormattingParams) - case onTypeFormatting(DocumentOnTypeFormattingParams) - case references(ReferenceParams) - case foldingRange(FoldingRangeParams) - case semanticTokensFull(SemanticTokensParams) - case semanticTokensFullDelta(SemanticTokensDeltaParams) - case semanticTokensRange(SemanticTokensRangeParams) - case callHierarchyIncomingCalls(CallHierarchyIncomingCallsParams) - case callHierarchyOutgoingCalls(CallHierarchyOutgoingCallsParams) - case custom(String, LSPAny) + case initialize(InitializeParams, Handler) + case shutdown(Handler) + case workspaceExecuteCommand(ExecuteCommandParams, Handler) + case workspaceInlayHintRefresh(Handler) + case workspaceWillCreateFiles(CreateFilesParams, Handler) + case workspaceWillRenameFiles(RenameFilesParams, Handler) + case workspaceWillDeleteFiles(DeleteFilesParams, Handler) + case workspaceSymbol(WorkspaceSymbolParams, Handler) + case workspaceSymbolResolve(WorkspaceSymbol, Handler) + case textDocumentWillSaveWaitUntil(WillSaveTextDocumentParams, Handler<[TextEdit]?>) + case completion(CompletionParams, Handler) + case completionItemResolve(CompletionItem, Handler) + case hover(TextDocumentPositionParams, Handler) + case signatureHelp(TextDocumentPositionParams, Handler) + case declaration(TextDocumentPositionParams, Handler) + case definition(TextDocumentPositionParams, Handler) + case typeDefinition(TextDocumentPositionParams, Handler) + case implementation(TextDocumentPositionParams, Handler) + case diagnostics(DocumentDiagnosticParams, Handler) + case documentHighlight(DocumentHighlightParams, Handler) + case documentSymbol(DocumentSymbolParams, Handler) + case codeAction(CodeActionParams, Handler) + case codeActionResolve(CodeAction, Handler) + case codeLens(CodeLensParams, Handler) + case codeLensResolve(CodeLens, Handler) + case selectionRange(SelectionRangeParams, Handler) + case linkedEditingRange(LinkedEditingRangeParams, Handler) + case prepareCallHierarchy(CallHierarchyPrepareParams, Handler) + case prepareRename(PrepareRenameParams, Handler) + case rename(RenameParams, Handler) + case inlayHint(InlayHintParams, Handler) + case inlayHintResolve(InlayHint, Handler) + case documentLink(DocumentLinkParams, Handler) + case documentLinkResolve(DocumentLink, Handler) + case documentColor(DocumentColorParams, Handler) + case colorPresentation(ColorPresentationParams, Handler) + case formatting(DocumentFormattingParams, Handler) + case rangeFormatting(DocumentRangeFormattingParams, Handler) + case onTypeFormatting(DocumentOnTypeFormattingParams, Handler) + case references(ReferenceParams, Handler) + case foldingRange(FoldingRangeParams, Handler) + // case semanticTokens(SemanticTokensParams, Handler) + case moniker(MonikerParams, Handler) + case semanticTokensFull(SemanticTokensParams, Handler) + case semanticTokensFullDelta(SemanticTokensDeltaParams, Handler) + case semanticTokensRange(SemanticTokensRangeParams, Handler) + case callHierarchyIncomingCalls(CallHierarchyIncomingCallsParams, Handler) + case callHierarchyOutgoingCalls(CallHierarchyOutgoingCallsParams, Handler) + case custom(String, LSPAny, Handler) public var method: Method { switch self { @@ -234,8 +279,6 @@ public enum ClientRequest: Sendable, Hashable { return .textDocumentSelectionRange case .linkedEditingRange: return .textDocumentLinkedEditingRange - case .moniker: - return .textDocumentMoniker case .prepareCallHierarchy: return .textDocumentPrepareCallHierarchy case .prepareRename: @@ -266,6 +309,8 @@ public enum ClientRequest: Sendable, Hashable { return .textDocumentReferences case .foldingRange: return .textDocumentFoldingRange + case .moniker: + return .textDocumentMoniker case .semanticTokensFull: return .textDocumentSemanticTokensFull case .semanticTokensFullDelta: @@ -323,6 +368,7 @@ public enum ServerNotification: Sendable, Hashable { public enum ServerRequest: Sendable { public typealias Handler = @Sendable (Result) async -> Void + public typealias VoidHandler = @Sendable () async -> Void public typealias ErrorOnlyHandler = @Sendable (AnyJSONRPCResponseError?) async -> Void public enum Method: String { diff --git a/Tests/LanguageServerProtocolTests/ServerTests.swift b/Tests/LanguageServerProtocolTests/ServerTests.swift index afb971e..629d535 100644 --- a/Tests/LanguageServerProtocolTests/ServerTests.swift +++ b/Tests/LanguageServerProtocolTests/ServerTests.swift @@ -1,5 +1,6 @@ import XCTest import LanguageServerProtocol +import LSPClient final class ServerTests: XCTestCase { func testNonOptionalSend() async throws {