turbo-editors/turbo-corepublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on v1.0.1 · k33g · 18h ago
talk-to-a-language-server.md · 79 lines · 4.0 KBmarkdown
Blame HistoryOpen raw

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:

  1. Write a file that stops short of the thing being completed.
  2. Open it, then start the server.
  3. Take one turn of the event loop with editor.Tick().
  4. Type the rest into the buffer with editor.Handle(...).
  5. 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

 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
# 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

```go
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

```go
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

```go
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:

1. Write a file that stops short of the thing being completed.
2. Open it, **then** start the server.
3. Take one turn of the event loop with `editor.Tick()`.
4. Type the rest into the buffer with `editor.Handle(...)`.
5. 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](../reference/profile.md)
- What the editor exposes for driving it: [app reference](../reference/app.md)