turbo-editors/turbo-corepublic Fork 0
v0.9.0
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.

README.md · 76 lines · 5.7 KBmarkdown Blame HistoryRaw
🛟 Updated. 28d5985 k33g 16h ago1# lsp
2
3A small Language Server Protocol client: enough of it to ask **gopls** for completions, hovers, definitions and diagnostics.
4
5The protocol is JSON-RPC 2.0 inside HTTP-style frames, which is little enough to implement directly. Doing so keeps the editor's dependency list at two modules and puts the whole conversation somewhere it can be read.
6
7The JSON-RPC half is **[`jsonrpc`](../jsonrpc/)**, shared with the Agent Client Protocol client. What is here is the framing, the methods, and the conversation a language server expects.
8
9## Layers
10
11| File | What it is |
12| --- | --- |
13| `framing.go` | `Content-Length` frames on a stream, and the `Framing` that presents them as a `jsonrpc.Framer` |
14| `conn.go` | The names this package re-exports from `jsonrpc`, and `NewConn` with the framing already chosen |
15| `protocol.go` | The subset of LSP types this editor uses |
16| `client.go` | The conversation: handshake, document sync, completion, hover, definition |
17| `server.go` | Finding gopls and running it as a child process |
18
19`NewClient` takes an `io.ReadWriteCloser`, not a command. That is what lets the entire client be tested against a server **in the same process**, over `net.Pipe` — real framing, real concurrency, real decoding, no subprocess and no timing to get lucky with. `StartServer` is the thin layer that builds that stream out of a child process's pipes.
20
21## Two things that are easy to get wrong
22
23**Columns are UTF-16 code units.** The editor counts columns in runes; the protocol counts them in UTF-16 code units. `RuneToUTF16` and `UTF16ToRune` convert at the boundary. On plain ASCII the two agree, which is exactly why getting this wrong survives testing until someone opens a file with an accent in it.
24
25**gopls asks its client questions.** It requests `workspace/configuration` during start-up and *waits for the reply*, so a client that ignores server-to-client requests never finishes initialising. `jsonrpc.Conn` routes them to a handler, and the client answers with empty settings — "use your defaults". Every such question can be answered on the spot, which is why `answerRequest` replies immediately and the deferred half of `jsonrpc.Request` goes unused here.
26
27## Three shapes for one answer
28
29`textDocument/documentSymbol` may be answered with a nested `DocumentSymbol[]` — a tree, no URI, two ranges per node — or with a flat `SymbolInformation[]`, and `workspace/symbol` adds a third: a symbol whose location has a URI and **no range at all**. All three arrive here and one `Symbol` leaves.
30
31Three details in the flattening are decisions rather than mechanics:
32
33- **The two shapes are told apart by `selectionRange`, not by `children`.** Children are optional, so a file whose symbols happen to have none would be read as flat — and then every symbol would lose its position, silently.
34- **A symbol is located by its `selectionRange`**, the name, not by `range`, the whole declaration. Jumping to a function should put the cursor on the function, not on the doc comment above it.
35- **The nested shape carries no URI**, so the file that was asked about is put back in. Losing it means every jump goes nowhere.
36
37## A missing server is not a failure
38
39`FindServer` looks on `PATH`, then in the directories the profile names — `GOPATH/bin` for Go, `~/.cargo/bin` for Rust — where each language's own installer puts things, and which are very often not on `PATH`. When it finds nothing it returns `ErrServerNotFound` naming the executable and every directory it searched, and the profile's `InstallHint` is the single command a user can copy:
40
41```
42go install golang.org/x/tools/gopls@latest
43```
44
45Editing and colouring work perfectly well without a language server; only completion is lost.
46
47Every request is bounded: `RequestTimeout` (3 s) for the ordinary ones, `InitializeTimeout` (30 s) for the handshake, because a cold gopls has a module graph to load before it can say hello. A server that stops answering slows the editor down but never stops it.
48
49## Public API
50
51| | |
52| --- | --- |
53| `FindServer(server profile.Server) (string, error)` | Where the editor's language server is, or `ErrServerNotFound` |
54| `StartServer(ctx, server, root, clientName) (*Server, error)` | Find it, run it, complete the handshake |
55| `StartServerCommand(ctx, command, args, root, clientName)` | The same with the executable named explicitly |
56| `(*Server) Client() *Client` / `Stop(ctx) error` | |
57| `NewClient(stream, root) *Client` | A client over any stream — what the tests use |
58| `(*Client) Initialize / Run / Ready` | Handshake and read loop |
59| `(*Client) DidOpen / DidChange / DidSave / DidClose` | Document synchronisation, whole-document |
60| `(*Client) Complete / Hover` | The two requests answered with something to read |
61| `(*Client) Definition / TypeDefinition / Implementation / References` | The four answered with places in the code. One decoder serves all four, because the protocol lets a server answer a single location as a bare object rather than an array of one — and several do. |
62| `(*Client) DocumentSymbols / WorkspaceSymbols` | What a file declares, and what a project holds matching a query |
63| `Symbol`, `SymbolKind` | One shape for the three the protocol has |
64| `(*Client) OnDiagnostics`, `OnLog` | What the server says unprompted |
65| `PathToURI` / `URIToPath` | `file://` conversion, Windows drive letters included |
66| `RuneToUTF16` / `UTF16ToRune` | Column conversion |
67| `(CompletionItem) Insertion() string` | The text to insert, snippet placeholders stripped |
68
69## Tests
70
71The suite runs against an in-process fake server, and — when gopls is installed — against the real one:
72
73```sh
74make test # skips the gopls test if it is not installed
75go test -short ./lsp/ # never starts a language server
76```