ruitl-lsp
Language Server Protocol implementation for RUITL templates. Built on
tower-lsp, talks JSON-RPC over
stdio.
What it does
- Diagnostics — parses every
.ruitlon open/change/save. Parser and codegen errors surface astextDocument/publishDiagnosticswith ranges derived from the compiler'sat line L, column Cformat. - Formatting —
textDocument/formattingreturns a singleTextEditreplacing the buffer with canonical output fromruitl_compiler::format::format_source. Clients typically wire this to "format on save". - Completion — triggered on
@(component invocation) and<(HTML tag). Component list comes from the workspace index; HTML tag list is a static HTML5 allowlist. When the cursor sits inside@Component(...)the completion list switches to that component's declared props (with their types in the detail slot). - Hover — hovering
@Componentreferences renders the component's name and full props signature as Markdown. - Go-to-definition — on
@Componentreferences, returns the location of the matchingcomponent Name {}declaration. Works across all open documents via the workspace index.
What it doesn't do (yet)
- Rust-aware completion inside
{...}— needs a rust-analyzer bridge. Explicitly out of scope. - Workspace file discovery — the index only covers documents the
editor has opened. Closed
.ruitlfiles aren't indexed until first open. Good next-pass target (walk workspace oninitialize). - Rename refactor (
textDocument/rename) — feasible atop the symbol index. Not yet wired.
Install
# Installs `ruitl-lsp` binary into ~/.cargo/bin/
Or run from the workspace:
# Binary at target/debug/ruitl-lsp
Editor wiring
Neovim (via nvim-lspconfig)
local lspconfig = require
local configs = require
if not configs.
lspconfig..
vim..
Helix
Add to ~/.config/helix/languages.toml:
[[]]
= "ruitl"
= "source.ruitl"
= ["ruitl"]
= ["ruitl.toml", "Cargo.toml"]
= ["ruitl-lsp"]
[]
= "ruitl-lsp"
VS Code
VS Code needs a thin extension to translate language-id. Minimal wiring
in extension.ts:
import * as vscode from "vscode";
import { LanguageClient, ServerOptions, TransportKind } from "vscode-languageclient/node";
let client: LanguageClient | undefined;
export function activate(ctx: vscode.ExtensionContext) {
const server: ServerOptions = {
command: "ruitl-lsp",
transport: TransportKind.stdio,
};
client = new LanguageClient("ruitl-lsp", "RUITL", server, {
documentSelector: [{ scheme: "file", language: "ruitl" }],
});
client.start();
}
export function deactivate() {
return client?.stop();
}
Pair with a languages contribution in package.json:
"contributes":
Zed
Zed LSPs must be wired through a real extension — bare settings.json
entries don't work. This repo ships a scaffold extension at
zed-extension-ruitl/ that registers the
RUITL language, points at the tree-sitter grammar, and launches
ruitl-lsp.
Install it as a dev extension:
- Build the LSP binary and put it on PATH:
- In Zed, open the command palette and run
zed: install dev extension. Select thezed-extension-ruitl/directory. Zed compiles it to WASM and activates it. - Open a
.ruitlfile. Highlighting + diagnostics + hover + go-to-def should appear.
Full instructions and troubleshooting in the extension's README.
Note: A stray lsp.ruitl-lsp entry in settings.json alone will NOT
work — Zed only routes LSPs to files whose language is registered via an
extension. The grammar + language config must come from the extension
too, not from a raw settings block.
Debugging
- Run the server manually and pipe in a hand-crafted
initializerequest to verify framing:# Then type Content-Length:-framed JSON on stdin. - LSP log messages (
window/logMessage) appear in the editor's language server log view. - The integration test at
ruitl_lsp/tests/stdio_roundtrip.rsis a reference for the expected JSON-RPC traffic.
Contributing
- Unit tests live in
src/lib.rs(mod tests) — they exercise the purediagnose()function without spawning the server. - End-to-end tests live in
tests/stdio_roundtrip.rs. Drive the server through an in-memorytokio::io::duplexpair; no real process spawn needed. - When adding a new notification handler, add both a unit test for the synchronous logic and an integration test that exercises the wire protocol.