Build language servers in 30 lines of Rust.
lsp-builder wraps tower-lsp into a builder pattern that eliminates the protocol boilerplate. You write the language logic; it handles the LSP plumbing.
| tower-lsp (manual) | lsp-builder |
|---|---|
struct Backend { client: Client, ... }
#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, _: InitializeParams)
-> Result<InitializeResult> {
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(...),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![...]),
..Default::default()
}),
hover_provider: Some(...),
..Default::default()
},
..Default::default()
})
}
async fn completion(&self, params: CompletionParams)
-> Result<Option<CompletionResponse>> {
// 30+ lines of conversion code...
}
async fn hover(&self, params: HoverParams)
-> Result<Option<Hover>> {
// more conversion code...
}
// did_open, did_change, did_close, shutdown...
} |
use lsp_builder::*;
let config = LspBuilder::new("my-lsp")
.with_completions(|ctx| {
vec![Completion::new("hello", "A greeting")]
})
.with_hover(|ctx| {
Some(HoverInfo::new("**hello** — A greeting"))
})
.build();
config.run_stdio().await; |
Add to your Cargo.toml:
[dependencies]
lsp-builder = { git = "https://github.com/billy-and-the-oceans/lsp-builder" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Then write your server:
use lsp_builder::{Completion, CompletionKind, HoverInfo, LspBuilder};
#[tokio::main]
async fn main() {
let keywords = vec![
("let", "Declare a variable"),
("fn", "Declare a function"),
("if", "Conditional branch"),
];
let config = LspBuilder::new("my-lsp")
.with_completions(move |ctx| {
let prefix = ctx.word_at_position().unwrap_or("");
keywords
.iter()
.filter(|(kw, _)| kw.starts_with(prefix))
.map(|(kw, desc)| {
Completion::new(*kw, *desc).with_kind(CompletionKind::Keyword)
})
.collect()
})
.with_hover(|ctx| {
let word = ctx.word_at_position()?;
match word {
"let" => Some(HoverInfo::new("**let** — Bind a value")),
"fn" => Some(HoverInfo::new("**fn** — Define a function")),
_ => None,
}
})
.build();
config.run_stdio().await;
}Run it: cargo run — then point your editor's LSP client at the process.
- Builder pattern — configure only what you need, get sensible defaults for the rest
- Closure API — pass a function for quick prototyping
- Trait object API — implement
CompletionProvider,HoverProvider, etc. for complex logic - Tree-sitter integration — optional incremental parsing with automatic document management
- Thread-safe document store — built-in
DashMap-backed store with parse tree caching - Custom IO —
run_stdio()for standard LSP, orrun_with_streams()for testing/custom transport
| Feature | Closure API | Trait API |
|---|---|---|
| Completions | with_completions() |
with_completion_provider() |
| Hover | with_hover() |
with_hover_provider() |
| Diagnostics | with_diagnostics() |
with_diagnostic_provider() |
| Go to Definition | — | with_definition_provider() |
| Find References | — | with_references_provider() |
| Document Formatting | — | with_formatting_provider() |
If your language has a tree-sitter grammar, lsp-builder handles incremental parsing automatically:
let config = LspBuilder::new("my-lsp")
.with_language(tree_sitter_my_lang::language())
.with_completions(|ctx| {
// ctx.content has the source text
// DocumentStore automatically maintains parse trees
vec![]
})
.build();The DocumentStore detects edit regions between versions and uses tree-sitter's incremental parsing to re-parse only what changed.
lsp-builder powers Kotonoha LSP, a language server for a music notation DSL. It provides completions, hover, diagnostics, and go-to-definition for a custom tree-sitter grammar.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.