How to talk to a language server
This guide shows how to point an editor built on turbo-core at a language server, so that completion, hover and go-to-definition work. It assumes you have an editor and a server that speaks LSP over standard input and output.
Steps
1. Describe the server in your profile
Server: profile.Server{
Command: "rust-analyzer",
Args: nil,
InstallHint: "rustup component add rust-analyzer",
Dirs: []string{cargoBinDir()},
},
| Field | What it is for |
|---|---|
Command |
the executable's name, looked up on PATH |
Args |
what it is started with — gopls wants serve, rust-analyzer wants nothing |
InstallHint |
one command the user can copy, shown on the status bar when the server is missing |
Dirs |
extra directories to search when PATH has nothing |
Dirs matters more than it looks. Every language's own installer puts binaries somewhere that is very often not on PATH — GOPATH/bin, ~/.cargo/bin — and "completion silently does nothing" is what the user sees when the editor cannot find a server they believe they installed.
2. Say where the project starts
RootMarkers: []string{"Cargo.toml"},
The server is started in the nearest directory at or above the file that holds one of these. A server given the wrong root loads the wrong code and then answers nothing at all, with no error — which is the most confusing way completion can fail.
3. Start it from your command
editor.StartLanguageServer(ctx, app.ProjectRoot(p, files))
defer editor.Language().Stop(context.Background())
Start it after opening the files, not before. The editor announces the documents that are already open on the first turn of its event loop in which the server is ready, which is what makes this order safe.
Variants
A file is created inside the editor
Nothing to do. A window that begins Untitled has no path, so the server hears nothing about it — until the first save, which announces the document rather than merely reporting a write. Completion, hover and diagnostics work in that window from the moment it has a name, with no restart. A Save As under a genuinely new name also closes the old document on the server, so no ghost stays open there.
The server needs configuration
turbo-core answers workspace/configuration with an empty object. If your server insists on settings before it will work, that answer is in lsp.Client.handleRequest and is not yet configurable — say so to your users rather than letting completion fail silently.
There is no server for your language
Leave Server at its zero value. app.Language is a no-op when nothing is connected, so nothing else in the editor has to check: colouring, editing, themes, the file tree and terminal windows all work exactly the same.
The command exists but does not run
This is real and it is worth handling. rustup installs a shim called rust-analyzer whether or not the component is there, and the shim fails only when it is run. Finding the file is not the same as being able to use it; if you check for a server in an installer script, run it rather than stat it.
Verifying it end to end
The test worth writing is the one that types text the server has never seen on disk:
- Write a file that stops short of the thing being completed.
- Open it, then start the server.
- Take one turn of the event loop with
editor.Tick(). - Type the rest into the buffer with
editor.Handle(...). - Ask for a completion.
A fixture that already contains the text proves nothing: the server answers from disk for anything it has not been told is open, so such a test passes whether or not the editor said a word.
Expect to have to ask more than once. rust-analyzer answers an empty list until it has finished loading the workspace, and says so with a $/progress notification this client does not read.
See also
- Every field: profile reference
- What the editor exposes for driving it: app reference
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|