Language servers
Development reference · View source
Setting up a server
rust-analyzer is configured out of the box. For another language:
- Install the server so it is on
PATH. - Add it under
lspin the configuration file. - Run
:config-reload. - Make sure the workspace allows language servers: answer the prompt shown
when it first opened, or run
:lsp-trust(see Permission to run servers ). A reload does not ask again.
lsp:
enable: true
markdown:
command: marksman
args: ["server"]
| Field | Meaning |
|---|---|
command | Executable name or absolute path |
args | Optional list of arguments. The server is started directly, not through a shell. |
initialization_options | Optional; passed verbatim as initializationOptions |
Language keys are Runyte’s language names, so a buffer’s language is the
same question for highlighting and for LSP: rust, python, swift, c,
cpp, javascript, typescript, tsx, html, css, go, bash, java,
kotlin, json, jsonc, sql, lua, c-sharp, zig, cmake, proto,
make, ini, toml, yaml, and markdown. Other keys under lsp are
rejected, apart from enable and the servers wrapper.
lsp.servers.<language>is an accepted compatibility spelling oflsp.<language>, which is preferred.- Copy-ready examples for the servers in Runyte’s compatibility tests are in docs/lsp/ .
- Server definitions are YAML-only.
Space o ocan only turn LSP on or off as a whole. :help lsprepeats this setup inside the editor.
Reloading and restarting:
| Command | Effect |
|---|---|
:config-reload | Adopt new definitions; restart only languages whose definition changed |
:lsp-restart [language] | Restart a stopped server from the configuration already loaded. It does not re-read YAML, so it is the wrong command after editing the file. |
:lsp-status (Space l ?) | Report servers that started or failed. The report replaces the command’s echo while that echo is still current; :not keeps the full report. |
:service-health | Show whether the active document has a configured and attached server |
lsp.enable is decided at startup. To turn LSP on or off, reopen an ide-mode
Runyte, or restart a persistent session with
runyte --session-restart [WORKSPACE] (passing the same --config PATH if
the host used a non-default one).
Failures cost only the language features. A server that is missing,
crashes, or answers slowly never makes the editor wait. Diagnostics from a
stopped server are dropped rather than left as stale claims. A launch error
appears in :lsp-status after the first start attempt and in the
notification center.
Permission to run servers
The first time a workspace opens with LSP enabled, Runyte asks whether language servers may run there.
| Choice | Lasts |
|---|---|
| Keep LSP disabled (default) | — |
| Allow LSP once | Until the ide-mode editor or persistent host stops |
| Always allow LSP | Remembered for this exact workspace |
| Escape | Dismisses without changing permission; on a first visit LSP stays off |
Editing, search, and highlighting work either way.
Why it asks. Language servers can run project code — build scripts, plugins — with your permissions. Approval covers every configured server, custom ones included, and future code changes in the workspace. It is permission to run those tools, not a security review or a sandbox.
Changing your mind. :lsp-trust reopens the choices, including
revocation.
- Disabling LSP there stops this host’s servers and clears their editor state. It cannot undo code already run.
- A persistent host owns the permission, so detaching and reattaching cannot bypass it. Other running editors keep their own decision until changed or restarted.
- Setting
lsp.enable: falsesuppresses the question and keeps approval from starting LSP.
Where decisions are stored. Private lsp-trust/ storage under Runyte’s
per-user platform cache, separate from .runyte/.
- Clearing that cache makes Runyte ask again.
- A workspace rooted at your home directory may use Runyte’s standard cache under it; other project-local cache overrides are rejected.
- Records use the canonical workspace root: symlinked aliases share a decision, but nested workspaces, other worktrees, and clones elsewhere do not.
When storage fails:
- An unreadable decision keeps LSP disabled and asks again.
- If storage is unavailable, the prompt explains why and offers only Keep LSP disabled for now and Allow LSP once.
- A failed save keeps the prompt open with the error and those temporary choices.
- Keep LSP disabled for now stops this editor’s servers without changing any remembered decision.
- Allow LSP once needs no new storage, but first removes an older remembered decision; if that fails, the choice stays pending. The next launch asks again.
- Reopening
:lsp-trustretries the store once it is repaired.
What a server is asked
Runyte only sends requests for capabilities the server advertised in its handshake. Hover, completion, signature help, goto (definition, declaration, type definition, implementation), references, document and workspace symbols, rename, code actions, and formatting are gated independently.
- Asking for one the server never advertised reports it as unavailable on the interaction line. It is not kept as a notification, since it is expected rather than a fault. Typing near a trigger character a server does not support costs no round trip.
- A
Method not foundfrom a server that did advertise the capability is a protocol violation, reported and kept as an ERROR.
Trigger characters come from the server too:
| Server | Completion after | Signature help |
|---|---|---|
| clangd | /, " | Seven delimiters |
| Pyright | [, " | Among its characters, names ) but answers nothing there, closing the popup |
| rust-analyzer | ' | |
| gopls | Two delimiters | |
| sourcekit-lsp, Marksman | None | |
| A server advertising the capability but naming no characters | ., : | (, , |
- The table names only the characters discussed here, not each server’s full list; a blank cell says nothing.
- Some servers name the closing
), so the inner)off(g(a), b)asks again instead of dismissing the popup; clangd and typescript-language-server answer with the enclosing signature. - A server may name retrigger characters, active only while a popup shows.
Runyte advertises the
contextSupportthat allows them and tells the server which character asked and whether a popup was open. - A server naming
)neither way has the popup closed locally. tests/lsp_real_servers.rsrecords what each server in the compatibility matrix does.
Edits from a server
- Servers may not create, rename, or delete files. Such operations in a workspace edit are reported and skipped, and files a rename touches are opened as buffers rather than written behind your back.
- Text edits must name absolute local files inside the project and exact, forward-ordered character boundaries. A malformed, remote, out-of-range, or overlapping edit rejects the whole multi-file change.
- Open target buffers are checked against the revisions the request was made from, including targets other than the source buffer.
- Versioned diagnostics are ignored once their document has moved on.
Compatibility tests
An opt-in Docker matrix tests real servers: Python (Pyright), Swift (SourceKit-LSP), C and C++ (clangd), JavaScript (typescript-language-server), Go (gopls), Rust (rust-analyzer), and Markdown (Marksman).
tests/lsp/run.sh
- The image pins every server toolchain. Each test uses Runyte’s production stdio transport for the full handshake, in a disposable project.
- After initial symbol and definition checks, each sends an unsaved incremental change with non-ASCII text and resolves a definition that exists only in the changed document.
- Fixtures then exercise completion, hover, signature help, references,
rename, formatting, code actions, and diagnostics, as far as each pinned
server meaningfully implements them.
tests/lsp_real_servers.rsmarks each feature as tested, advertised-only, or unsupported per server. - Rust, Go, and JavaScript also resolve definitions across files.
- Diagnostic fixtures check the reported range and the publication that clears it after a fix.
- Ordinary
cargo testskips the matrix: building its image downloads several large toolchains, and running it starts eight real servers.