Skip to main content

lattice_lsp/
lib.rs

1//! `lattice-lsp` -- the LSP client (DESIGN.md §5.4, Phase 4).
2//!
3//! ## Why hand-rolled
4//!
5//! `tower-lsp` is server-side. `async-lsp` brings tower middleware
6//! that doesn't fit our actor model (every server is one tokio task
7//! with a mailbox + oneshot replies, identical to
8//! `lattice-runtime::RopeDocumentHandle`). The wire protocol is a few
9//! hundred lines of framing + JSON-RPC; reusing our existing
10//! cancellation primitives (`CancellationToken`) is cleaner than
11//! adapting middleware.
12//!
13//! ## Layering
14//!
15//! - [`framing`] -- LSP's `Content-Length` header parser. Pure;
16//!   stream-agnostic; tested against partial reads, malformed
17//!   headers, and oversized bodies.
18//! - [`jsonrpc`] -- typed JSON-RPC 2.0 messages: requests with id
19//!   correlation, responses, notifications, and `ResponseError`.
20//!   No transport assumptions.
21//! - [`codec`] -- glues framing + jsonrpc onto
22//!   `tokio::io::AsyncBufRead` / `AsyncWrite`. Yields one
23//!   [`jsonrpc::Message`] per `read_message` call; encodes one per
24//!   `write_message`.
25//! - [`transport`] -- spawns a child process and exposes its
26//!   stdio as a [`codec`] reader / writer pair. Per
27//!   DESIGN.md §5.4 each (workspace, server-id) gets one transport.
28//! - **Future modules** (folded in across 4.1.b–4.4):
29//!   - `actor` -- the per-server tokio task (mailbox + dispatch).
30//!   - `client` -- the editor-facing `LspHandle` analog of
31//!     `RopeDocumentHandle`.
32//!   - `sync` -- `AppliedEdit` ↔ `TextDocumentContentChangeEvent`.
33//!   - `position` -- utf-8 ↔ utf-16 column conversion.
34//!   - `capabilities` -- client capability advertisement +
35//!     server capability gating.
36//!
37//! ## Performance discipline
38//!
39//! All public methods that talk to a server return `Pending<T>`
40//! (matching §5.2.1's dispatch envelope); nothing blocks the UI.
41//! LSP requests are §5.2.5 *Background*-class -- they have no
42//! sync-prelude budget and may be cancelled / superseded freely.
43//! Per-call performance characteristics live in
44//! `benches/lsp.rs` and are mirrored in `docs/dev/operations/benchmarks.md`.
45
46#![cfg_attr(test, allow(clippy::unwrap_used, clippy::panic))]
47
48pub mod actor;
49pub mod apply_edit;
50// M-async.5: `attach_driver` retired. `LspMode::on_activate`
51// now drives the supervisor's `open_buffer` directly; the
52// bus's `DocumentOpened` event still fires for other
53// subscribers but LSP no longer keys off it.
54pub mod buffer_names;
55pub mod cache;
56pub mod capabilities;
57pub mod codec;
58pub mod completion;
59pub mod config;
60pub mod configuration;
61pub mod diagnostics;
62pub mod diagnostics_layer;
63pub mod dynamic_registration;
64pub mod error;
65// EP.3: the language server as a producer of the core error list.
66pub mod error_list_feed;
67pub mod events;
68pub mod fan_in;
69pub mod features;
70pub mod file_watcher;
71pub mod framing;
72pub mod help_views;
73pub mod install;
74/// JSON-RPC 2.0 message types now live in `lattice-protocol` (IDE-protocol
75/// Risk 3 lift) so `lattice-claude-code` can share them. Re-exported here
76/// as `lattice_lsp::jsonrpc` so existing `crate::jsonrpc::*` paths,
77/// downstream callers, tests, and benches keep resolving unchanged.
78pub use lattice_protocol::jsonrpc;
79pub mod logging;
80pub mod modeline;
81pub mod modes;
82pub mod pending;
83pub mod position;
84pub mod providers;
85pub mod show_document;
86pub mod show_message_request;
87pub mod supervisor;
88pub mod sync;
89pub mod transport;
90
91pub use actor::{ServerHandle, spawn, spawn_with_io};
92pub use apply_edit::{ApplyEditBus, ApplyEditOutcome, InboundApplyEdit};
93pub use buffer_names::{
94    LSP_SUBSYSTEM_LOG_NAME, lsp_server_log_name, lsp_server_trace_log_name,
95    parse_lsp_server_log_name, parse_lsp_trace_log_name,
96};
97pub use capabilities::{Capabilities, FileOpKind, client_capabilities};
98pub use codec::{LspReader, LspWriter};
99pub use config::{ServerConfig, builtin_servers, resolve_workspace_root};
100pub use configuration::{ConfigurationBus, InboundConfigurationRequest};
101pub use diagnostics::{DIAGNOSTICS_CHANNEL_CAPACITY, DiagnosticEvent, DiagnosticsBus};
102pub use diagnostics_layer::{
103    DiagnosticsLayer, InlineDiagnosticSummary, SeverityCounts, pump_diagnostics,
104};
105pub use dynamic_registration::{DynamicRegistration, DynamicRegistry};
106pub use error::{LspError, LspResult};
107pub use events::{
108    LspActorExitReason, LspActorExited, LspBufferAttached, LspBufferDetached, LspCodeLensRefresh,
109    LspDiagnosticRefresh, LspDocumentChanged, LspInlayHintRefresh, LspLogPushed, LspProgressKind,
110    LspProgressUpdate, LspSemanticTokensRefresh, LspServerHealth, LspServerStatusChanged,
111};
112pub use file_watcher::{WatcherSubscriptions, compile_with_workspace_root};
113pub use framing::{FrameError, FrameHeader};
114pub use install::install;
115pub use lattice_protocol::jsonrpc::{
116    Message, Notification, Request, RequestId, Response, ResponseError,
117};
118pub use logging::{
119    InstanceKey, LogLevel, LogRecord, LogRing, LogSource, LspLogger, format_log_event_line,
120    level_tag as log_level_tag,
121};
122pub use pending::{InvocationId, Pending};
123pub use show_document::{InboundShowDocument, ShowDocumentBus, ShowDocumentOutcome};
124pub use show_message_request::{
125    InboundShowMessageRequest, ShowMessageRequestBus, ShowMessageRequestOutcome,
126};
127pub use supervisor::{
128    ActorKey, LspSupervisor, LspSupervisorHandle, RestartReport, SupervisorSnapshot,
129};
130pub use sync::{DocSync, uri_from_str};
131
132// Re-export commonly-used LSP types so consumers don't need
133// a direct `lsp-types` dep just to spell them out in
134// signatures. lsp-types changes shape across major versions;
135// the re-exports give us a stable seam if we need to swap.
136// 5.8.J / refactor #70: lattice-lsp is the canonical owner of the
137// `lsp_types` substrate; downstream crates reach types through this
138// crate rather than depending on `lsp-types` directly. The module
139// re-export gives them the full namespace (~108 distinct types are
140// used workspace-wide; selective re-exports were unwieldy).
141pub use lsp_types;
142
143// Convenience re-exports for the most-used items so callers can
144// write `lattice_lsp::Diagnostic` instead of
145// `lattice_lsp::lsp_types::Diagnostic` for the common cases.
146pub use lsp_types::{
147    Diagnostic, DiagnosticSeverity, InlayHint, InlayHintLabel, InlayHintLabelPart,
148    Position as LspPosition, Range as LspRange, Uri,
149};
150pub use transport::{ChildTransport, TransportError};
151
152/// Flatten an LSP [`InlayHintLabel`] into a single plain `String`.
153/// `String` variants return as-is; `LabelParts(Vec<LabelPart>)`
154/// concatenates each part's `value`. Tooltip / command / location
155/// fields on label parts are ignored — they're resolve / hover
156/// affordances, not a paint concern.
157///
158/// Phase 5.8.N: hoisted out of both renderer peers. TUI had it as
159/// `lattice-ui-tui::render::inlay_hint_label_text`; GPUI had the
160/// equivalent inlined in `lattice-ui-gpui::window`. Renderer-
161/// neutral since it operates purely on an LSP type; lives here
162/// because lattice-lsp is the canonical owner of `lsp_types`.
163pub fn inlay_hint_label_text(label: &InlayHintLabel) -> String {
164    match label {
165        InlayHintLabel::String(s) => s.clone(),
166        InlayHintLabel::LabelParts(parts) => parts.iter().map(|p| p.value.clone()).collect(),
167    }
168}
169
170#[cfg(test)]
171mod inlay_hint_label_tests {
172    use super::*;
173
174    #[test]
175    fn string_variant_returns_as_is() {
176        let label = InlayHintLabel::String(": i32".into());
177        assert_eq!(inlay_hint_label_text(&label), ": i32");
178    }
179
180    #[test]
181    fn parts_variant_concatenates_values() {
182        let parts = InlayHintLabel::LabelParts(vec![
183            InlayHintLabelPart {
184                value: "size".into(),
185                tooltip: None,
186                location: None,
187                command: None,
188            },
189            InlayHintLabelPart {
190                value: ": ".into(),
191                tooltip: None,
192                location: None,
193                command: None,
194            },
195            InlayHintLabelPart {
196                value: "usize".into(),
197                tooltip: None,
198                location: None,
199                command: None,
200            },
201        ]);
202        assert_eq!(inlay_hint_label_text(&parts), "size: usize");
203    }
204
205    #[test]
206    fn empty_parts_returns_empty_string() {
207        let parts = InlayHintLabel::LabelParts(vec![]);
208        assert_eq!(inlay_hint_label_text(&parts), "");
209    }
210}