HN
Today

Why building a Rust LSP is hard

Building a Rust Language Server Protocol (LSP) is a notoriously difficult task, often involving a "guess game" of user intent rather than strict compiler logic. This post delves into the architectural complexities, comparing rust-analyzer's incremental approach with Rust Glancer's eager indexing. It highlights how challenges like partial information, concurrency, and handling imperfect code make LSP development a fascinating, if arduous, endeavor.

3
Score
0
Comments
#7
Highest Rank
7h
on Front Page
First Seen
Sep 19, 2:00 AM
Last Seen
Sep 19, 8:00 AM
Rank Over Time
1877981012

The Lowdown

The author, building the experimental Rust LSP "Rust Glancer," offers an insightful architectural overview into the myriad complexities of developing a language server for Rust. Drawing inspiration from matklad's pioneering work on rust-analyzer, the post explains why tasks that seem simple often prove deeply challenging, requiring ingenious solutions to provide useful feedback from partial or constantly changing information.

  • Initialization Dilemma: LSPs face an immediate challenge upon initialize request: how to provide useful responses without blocking for extensive indexing, leading to varied strategies for quick, partial initialization.
  • The Compiler-LSP Divide: Unlike compilers that require complete, correct code, LSPs must deliver real-time, useful feedback on incomplete and often syntactically incorrect code, making "done" a fluid concept.
  • Managing State and Concurrency: Handling unsaved files, external edits, and race conditions necessitates virtual file systems, "source generations," and parallel query execution with cancellation mechanisms.
  • Layered Indexing: Different LSP features demand varying degrees of semantic understanding. Simple requests like documentSymbol might only need parsing, while hover requires definition maps, and references demands full workspace analysis, including type inference.
  • Architectural Philosophies: The post contrasts rust-analyzer's salsa-driven incremental, on-demand computation with Rust Glancer's eager, disk-persisted indexing, each optimizing for different priorities like responsiveness or instant restarts.
  • Workspace Discovery Woes: Determining project boundaries and user intent (e.g., whether to index multiple workspaces in a folder) is ambiguous, leading to distinct approaches in how LSPs discover and analyze codebases.
  • Protocol Peculiarities: The LSP's reliance on lines, columns, and UTF-16, coupled with requirements for handling line endings and Rust editions, forces implementation details like line indexes and necessitates working with various metadata.
  • Indexing Stages: A high-level breakdown of the necessary steps includes parsing, item tree building, definition map construction (with macro resolution), item index linking, and the complex body resolution involving type inference and trait solving.
  • The Inevitable Guessing Game: LSPs must anticipate user intent, suggesting completions or corrections for incomplete code (e.g., fn fo becoming fn foo() {}), extending beyond strict compiler diagnostics to provide a fluent user experience.
  • Cursor Centrality: All LSP interactions revolve around the cursor's position, requiring sophisticated logic to determine the semantic element under it and generate context-aware suggestions like dot completions or field suggestions within structs.

Ultimately, building a robust Rust LSP is portrayed as a continuous act of sophisticated guesswork, where the server must intelligently interpret potentially incorrect states and infer user intentions, rather than strictly enforcing correctness like a compiler. This inherent ambiguity, coupled with the vast number of interconnected technical considerations, makes LSP development both exceptionally challenging and deeply rewarding.