| 🛟 Updated. 28d5985 k33g 18h ago | 1 | # How to talk to a language server |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Steps |
| 6 | |
| 7 | ### 1. Describe the server in your profile |
| 8 | |
| 9 | ```go |
| 10 | Server: profile.Server{ |
| 11 | Command: "rust-analyzer", |
| 12 | Args: nil, |
| 13 | InstallHint: "rustup component add rust-analyzer", |
| 14 | Dirs: []string{cargoBinDir()}, |
| 15 | }, |
| 16 | ``` |
| 17 | |
| 18 | | Field | What it is for | |
| 19 | | --- | --- | |
| 20 | | `Command` | the executable's name, looked up on PATH | |
| 21 | | `Args` | what it is started with — gopls wants `serve`, rust-analyzer wants nothing | |
| 22 | | `InstallHint` | one command the user can copy, shown on the status bar when the server is missing | |
| 23 | | `Dirs` | extra directories to search when PATH has nothing | |
| 24 | |
| 25 | `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. |
| 26 | |
| 27 | ### 2. Say where the project starts |
| 28 | |
| 29 | ```go |
| 30 | RootMarkers: []string{"Cargo.toml"}, |
| 31 | ``` |
| 32 | |
| 33 | 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. |
| 34 | |
| 35 | ### 3. Start it from your command |
| 36 | |
| 37 | ```go |
| 38 | editor.StartLanguageServer(ctx, app.ProjectRoot(p, files)) |
| 39 | defer editor.Language().Stop(context.Background()) |
| 40 | ``` |
| 41 | |
| 42 | 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. |
| 43 | |
| 44 | ## Variants |
| 45 | |
| 46 | ### A file is created inside the editor |
| 47 | |
| 📦 Turbo Core — a save that creates a file tells the server (workspace/didChangeWatchedFiles), so moon-lsp diagnoses a new .mbt from its first save 3561e52 k33g 9h ago | 48 | 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. The save that creates the file also tells the server a file appeared (`workspace/didChangeWatchedFiles`): a server that lists a package's files from the directory — moon-lsp — would otherwise know the document and still never diagnose it. A Save As under a genuinely new name also closes the old document on the server, so no ghost stays open there. |
| 🛟 Updated. 28d5985 k33g 18h ago | 49 | |
| 50 | ### The server needs configuration |
| 51 | |
| 52 | 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. |
| 53 | |
| 54 | ### There is no server for your language |
| 55 | |
| 56 | 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. |
| 57 | |
| 58 | ### The command exists but does not run |
| 59 | |
| 60 | 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. |
| 61 | |
| 62 | ## Verifying it end to end |
| 63 | |
| 64 | The test worth writing is the one that types text the server has never seen on disk: |
| 65 | |
| 66 | 1. Write a file that stops short of the thing being completed. |
| 67 | 2. Open it, **then** start the server. |
| 68 | 3. Take one turn of the event loop with `editor.Tick()`. |
| 69 | 4. Type the rest into the buffer with `editor.Handle(...)`. |
| 70 | 5. Ask for a completion. |
| 71 | |
| 72 | 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. |
| 73 | |
| 74 | 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. |
| 75 | |
| 76 | ## See also |
| 77 | |
| 78 | - Every field: [profile reference](../reference/profile.md) |
| 79 | - What the editor exposes for driving it: [app reference](../reference/app.md) |