lspf 0.1.0-alpha.3

A Rust framework for building extensible LSP language servers
Documentation

lspf

crates.io docs.rs License: MIT OR Apache-2.0

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 and 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

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.

Install

[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).
  • 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).
  • 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_channel transport wraps a JS MessagePort for in-browser Monaco / Theia-web integration (ADR 0011).

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
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.

Architecture

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, the Layer/Service generalization, and more.

Roadmap

The 0.1.x series works through the ADRs in order. The headline milestones:

  • 0.1.xstdio 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.xLayer/Service generalization (ADR 0010), default stack: lifecycle, panic catching, $/cancelRequest, bounded concurrency (64 in-flight by default), tracing spans.
  • 0.3.xtcp and websocket transports; concurrent spawn-based dispatch.
  • 0.4.xworker_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:

cargo run --example hello

More examples land as the framework grows.

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.

To generate a local HTML coverage report, run:

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

at your option.