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. Capabilities are auto-derived from the
LanguageServer trait, the default layer stack installs lifecycle, panic
catching, $/cancelRequest routing, bounded concurrency, and tracing
spans, and outgoing helpers (publish_diagnostics, show_message,
apply_edit, …) are exposed on the per-request Context every handler
receives.
Status:
0.1.0-alpha.3is the third alpha; the first non-alpha0.1.0release is still planned, gated on theLayer/Servicegeneralization landing. The architecture is scoped inCONTEXT.mdanddocs/adr/; thestdiotransport, theLanguageServertrait, and the basic dispatcher are wired up. Subsequent commits add theLayer/Servicegeneralization, the remaining transports (TCP, WebSocket, worker-channel for WASM), and the full pygls-equivalent outgoing helper coverage.
use lspf::types::{
Diagnostic, DiagnosticSeverity, DidOpenTextDocumentParams, Position,
PublishDiagnosticsParams, Range,
};
use lspf::{Context, LanguageServer};
struct Hello;
impl LanguageServer for Hello {
async fn text_document_did_open(
&self,
ctx: &Context,
params: DidOpenTextDocumentParams,
) {
ctx.publish_diagnostics(PublishDiagnosticsParams {
uri: params.text_document.uri,
version: Some(params.text_document.version),
diagnostics: vec![Diagnostic {
range: Range {
start: Position { line: 0, character: 0 },
end: Position { line: 0, character: 0 },
},
severity: Some(DiagnosticSeverity::INFORMATION),
source: Some("lspf-hello".into()),
message: "lspf saw this document open".into(),
..Diagnostic::default()
}],
});
}
}
#[tokio::main]
async fn main() -> lspf::Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
lspf::stdio(Hello).serve().await
}A runnable copy lives at examples/hello/main.rs.
[dependencies]
lspf = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tracing-subscriber = "0.3"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.
- Async-first. The framework is
async fnend to end; notower::Layerinterop, no sync escape hatch. - Smallest viable server. Implement the
LanguageServertrait, hand the value tolspf::stdio(...), and you have a working LSP server. - Capabilities auto-derived. Each LSP feature is an associated
conston the trait; the framework turns the consts into theServerCapabilitiesresponse for you (ADR 0004). - Composability, on our terms. A focused
Layertrait (narrower thantower::Layer) adds cross-cutting behavior without a third-party dependency on the dispatcher (ADR 0010). - pygls-grade helpers out of the box. The full set of pygls's
outgoing notifications and requests ships as methods on
Context(ADR 0008). - WASM-friendly. The
worker_channeltransport wraps a JSMessagePortfor in-browser Monaco / Theia-web integration (ADR 0011).
The vocabulary below is taken from CONTEXT.md; the
project deliberately standardizes on these terms in the public API and
the docs.
| Term | Meaning |
|---|---|
| Handler | An async fn registered to respond to an LSP method or notification. |
| Built-in handler | A handler the framework ships out of the box (lifecycle, text-document sync). |
| User handler | A handler you register. User handlers override built-ins via registration, not subclassing. |
| Document | A text resource the framework tracks on your behalf (URI, language id, version, contents). |
| Documents | The concurrency-safe handle to every tracked document, available on every handler's Context. |
| Command | A user-registered async closure dispatched on workspace/executeCommand by name. |
| Context | Per-request framework-state handle (Documents, outgoing helpers, request id, tracing span). |
| Transport | The message-framed channel over which LSP JSON-RPC envelopes flow. |
| Layer | A composable wrapper around a Service that adds cross-cutting behavior. |
| Service | The internal abstraction the dispatcher and every Layer implement. |
| Default stack | The built-in set of Layers installed by the transport builders. |
The full design lives next to the code:
CONTEXT.md— domain language and shared vocabulary.docs/adr/— 14 architecture decision records covering async-only runtime, the dispatcher design, capability auto-derivation, the cancellation model, the transport shape, theLayer/Servicegeneralization, and more.
The 0.1.x series works through the ADRs in order. The headline
milestones:
- 0.1.x —
stdiotransport,LanguageServertrait, basic dispatcher, capability auto-derivation,Context-based outgoing helpers (publish_diagnosticsis wired in 0.1.0; the rest of the pygls-equivalent set follows). - 0.2.x —
Layer/Servicegeneralization (ADR 0010), default stack: lifecycle, panic catching,$/cancelRequest, bounded concurrency (64 in-flight by default),tracingspans. - 0.3.x —
tcpandwebsockettransports; concurrent spawn-based dispatch. - 0.4.x —
worker_channeltransport for WASM-in-browser; full pygls-equivalent outgoing helper coverage onContext.
Run the hello example against a real editor, or point any LSP-aware tool at the spawned process:
cargo run --example helloMore examples land as the framework grows.
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.
To generate a local HTML coverage report, run:
cargo coverageThen open target/coverage/html/index.html. CI also uploads the
report as an artifact on every pull request and main push.
Dual-licensed under either of
at your option.