Skip to main content

strop_lsp/
protocol.rs

1//! Shared request ownership, diagnostics and negotiated coordinate domains.
2use std::path::PathBuf;
3use strop_core::id::{BufferRevision, ByteColumn, DocumentId, LineIndex};
4use strop_workspace::ResourceLocation;
5
6/// Diagnostic severity (R13): a named domain, never a raw u8. Variant
7/// order matches the LSP rank, so `min_by_key` keeps the worst entry.
8#[derive(
9    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
10)]
11pub enum Severity {
12    Error,
13    Warning,
14    Information,
15    Hint,
16}
17
18impl Severity {
19    /// The LSP wire code (1=error … 4=hint) — display/trace boundary only.
20    pub const fn code(self) -> u8 {
21        match self {
22            Self::Error => 1,
23            Self::Warning => 2,
24            Self::Information => 3,
25            Self::Hint => 4,
26        }
27    }
28
29    /// Gutter/picker letter.
30    pub const fn char(self) -> char {
31        match self {
32            Self::Error => 'E',
33            Self::Warning => 'W',
34            Self::Information => 'I',
35            Self::Hint => 'H',
36        }
37    }
38}
39
40/// A text-document wire version on one connection. Monotonic across
41/// reopens so a stale versioned diagnostic can never relabel itself as
42/// belonging to a new document incarnation.
43#[derive(
44    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
45)]
46#[serde(transparent)]
47pub struct WireVersion(i32);
48
49impl WireVersion {
50    pub const fn new(value: i32) -> Self {
51        Self(value)
52    }
53    pub const fn get(self) -> i32 {
54        self.0
55    }
56    pub(crate) fn next(self) -> Option<Self> {
57        self.0.checked_add(1).map(Self)
58    }
59}
60
61/// A diagnostic as the server sent it: server-domain columns until the
62/// editor resolves them against its rope with the negotiated encoding.
63#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
64pub struct Diag {
65    pub line: LineIndex,
66    pub col: ServerColumn,
67    pub end_line: LineIndex,
68    pub end_col: ServerColumn,
69    pub severity: Severity,
70    pub message: String,
71}
72
73impl Diag {
74    /// Convert to the editor's byte domain. Lines beyond the current
75    /// document (the server computed on older content) clamp instead of
76    /// panicking; empty documents resolve to line 0.
77    pub fn resolve(self, encoding: PositionEncoding, buffer: &strop_core::Buffer) -> ResolvedDiag {
78        let last = buffer.len_lines().saturating_sub(1);
79        let line = LineIndex::new(self.line.get().min(last));
80        let end_line = LineIndex::new(self.end_line.get().min(last));
81        let start_text = buffer.line_text(line);
82        let end_text = if end_line == line {
83            start_text.clone()
84        } else {
85            buffer.line_text(end_line)
86        };
87        ResolvedDiag {
88            line,
89            col: to_byte_col(&start_text, self.col, encoding),
90            end_line,
91            end_col: to_byte_col(&end_text, self.end_col, encoding),
92            severity: self.severity,
93            message: self.message,
94        }
95    }
96}
97
98/// A diagnostic in the editor's byte domain: columns are UTF-8 byte
99/// offsets into the line, ready for gutter/underline math.
100#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
101pub struct ResolvedDiag {
102    pub line: LineIndex,
103    pub col: ByteColumn,
104    pub end_line: LineIndex,
105    pub end_col: ByteColumn,
106    pub severity: Severity,
107    pub message: String,
108}
109
110impl ResolvedDiag {
111    pub fn severity_char(&self) -> char {
112        self.severity.char()
113    }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
117#[serde(transparent)]
118pub struct ServerId(u64);
119impl ServerId {
120    pub const fn new(value: u64) -> Self {
121        Self(value)
122    }
123    pub const fn get(self) -> u64 {
124        self.0
125    }
126    pub(crate) fn allocate() -> Self {
127        static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
128        match NEXT.fetch_update(
129            std::sync::atomic::Ordering::Relaxed,
130            std::sync::atomic::Ordering::Relaxed,
131            |n| n.checked_add(1),
132        ) {
133            Ok(value) => Self(value),
134            Err(_) => panic!("LSP server identity exhausted"),
135        }
136    }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
140#[serde(transparent)]
141pub struct RequestId(u64);
142impl RequestId {
143    pub const fn new(value: u64) -> Self {
144        Self(value)
145    }
146    pub const fn get(self) -> u64 {
147        self.0
148    }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
152pub struct RequestStamp {
153    pub request: RequestId,
154    pub server: ServerId,
155    pub document: DocumentId,
156    pub revision: BufferRevision,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
160pub enum PositionEncoding {
161    Utf8,
162    Utf16,
163}
164
165/// Byte column → server column for one line's text.
166pub fn to_server_col(line: &str, byte_col: ByteColumn, enc: PositionEncoding) -> ServerColumn {
167    crate::to_server_col_slice(line.into(), byte_col, enc)
168}
169
170/// Server column → byte column for one line's text.
171pub fn to_byte_col(line: &str, server_col: ServerColumn, enc: PositionEncoding) -> ByteColumn {
172    crate::to_byte_col_slice(line.into(), server_col, enc)
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
176pub enum LocKind {
177    References,
178    Implementation,
179    TypeDefinition,
180    Declaration,
181}
182impl LocKind {
183    pub fn label(self) -> &'static str {
184        match self {
185            Self::References => "references",
186            Self::Implementation => "implementation",
187            Self::TypeDefinition => "type definition",
188            Self::Declaration => "declaration",
189        }
190    }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
194pub enum RequestKind {
195    Goto,
196    Hover,
197    SwitchHeader,
198    Locations(LocKind),
199    Format,
200    Rename,
201    CodeAction,
202    /// All symbols in one document — no position rides the request.
203    DocumentSymbols,
204}
205
206impl RequestKind {
207    pub fn label(self) -> &'static str {
208        match self {
209            Self::Goto => "goto definition",
210            Self::Hover => "hover",
211            Self::SwitchHeader => "switch source/header",
212            Self::Locations(kind) => kind.label(),
213            Self::Format => "format",
214            Self::Rename => "rename",
215            Self::CodeAction => "code action",
216            Self::DocumentSymbols => "document symbols",
217        }
218    }
219}
220
221/// Why a request was never admitted (R9: no silent `None`). Refused
222/// requests get no stamp and no wire traffic; the caller reports them.
223#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
224pub enum RequestRefusal {
225    /// The document is not open on this connection.
226    NotOpen,
227    /// The buffer moved past the captured revision — re-request.
228    StaleRevision,
229    /// The server advertised no provider for this request kind.
230    Unsupported,
231    /// The monotonic request-id domain has no unused identity.
232    IdentityExhausted,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
236#[serde(transparent)]
237pub struct ServerColumn(usize);
238impl ServerColumn {
239    pub const fn new(value: usize) -> Self {
240        Self(value)
241    }
242    pub const fn get(self) -> usize {
243        self.0
244    }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
248pub struct ServerPosition {
249    pub line: LineIndex,
250    pub column: ServerColumn,
251}
252
253/// One document-symbol row, flattened from either reply shape:
254/// hierarchical `DocumentSymbol[]` (container = ancestor path) or
255/// legacy flat `SymbolInformation[]` (container = its containerName).
256#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
257pub struct ProtoSymbol {
258    pub name: String,
259    pub container: String,
260    /// SymbolKind's LSP name (`Function`, `Struct`, …).
261    pub kind: String,
262    pub location: ServerLocation,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
266pub struct ServerLocation {
267    /// Endpoint-scoped identity of the target: a remote location can
268    /// never alias the analogous local path.
269    pub doc: ResourceLocation,
270    pub position: ServerPosition,
271}
272
273/// One text replacement in the server domain: lines/columns are the
274/// server's negotiated coordinates until the editor resolves them
275/// against its rope, exactly like [`Diag`].
276#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
277pub struct ServerEdit {
278    pub start: ServerPosition,
279    pub end: ServerPosition,
280    pub new_text: String,
281}
282
283/// A code action's usable payload. Command-only actions carry
284/// `edits: None` with `has_external_command: true`. An action whose
285/// edit cannot be applied (file operations, unverifiable versions)
286/// keeps its title with `edits: None` and `has_external_command:
287/// false` — the editor lists it but marks it inapplicable.
288#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
289pub struct ProtoAction {
290    pub title: String,
291    pub edits: Option<Vec<(ResourceLocation, Vec<ServerEdit>)>>,
292    pub has_external_command: bool,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
296pub struct ReplyContext {
297    pub stamp: RequestStamp,
298    pub encoding: PositionEncoding,
299    pub kind: RequestKind,
300}
301
302/// Source position remains byte-native until initialize negotiates
303/// encoding. Serializable: the replay tape records admissions and
304/// relaunches against the identical payload.
305#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
306pub struct RequestInput {
307    pub document: DocumentId,
308    pub revision: BufferRevision,
309    #[serde(with = "strop_core::path_serde")]
310    pub path: PathBuf,
311    pub line: LineIndex,
312    pub byte_col: ByteColumn,
313    pub line_text: crate::FrozenLine,
314    pub kind: RequestKind,
315    /// The rename target; `None` for every non-rename request. Old
316    /// tapes decode without it.
317    #[serde(default)]
318    pub rename_to: Option<String>,
319}
320
321/// An admitted request: its owning stamp plus the captured input.
322#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
323pub struct PendingRequest {
324    pub stamp: RequestStamp,
325    pub input: RequestInput,
326    /// Format options ride the admission record, not the input: a
327    /// formatting request has no cursor position, and the tape
328    /// serializes this record at `lsp.launch`, so a replayed format
329    /// relaunches with the recorded tab width. `None` for non-format
330    /// requests; old tapes decode without it.
331    #[serde(default)]
332    pub tab_width: Option<usize>,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
336pub struct DiagnosticContext {
337    pub server: ServerId,
338    pub document: DocumentId,
339    /// Current sent revision at receipt, not proof of computation freshness
340    /// for versionless diagnostics.
341    pub revision: BufferRevision,
342    pub encoding: PositionEncoding,
343    pub version: Option<WireVersion>,
344}
345
346#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
347pub enum LspEvent {
348    Diagnostics {
349        context: DiagnosticContext,
350        /// The diagnosed document: endpoint-scoped, so remote
351        /// diagnostics never collide with same-bytes local paths.
352        doc: ResourceLocation,
353        diags: Vec<Diag>,
354    },
355    Ready {
356        server: ServerId,
357        name: String,
358    },
359    Failed {
360        server: ServerId,
361        name: String,
362        hint: String,
363    },
364    /// A server-initiated `window/showMessage`: user-facing, from the
365    /// owning server. `window/logMessage` stays in the trace — it is
366    /// logging, not a message.
367    ServerMessage {
368        server: ServerId,
369        name: String,
370        text: String,
371    },
372    HoverText {
373        context: ReplyContext,
374        text: String,
375    },
376    GotoLocation {
377        context: ReplyContext,
378        location: ServerLocation,
379    },
380    Locations {
381        context: ReplyContext,
382        kind: LocKind,
383        items: Vec<ServerLocation>,
384    },
385    /// Formatting reply: the document's replacement spans in
386    /// server-domain positions (empty when the server has no changes).
387    Edits {
388        context: ReplyContext,
389        edits: Vec<ServerEdit>,
390    },
391    /// Rename or an edit-bearing code action: per-resource edit groups
392    /// in server-domain positions.
393    WorkspaceEdits {
394        context: ReplyContext,
395        edits: Vec<(ResourceLocation, Vec<ServerEdit>)>,
396    },
397    /// Code-action reply: the server's listed actions with their
398    /// usable payloads.
399    ActionList {
400        context: ReplyContext,
401        actions: Vec<ProtoAction>,
402    },
403    /// Document-symbol reply: the flattened tree (0047 §1) — both
404    /// server reply shapes land in the same row form.
405    Symbols {
406        context: ReplyContext,
407        symbols: Vec<ProtoSymbol>,
408    },
409    /// context is the ORIGINAL request's — never re-derived.
410    Note {
411        context: ReplyContext,
412        text: String,
413    },
414}