lspf
A Rust framework for building extensible LSP (Language Server Protocol) language servers.
lspf is async-only and designed so a developer can stand up a working
language server in very little code. You register typed handlers on a
Server, hand it to a transport, and the framework owns the protocol:
lifecycle, document synchronization, cancellation, bounded concurrency,
tracing spans, and typed server-to-client traffic through Client.
Status: early-stage. 0.3 is the current surface of this repository — the sealed feature catalog covering the stable LSP 3.17 features, Commands, the multi-root
Workspace,FileProvider-backed unopened-file lookup, and configurable document synchronization — which the examples below use. It is not published yet: crates.io still carries 0.2, and the changelog records what 0.3 adds on top of it. Hover, completion, and commands were the standard features implemented in 0.2; the first-party TCP, WebSocket, and WASM worker transports are still planned.
Quick start
use Arc;
use ;
use ;
/// Only your own application state — the framework owns the documents, the
/// workspace, and the client, and hands them to handlers through `Context`.
;
/// A standard typed feature. The `features::hover()` descriptor fixes the
/// wire method, this handler's parameter and result types, and the
/// `hoverProvider` capability the server will advertise — all at once.
async
/// A second typed feature; the options supplied here are exactly what the
/// generated `completionProvider` advertises.
async
/// A typed Command, dispatched by name beneath `workspace/executeCommand`.
/// Registering it adds the name to the generated `executeCommandProvider`,
/// in registration order.
async
async
No handwritten ServerCapabilities and no framework change are involved:
the capabilities come from the registrations themselves. A runnable copy of
the complete journey — hover, completion plus resolve, Commands, document
synchronization, multi-root workspace state, and unopened-file lookup — lives
at crates/lspf-hello/src/main.rs, the
installable template server described under Editor setup,
with an end-to-end stdio test beside it. The
features, capabilities, and the workspace
guide walks through each piece.
Install
[]
= "0.2"
= { = "1", = ["macros", "rt-multi-thread"] }
= "0.1"
= { = "0.3", = ["env-filter"] }
0.2 is the latest published release; the quickstart above targets the 0.3
surface this repository carries. Until 0.3 ships, depend on the repository
directly for it:
[]
= { = "https://github.com/meymchen/lspf" }
0.1.x is the older LanguageServer trait API, which 0.2 removes; the
migration guide maps one onto the other.
lspf's own Cargo.toml already pulls in lsp-types, tokio, tracing,
serde, and the rest of the runtime stack, so you only need to opt in to the
tokio features you actually use.
Why lspf
- Async-first. The framework is
async fnend to end; notower::Layerinterop, no sync escape hatch. - Smallest viable server. Register your handlers on
Server::builder, hand the builtServertolspf::stdio(...), and you have a working LSP server. - Framework-owned document state. Incremental text changes are applied
to the concurrency-safe, rope-backed
Documentsthe framework owns before your hook runs; handlers read them through aDocumentsViewthat has no mutation operation. - A multi-root
Workspace. Client announcements — folders, root URI, configuration, trace level — live in one cloneable handle, mutated only by the protocol and read throughContext; unopened files resolve through a configurableFileProvider. - Capabilities that cannot drift.
ServerCapabilitiesare generated from the same registrations that dispatch, so what the server advertises is what it serves; conflicting registrations are build errors, never silent last-write-wins. - Safe concurrent dispatch. Requests and notifications run with a
configurable concurrency limit (64 by default);
$/cancelRequestpropagates through aCancellationToken. - Protocol details handled for you. Lifecycle ordering, JSON-RPC framing, text synchronization, and UTF-8/UTF-16 position negotiation are built in.
- Transport escape hatch.
stdiois provided; implement the publicTransporttraits to embed lspf in tests or another message channel.
Concepts
The vocabulary below is taken from CONTEXT.md; the
project deliberately standardizes on these terms in the public API and
the docs.
| Term | Meaning |
|---|---|
Server |
Owns exactly one LSP connection; built by Server::builder(state) and served over a Transport. |
| Handler | An async function registered for one LSP method. User handlers take priority over the built-ins. |
| Built-in handler | A handler the framework ships. Lifecycle, document sync, and cancellation are protocol built-ins. |
| Post-mutation hook | What registering a built-in document notification records: it observes the mutation, never replaces it. |
Command |
A user closure dispatched by name on workspace/executeCommand. |
Document |
A text resource tracked by the framework: URI, language id, version, and rope-backed contents. |
DocumentsView |
The read-only document handle a handler reaches through ctx.documents(). |
Workspace |
The cloneable handle to the connection's workspace state: folders, configuration, and documents. |
FileProvider |
The configurable resolver for resources that are not open in the editor. |
Context |
The cheap-to-clone framework-state handle every handler receives: documents, workspace, client, scope. |
Client |
The typed handle for server-to-client notifications and requests (ctx.client()). |
CancellationToken |
The cancellation signal passed to request handlers. |
Transport |
A message-framed channel split into reader and writer halves for the protocol engine. |
Outcome |
How one connection ended, returned by serving; it carries the LSP exit code but never exits the process. |
Architecture
The full design lives next to the code:
CONTEXT.md— domain language and shared vocabulary.docs/adr/— 24 architecture decision records covering the async-only runtime, the typed Router and capability catalog, the protocol engine and outbound request broker, the cancellation model, the transport shape, theLayer/Servicestack, position encoding, and more. ADRs describe architectural direction as well as shipped behavior; an accepted ADR does not by itself mean the feature has been implemented.docs/guides/features-and-workspace.md— how to register features, where capabilities come from, who owns the workspace and documents, how Commands dispatch, and howFileProviderconfiguration works. Every example in it compiles as a doctest.
Roadmap
Available today:
stdioplus the public custom-transport interface.- The built
Server: typed requests, notifications, commands, the sealed feature catalog covering the stable LSP 3.17 features, userLayers, and the oneconfigure_initializetransaction. - Lifecycle, incremental or full text-document synchronization, and the post-mutation document hooks.
- The multi-root
Workspace, latest configuration settings, andFileProvider-backed unopened-file lookup. - Typed server-to-client notifications and correlated requests through
Client. - Concurrent dispatch, bounded concurrency, request cancellation, and
tracingspans. - Rope-backed documents with UTF-8/UTF-16 position negotiation.
Planned, without a committed release number:
- First-party TCP, WebSocket, and WASM worker transports.
Examples
Run the template server straight from the workspace, or point any LSP-aware tool at the spawned process:
It is the complete typed journey — hover, completion plus resolve, Commands,
document synchronization, multi-root workspace state, and unopened-file
lookup — verified end to end by
crates/lspf-hello/tests/e2e.rs.
To wire it into a real editor instead, see Editor setup.
Editor setup
This repository is a Cargo workspace with two members:
crates/lspf— the framework library you depend on (lspf = "0.2").crates/lspf-hello— an installable template server. It builds alspf-hellobinary that speaks LSP over stdio: it answers hover and completion (with resolve), dispatches thelspf-hello.workspaceRootsandlspf-hello.readFileCommands, reads unopened files through anOsFileProvider, and — on everytextDocument/didOpen— publishes an informational diagnostic ("lspf saw this document open"). Fork it as the starting point for your own language server.
Install the server
This installs the lspf-hello binary into Cargo's bin directory
(~/.cargo/bin by default). Make sure that directory is on your PATH so
your editor can launch the server by name.
VS Code
VS Code has no built-in generic LSP client, so install a thin generic-client
extension such as Generic LSP Client
(v2),
then add this to your settings.json:
Open any plain-text (.txt) file and you should see the
"lspf saw this document open" diagnostic on line 1.
During framework development you can skip the install and use the bundled
tools/vscode-test-clientinstead, which launches the freshly built binary fromtarget/.
Zed
Zed currently requires a language extension to register each language-server
adapter. Its lsp.<name>.binary setting can override the executable for an
adapter that Zed already knows, but it cannot register a new arbitrary server
such as lspf-hello from settings.json alone.
This repository does not yet ship a Zed extension. See Zed's
language extension documentation
to create a development extension that registers lspf-hello, or use the
VS Code test client above for the repository's supported editor smoke-test
path.
Troubleshooting
lspf-hellonot found / "command not found". The binary isn't on yourPATH. Confirmwhich lspf-helloresolves; if not, add~/.cargo/binto yourPATH, or use the absolute path in the editor config above.- The server doesn't start or no diagnostic appears. Make sure you
ran
cargo install --path crates/lspf-helloafter your latest changes, and that your editor client routes the opened file to this server. The example editor setup targets plain-text files; the server itself does not filterdidOpenby language id. Runlspf-helloin a terminal withRUST_LOG=lspf=traceto confirm it starts and to see LSP traffic on stderr. - Edited the config but nothing changed. Editors read LSP settings at
startup — reload the window after editing
settings.json(VS Code: Developer: Reload Window; Zed: reopen the workspace).
Contributing
Issues live on the GitHub tracker at
meymchen/lspf, managed via
gh. Triage uses a fixed label set — needs-triage, needs-info,
ready-for-agent, ready-for-human, wontfix — so an agent or a
human can pick up an issue without re-classifying it.
Before opening a PR, please skim:
CONTEXT.md— make sure the change respects the project's vocabulary.- The relevant
docs/adr/*.md— if the change revisits a decision, either justify the deviation in the PR description or write a new ADR.
Lint all Markdown with the repository's shared configuration (Node.js 24):
Most mechanical Markdown issues can be fixed locally before reviewing the result:
To generate a local HTML coverage report, run:
Then open target/coverage/html/index.html. CI also uploads the
report as an artifact on every pull request and main push.
License
Dual-licensed under either of
at your option.