diff --git a/README.md b/README.md index a695fee..44a742f 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,46 @@ # LanguageClient -This is a Swift library for abstracting and interacting with language servers that implement the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). It is built on top of the [LanguageServerProtocol](https://github.com/ChimeHQ/LanguageServerProtocol) library. +This is a Swift library for abstracting and interacting with language servers that implement the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). It is built on top of the [LanguageServerProtocol][languageserverprotocol] library. ## General Design -This library is all based around the `Server` protocol from the LanguageServerProtocol library. The idea is to wrap up and expose progressively more-complex behavior. This helps to keep things manageable, while also offering lower-complexity types for less-demanding needs. It was also just the first thing I tried that worked out reasonably well. +This library is all based around the `Server` protocol from LanguageServerProtocol. The idea is to wrap up and expose progressively more-complex behavior. This helps to keep things manageable, while also offering lower-complexity types for less-demanding needs. It was also just the first thing I tried that worked out reasonably well. + +### Environment + +Setting correct environment variables is often critical for a language server. An executable on macOS will **not** inherent the user's shell environment. Capturing shell environment variables is tricky business. Despite its name, `ProcessInfo.processInfo.userEnvironment` captures the `process` environment, not the user's. + +If you need help here, check out [ProcessEnv](https://github.com/chimehq/processenv). + +### Message Ordering + +The Language Server protocol is stateful. Some message types are order-dependent. This is something you must be aware of when working with `async` methods. I have found a queue to be essential. Here's [one](https://github.com/mattmassicotte/Queue), if you find yourself looking. ## Usage +For details on how to respond to server requests and notifications, check out the [LanguageServerProtocol][languageserverprotocol] library documenation. + ### Local Process This is how you run a local server with not extra funtionality. It uses an extension on the [JSONRPC](https://github.com/ChimeHQ/JSONRPC) `DataChannel` type to start up and communicate with a long-running process. ```swift -let params = Process.ExecutionParameters(path: "/path/to/server-executable", - arguments: [], - environment: []) -let channel = DataChannel.localProcessChannel(parameters: params, terminationHandler: { print("terminated") }) -let server = JSONRPCServer(dataChannel: channel) +// Set up parameters to launch the server process +let params = Process.ExecutionParameters( + path: "/path/to/server-executable", + arguments: [], + environment: ProcessInfo.processInfo.userEnvironment +) + +// create a DataChannel to handle communication +let channel = try DataChannel.localProcessChannel( + parameters: params, + terminationHandler: { print("terminated") } +) + +// finally, make a server you can interact with +let server = JSONRPCServerConnection(dataChannel: channel) ``` ### InitializingServer @@ -34,11 +56,21 @@ import LanguageServerProtocol import LSPClient import Foundation -let executionParams = Process.ExecutionParameters(path: "/usr/bin/sourcekit-lsp", environment: ProcessInfo.processInfo.userEnvironment) +let executionParams = Process.ExecutionParameters( + path: "/usr/bin/sourcekit-lsp", + environment: ProcessInfo.processInfo.userEnvironment +) + +let channel = try DataChannel.localProcessChannel( + parameters: executionParams, + terminationHandler: { print("terminated") } +) -let channel = DataChannel.localProcessChannel(parameters: executionParams, terminationHandler: { print("terminated") }) let localServer = JSONRPCServerConnection(dataChannel: channel) +let docURL = URL(fileURLWithPath: "/path/to/your/test.swift") +let projectURL = docURL.deletingLastPathComponent() + let provider: InitializingServer.InitializeParamsProvider = { // you may need to fill in more of the textDocument field for completions // to work, depending on your server @@ -47,64 +79,110 @@ 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.path(percentEncoded: false), initializationOptions: nil, capabilities: capabilities, trace: nil, workspaceFolders: nil) } -let server = InitializingServer(server: localServer, initializeParamsProvider: provider) -let docURL = URL(fileURLWithPath: "/path/to/your/test.swift") -let projectURL = docURL.deletingLastPathComponent() +let server = InitializingServer(server: localServer, initializeParamsProvider: provider) Task { let docContent = try String(contentsOf: docURL) - - let doc = TextDocumentItem(uri: docURL.absoluteString, - languageId: .swift, - version: 1, - text: docContent) + + let doc = TextDocumentItem( + uri: docURL.absoluteString, + languageId: .swift, + version: 1, + text: docContent + ) + let docParams = DidOpenTextDocumentParams(textDocument: doc) - + try await server.textDocumentDidOpen(params: docParams) - + // make sure to pick a reasonable position within your test document let pos = Position(line: 5, character: 25) - let completionParams = CompletionParams(uri: docURL.absoluteString, - position: pos, - triggerKind: .invoked, - triggerCharacter: nil) + let completionParams = CompletionParams( + uri: docURL.absoluteString, + position: pos, + triggerKind: .invoked, + triggerCharacter: nil + ) + let completions = try await server.completion(params: completionParams) - + print("completions: ", completions) } ``` ### RestartingServer -`Server` wrapper that provides transparent server-side state restoration should the underlying process crash. It uses `InitializingServer` internally. +`Server` wrapper that provides transparent server-side state restoration should the underlying process crash. It uses `InitializingServer` internally. Using this type is the most-involved, because it needs to be able to query the current state of the project editor to do its state restoration. -### FileEventAsyncSequence +```swift +let executionParams = Process.ExecutionParameters( + path: "/usr/bin/sourcekit-lsp", + environment: ProcessInfo.processInfo.userEnvironment +) + +let projectURL = URL(fileURLWithPath: "path/to/open/project") + +let serverProvider: MyServer.ServerProvider = { + let channel = try DataChannel.localProcessChannel( + parameters: executionParams, + terminationHandler: { print("terminated") } + ) + + return JSONRPCServerConnection(dataChannel: channel) +} -An `AsyncSequence` that uses FS events and glob patterns to handle `DidChangeWatchedFiles`. It is available only for macOS. +let openDocumentProvider: MyServer.TextDocumentItemProvider = { uri in + // you will have to use the provided uri to look up the actual content of the real document + return TextDocumentItem( + uri: uri, + languageId: "swift", + version: 1, + text: "contents of file" + ) +} -## Environment +let paramProvider: InitializingServer.InitializeParamsProvider = { + // most of these are placeholders, you will probably need more configuration + let capabilities = ClientCapabilities(workspace: nil, + textDocument: nil, + window: nil, + general: nil, + experimental: nil) + + return InitializeParams(processId: Int(ProcessInfo.processInfo.processIdentifier), + locale: nil, + rootPath: nil, + rootUri: projectURL.path(percentEncoded: false), + initializationOptions: nil, + capabilities: capabilities, + trace: nil, + workspaceFolders: nil) +} -Setting correct environment variables can be critical for a language server. An executable on macOS will **not** inherent the user's shell environment. Capturing shell environment variables is tricky business. Despite its name, `ProcessInfo.processInfo.userEnvironment` captures the `process` environment, not the user's. +let config = MyServer.Configuration( + serverProvider: serverProvider, + textDocumentItemProvider: openDocumentProvider, + initializeParamsProvider: paramProvider) -If you need help here, check out [ProcessEnv](https://github.com/chimehq/processenv) and [ProcessService](https://github.com/chimeHQ/ProcessService). +let server = RestartingServer(configuration: config) +``` -## Message Ordering +### FileEventAsyncSequence -The Language Server protocol is stateful. Some message types are order-dependent. This is something you must be aware of when working with `async` methods. I have found a queue to be essential. Here's [one](https://github.com/mattmassicotte/Queue), if you find yourself looking. +An `AsyncSequence` that uses FS events and glob patterns to handle `DidChangeWatchedFiles`. It is available only for macOS. ## Suggestions or Feedback @@ -118,3 +196,5 @@ Please note that this project is released with a [Contributor Code of Conduct](C [platforms badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FChimeHQ%2FLanguageClient%2Fbadge%3Ftype%3Dplatforms [documentation]: https://swiftpackageindex.com/ChimeHQ/LanguageClient/main/documentation [documentation badge]: https://img.shields.io/badge/Documentation-DocC-blue + +[languageserverprotocol]: https://github.com/ChimeHQ/LanguageServerProtocol