lexd_lsp/lib.rs
1//! Language Server Protocol (LSP) implementation for Lex
2//!
3//! This crate provides language server capabilities for the Lex format, enabling rich editor
4//! support in any LSP-compatible editor (VSCode, Neovim, Emacs, Sublime, etc.).
5//!
6//! Position Encoding — read before touching any `Position`
7//!
8//! In this server, `Position.character` is a **UTF-8 byte offset** into the line —
9//! NOT a UTF-16 code unit (the LSP default) nor a `char` count. This is the
10//! load-bearing convention every position↔text conversion here must honor.
11//!
12//! It originates upstream: lex-core's `SourceLocation::byte_to_position` computes
13//! `column = byte_offset - line_start`, and `to_lsp_position` forwards that value to
14//! LSP unchanged. Every consumer of a `Position` in this crate must therefore read
15//! `.character` back as a byte offset to stay consistent.
16//!
17//! Practical consequence: to take the text up to a caret, slice on the byte offset
18//! (`line.get(..pos.character as usize)`, which returns `None` rather than panicking
19//! on an out-of-bounds or mid-`char` index) — never
20//! `line.chars().take(pos.character as usize)`.
21//! A `char`-based count silently over-reads past the caret on any line containing
22//! multi-byte characters (this caused a real bug in #740). See `slice_text_by_range`
23//! in `server.rs` for the canonical byte-offset slicing routine, including the
24//! multi-byte-boundary guards.
25//!
26//! Caveat — not yet uniform: `indent_level_from_position` in `server.rs` still uses
27//! the `chars().take()` form. It is currently harmless there (it only consumes leading
28//! ASCII indentation, where byte and `char` counts coincide), but it does not yet
29//! follow this convention and is a separate cleanup, not a reason to copy the pattern.
30//!
31//! Design Decision: tower-lsp
32//!
33//! After evaluating the Rust LSP ecosystem, we chose tower-lsp as our framework:
34//!
35//! Considered Options:
36//! 1. tower-lsp: High-level async framework built on Tower (~204K monthly downloads)
37//! 2. lsp-server: Low-level sync library from rust-analyzer (~309K monthly downloads)
38//! 3. async-lsp: Low-level async with full Tower integration (~135 GitHub stars)
39//!
40//! Why tower-lsp:
41//! - Best balance of ease-of-use and functionality for a new LSP project
42//! - Strong ecosystem support with extensive documentation and examples
43//! - Modern async/await patterns ideal for Lex's structured parsing needs
44//! - Built-in LSP 3.18 support with proposed features
45//! - Active community with production usage in many language servers
46//! - Good integration with Rust async ecosystem (tokio, futures)
47//!
48//! Trade-offs:
49//! - Less flexible than async-lsp for custom Tower layers (acceptable for our needs)
50//! - Requires &self for trait methods, forcing Arc<Mutex<>> for mutable state (standard pattern)
51//! - Notification ordering is async (not an issue for initial feature set)
52//!
53//! Future Migration Path:
54//! If we later need precise notification ordering or custom middleware, we can migrate
55//! to async-lsp with minimal disruption as both use similar async patterns.
56//!
57//! Feature Set
58//!
59//! Lex is a structured document format, not a programming language. LSP features are selected
60//! to optimize document authoring and navigation workflows:
61//!
62//! Core Features:
63//!
64//! a. Syntax Highlighting
65
66//! 1. Semantic Tokens (textDocument/semanticTokens/*):
67//! - Syntax highlighting for sessions, lists, definitions, annotations
68//! - Inline formatting: bold, italic, code, math
69//! - References, footnotes, citations
70//! - Verbatim blocks with language-specific highlighting
71//! 2. Document Symbols (textDocument/documentSymbol):
72//! - Hierarchical outline view of document structure
73//! - Sessions with nesting (1., 1.1., 1.1.1., etc.)
74//! - Definitions, annotations, lists as navigable symbols
75//! 3. Hover Information (textDocument/hover):
76//! - Preview footnote/citation content on hover
77//! - Show annotation metadata
78//! - Preview definition content when hovering over reference
79//! 4. Folding Ranges (textDocument/foldingRange):
80//! - Fold sessions and nested content
81//! - Fold list items with children
82//! - Fold annotations, definitions, verbatim blocks
83//!
84//! b. Navigation
85//!
86//! 5. Go to Definition / Find References (textDocument/definition, textDocument/references):
87//! - Find all references to footnotes/citations
88//! - Jump from footnote reference [42] to annotation
89//! - Jump from citation [@spec2025] to bibliography entry
90//! - Jump from internal reference [TK-rootlist] to target
91//! 6. Document Links (textDocument/documentLink):
92//! - Clickable links in text
93//! - Verbatim block src parameters (images, includes)
94//! - External references
95//!
96//! c. Editing
97//!
98//! 7. Document Formatting (textDocument/formatting, textDocument/rangeFormatting):
99//! - Fix indentation issues
100//! - Normalize blank lines
101//! - Align list markers //!
102//!
103//!
104//!
105//! Architecture
106//!
107//! The server follows a layered architecture:
108//!
109//! LSP Layer (tower-lsp):
110//! - Handles JSON-RPC communication
111//! - Protocol handshaking and capability negotiation
112//! - Request/response routing
113//!
114//! Server Layer (this crate):
115//! - Implements LanguageServer trait
116//! - Manages document state and parsing
117//! - Coordinates feature implementations
118//! - Very thing, mostly calls the the feature layers over lex-parser
119//! - Thin tests just asserting the right things are being called and returned
120//!
121//! Feature Layer:
122//! - Each feature operates on Lex AST
123//! - Stateless transformations where possible
124//! - All logic and dense unit tests
125//!
126//!
127//! Testing Strategy
128//!
129//! Following Lex project conventions:
130//! - Use official sample files from specs/ for all tests
131//! - Use lexplore loader for consistent test data
132//! - Use ast_assertions library for AST validation
133//! - Test each feature in isolation and integration
134//! - Test against kitchensink and trifecta fixtures
135//!
136//! Non-Features
137//!
138//! The following LSP features are intentionally excluded as they don't apply to document formats:
139//! - Code Lens: Not applicable to documents
140//! - Type Hierarchy: No type system
141//! - Implementation: No interfaces/implementations
142//! - Moniker: For cross-repo linking, not needed
143//! - Linked Editing Range: For paired tags (HTML/XML)
144//! - Diagnostics: we don't have a clear vision for how that would work.
145//!
146//! Error Handling and Robustness
147//!
148//! The server is designed to be highly robust and crash-resistant, following these principles:
149//!
150//! 1. No Panics:
151//! - We strictly avoid `unwrap()` and `expect()` in production code paths.
152//! - All potential failure points (parsing, serialization, IO) return `Result`.
153//! - Errors are propagated up the stack and handled gracefully.
154//!
155//! 2. Graceful Degradation:
156//! - If a feature fails (e.g., semantic tokens calculation), we log the error and return
157//! an empty result or `None` rather than crashing the server.
158//! - This ensures that a bug in one feature doesn't bring down the entire editor experience.
159//!
160//! 3. Error Propagation:
161//! - The `lex-parser` crate returns `Result` types for all parsing operations.
162//! - The `lexd-lsp` server maps these internal errors to appropriate LSP error codes
163//! (e.g., `InternalError`, `InvalidRequest`) when communicating with the client.
164//!
165//! 4. Property-Based Testing:
166//! - We use `proptest` to fuzz the server with random inputs (commands, document text)
167//! to uncover edge cases and ensure stability under unexpected conditions.
168//!
169//! Usage
170//!
171//! This crate provides both a library and binary:
172//!
173//! Library:
174//! ```rust
175//! use lexd_lsp::LexLanguageServer;
176//! use tower_lsp::Server;
177//!
178//! #[tokio::main]
179//! async fn main() {
180//! let stdin = tokio::io::stdin();
181//! let stdout = tokio::io::stdout();
182//!
183//! let (service, socket) = LspService::new(|client| LexLanguageServer::new(client));
184//! Server::new(stdin, stdout, socket).serve(service).await;
185//! }
186//! ```
187//!
188//! Binary:
189//! $ lexd-lsp
190//! Starts the language server on stdin/stdout for editor integration.
191//!
192
193pub mod extension_dispatch;
194pub mod features;
195pub mod server;
196pub mod trust_prompt;
197
198pub use server::LexLanguageServer;