Skip to content

Commit

Permalink
More docs on how to use the servers
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Nov 30, 2023
1 parent 6e59719 commit 681fee0
Showing 1 changed file with 115 additions and 35 deletions.
150 changes: 115 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

0 comments on commit 681fee0

Please sign in to comment.