Skip to main content

blit_remote/
lsp.rs

1//! Language intelligence wire protocol (docs/design/lsp.md).
2//!
3//! The server terminates LSP and projects it into blit-native records:
4//! per-backend phase/capabilities are *pushed* as whole-snapshot
5//! `LSP_STATE` messages ([`LspStateMirror`]), diagnostics are *pushed* as
6//! per-file replacement sets against a server-held cache
7//! ([`LspDiagMirror`]), and point-in-time answers are *pulled* through the
8//! single nonce-correlated `LSP_QUERY` opcode whose `kind` byte selects
9//! the operation.
10//!
11//! Positions are 0-based lines with UTF-8 byte columns in both
12//! directions; the server transcodes to each backend's negotiated
13//! encoding. All integers little-endian, tightly packed, as everywhere in
14//! the protocol.
15
16use std::collections::BTreeMap;
17
18/// `S2C_HELLO` feature bit: server supports the `LSP_*` message family.
19pub const FEATURE_LSP: u32 = 1 << 8;
20
21// C2S opcodes.
22
23/// Attach to the workspace containing a path: [0x60][nonce:2][flags:1][diag_latency_ms:2][path_len:2][path:N]
24/// `path` is plain UTF-8 (client-chosen filesystem location, like
25/// `FS_SYNC`); the server walks upward for root markers.
26pub const C2S_LSP_OPEN: u8 = 0x60;
27/// Release an attachment (backends stay warm): [0x61][lsp_id:2]
28pub const C2S_LSP_CLOSE: u8 = 0x61;
29/// Acknowledge a pushed update: [0x62][lsp_id:2][stream:1][update_id:4]
30/// `stream` is [`LSP_STREAM_STATE`] or [`LSP_STREAM_DIAG`].
31pub const C2S_LSP_ACK: u8 = 0x62;
32/// Point-in-time query: [0x63][nonce:2][lsp_id:2][kind:1][flags:1][line:4][col:4][path_len:2][path:N][arg_len:2][arg:N]
33/// `kind` is one of `LSP_QUERY_*`; `line`/`col` are ignored by the symbol
34/// kinds (for `WS_SYMBOLS` the `line` field is reserved as a future
35/// SymbolKind bitmask filter); `arg` carries the `WS_SYMBOLS` query
36/// string or the `RENAME` new name.
37pub const C2S_LSP_QUERY: u8 = 0x63;
38/// Advisory cancel of an in-flight query: [0x64][nonce:2]
39pub const C2S_LSP_CANCEL: u8 = 0x64;
40/// Enumerate every live backend, daemon-wide: [0x65][nonce:2]
41pub const C2S_LSP_SERVERS: u8 = 0x65;
42/// Shut one backend down by `server_ref`: [0x66][nonce:2][server_ref:2]
43/// A later query respawns it; observability before force.
44pub const C2S_LSP_STOP: u8 = 0x66;
45/// Buffer overlay: [0x67][lsp_id:2][flags:1][path_len:2][path:N][text:LZ4]
46/// The full live buffer of an open editor (emitted-form path, like query
47/// paths); while overlaid, the engine's byte source for the document is
48/// the overlay, not disk (docs/design/lsp.md "LSP_BUFFER"). Flags bit 0
49/// [`LSP_BUFFER_RELEASE`] drops the overlay (`text` empty). No reply:
50/// idempotent last-writer-wins state on an ordered transport.
51pub const C2S_LSP_BUFFER: u8 = 0x67;
52
53// S2C opcodes.
54
55/// Open outcome: [0x60][nonce:2][lsp_id:2][status:1][flags:1][root_len:2][root:N][detail_len:2][detail:N]
56/// On failure `lsp_id` = [`LSP_ID_INVALID`] and `detail` carries a
57/// diagnostic; on success `root` is the canonical workspace root,
58/// escaped.
59pub const S2C_LSP_OPENED: u8 = 0x60;
60/// Whole-state snapshot: [0x61][lsp_id:2][state_id:4][flags:1][records:LZ4]
61/// One `SERVER` record per live backend of the attachment.
62pub const S2C_LSP_STATE: u8 = 0x61;
63/// Diagnostics update: [0x62][lsp_id:2][update_id:4][flags:1][records:LZ4]
64/// Per-file replacement sets; bit 0 [`LSP_DIAG_FULL`] carries the
65/// complete workspace state (drop everything, then apply).
66pub const S2C_LSP_DIAG: u8 = 0x62;
67/// Query response: [0x63][nonce:2][status:1][flags:1][detail_len:2][detail:N][records:LZ4]
68/// `detail` is a human-readable failure reason (empty on success).
69pub const S2C_LSP_QUERY: u8 = 0x63;
70/// Attachment ended server-side: [0x64][lsp_id:2][reason:1]
71pub const S2C_LSP_CLOSED: u8 = 0x64;
72/// Backend enumeration: [0x65][nonce:2][status:1][flags:1][records:LZ4]
73/// `SERVER` records as in `LSP_STATE` plus the escaped root.
74pub const S2C_LSP_SERVERS: u8 = 0x65;
75/// Stop outcome: [0x66][nonce:2][status:1]
76pub const S2C_LSP_STOPPED: u8 = 0x66;
77
78// Unified status table (docs/design/lsp.md "Statuses"): the git.md codes
79// 0-9 with the same numbers and semantics where they overlap, plus
80// WARMING.
81pub const LSP_STATUS_OK: u8 = 0;
82/// `lsp_id` unknown or already closed.
83pub const LSP_STATUS_UNKNOWN_ID: u8 = 1;
84/// Path, symbol, or backend does not exist; discovery failures name the
85/// missing binary in the detail field.
86pub const LSP_STATUS_NOT_FOUND: u8 = 2;
87/// The element cannot answer this query (e.g. rename on a non-symbol).
88pub const LSP_STATUS_WRONG_TYPE: u8 = 3;
89pub const LSP_STATUS_PERMISSION: u8 = 4;
90/// Over a size cap; truncation flags cover the paginatable cases.
91pub const LSP_STATUS_TOO_LARGE: u8 = 5;
92/// A budget was exhausted with no way to truncate.
93pub const LSP_STATUS_BUDGET: u8 = 6;
94/// Malformed request (unknown flags, kind, or field combination).
95pub const LSP_STATUS_INVALID: u8 = 7;
96/// Ended by `LSP_CANCEL`.
97pub const LSP_STATUS_CANCELLED: u8 = 8;
98/// Diagnostic in the message's detail field where it has one.
99pub const LSP_STATUS_OTHER: u8 = 9;
100/// The backing server has not finished initialize/indexing; retryable.
101pub const LSP_STATUS_WARMING: u8 = 10;
102
103/// Human-readable name for an `LSP_STATUS_*` code.
104pub fn lsp_status_text(status: u8) -> &'static str {
105    match status {
106        LSP_STATUS_OK => "ok",
107        LSP_STATUS_UNKNOWN_ID => "unknown attachment",
108        LSP_STATUS_NOT_FOUND => "not found",
109        LSP_STATUS_WRONG_TYPE => "wrong type",
110        LSP_STATUS_PERMISSION => "permission denied",
111        LSP_STATUS_TOO_LARGE => "too large",
112        LSP_STATUS_BUDGET => "budget exhausted",
113        LSP_STATUS_INVALID => "invalid request",
114        LSP_STATUS_CANCELLED => "cancelled",
115        LSP_STATUS_WARMING => "warming up",
116        _ => "error",
117    }
118}
119
120// C2S_LSP_OPEN flags.
121
122/// Stream `LSP_STATE`.
123pub const LSP_OPEN_WATCH: u8 = 1 << 0;
124/// Stream `LSP_DIAG`; implies `WATCH`.
125pub const LSP_OPEN_DIAGS: u8 = 1 << 1;
126/// Resolve the workspace path from a pty's live cwd: a trailing
127/// `[src_pty_id:2]` names a pty and the server joins `path` onto its cwd
128/// before the root-marker walk (docs/ide.md Decision 3).
129pub const LSP_OPEN_FROM_PTY: u8 = 1 << 2;
130
131/// `lsp_id` value reporting an open failure.
132pub const LSP_ID_INVALID: u16 = 0xFFFF;
133
134// C2S_LSP_ACK streams.
135
136pub const LSP_STREAM_STATE: u8 = 0;
137pub const LSP_STREAM_DIAG: u8 = 1;
138
139// C2S_LSP_QUERY kinds.
140
141/// → `LOCATION` records.
142pub const LSP_QUERY_DEFINITION: u8 = 1;
143/// → `LOCATION` records; flags bit 0 [`LSP_REFS_INCLUDE_DECLARATION`].
144pub const LSP_QUERY_REFERENCES: u8 = 2;
145/// → one `MARKUP` record (plus an optional `LOCATION` for the range).
146pub const LSP_QUERY_HOVER: u8 = 3;
147/// → `SYMBOL` records, pre-order; `line`/`col` ignored.
148pub const LSP_QUERY_DOC_SYMBOLS: u8 = 4;
149/// → `SYMBOL` records; `path` empty, `arg` = query string.
150pub const LSP_QUERY_WS_SYMBOLS: u8 = 5;
151/// → `EDIT` records; `arg` = new name. Data, never applied.
152pub const LSP_QUERY_RENAME: u8 = 6;
153/// → `COMPLETION` records; the response's [`LSP_RESP_INCOMPLETE`] flag
154/// mirrors the server's `isIncomplete` (retype should re-query).
155pub const LSP_QUERY_COMPLETION: u8 = 7;
156/// → `SIGNATURE` records, active signature first.
157pub const LSP_QUERY_SIGNATURE: u8 = 8;
158
159// C2S_LSP_QUERY flags.
160
161/// `REFERENCES`: include the declaration itself.
162pub const LSP_REFS_INCLUDE_DECLARATION: u8 = 1 << 0;
163
164// C2S_LSP_BUFFER flags.
165
166/// Drop the overlay and revert the document to disk truth (`text` must
167/// be empty).
168pub const LSP_BUFFER_RELEASE: u8 = 1 << 0;
169
170// S2C_LSP_DIAG flags.
171
172/// The update carries complete workspace diagnostic state: drop
173/// everything, then apply. Every `DIAGS` subscribe begins with one (the
174/// cache replay), and the server may send one at any time instead of an
175/// incremental update.
176pub const LSP_DIAG_FULL: u8 = 1 << 0;
177
178// S2C response flags (query, state, diag, servers).
179
180/// The entries budget was hit; records present are valid.
181pub const LSP_RESP_TRUNCATED: u8 = 1 << 0;
182/// A `RENAME` plan dropped file operations it cannot project (create /
183/// rename / delete of whole files in a `WorkspaceEdit`): the returned
184/// `EDIT` records are the text edits only, so the plan is incomplete.
185pub const LSP_RESP_INCOMPLETE: u8 = 1 << 1;
186
187// S2C_LSP_CLOSED reasons.
188
189pub const LSP_CLOSED_CLIENT_REQUEST: u8 = 0;
190pub const LSP_CLOSED_ROOT_GONE: u8 = 1;
191pub const LSP_CLOSED_PERMISSION_LOST: u8 = 2;
192pub const LSP_CLOSED_BACKEND_FAILED: u8 = 3;
193pub const LSP_CLOSED_RESOURCE_LIMIT: u8 = 4;
194
195// SERVER record phases.
196
197pub const LSP_PHASE_SPAWNING: u8 = 0;
198pub const LSP_PHASE_INITIALIZING: u8 = 1;
199pub const LSP_PHASE_INDEXING: u8 = 2;
200pub const LSP_PHASE_READY: u8 = 3;
201pub const LSP_PHASE_FAILED: u8 = 4;
202
203/// `progress_pct` value when the backend reports no percentage.
204pub const LSP_PROGRESS_UNKNOWN: u8 = 255;
205
206// SERVER record capability bits (`caps:4`), aligned with query kinds:
207// bit `kind - 1`.
208
209pub const LSP_CAP_DEFINITION: u32 = 1 << 0;
210pub const LSP_CAP_REFERENCES: u32 = 1 << 1;
211pub const LSP_CAP_HOVER: u32 = 1 << 2;
212pub const LSP_CAP_DOC_SYMBOLS: u32 = 1 << 3;
213pub const LSP_CAP_WS_SYMBOLS: u32 = 1 << 4;
214pub const LSP_CAP_RENAME: u32 = 1 << 5;
215pub const LSP_CAP_COMPLETION: u32 = 1 << 6;
216pub const LSP_CAP_SIGNATURE: u32 = 1 << 7;
217
218// COMPLETION record flags.
219
220pub const LSP_COMPLETION_DEPRECATED: u8 = 1 << 0;
221/// `insert` is LSP snippet syntax; a client without snippet UI degrades
222/// it to plain text.
223pub const LSP_COMPLETION_SNIPPET: u8 = 1 << 1;
224pub const LSP_COMPLETION_PRESELECT: u8 = 1 << 2;
225
226// SIGNATURE record flags.
227
228/// The active signature (emitted first).
229pub const LSP_SIGNATURE_ACTIVE: u8 = 1 << 0;
230
231/// `SIGNATURE.active_param` value when no parameter is active.
232pub const LSP_SIGNATURE_NO_PARAM: u16 = 0xFFFF;
233
234// DIAG record severities (LSP values).
235
236pub const LSP_SEVERITY_ERROR: u8 = 1;
237pub const LSP_SEVERITY_WARNING: u8 = 2;
238pub const LSP_SEVERITY_INFO: u8 = 3;
239pub const LSP_SEVERITY_HINT: u8 = 4;
240
241// DIAG record flags (LSP diagnostic tags).
242
243pub const LSP_DIAG_UNNECESSARY: u8 = 1 << 0;
244pub const LSP_DIAG_DEPRECATED: u8 = 1 << 1;
245
246// MARKUP record formats.
247
248pub const LSP_MARKUP_PLAIN: u8 = 0;
249pub const LSP_MARKUP_MARKDOWN: u8 = 1;
250
251// SYMBOL record flags.
252
253pub const LSP_SYMBOL_DEPRECATED: u8 = 1 << 0;
254
255// Record kinds, namespaced per message type.
256
257pub const LSP_STATE_RECORD_SERVER: u8 = 0x01;
258pub const LSP_DIAG_RECORD_FILE: u8 = 0x01;
259pub const LSP_DIAG_RECORD_DIAG: u8 = 0x02;
260pub const LSP_QUERY_RECORD_LOCATION: u8 = 0x01;
261pub const LSP_QUERY_RECORD_MARKUP: u8 = 0x02;
262pub const LSP_QUERY_RECORD_SYMBOL: u8 = 0x03;
263pub const LSP_QUERY_RECORD_EDIT: u8 = 0x04;
264pub const LSP_QUERY_RECORD_COMPLETION: u8 = 0x05;
265pub const LSP_QUERY_RECORD_SIGNATURE: u8 = 0x06;
266
267/// BLAKE3 truncated to 128 bits, as in the fs family: the content
268/// version a record describes.
269pub type LspHash = [u8; 16];
270
271/// The all-zero hash: content version unknown.
272pub const LSP_HASH_NONE: LspHash = [0; 16];
273
274/// Decompress a `compress_prepend_size` payload, refusing declared sizes
275/// over the protocol-wide [`crate::MAX_DECOMPRESSED`] *before* allocating
276/// (docs/protocol.md "Compressed payloads").
277fn decompress_guarded(data: &[u8]) -> Option<Vec<u8>> {
278    if data.len() < 4 {
279        return None;
280    }
281    let declared = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
282    if declared > crate::MAX_DECOMPRESSED {
283        return None;
284    }
285    lz4_flex::decompress_size_prepended(data).ok()
286}
287
288// ---------------------------------------------------------------------------
289// Field codec helpers
290// ---------------------------------------------------------------------------
291
292fn push_str(buf: &mut Vec<u8>, s: &str) {
293    let b = s.as_bytes();
294    buf.extend_from_slice(&(b.len() as u16).to_le_bytes());
295    buf.extend_from_slice(b);
296}
297
298/// A u32-length-prefixed byte string (diagnostic messages, markup, edit
299/// text).
300fn push_bytes(buf: &mut Vec<u8>, b: &[u8]) {
301    buf.extend_from_slice(&(b.len() as u32).to_le_bytes());
302    buf.extend_from_slice(b);
303}
304
305fn take_u8(b: &mut &[u8]) -> Option<u8> {
306    let (&x, rest) = b.split_first()?;
307    *b = rest;
308    Some(x)
309}
310
311fn take_u16(b: &mut &[u8]) -> Option<u16> {
312    if b.len() < 2 {
313        return None;
314    }
315    let v = u16::from_le_bytes([b[0], b[1]]);
316    *b = &b[2..];
317    Some(v)
318}
319
320fn take_u32(b: &mut &[u8]) -> Option<u32> {
321    if b.len() < 4 {
322        return None;
323    }
324    let v = u32::from_le_bytes(b[0..4].try_into().unwrap());
325    *b = &b[4..];
326    Some(v)
327}
328
329fn take_u64(b: &mut &[u8]) -> Option<u64> {
330    if b.len() < 8 {
331        return None;
332    }
333    let v = u64::from_le_bytes(b[0..8].try_into().unwrap());
334    *b = &b[8..];
335    Some(v)
336}
337
338fn take_hash(b: &mut &[u8]) -> Option<LspHash> {
339    if b.len() < 16 {
340        return None;
341    }
342    let hash: LspHash = b[0..16].try_into().unwrap();
343    *b = &b[16..];
344    Some(hash)
345}
346
347fn take_str<'a>(b: &mut &'a [u8]) -> Option<&'a str> {
348    let len = take_u16(b)? as usize;
349    if b.len() < len {
350        return None;
351    }
352    let s = std::str::from_utf8(&b[..len]).ok()?;
353    *b = &b[len..];
354    Some(s)
355}
356
357fn take_text<'a>(b: &mut &'a [u8]) -> Option<&'a str> {
358    let len = take_u32(b)? as usize;
359    if b.len() < len {
360        return None;
361    }
362    let s = std::str::from_utf8(&b[..len]).ok()?;
363    *b = &b[len..];
364    Some(s)
365}
366
367/// Check `msg` starts with `opcode` and return the body after it.
368fn body_of(msg: &[u8], opcode: u8) -> Option<&[u8]> {
369    if msg.first() != Some(&opcode) {
370        return None;
371    }
372    Some(&msg[1..])
373}
374
375// Record framing: [record_len:4][kind:1][…], unknown kinds skippable,
376// malformed records end the payload.
377
378fn begin_record(buf: &mut Vec<u8>) -> usize {
379    let start = buf.len();
380    buf.extend_from_slice(&0u32.to_le_bytes());
381    start
382}
383
384fn end_record(buf: &mut [u8], start: usize) {
385    let len = (buf.len() - start - 4) as u32;
386    buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
387}
388
389/// Pop the next record: `(kind, body)`. `None` ends iteration (clean end
390/// or malformed framing).
391fn next_record<'a>(data: &mut &'a [u8]) -> Option<(u8, &'a [u8])> {
392    let mut b = *data;
393    let len = take_u32(&mut b)? as usize;
394    if b.len() < len || len == 0 {
395        return None;
396    }
397    let record = &b[..len];
398    *data = &b[len..];
399    Some((record[0], &record[1..]))
400}
401
402// ---------------------------------------------------------------------------
403// C2S message builders and parsers
404// ---------------------------------------------------------------------------
405
406pub fn msg_lsp_open(nonce: u16, flags: u8, diag_latency_ms: u16, path: &str) -> Vec<u8> {
407    let mut msg = Vec::with_capacity(8 + path.len());
408    msg.push(C2S_LSP_OPEN);
409    msg.extend_from_slice(&nonce.to_le_bytes());
410    msg.push(flags);
411    msg.extend_from_slice(&diag_latency_ms.to_le_bytes());
412    push_str(&mut msg, path);
413    msg
414}
415
416/// Build a `C2S_LSP_OPEN` whose workspace path the server resolves from a
417/// pty's live cwd: sets `LSP_OPEN_FROM_PTY` and appends `[src_pty_id:2]`
418/// after the path (docs/ide.md Decision 3).
419pub fn msg_lsp_open_from_pty(
420    nonce: u16,
421    flags: u8,
422    diag_latency_ms: u16,
423    path: &str,
424    src_pty_id: u16,
425) -> Vec<u8> {
426    let mut msg = msg_lsp_open(nonce, flags | LSP_OPEN_FROM_PTY, diag_latency_ms, path);
427    msg.extend_from_slice(&src_pty_id.to_le_bytes());
428    msg
429}
430
431/// Extract the trailing `src_pty_id` from a `FROM_PTY` `C2S_LSP_OPEN`.
432pub fn lsp_open_src_pty(msg: &[u8]) -> Option<u16> {
433    if msg.first().copied() != Some(C2S_LSP_OPEN) || msg.len() < 8 {
434        return None;
435    }
436    if msg[3] & LSP_OPEN_FROM_PTY == 0 {
437        return None;
438    }
439    let path_len = u16::from_le_bytes([msg[6], msg[7]]) as usize;
440    let off = 8usize.checked_add(path_len)?;
441    let b = msg.get(off..off + 2)?;
442    Some(u16::from_le_bytes([b[0], b[1]]))
443}
444
445/// Rebase a `FROM_PTY` `C2S_LSP_OPEN` onto a resolved `cwd` (join `cwd`/`path`,
446/// clear `FROM_PTY`), producing a plain path-based open the handler consumes
447/// unchanged. `cwd` `None` (source pty gone) keeps `path` verbatim.
448pub fn lsp_open_rebase(msg: &[u8], cwd: Option<&str>) -> Option<Vec<u8>> {
449    lsp_open_src_pty(msg)?;
450    let (nonce, flags, diag_ms, path) = parse_lsp_open(msg)?;
451    let joined = cwd.map(|dir| {
452        std::path::Path::new(dir)
453            .join(path)
454            .to_string_lossy()
455            .into_owned()
456    });
457    let eff = joined.as_deref().unwrap_or(path);
458    Some(msg_lsp_open(
459        nonce,
460        flags & !LSP_OPEN_FROM_PTY,
461        diag_ms,
462        eff,
463    ))
464}
465
466/// Parse `C2S_LSP_OPEN` into `(nonce, flags, diag_latency_ms, path)`.
467pub fn parse_lsp_open(msg: &[u8]) -> Option<(u16, u8, u16, &str)> {
468    let mut b = body_of(msg, C2S_LSP_OPEN)?;
469    let nonce = take_u16(&mut b)?;
470    let flags = take_u8(&mut b)?;
471    let diag_latency_ms = take_u16(&mut b)?;
472    let path = take_str(&mut b)?;
473    Some((nonce, flags, diag_latency_ms, path))
474}
475
476pub fn msg_lsp_close(lsp_id: u16) -> Vec<u8> {
477    let mut msg = Vec::with_capacity(3);
478    msg.push(C2S_LSP_CLOSE);
479    msg.extend_from_slice(&lsp_id.to_le_bytes());
480    msg
481}
482
483pub fn parse_lsp_close(msg: &[u8]) -> Option<u16> {
484    let mut b = body_of(msg, C2S_LSP_CLOSE)?;
485    take_u16(&mut b)
486}
487
488pub fn msg_lsp_ack(lsp_id: u16, stream: u8, update_id: u32) -> Vec<u8> {
489    let mut msg = Vec::with_capacity(8);
490    msg.push(C2S_LSP_ACK);
491    msg.extend_from_slice(&lsp_id.to_le_bytes());
492    msg.push(stream);
493    msg.extend_from_slice(&update_id.to_le_bytes());
494    msg
495}
496
497/// Parse `C2S_LSP_ACK` into `(lsp_id, stream, update_id)`.
498pub fn parse_lsp_ack(msg: &[u8]) -> Option<(u16, u8, u32)> {
499    let mut b = body_of(msg, C2S_LSP_ACK)?;
500    let lsp_id = take_u16(&mut b)?;
501    let stream = take_u8(&mut b)?;
502    let update_id = take_u32(&mut b)?;
503    Some((lsp_id, stream, update_id))
504}
505
506/// A decoded `C2S_LSP_QUERY` request.
507#[derive(Clone, Debug, PartialEq, Eq)]
508pub struct LspQueryRequest<'a> {
509    pub nonce: u16,
510    pub lsp_id: u16,
511    /// One of `LSP_QUERY_*`.
512    pub kind: u8,
513    pub flags: u8,
514    /// 0-based; ignored by the symbol kinds.
515    pub line: u32,
516    /// UTF-8 byte offset within the line; ignored by the symbol kinds.
517    pub col: u32,
518    /// Escaped form; empty for `WS_SYMBOLS`.
519    pub path: &'a str,
520    /// `WS_SYMBOLS` query string or `RENAME` new name; empty otherwise.
521    pub arg: &'a str,
522}
523
524pub fn msg_lsp_query(req: &LspQueryRequest<'_>) -> Vec<u8> {
525    let mut msg = Vec::with_capacity(19 + req.path.len() + req.arg.len());
526    msg.push(C2S_LSP_QUERY);
527    msg.extend_from_slice(&req.nonce.to_le_bytes());
528    msg.extend_from_slice(&req.lsp_id.to_le_bytes());
529    msg.push(req.kind);
530    msg.push(req.flags);
531    msg.extend_from_slice(&req.line.to_le_bytes());
532    msg.extend_from_slice(&req.col.to_le_bytes());
533    push_str(&mut msg, req.path);
534    push_str(&mut msg, req.arg);
535    msg
536}
537
538pub fn parse_lsp_query(msg: &[u8]) -> Option<LspQueryRequest<'_>> {
539    let mut b = body_of(msg, C2S_LSP_QUERY)?;
540    let nonce = take_u16(&mut b)?;
541    let lsp_id = take_u16(&mut b)?;
542    let kind = take_u8(&mut b)?;
543    let flags = take_u8(&mut b)?;
544    let line = take_u32(&mut b)?;
545    let col = take_u32(&mut b)?;
546    let path = take_str(&mut b)?;
547    let arg = take_str(&mut b)?;
548    Some(LspQueryRequest {
549        nonce,
550        lsp_id,
551        kind,
552        flags,
553        line,
554        col,
555        path,
556        arg,
557    })
558}
559
560pub fn msg_lsp_cancel(nonce: u16) -> Vec<u8> {
561    let mut msg = Vec::with_capacity(3);
562    msg.push(C2S_LSP_CANCEL);
563    msg.extend_from_slice(&nonce.to_le_bytes());
564    msg
565}
566
567pub fn parse_lsp_cancel(msg: &[u8]) -> Option<u16> {
568    let mut b = body_of(msg, C2S_LSP_CANCEL)?;
569    take_u16(&mut b)
570}
571
572pub fn msg_lsp_servers(nonce: u16) -> Vec<u8> {
573    let mut msg = Vec::with_capacity(3);
574    msg.push(C2S_LSP_SERVERS);
575    msg.extend_from_slice(&nonce.to_le_bytes());
576    msg
577}
578
579pub fn parse_lsp_servers(msg: &[u8]) -> Option<u16> {
580    let mut b = body_of(msg, C2S_LSP_SERVERS)?;
581    take_u16(&mut b)
582}
583
584pub fn msg_lsp_stop(nonce: u16, server_ref: u16) -> Vec<u8> {
585    let mut msg = Vec::with_capacity(5);
586    msg.push(C2S_LSP_STOP);
587    msg.extend_from_slice(&nonce.to_le_bytes());
588    msg.extend_from_slice(&server_ref.to_le_bytes());
589    msg
590}
591
592/// Parse `C2S_LSP_STOP` into `(nonce, server_ref)`.
593pub fn parse_lsp_stop(msg: &[u8]) -> Option<(u16, u16)> {
594    let mut b = body_of(msg, C2S_LSP_STOP)?;
595    let nonce = take_u16(&mut b)?;
596    let server_ref = take_u16(&mut b)?;
597    Some((nonce, server_ref))
598}
599
600/// Build a `C2S_LSP_BUFFER`: `text` is the full buffer content,
601/// LZ4-compressed on the wire. A [`LSP_BUFFER_RELEASE`] must carry empty
602/// `text`.
603pub fn msg_lsp_buffer(lsp_id: u16, flags: u8, path: &str, text: &[u8]) -> Vec<u8> {
604    let compressed = lz4_flex::compress_prepend_size(text);
605    let mut msg = Vec::with_capacity(6 + path.len() + compressed.len());
606    msg.push(C2S_LSP_BUFFER);
607    msg.extend_from_slice(&lsp_id.to_le_bytes());
608    msg.push(flags);
609    push_str(&mut msg, path);
610    msg.extend_from_slice(&compressed);
611    msg
612}
613
614/// Parse `C2S_LSP_BUFFER` into `(lsp_id, flags, path, text)` with the
615/// text decompressed.
616pub fn parse_lsp_buffer(msg: &[u8]) -> Option<(u16, u8, &str, Vec<u8>)> {
617    let mut b = body_of(msg, C2S_LSP_BUFFER)?;
618    let lsp_id = take_u16(&mut b)?;
619    let flags = take_u8(&mut b)?;
620    let path = take_str(&mut b)?;
621    let text = decompress_guarded(b)?;
622    Some((lsp_id, flags, path, text))
623}
624
625// ---------------------------------------------------------------------------
626// S2C message builders and parsers
627// ---------------------------------------------------------------------------
628
629pub fn msg_lsp_opened(
630    nonce: u16,
631    lsp_id: u16,
632    status: u8,
633    flags: u8,
634    root: &str,
635    detail: &str,
636) -> Vec<u8> {
637    let mut msg = Vec::with_capacity(11 + root.len() + detail.len());
638    msg.push(S2C_LSP_OPENED);
639    msg.extend_from_slice(&nonce.to_le_bytes());
640    msg.extend_from_slice(&lsp_id.to_le_bytes());
641    msg.push(status);
642    msg.push(flags);
643    push_str(&mut msg, root);
644    push_str(&mut msg, detail);
645    msg
646}
647
648/// A decoded `S2C_LSP_OPENED`.
649#[derive(Clone, Debug, PartialEq, Eq)]
650pub struct LspOpened<'a> {
651    pub nonce: u16,
652    pub lsp_id: u16,
653    pub status: u8,
654    pub flags: u8,
655    /// Escaped canonical workspace root; empty on failure.
656    pub root: &'a str,
657    /// Diagnostic on failure.
658    pub detail: &'a str,
659}
660
661pub fn parse_lsp_opened(msg: &[u8]) -> Option<LspOpened<'_>> {
662    let mut b = body_of(msg, S2C_LSP_OPENED)?;
663    let nonce = take_u16(&mut b)?;
664    let lsp_id = take_u16(&mut b)?;
665    let status = take_u8(&mut b)?;
666    let flags = take_u8(&mut b)?;
667    let root = take_str(&mut b)?;
668    let detail = take_str(&mut b)?;
669    Some(LspOpened {
670        nonce,
671        lsp_id,
672        status,
673        flags,
674        root,
675        detail,
676    })
677}
678
679pub fn msg_lsp_state(lsp_id: u16, state_id: u32, flags: u8, records: &[u8]) -> Vec<u8> {
680    let compressed = lz4_flex::compress_prepend_size(records);
681    let mut msg = Vec::with_capacity(8 + compressed.len());
682    msg.push(S2C_LSP_STATE);
683    msg.extend_from_slice(&lsp_id.to_le_bytes());
684    msg.extend_from_slice(&state_id.to_le_bytes());
685    msg.push(flags);
686    msg.extend_from_slice(&compressed);
687    msg
688}
689
690/// Parse `S2C_LSP_STATE` into `(lsp_id, state_id, flags, records)` with
691/// the records decompressed.
692pub fn parse_lsp_state(msg: &[u8]) -> Option<(u16, u32, u8, Vec<u8>)> {
693    let mut b = body_of(msg, S2C_LSP_STATE)?;
694    let lsp_id = take_u16(&mut b)?;
695    let state_id = take_u32(&mut b)?;
696    let flags = take_u8(&mut b)?;
697    let records = decompress_guarded(b)?;
698    Some((lsp_id, state_id, flags, records))
699}
700
701pub fn msg_lsp_diag(lsp_id: u16, update_id: u32, flags: u8, records: &[u8]) -> Vec<u8> {
702    let compressed = lz4_flex::compress_prepend_size(records);
703    let mut msg = Vec::with_capacity(8 + compressed.len());
704    msg.push(S2C_LSP_DIAG);
705    msg.extend_from_slice(&lsp_id.to_le_bytes());
706    msg.extend_from_slice(&update_id.to_le_bytes());
707    msg.push(flags);
708    msg.extend_from_slice(&compressed);
709    msg
710}
711
712/// Parse `S2C_LSP_DIAG` into `(lsp_id, update_id, flags, records)` with
713/// the records decompressed.
714pub fn parse_lsp_diag(msg: &[u8]) -> Option<(u16, u32, u8, Vec<u8>)> {
715    let mut b = body_of(msg, S2C_LSP_DIAG)?;
716    let lsp_id = take_u16(&mut b)?;
717    let update_id = take_u32(&mut b)?;
718    let flags = take_u8(&mut b)?;
719    let records = decompress_guarded(b)?;
720    Some((lsp_id, update_id, flags, records))
721}
722
723pub fn msg_lsp_query_resp(
724    nonce: u16,
725    status: u8,
726    flags: u8,
727    detail: &str,
728    records: &[u8],
729) -> Vec<u8> {
730    let compressed = lz4_flex::compress_prepend_size(records);
731    let mut msg = Vec::with_capacity(7 + detail.len() + compressed.len());
732    msg.push(S2C_LSP_QUERY);
733    msg.extend_from_slice(&nonce.to_le_bytes());
734    msg.push(status);
735    msg.push(flags);
736    push_str(&mut msg, detail);
737    msg.extend_from_slice(&compressed);
738    msg
739}
740
741/// A decoded `S2C_LSP_QUERY` response.
742#[derive(Clone, Debug, PartialEq, Eq)]
743pub struct LspQueryResp {
744    pub nonce: u16,
745    pub status: u8,
746    pub flags: u8,
747    /// Human-readable failure reason (e.g. the upstream server's own
748    /// error message on `OTHER`); empty on success.
749    pub detail: String,
750    /// Decompressed response records.
751    pub records: Vec<u8>,
752}
753
754/// Parse `S2C_LSP_QUERY` with the records decompressed.
755pub fn parse_lsp_query_resp(msg: &[u8]) -> Option<LspQueryResp> {
756    let mut b = body_of(msg, S2C_LSP_QUERY)?;
757    let nonce = take_u16(&mut b)?;
758    let status = take_u8(&mut b)?;
759    let flags = take_u8(&mut b)?;
760    let detail = take_str(&mut b)?.to_string();
761    let records = decompress_guarded(b)?;
762    Some(LspQueryResp {
763        nonce,
764        status,
765        flags,
766        detail,
767        records,
768    })
769}
770
771pub fn msg_lsp_closed(lsp_id: u16, reason: u8) -> Vec<u8> {
772    let mut msg = Vec::with_capacity(4);
773    msg.push(S2C_LSP_CLOSED);
774    msg.extend_from_slice(&lsp_id.to_le_bytes());
775    msg.push(reason);
776    msg
777}
778
779/// Parse `S2C_LSP_CLOSED` into `(lsp_id, reason)`.
780pub fn parse_lsp_closed(msg: &[u8]) -> Option<(u16, u8)> {
781    let mut b = body_of(msg, S2C_LSP_CLOSED)?;
782    let lsp_id = take_u16(&mut b)?;
783    let reason = take_u8(&mut b)?;
784    Some((lsp_id, reason))
785}
786
787pub fn msg_lsp_servers_resp(nonce: u16, status: u8, flags: u8, records: &[u8]) -> Vec<u8> {
788    let compressed = lz4_flex::compress_prepend_size(records);
789    let mut msg = Vec::with_capacity(5 + compressed.len());
790    msg.push(S2C_LSP_SERVERS);
791    msg.extend_from_slice(&nonce.to_le_bytes());
792    msg.push(status);
793    msg.push(flags);
794    msg.extend_from_slice(&compressed);
795    msg
796}
797
798/// Parse `S2C_LSP_SERVERS` into `(nonce, status, flags, records)` with
799/// the records decompressed.
800pub fn parse_lsp_servers_resp(msg: &[u8]) -> Option<(u16, u8, u8, Vec<u8>)> {
801    let mut b = body_of(msg, S2C_LSP_SERVERS)?;
802    let nonce = take_u16(&mut b)?;
803    let status = take_u8(&mut b)?;
804    let flags = take_u8(&mut b)?;
805    let records = decompress_guarded(b)?;
806    Some((nonce, status, flags, records))
807}
808
809pub fn msg_lsp_stopped(nonce: u16, status: u8) -> Vec<u8> {
810    let mut msg = Vec::with_capacity(4);
811    msg.push(S2C_LSP_STOPPED);
812    msg.extend_from_slice(&nonce.to_le_bytes());
813    msg.push(status);
814    msg
815}
816
817/// Parse `S2C_LSP_STOPPED` into `(nonce, status)`.
818pub fn parse_lsp_stopped(msg: &[u8]) -> Option<(u16, u8)> {
819    let mut b = body_of(msg, S2C_LSP_STOPPED)?;
820    let nonce = take_u16(&mut b)?;
821    let status = take_u8(&mut b)?;
822    Some((nonce, status))
823}
824
825// ---------------------------------------------------------------------------
826// LSP_STATE records
827// ---------------------------------------------------------------------------
828
829/// One decoded record from an `LSP_STATE` payload.
830#[derive(Clone, Debug, PartialEq, Eq)]
831pub enum LspStateRecord<'a> {
832    /// SERVER 0x01: [kind:1][server_ref:2][phase:1][progress_pct:1][caps:4]
833    /// [epoch:4][refused_edits:4][rss:8][id_len:2][id:N][msg_len:2][msg:N]
834    Server {
835        /// Daemon-scoped backend id (`LSP_STOP` target).
836        server_ref: u16,
837        /// One of `LSP_PHASE_*`.
838        phase: u8,
839        /// 0-100, or [`LSP_PROGRESS_UNKNOWN`].
840        progress_pct: u8,
841        /// `LSP_CAP_*` bits.
842        caps: u32,
843        /// Increments on dynamic capability (re)registration.
844        epoch: u32,
845        /// `workspace/applyEdit` requests answered `applied:false`.
846        refused_edits: u32,
847        /// Best-effort resident set size in bytes; 0 = unknown.
848        rss: u64,
849        /// Server id from the discovery table (e.g. `rust-analyzer`).
850        id: &'a str,
851        /// Last progress or showMessage line.
852        msg: &'a str,
853    },
854}
855
856/// Append one record to an uncompressed `LSP_STATE` records buffer.
857pub fn append_lsp_state_record(buf: &mut Vec<u8>, record: &LspStateRecord<'_>) {
858    let start = begin_record(buf);
859    match record {
860        LspStateRecord::Server {
861            server_ref,
862            phase,
863            progress_pct,
864            caps,
865            epoch,
866            refused_edits,
867            rss,
868            id,
869            msg,
870        } => {
871            buf.push(LSP_STATE_RECORD_SERVER);
872            buf.extend_from_slice(&server_ref.to_le_bytes());
873            buf.push(*phase);
874            buf.push(*progress_pct);
875            buf.extend_from_slice(&caps.to_le_bytes());
876            buf.extend_from_slice(&epoch.to_le_bytes());
877            buf.extend_from_slice(&refused_edits.to_le_bytes());
878            buf.extend_from_slice(&rss.to_le_bytes());
879            push_str(buf, id);
880            push_str(buf, msg);
881        }
882    }
883    end_record(buf, start);
884}
885
886pub struct LspStateRecordIter<'a> {
887    data: &'a [u8],
888}
889
890/// Iterate records in an uncompressed `LSP_STATE` payload.
891pub fn lsp_state_records(data: &[u8]) -> LspStateRecordIter<'_> {
892    LspStateRecordIter { data }
893}
894
895impl<'a> Iterator for LspStateRecordIter<'a> {
896    type Item = LspStateRecord<'a>;
897
898    fn next(&mut self) -> Option<LspStateRecord<'a>> {
899        loop {
900            let (kind, mut b) = next_record(&mut self.data)?;
901            match kind {
902                LSP_STATE_RECORD_SERVER => {
903                    let server_ref = take_u16(&mut b)?;
904                    let phase = take_u8(&mut b)?;
905                    let progress_pct = take_u8(&mut b)?;
906                    let caps = take_u32(&mut b)?;
907                    let epoch = take_u32(&mut b)?;
908                    let refused_edits = take_u32(&mut b)?;
909                    let rss = take_u64(&mut b)?;
910                    let id = take_str(&mut b)?;
911                    let msg = take_str(&mut b)?;
912                    return Some(LspStateRecord::Server {
913                        server_ref,
914                        phase,
915                        progress_pct,
916                        caps,
917                        epoch,
918                        refused_edits,
919                        rss,
920                        id,
921                        msg,
922                    });
923                }
924                _ => continue, // unknown kind: skip via record_len
925            }
926        }
927    }
928}
929
930// ---------------------------------------------------------------------------
931// LSP_SERVERS records
932// ---------------------------------------------------------------------------
933
934/// One decoded record from an `LSP_SERVERS` payload: the `LSP_STATE`
935/// `SERVER` layout plus the escaped workspace root.
936#[derive(Clone, Debug, PartialEq, Eq)]
937pub enum LspServersRecord<'a> {
938    /// SERVER 0x01: the `LSP_STATE` layout + [root_len:2][root:N]
939    Server {
940        server_ref: u16,
941        phase: u8,
942        progress_pct: u8,
943        caps: u32,
944        epoch: u32,
945        refused_edits: u32,
946        rss: u64,
947        id: &'a str,
948        msg: &'a str,
949        /// Escaped canonical workspace root.
950        root: &'a str,
951    },
952}
953
954/// Append one record to an uncompressed `LSP_SERVERS` records buffer.
955pub fn append_lsp_servers_record(buf: &mut Vec<u8>, record: &LspServersRecord<'_>) {
956    let start = begin_record(buf);
957    match record {
958        LspServersRecord::Server {
959            server_ref,
960            phase,
961            progress_pct,
962            caps,
963            epoch,
964            refused_edits,
965            rss,
966            id,
967            msg,
968            root,
969        } => {
970            buf.push(LSP_STATE_RECORD_SERVER);
971            buf.extend_from_slice(&server_ref.to_le_bytes());
972            buf.push(*phase);
973            buf.push(*progress_pct);
974            buf.extend_from_slice(&caps.to_le_bytes());
975            buf.extend_from_slice(&epoch.to_le_bytes());
976            buf.extend_from_slice(&refused_edits.to_le_bytes());
977            buf.extend_from_slice(&rss.to_le_bytes());
978            push_str(buf, id);
979            push_str(buf, msg);
980            push_str(buf, root);
981        }
982    }
983    end_record(buf, start);
984}
985
986pub struct LspServersRecordIter<'a> {
987    data: &'a [u8],
988}
989
990/// Iterate records in an uncompressed `LSP_SERVERS` payload.
991pub fn lsp_servers_records(data: &[u8]) -> LspServersRecordIter<'_> {
992    LspServersRecordIter { data }
993}
994
995impl<'a> Iterator for LspServersRecordIter<'a> {
996    type Item = LspServersRecord<'a>;
997
998    fn next(&mut self) -> Option<LspServersRecord<'a>> {
999        loop {
1000            let (kind, mut b) = next_record(&mut self.data)?;
1001            match kind {
1002                LSP_STATE_RECORD_SERVER => {
1003                    let server_ref = take_u16(&mut b)?;
1004                    let phase = take_u8(&mut b)?;
1005                    let progress_pct = take_u8(&mut b)?;
1006                    let caps = take_u32(&mut b)?;
1007                    let epoch = take_u32(&mut b)?;
1008                    let refused_edits = take_u32(&mut b)?;
1009                    let rss = take_u64(&mut b)?;
1010                    let id = take_str(&mut b)?;
1011                    let msg = take_str(&mut b)?;
1012                    let root = take_str(&mut b)?;
1013                    return Some(LspServersRecord::Server {
1014                        server_ref,
1015                        phase,
1016                        progress_pct,
1017                        caps,
1018                        epoch,
1019                        refused_edits,
1020                        rss,
1021                        id,
1022                        msg,
1023                        root,
1024                    });
1025                }
1026                _ => continue, // unknown kind: skip via record_len
1027            }
1028        }
1029    }
1030}
1031
1032// ---------------------------------------------------------------------------
1033// LSP_DIAG records
1034// ---------------------------------------------------------------------------
1035
1036/// One decoded record from an `LSP_DIAG` payload.
1037#[derive(Clone, Debug, PartialEq, Eq)]
1038pub enum LspDiagRecord<'a> {
1039    /// FILE 0x01: [kind:1][hash:16][n:2][path_len:2][path:N]
1040    /// Replaces the file's entire diagnostic set (the following `n`
1041    /// `Diag` records); `n` = 0 clears. `hash` names the content version
1042    /// the set describes; zero when unknown.
1043    File {
1044        hash: LspHash,
1045        n: u16,
1046        path: &'a str,
1047    },
1048    /// DIAG 0x02: [kind:1][severity:1][flags:1][line:4][col:4][end_line:4][end_col:4]
1049    /// [code_len:2][code:N][src_len:2][source:N][msg_len:4][msg:N]
1050    Diag {
1051        /// One of `LSP_SEVERITY_*`.
1052        severity: u8,
1053        /// `LSP_DIAG_UNNECESSARY` / `LSP_DIAG_DEPRECATED`.
1054        flags: u8,
1055        line: u32,
1056        col: u32,
1057        end_line: u32,
1058        end_col: u32,
1059        code: &'a str,
1060        /// The producing backend (e.g. `rust-analyzer`, `clippy`).
1061        source: &'a str,
1062        msg: &'a str,
1063    },
1064}
1065
1066/// Append one record to an uncompressed `LSP_DIAG` records buffer.
1067pub fn append_lsp_diag_record(buf: &mut Vec<u8>, record: &LspDiagRecord<'_>) {
1068    let start = begin_record(buf);
1069    match record {
1070        LspDiagRecord::File { hash, n, path } => {
1071            buf.push(LSP_DIAG_RECORD_FILE);
1072            buf.extend_from_slice(hash);
1073            buf.extend_from_slice(&n.to_le_bytes());
1074            push_str(buf, path);
1075        }
1076        LspDiagRecord::Diag {
1077            severity,
1078            flags,
1079            line,
1080            col,
1081            end_line,
1082            end_col,
1083            code,
1084            source,
1085            msg,
1086        } => {
1087            buf.push(LSP_DIAG_RECORD_DIAG);
1088            buf.push(*severity);
1089            buf.push(*flags);
1090            buf.extend_from_slice(&line.to_le_bytes());
1091            buf.extend_from_slice(&col.to_le_bytes());
1092            buf.extend_from_slice(&end_line.to_le_bytes());
1093            buf.extend_from_slice(&end_col.to_le_bytes());
1094            push_str(buf, code);
1095            push_str(buf, source);
1096            push_bytes(buf, msg.as_bytes());
1097        }
1098    }
1099    end_record(buf, start);
1100}
1101
1102pub struct LspDiagRecordIter<'a> {
1103    data: &'a [u8],
1104}
1105
1106/// Iterate records in an uncompressed `LSP_DIAG` payload.
1107pub fn lsp_diag_records(data: &[u8]) -> LspDiagRecordIter<'_> {
1108    LspDiagRecordIter { data }
1109}
1110
1111impl<'a> Iterator for LspDiagRecordIter<'a> {
1112    type Item = LspDiagRecord<'a>;
1113
1114    fn next(&mut self) -> Option<LspDiagRecord<'a>> {
1115        loop {
1116            let (kind, mut b) = next_record(&mut self.data)?;
1117            match kind {
1118                LSP_DIAG_RECORD_FILE => {
1119                    let hash = take_hash(&mut b)?;
1120                    let n = take_u16(&mut b)?;
1121                    let path = take_str(&mut b)?;
1122                    return Some(LspDiagRecord::File { hash, n, path });
1123                }
1124                LSP_DIAG_RECORD_DIAG => {
1125                    let severity = take_u8(&mut b)?;
1126                    let flags = take_u8(&mut b)?;
1127                    let line = take_u32(&mut b)?;
1128                    let col = take_u32(&mut b)?;
1129                    let end_line = take_u32(&mut b)?;
1130                    let end_col = take_u32(&mut b)?;
1131                    let code = take_str(&mut b)?;
1132                    let source = take_str(&mut b)?;
1133                    let msg = take_text(&mut b)?;
1134                    return Some(LspDiagRecord::Diag {
1135                        severity,
1136                        flags,
1137                        line,
1138                        col,
1139                        end_line,
1140                        end_col,
1141                        code,
1142                        source,
1143                        msg,
1144                    });
1145                }
1146                _ => continue, // unknown kind: skip via record_len
1147            }
1148        }
1149    }
1150}
1151
1152// ---------------------------------------------------------------------------
1153// LSP_QUERY response records
1154// ---------------------------------------------------------------------------
1155
1156/// One decoded record from an `LSP_QUERY` response payload.
1157#[derive(Clone, Debug, PartialEq, Eq)]
1158pub enum LspQueryRecord<'a> {
1159    /// LOCATION 0x01: [kind:1][flags:1][hash:16][line:4][col:4][end_line:4][end_col:4][path_len:2][path:N]
1160    Location {
1161        flags: u8,
1162        /// Content version the location refers into; zero when unknown.
1163        hash: LspHash,
1164        line: u32,
1165        col: u32,
1166        end_line: u32,
1167        end_col: u32,
1168        path: &'a str,
1169    },
1170    /// MARKUP 0x02: [kind:1][format:1][text_len:4][text:N]
1171    Markup {
1172        /// [`LSP_MARKUP_PLAIN`] or [`LSP_MARKUP_MARKDOWN`].
1173        format: u8,
1174        text: &'a str,
1175    },
1176    /// SYMBOL 0x03: [kind:1][sym_kind:1][flags:1][depth:1][line:4][col:4]
1177    /// [end_line:4][end_col:4][name_len:2][name:N][path_len:2][path:N]
1178    /// `depth` nests document outlines (pre-order); 0 at the top level.
1179    Symbol {
1180        /// LSP SymbolKind value.
1181        sym_kind: u8,
1182        /// [`LSP_SYMBOL_DEPRECATED`].
1183        flags: u8,
1184        depth: u8,
1185        line: u32,
1186        col: u32,
1187        end_line: u32,
1188        end_col: u32,
1189        name: &'a str,
1190        path: &'a str,
1191    },
1192    /// EDIT 0x04: [kind:1][flags:1][hash:16][line:4][col:4][end_line:4][end_col:4]
1193    /// [new_len:4][new_text:N][path_len:2][path:N]
1194    /// One ordered edit of a rename plan, against the content version
1195    /// named by `hash`. Data, never applied.
1196    Edit {
1197        flags: u8,
1198        hash: LspHash,
1199        line: u32,
1200        col: u32,
1201        end_line: u32,
1202        end_col: u32,
1203        new_text: &'a str,
1204        path: &'a str,
1205    },
1206    /// COMPLETION 0x05: [kind:1][item_kind:1][flags:1][line:4][col:4]
1207    /// [end_line:4][end_col:4][label_len:2][label:N][insert_len:2][insert:N]
1208    /// [detail_len:2][detail:N]
1209    /// The range is the item's primary replace range (zero range → the
1210    /// client picks its own word boundary); empty `insert` → insert the
1211    /// label.
1212    Completion {
1213        /// LSP CompletionItemKind value; 0 unknown.
1214        item_kind: u8,
1215        /// `LSP_COMPLETION_*` bits.
1216        flags: u8,
1217        line: u32,
1218        col: u32,
1219        end_line: u32,
1220        end_col: u32,
1221        label: &'a str,
1222        insert: &'a str,
1223        detail: &'a str,
1224    },
1225    /// SIGNATURE 0x06: [kind:1][flags:1][active_param:2][param_start:2]
1226    /// [param_end:2][label_len:2][label:N][doc_len:4][doc:N]
1227    /// One signature, the active one first ([`LSP_SIGNATURE_ACTIVE`]);
1228    /// `param_start`/`param_end` bound the active parameter within
1229    /// `label` in UTF-8 bytes (0,0 = unknown).
1230    Signature {
1231        /// `LSP_SIGNATURE_*` bits.
1232        flags: u8,
1233        /// Active parameter index, or [`LSP_SIGNATURE_NO_PARAM`].
1234        active_param: u16,
1235        param_start: u16,
1236        param_end: u16,
1237        label: &'a str,
1238        doc: &'a str,
1239    },
1240}
1241
1242/// Append one record to an uncompressed `LSP_QUERY` response buffer.
1243pub fn append_lsp_query_record(buf: &mut Vec<u8>, record: &LspQueryRecord<'_>) {
1244    let start = begin_record(buf);
1245    match record {
1246        LspQueryRecord::Location {
1247            flags,
1248            hash,
1249            line,
1250            col,
1251            end_line,
1252            end_col,
1253            path,
1254        } => {
1255            buf.push(LSP_QUERY_RECORD_LOCATION);
1256            buf.push(*flags);
1257            buf.extend_from_slice(hash);
1258            buf.extend_from_slice(&line.to_le_bytes());
1259            buf.extend_from_slice(&col.to_le_bytes());
1260            buf.extend_from_slice(&end_line.to_le_bytes());
1261            buf.extend_from_slice(&end_col.to_le_bytes());
1262            push_str(buf, path);
1263        }
1264        LspQueryRecord::Markup { format, text } => {
1265            buf.push(LSP_QUERY_RECORD_MARKUP);
1266            buf.push(*format);
1267            push_bytes(buf, text.as_bytes());
1268        }
1269        LspQueryRecord::Symbol {
1270            sym_kind,
1271            flags,
1272            depth,
1273            line,
1274            col,
1275            end_line,
1276            end_col,
1277            name,
1278            path,
1279        } => {
1280            buf.push(LSP_QUERY_RECORD_SYMBOL);
1281            buf.push(*sym_kind);
1282            buf.push(*flags);
1283            buf.push(*depth);
1284            buf.extend_from_slice(&line.to_le_bytes());
1285            buf.extend_from_slice(&col.to_le_bytes());
1286            buf.extend_from_slice(&end_line.to_le_bytes());
1287            buf.extend_from_slice(&end_col.to_le_bytes());
1288            push_str(buf, name);
1289            push_str(buf, path);
1290        }
1291        LspQueryRecord::Edit {
1292            flags,
1293            hash,
1294            line,
1295            col,
1296            end_line,
1297            end_col,
1298            new_text,
1299            path,
1300        } => {
1301            buf.push(LSP_QUERY_RECORD_EDIT);
1302            buf.push(*flags);
1303            buf.extend_from_slice(hash);
1304            buf.extend_from_slice(&line.to_le_bytes());
1305            buf.extend_from_slice(&col.to_le_bytes());
1306            buf.extend_from_slice(&end_line.to_le_bytes());
1307            buf.extend_from_slice(&end_col.to_le_bytes());
1308            push_bytes(buf, new_text.as_bytes());
1309            push_str(buf, path);
1310        }
1311        LspQueryRecord::Completion {
1312            item_kind,
1313            flags,
1314            line,
1315            col,
1316            end_line,
1317            end_col,
1318            label,
1319            insert,
1320            detail,
1321        } => {
1322            buf.push(LSP_QUERY_RECORD_COMPLETION);
1323            buf.push(*item_kind);
1324            buf.push(*flags);
1325            buf.extend_from_slice(&line.to_le_bytes());
1326            buf.extend_from_slice(&col.to_le_bytes());
1327            buf.extend_from_slice(&end_line.to_le_bytes());
1328            buf.extend_from_slice(&end_col.to_le_bytes());
1329            push_str(buf, label);
1330            push_str(buf, insert);
1331            push_str(buf, detail);
1332        }
1333        LspQueryRecord::Signature {
1334            flags,
1335            active_param,
1336            param_start,
1337            param_end,
1338            label,
1339            doc,
1340        } => {
1341            buf.push(LSP_QUERY_RECORD_SIGNATURE);
1342            buf.push(*flags);
1343            buf.extend_from_slice(&active_param.to_le_bytes());
1344            buf.extend_from_slice(&param_start.to_le_bytes());
1345            buf.extend_from_slice(&param_end.to_le_bytes());
1346            push_str(buf, label);
1347            push_bytes(buf, doc.as_bytes());
1348        }
1349    }
1350    end_record(buf, start);
1351}
1352
1353pub struct LspQueryRecordIter<'a> {
1354    data: &'a [u8],
1355}
1356
1357/// Iterate records in an uncompressed `LSP_QUERY` response payload.
1358pub fn lsp_query_records(data: &[u8]) -> LspQueryRecordIter<'_> {
1359    LspQueryRecordIter { data }
1360}
1361
1362impl<'a> Iterator for LspQueryRecordIter<'a> {
1363    type Item = LspQueryRecord<'a>;
1364
1365    fn next(&mut self) -> Option<LspQueryRecord<'a>> {
1366        loop {
1367            let (kind, mut b) = next_record(&mut self.data)?;
1368            match kind {
1369                LSP_QUERY_RECORD_LOCATION => {
1370                    let flags = take_u8(&mut b)?;
1371                    let hash = take_hash(&mut b)?;
1372                    let line = take_u32(&mut b)?;
1373                    let col = take_u32(&mut b)?;
1374                    let end_line = take_u32(&mut b)?;
1375                    let end_col = take_u32(&mut b)?;
1376                    let path = take_str(&mut b)?;
1377                    return Some(LspQueryRecord::Location {
1378                        flags,
1379                        hash,
1380                        line,
1381                        col,
1382                        end_line,
1383                        end_col,
1384                        path,
1385                    });
1386                }
1387                LSP_QUERY_RECORD_MARKUP => {
1388                    let format = take_u8(&mut b)?;
1389                    let text = take_text(&mut b)?;
1390                    return Some(LspQueryRecord::Markup { format, text });
1391                }
1392                LSP_QUERY_RECORD_SYMBOL => {
1393                    let sym_kind = take_u8(&mut b)?;
1394                    let flags = take_u8(&mut b)?;
1395                    let depth = take_u8(&mut b)?;
1396                    let line = take_u32(&mut b)?;
1397                    let col = take_u32(&mut b)?;
1398                    let end_line = take_u32(&mut b)?;
1399                    let end_col = take_u32(&mut b)?;
1400                    let name = take_str(&mut b)?;
1401                    let path = take_str(&mut b)?;
1402                    return Some(LspQueryRecord::Symbol {
1403                        sym_kind,
1404                        flags,
1405                        depth,
1406                        line,
1407                        col,
1408                        end_line,
1409                        end_col,
1410                        name,
1411                        path,
1412                    });
1413                }
1414                LSP_QUERY_RECORD_EDIT => {
1415                    let flags = take_u8(&mut b)?;
1416                    let hash = take_hash(&mut b)?;
1417                    let line = take_u32(&mut b)?;
1418                    let col = take_u32(&mut b)?;
1419                    let end_line = take_u32(&mut b)?;
1420                    let end_col = take_u32(&mut b)?;
1421                    let new_text = take_text(&mut b)?;
1422                    let path = take_str(&mut b)?;
1423                    return Some(LspQueryRecord::Edit {
1424                        flags,
1425                        hash,
1426                        line,
1427                        col,
1428                        end_line,
1429                        end_col,
1430                        new_text,
1431                        path,
1432                    });
1433                }
1434                LSP_QUERY_RECORD_COMPLETION => {
1435                    let item_kind = take_u8(&mut b)?;
1436                    let flags = take_u8(&mut b)?;
1437                    let line = take_u32(&mut b)?;
1438                    let col = take_u32(&mut b)?;
1439                    let end_line = take_u32(&mut b)?;
1440                    let end_col = take_u32(&mut b)?;
1441                    let label = take_str(&mut b)?;
1442                    let insert = take_str(&mut b)?;
1443                    let detail = take_str(&mut b)?;
1444                    return Some(LspQueryRecord::Completion {
1445                        item_kind,
1446                        flags,
1447                        line,
1448                        col,
1449                        end_line,
1450                        end_col,
1451                        label,
1452                        insert,
1453                        detail,
1454                    });
1455                }
1456                LSP_QUERY_RECORD_SIGNATURE => {
1457                    let flags = take_u8(&mut b)?;
1458                    let active_param = take_u16(&mut b)?;
1459                    let param_start = take_u16(&mut b)?;
1460                    let param_end = take_u16(&mut b)?;
1461                    let label = take_str(&mut b)?;
1462                    let doc = take_text(&mut b)?;
1463                    return Some(LspQueryRecord::Signature {
1464                        flags,
1465                        active_param,
1466                        param_start,
1467                        param_end,
1468                        label,
1469                        doc,
1470                    });
1471                }
1472                _ => continue, // unknown kind: skip via record_len
1473            }
1474        }
1475    }
1476}
1477
1478// ---------------------------------------------------------------------------
1479// Client-side reducers
1480// ---------------------------------------------------------------------------
1481
1482/// One backend's projected state, from a `SERVER` record.
1483#[derive(Clone, Debug, PartialEq, Eq)]
1484pub struct LspServerState {
1485    pub phase: u8,
1486    pub progress_pct: u8,
1487    pub caps: u32,
1488    pub epoch: u32,
1489    pub refused_edits: u32,
1490    pub rss: u64,
1491    pub id: String,
1492    pub msg: String,
1493}
1494
1495/// The complete client obligation for `LSP_STATE`: replace the whole
1496/// map, ack.
1497#[derive(Clone, Debug, Default, PartialEq, Eq)]
1498pub struct LspStateMirror {
1499    /// Keyed by `server_ref`.
1500    pub servers: BTreeMap<u16, LspServerState>,
1501    /// The last snapshot's flags.
1502    pub flags: u8,
1503}
1504
1505impl LspStateMirror {
1506    pub fn new() -> Self {
1507        Self::default()
1508    }
1509
1510    /// Apply one `LSP_STATE` message (starting at the opcode byte),
1511    /// replacing the whole state. Returns `Some(state_id)` to
1512    /// acknowledge, `None` if malformed.
1513    pub fn apply_state(&mut self, msg: &[u8]) -> Option<u32> {
1514        let (_lsp_id, state_id, flags, records) = parse_lsp_state(msg)?;
1515        let mut next = LspStateMirror {
1516            flags,
1517            ..Default::default()
1518        };
1519        for record in lsp_state_records(&records) {
1520            match record {
1521                LspStateRecord::Server {
1522                    server_ref,
1523                    phase,
1524                    progress_pct,
1525                    caps,
1526                    epoch,
1527                    refused_edits,
1528                    rss,
1529                    id,
1530                    msg,
1531                } => {
1532                    next.servers.insert(
1533                        server_ref,
1534                        LspServerState {
1535                            phase,
1536                            progress_pct,
1537                            caps,
1538                            epoch,
1539                            refused_edits,
1540                            rss,
1541                            id: id.to_string(),
1542                            msg: msg.to_string(),
1543                        },
1544                    );
1545                }
1546            }
1547        }
1548        *self = next;
1549        Some(state_id)
1550    }
1551}
1552
1553/// One diagnostic, owned, from a `DIAG` record.
1554#[derive(Clone, Debug, PartialEq, Eq)]
1555pub struct LspDiagnostic {
1556    pub severity: u8,
1557    pub flags: u8,
1558    pub line: u32,
1559    pub col: u32,
1560    pub end_line: u32,
1561    pub end_col: u32,
1562    pub code: String,
1563    pub source: String,
1564    pub msg: String,
1565}
1566
1567/// One file's diagnostic set.
1568#[derive(Clone, Debug, Default, PartialEq, Eq)]
1569pub struct LspFileDiags {
1570    /// Content version the set describes; zero when unknown.
1571    pub hash: LspHash,
1572    pub diags: Vec<LspDiagnostic>,
1573}
1574
1575/// The complete client obligation for `LSP_DIAG`: apply per-file
1576/// replacement sets, ack. Absence of a path means unknown, not clean.
1577#[derive(Clone, Debug, Default, PartialEq, Eq)]
1578pub struct LspDiagMirror {
1579    /// Keyed by escaped workspace-relative path.
1580    pub files: BTreeMap<String, LspFileDiags>,
1581}
1582
1583impl LspDiagMirror {
1584    pub fn new() -> Self {
1585        Self::default()
1586    }
1587
1588    /// Apply one `LSP_DIAG` message (starting at the opcode byte).
1589    /// Returns `Some(update_id)` to acknowledge, `None` if malformed.
1590    pub fn apply_diag(&mut self, msg: &[u8]) -> Option<u32> {
1591        let (_lsp_id, update_id, flags, records) = parse_lsp_diag(msg)?;
1592        if flags & LSP_DIAG_FULL != 0 {
1593            self.files.clear();
1594        }
1595        // `Diag` records attach to the most recent `File` record.
1596        let mut current: Option<String> = None;
1597        for record in lsp_diag_records(&records) {
1598            match record {
1599                LspDiagRecord::File { hash, n, path } => {
1600                    if n == 0 {
1601                        self.files.remove(path);
1602                        current = None;
1603                    } else {
1604                        let entry = self.files.entry(path.to_string()).or_default();
1605                        entry.hash = hash;
1606                        entry.diags.clear();
1607                        current = Some(path.to_string());
1608                    }
1609                }
1610                LspDiagRecord::Diag {
1611                    severity,
1612                    flags,
1613                    line,
1614                    col,
1615                    end_line,
1616                    end_col,
1617                    code,
1618                    source,
1619                    msg,
1620                } => {
1621                    if let Some(file) = current.as_ref().and_then(|p| self.files.get_mut(p)) {
1622                        file.diags.push(LspDiagnostic {
1623                            severity,
1624                            flags,
1625                            line,
1626                            col,
1627                            end_line,
1628                            end_col,
1629                            code: code.to_string(),
1630                            source: source.to_string(),
1631                            msg: msg.to_string(),
1632                        });
1633                    }
1634                }
1635            }
1636        }
1637        Some(update_id)
1638    }
1639}
1640
1641#[cfg(test)]
1642mod tests {
1643    use super::*;
1644
1645    #[test]
1646    fn lsp_open_from_pty_roundtrip_and_rebase() {
1647        let m = msg_lsp_open_from_pty(7, LSP_OPEN_WATCH, 500, "sub", 3);
1648        assert_eq!(
1649            m,
1650            vec![
1651                0x60, 0x07, 0x00, 0x05, 0xf4, 0x01, 0x03, 0x00, 0x73, 0x75, 0x62, 0x03, 0x00
1652            ]
1653        );
1654        assert_eq!(lsp_open_src_pty(&m), Some(3));
1655        assert_eq!(
1656            lsp_open_src_pty(&msg_lsp_open(7, LSP_OPEN_WATCH, 500, "sub")),
1657            None
1658        );
1659        let (_, flags, _, path) = parse_lsp_open(&m).unwrap();
1660        assert_eq!(flags & LSP_OPEN_FROM_PTY, LSP_OPEN_FROM_PTY);
1661        assert_eq!(path, "sub");
1662        // Rebase joins cwd + path and clears FROM_PTY.
1663        let reb = lsp_open_rebase(&m, Some("/w")).unwrap();
1664        let (_, rf, _, rp) = parse_lsp_open(&reb).unwrap();
1665        assert_eq!(rf & LSP_OPEN_FROM_PTY, 0);
1666        assert_eq!(rp, "/w/sub");
1667    }
1668
1669    fn hex(b: &[u8]) -> String {
1670        b.iter().map(|x| format!("{x:02x}")).collect()
1671    }
1672
1673    fn hash(fill: u8) -> LspHash {
1674        [fill; 16]
1675    }
1676
1677    #[test]
1678    fn request_roundtrips() {
1679        let msg = msg_lsp_open(7, LSP_OPEN_WATCH | LSP_OPEN_DIAGS, 500, "/src");
1680        assert_eq!(parse_lsp_open(&msg), Some((7, 3, 500, "/src")));
1681
1682        let msg = msg_lsp_close(9);
1683        assert_eq!(parse_lsp_close(&msg), Some(9));
1684
1685        let msg = msg_lsp_ack(9, LSP_STREAM_DIAG, 0x01020304);
1686        assert_eq!(parse_lsp_ack(&msg), Some((9, LSP_STREAM_DIAG, 0x01020304)));
1687
1688        let req = LspQueryRequest {
1689            nonce: 3,
1690            lsp_id: 9,
1691            kind: LSP_QUERY_REFERENCES,
1692            flags: LSP_REFS_INCLUDE_DECLARATION,
1693            line: 10,
1694            col: 4,
1695            path: "src/lib.rs",
1696            arg: "",
1697        };
1698        assert_eq!(parse_lsp_query(&msg_lsp_query(&req)), Some(req));
1699
1700        let msg = msg_lsp_cancel(3);
1701        assert_eq!(parse_lsp_cancel(&msg), Some(3));
1702
1703        let msg = msg_lsp_servers(4);
1704        assert_eq!(parse_lsp_servers(&msg), Some(4));
1705
1706        let msg = msg_lsp_stop(5, 2);
1707        assert_eq!(parse_lsp_stop(&msg), Some((5, 2)));
1708
1709        // Buffer overlay: text crosses LZ4, so roundtrip only (fixtures pin
1710        // uncompressed payloads elsewhere).
1711        let msg = msg_lsp_buffer(9, 0, "src/lib.rs", b"fn main() {}\n");
1712        assert_eq!(
1713            parse_lsp_buffer(&msg),
1714            Some((9, 0, "src/lib.rs", b"fn main() {}\n".to_vec()))
1715        );
1716        let msg = msg_lsp_buffer(9, LSP_BUFFER_RELEASE, "src/lib.rs", b"");
1717        assert_eq!(
1718            parse_lsp_buffer(&msg),
1719            Some((9, LSP_BUFFER_RELEASE, "src/lib.rs", Vec::new()))
1720        );
1721    }
1722
1723    #[test]
1724    fn response_roundtrips() {
1725        let msg = msg_lsp_opened(7, 1, LSP_STATUS_OK, 0, "/src", "");
1726        assert_eq!(
1727            parse_lsp_opened(&msg),
1728            Some(LspOpened {
1729                nonce: 7,
1730                lsp_id: 1,
1731                status: LSP_STATUS_OK,
1732                flags: 0,
1733                root: "/src",
1734                detail: "",
1735            })
1736        );
1737
1738        let msg = msg_lsp_opened(
1739            8,
1740            LSP_ID_INVALID,
1741            LSP_STATUS_NOT_FOUND,
1742            0,
1743            "",
1744            "gopls: not found on PATH",
1745        );
1746        let opened = parse_lsp_opened(&msg).unwrap();
1747        assert_eq!(opened.lsp_id, LSP_ID_INVALID);
1748        assert_eq!(opened.detail, "gopls: not found on PATH");
1749
1750        let mut records = Vec::new();
1751        append_lsp_state_record(
1752            &mut records,
1753            &LspStateRecord::Server {
1754                server_ref: 1,
1755                phase: LSP_PHASE_INDEXING,
1756                progress_pct: 42,
1757                caps: LSP_CAP_DEFINITION | LSP_CAP_HOVER,
1758                epoch: 2,
1759                refused_edits: 1,
1760                rss: 3 << 30,
1761                id: "rust-analyzer",
1762                msg: "indexing 42%",
1763            },
1764        );
1765        let msg = msg_lsp_state(1, 5, 0, &records);
1766        let (lsp_id, state_id, flags, decoded) = parse_lsp_state(&msg).unwrap();
1767        assert_eq!((lsp_id, state_id, flags), (1, 5, 0));
1768        assert_eq!(decoded, records);
1769
1770        let msg = msg_lsp_diag(1, 6, LSP_DIAG_FULL, &[]);
1771        assert_eq!(parse_lsp_diag(&msg), Some((1, 6, LSP_DIAG_FULL, vec![])));
1772
1773        let msg = msg_lsp_query_resp(3, LSP_STATUS_OK, 0, "", &[]);
1774        assert_eq!(
1775            parse_lsp_query_resp(&msg),
1776            Some(LspQueryResp {
1777                nonce: 3,
1778                status: LSP_STATUS_OK,
1779                flags: 0,
1780                detail: String::new(),
1781                records: vec![],
1782            })
1783        );
1784        // A failure carries the reason.
1785        let msg = msg_lsp_query_resp(4, LSP_STATUS_OTHER, 0, "gopls: boom", &[]);
1786        let resp = parse_lsp_query_resp(&msg).unwrap();
1787        assert_eq!(resp.status, LSP_STATUS_OTHER);
1788        assert_eq!(resp.detail, "gopls: boom");
1789
1790        let msg = msg_lsp_closed(1, LSP_CLOSED_BACKEND_FAILED);
1791        assert_eq!(parse_lsp_closed(&msg), Some((1, LSP_CLOSED_BACKEND_FAILED)));
1792
1793        let msg = msg_lsp_servers_resp(4, LSP_STATUS_OK, 0, &[]);
1794        assert_eq!(
1795            parse_lsp_servers_resp(&msg),
1796            Some((4, LSP_STATUS_OK, 0, vec![]))
1797        );
1798
1799        let msg = msg_lsp_stopped(5, LSP_STATUS_OK);
1800        assert_eq!(parse_lsp_stopped(&msg), Some((5, LSP_STATUS_OK)));
1801    }
1802
1803    #[test]
1804    fn state_record_roundtrip() {
1805        let record = LspStateRecord::Server {
1806            server_ref: 2,
1807            phase: LSP_PHASE_READY,
1808            progress_pct: LSP_PROGRESS_UNKNOWN,
1809            caps: LSP_CAP_RENAME,
1810            epoch: 0,
1811            refused_edits: 0,
1812            rss: 0,
1813            id: "gopls",
1814            msg: "",
1815        };
1816        let mut buf = Vec::new();
1817        append_lsp_state_record(&mut buf, &record);
1818        let decoded: Vec<_> = lsp_state_records(&buf).collect();
1819        assert_eq!(decoded, vec![record]);
1820    }
1821
1822    #[test]
1823    fn servers_record_roundtrip() {
1824        let record = LspServersRecord::Server {
1825            server_ref: 2,
1826            phase: LSP_PHASE_READY,
1827            progress_pct: 100,
1828            caps: 0x3F,
1829            epoch: 1,
1830            refused_edits: 0,
1831            rss: 1 << 20,
1832            id: "gopls",
1833            msg: "ready",
1834            root: "/work/api",
1835        };
1836        let mut buf = Vec::new();
1837        append_lsp_servers_record(&mut buf, &record);
1838        let decoded: Vec<_> = lsp_servers_records(&buf).collect();
1839        assert_eq!(decoded, vec![record]);
1840    }
1841
1842    #[test]
1843    fn diag_record_roundtrip() {
1844        let records = vec![
1845            LspDiagRecord::File {
1846                hash: hash(0xAB),
1847                n: 1,
1848                path: "src/lib.rs",
1849            },
1850            LspDiagRecord::Diag {
1851                severity: LSP_SEVERITY_ERROR,
1852                flags: LSP_DIAG_UNNECESSARY,
1853                line: 10,
1854                col: 4,
1855                end_line: 10,
1856                end_col: 9,
1857                code: "E0308",
1858                source: "rustc",
1859                msg: "mismatched types",
1860            },
1861            LspDiagRecord::File {
1862                hash: LSP_HASH_NONE,
1863                n: 0,
1864                path: "src/old.rs",
1865            },
1866        ];
1867        let mut buf = Vec::new();
1868        for r in &records {
1869            append_lsp_diag_record(&mut buf, r);
1870        }
1871        let decoded: Vec<_> = lsp_diag_records(&buf).collect();
1872        assert_eq!(decoded, records);
1873    }
1874
1875    #[test]
1876    fn query_record_roundtrip() {
1877        let records = vec![
1878            LspQueryRecord::Location {
1879                flags: 0,
1880                hash: hash(0xCD),
1881                line: 5,
1882                col: 0,
1883                end_line: 5,
1884                end_col: 12,
1885                path: "src/main.rs",
1886            },
1887            LspQueryRecord::Markup {
1888                format: LSP_MARKUP_MARKDOWN,
1889                text: "```rust\nfn main()\n```",
1890            },
1891            LspQueryRecord::Symbol {
1892                sym_kind: 12, // Function
1893                flags: LSP_SYMBOL_DEPRECATED,
1894                depth: 1,
1895                line: 3,
1896                col: 4,
1897                end_line: 9,
1898                end_col: 1,
1899                name: "main",
1900                path: "src/main.rs",
1901            },
1902            LspQueryRecord::Edit {
1903                flags: 0,
1904                hash: hash(0xEF),
1905                line: 3,
1906                col: 7,
1907                end_line: 3,
1908                end_col: 11,
1909                new_text: "run",
1910                path: "src/main.rs",
1911            },
1912            LspQueryRecord::Completion {
1913                item_kind: 3, // Function
1914                flags: LSP_COMPLETION_SNIPPET | LSP_COMPLETION_PRESELECT,
1915                line: 4,
1916                col: 2,
1917                end_line: 4,
1918                end_col: 6,
1919                label: "push",
1920                insert: "push(${1:value})",
1921                detail: "fn(&mut self, value: T)",
1922            },
1923            LspQueryRecord::Signature {
1924                flags: LSP_SIGNATURE_ACTIVE,
1925                active_param: 1,
1926                param_start: 14,
1927                param_end: 22,
1928                label: "fn get(&self, index: usize)",
1929                doc: "Returns a reference to an element.",
1930            },
1931        ];
1932        let mut buf = Vec::new();
1933        for r in &records {
1934            append_lsp_query_record(&mut buf, r);
1935        }
1936        let decoded: Vec<_> = lsp_query_records(&buf).collect();
1937        assert_eq!(decoded, records);
1938    }
1939
1940    #[test]
1941    fn unknown_record_kind_is_skipped() {
1942        let mut buf = Vec::new();
1943        // A future record kind: [record_len][0x7F][payload].
1944        let start = begin_record(&mut buf);
1945        buf.push(0x7F);
1946        buf.extend_from_slice(b"future");
1947        end_record(&mut buf, start);
1948        append_lsp_query_record(
1949            &mut buf,
1950            &LspQueryRecord::Markup {
1951                format: LSP_MARKUP_PLAIN,
1952                text: "hi",
1953            },
1954        );
1955        let decoded: Vec<_> = lsp_query_records(&buf).collect();
1956        assert_eq!(
1957            decoded,
1958            vec![LspQueryRecord::Markup {
1959                format: LSP_MARKUP_PLAIN,
1960                text: "hi",
1961            }]
1962        );
1963    }
1964
1965    #[test]
1966    fn malformed_record_ends_iteration() {
1967        let mut buf = Vec::new();
1968        append_lsp_diag_record(
1969            &mut buf,
1970            &LspDiagRecord::File {
1971                hash: LSP_HASH_NONE,
1972                n: 0,
1973                path: "a",
1974            },
1975        );
1976        // Declared length overruns the buffer: iteration must end, not panic.
1977        buf.extend_from_slice(&99u32.to_le_bytes());
1978        buf.push(LSP_DIAG_RECORD_FILE);
1979        let decoded: Vec<_> = lsp_diag_records(&buf).collect();
1980        assert_eq!(decoded.len(), 1);
1981    }
1982
1983    #[test]
1984    fn oversized_declared_length_is_rejected_before_allocation() {
1985        let mut msg = vec![S2C_LSP_STATE];
1986        msg.extend_from_slice(&1u16.to_le_bytes());
1987        msg.extend_from_slice(&1u32.to_le_bytes());
1988        msg.push(0);
1989        // A hostile 4 GiB declared size must be refused before allocating.
1990        msg.extend_from_slice(&(u32::MAX).to_le_bytes());
1991        msg.extend_from_slice(&[0; 8]);
1992        assert_eq!(parse_lsp_state(&msg), None);
1993    }
1994
1995    #[test]
1996    fn state_mirror_replaces_whole_state() {
1997        let mut records = Vec::new();
1998        append_lsp_state_record(
1999            &mut records,
2000            &LspStateRecord::Server {
2001                server_ref: 1,
2002                phase: LSP_PHASE_INDEXING,
2003                progress_pct: 10,
2004                caps: 0,
2005                epoch: 0,
2006                refused_edits: 0,
2007                rss: 0,
2008                id: "rust-analyzer",
2009                msg: "indexing",
2010            },
2011        );
2012        let mut mirror = LspStateMirror::new();
2013        assert_eq!(
2014            mirror.apply_state(&msg_lsp_state(1, 1, 0, &records)),
2015            Some(1)
2016        );
2017        assert_eq!(mirror.servers.len(), 1);
2018        assert_eq!(mirror.servers[&1].phase, LSP_PHASE_INDEXING);
2019
2020        // The next snapshot replaces, never merges.
2021        let mut records = Vec::new();
2022        append_lsp_state_record(
2023            &mut records,
2024            &LspStateRecord::Server {
2025                server_ref: 2,
2026                phase: LSP_PHASE_READY,
2027                progress_pct: 100,
2028                caps: 0x3F,
2029                epoch: 0,
2030                refused_edits: 0,
2031                rss: 0,
2032                id: "gopls",
2033                msg: "",
2034            },
2035        );
2036        assert_eq!(
2037            mirror.apply_state(&msg_lsp_state(1, 2, 0, &records)),
2038            Some(2)
2039        );
2040        assert_eq!(mirror.servers.len(), 1);
2041        assert!(mirror.servers.contains_key(&2));
2042    }
2043
2044    #[test]
2045    fn diag_mirror_applies_replacement_sets() {
2046        let mut mirror = LspDiagMirror::new();
2047
2048        // FULL replay: one file, one diagnostic.
2049        let mut records = Vec::new();
2050        append_lsp_diag_record(
2051            &mut records,
2052            &LspDiagRecord::File {
2053                hash: hash(1),
2054                n: 1,
2055                path: "a.rs",
2056            },
2057        );
2058        append_lsp_diag_record(
2059            &mut records,
2060            &LspDiagRecord::Diag {
2061                severity: LSP_SEVERITY_ERROR,
2062                flags: 0,
2063                line: 1,
2064                col: 0,
2065                end_line: 1,
2066                end_col: 5,
2067                code: "E1",
2068                source: "t",
2069                msg: "boom",
2070            },
2071        );
2072        assert_eq!(
2073            mirror.apply_diag(&msg_lsp_diag(1, 1, LSP_DIAG_FULL, &records)),
2074            Some(1)
2075        );
2076        assert_eq!(mirror.files.len(), 1);
2077        assert_eq!(mirror.files["a.rs"].diags.len(), 1);
2078
2079        // Incremental: replace a.rs with an empty set (n=0 removes).
2080        let mut records = Vec::new();
2081        append_lsp_diag_record(
2082            &mut records,
2083            &LspDiagRecord::File {
2084                hash: hash(2),
2085                n: 0,
2086                path: "a.rs",
2087            },
2088        );
2089        append_lsp_diag_record(
2090            &mut records,
2091            &LspDiagRecord::File {
2092                hash: hash(3),
2093                n: 1,
2094                path: "b.rs",
2095            },
2096        );
2097        append_lsp_diag_record(
2098            &mut records,
2099            &LspDiagRecord::Diag {
2100                severity: LSP_SEVERITY_WARNING,
2101                flags: 0,
2102                line: 2,
2103                col: 1,
2104                end_line: 2,
2105                end_col: 2,
2106                code: "",
2107                source: "t",
2108                msg: "hm",
2109            },
2110        );
2111        assert_eq!(mirror.apply_diag(&msg_lsp_diag(1, 2, 0, &records)), Some(2));
2112        assert!(!mirror.files.contains_key("a.rs"));
2113        assert_eq!(mirror.files["b.rs"].hash, hash(3));
2114
2115        // A later FULL drops files it does not re-list.
2116        assert_eq!(
2117            mirror.apply_diag(&msg_lsp_diag(1, 3, LSP_DIAG_FULL, &[])),
2118            Some(3)
2119        );
2120        assert!(mirror.files.is_empty());
2121    }
2122
2123    /// Byte-exact fixtures pinned across the Rust and TypeScript codecs
2124    /// (js/core/src/lsp.ts): drift fails on one side or the other.
2125    #[test]
2126    fn wire_fixtures() {
2127        assert_eq!(
2128            hex(&msg_lsp_open(
2129                0x0102,
2130                LSP_OPEN_WATCH | LSP_OPEN_DIAGS,
2131                500,
2132                "/src"
2133            )),
2134            "60020103f40104002f737263"
2135        );
2136        assert_eq!(hex(&msg_lsp_close(7)), "610700");
2137        assert_eq!(
2138            hex(&msg_lsp_ack(7, LSP_STREAM_DIAG, 0x01020304)),
2139            "6207000104030201"
2140        );
2141        assert_eq!(
2142            hex(&msg_lsp_query(&LspQueryRequest {
2143                nonce: 3,
2144                lsp_id: 7,
2145                kind: LSP_QUERY_DEFINITION,
2146                flags: 0,
2147                line: 10,
2148                col: 4,
2149                path: "a.rs",
2150                arg: "",
2151            })),
2152            "630300070001000a000000040000000400612e72730000"
2153        );
2154        assert_eq!(hex(&msg_lsp_cancel(10)), "640a00");
2155        assert_eq!(hex(&msg_lsp_servers(11)), "650b00");
2156        assert_eq!(hex(&msg_lsp_stop(12, 2)), "660c000200");
2157        assert_eq!(
2158            hex(&msg_lsp_opened(5, 1, LSP_STATUS_OK, 0, "/w", "")),
2159            "6005000100000002002f770000"
2160        );
2161        assert_eq!(hex(&msg_lsp_closed(1, LSP_CLOSED_ROOT_GONE)), "64010001");
2162        assert_eq!(hex(&msg_lsp_stopped(6, LSP_STATUS_OK)), "66060000");
2163
2164        // Uncompressed record-buffer fixtures (LZ4 output is
2165        // implementation-specific, so cross-codec pinning happens on the
2166        // record bytes, not the compressed message).
2167        let mut buf = Vec::new();
2168        append_lsp_diag_record(
2169            &mut buf,
2170            &LspDiagRecord::File {
2171                hash: hash(0xAB),
2172                n: 1,
2173                path: "a.rs",
2174            },
2175        );
2176        assert_eq!(
2177            hex(&buf),
2178            format!("1900000001{}01000400612e7273", "ab".repeat(16))
2179        );
2180        let mut buf = Vec::new();
2181        append_lsp_query_record(
2182            &mut buf,
2183            &LspQueryRecord::Markup {
2184                format: LSP_MARKUP_MARKDOWN,
2185                text: "hi",
2186            },
2187        );
2188        assert_eq!(hex(&buf), "080000000201020000006869");
2189        let mut buf = Vec::new();
2190        append_lsp_query_record(
2191            &mut buf,
2192            &LspQueryRecord::Completion {
2193                item_kind: 2,
2194                flags: 0,
2195                line: 1,
2196                col: 2,
2197                end_line: 1,
2198                end_col: 4,
2199                label: "ab",
2200                insert: "",
2201                detail: "T",
2202            },
2203        );
2204        assert_eq!(
2205            hex(&buf),
2206            "1c00000005020001000000020000000100000004000000020061620000010054"
2207        );
2208        let mut buf = Vec::new();
2209        append_lsp_query_record(
2210            &mut buf,
2211            &LspQueryRecord::Signature {
2212                flags: LSP_SIGNATURE_ACTIVE,
2213                active_param: 0,
2214                param_start: 2,
2215                param_end: 6,
2216                label: "f(a: T)",
2217                doc: "d",
2218            },
2219        );
2220        assert_eq!(
2221            hex(&buf),
2222            "16000000060100000200060007006628613a2054290100000064"
2223        );
2224    }
2225}