# lspf
[](https://crates.io/crates/lspf)
[](https://docs.rs/lspf)
[](#license)
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.3` is the third alpha; the first non-alpha
> `0.1.0` release is still planned, gated on the `Layer`/`Service`
> generalization landing. The architecture is scoped in
> [`CONTEXT.md`](./CONTEXT.md) and [`docs/adr/`](./docs/adr/); the `stdio`
> transport, the `LanguageServer` trait, and the basic dispatcher are
> wired up. Subsequent commits add the `Layer`/`Service` generalization,
> the remaining transports (TCP, WebSocket, worker-channel for WASM), and
> the full pygls-equivalent outgoing helper coverage.
## Quick start
```rust
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`](./examples/hello/main.rs).
## Install
```toml
[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.
## Why lspf
- **Async-first.** The framework is `async fn` end to end; no `tower::Layer`
interop, no sync escape hatch.
- **Smallest viable server.** Implement the `LanguageServer` trait, hand
the value to `lspf::stdio(...)`, and you have a working LSP server.
- **Capabilities auto-derived.** Each LSP feature is an associated `const`
on the trait; the framework turns the consts into the
`ServerCapabilities` response for you ([ADR 0004](./docs/adr/0004-capability-auto-derivation.md)).
- **Composability, on our terms.** A focused `Layer` trait (narrower than
`tower::Layer`) adds cross-cutting behavior without a third-party
dependency on the dispatcher ([ADR 0010](./docs/adr/0010-own-layer-trait-not-tower.md)).
- **pygls-grade helpers out of the box.** The full set of pygls's
outgoing notifications and requests ships as methods on `Context`
([ADR 0008](./docs/adr/0008-v1-scope-server-only-pygls-helper-coverage.md)).
- **WASM-friendly.** The `worker_channel` transport wraps a JS
`MessagePort` for in-browser Monaco / Theia-web integration
([ADR 0011](./docs/adr/0011-transport-shape-and-v1-adapters.md)).
## Concepts
The vocabulary below is taken from [`CONTEXT.md`](./CONTEXT.md); the
project deliberately standardizes on these terms in the public API and
the docs.
| 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 `Layer`s installed by the transport builders. |
## Architecture
The full design lives next to the code:
- [`CONTEXT.md`](./CONTEXT.md) — domain language and shared vocabulary.
- [`docs/adr/`](./docs/adr/) — 14 architecture decision records covering
async-only runtime, the dispatcher design, capability auto-derivation,
the cancellation model, the transport shape, the `Layer`/`Service`
generalization, and more.
## Roadmap
The `0.1.x` series works through the ADRs in order. The headline
milestones:
- **0.1.x** — `stdio` transport, `LanguageServer` trait, basic
dispatcher, capability auto-derivation, `Context`-based outgoing
helpers (`publish_diagnostics` is wired in 0.1.0; the rest of the
pygls-equivalent set follows).
- **0.2.x** — `Layer`/`Service` generalization (ADR 0010), default
stack: lifecycle, panic catching, `$/cancelRequest`, bounded
concurrency (64 in-flight by default), `tracing` spans.
- **0.3.x** — `tcp` and `websocket` transports; concurrent
spawn-based dispatch.
- **0.4.x** — `worker_channel` transport for WASM-in-browser; full
pygls-equivalent outgoing helper coverage on `Context`.
## Examples
Run the hello example against a real editor, or point any LSP-aware tool
at the spawned process:
```bash
cargo run --example hello
```
More examples land as the framework grows.
## Contributing
Issues live on the GitHub tracker at
[meymchen/lspf](https://github.com/meymchen/lspf/issues), 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`](./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:
```bash
cargo coverage
```
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
- [Apache License, Version 2.0](./LICENSE-APACHE)
- [MIT License](./LICENSE-MIT)
at your option.