lattice_lsp/buffer_names.rs
1//! Canonical synthetic buffer names for the LSP log family.
2//!
3//! Three flavours of LSP-owned Document buffer, one canonical
4//! name format each:
5//!
6//! - `*lsp*` — subsystem-wide log. Captures every record with
7//! `server_id.is_none()` (lifecycle, attach driver, supervisor
8//! chatter). [`LSP_SUBSYSTEM_LOG_NAME`].
9//! - `*lsp:<server>:<workspace>*` — per-instance log. One per
10//! `InstanceKey`, so two `rust-analyzer` actors on different
11//! workspaces get distinct buffers. [`lsp_server_log_name`].
12//! - `*lsp:<server>:<workspace>:trace*` — per-instance JSON-RPC
13//! wire trace. Mirrors the log buffer but only carries
14//! `LogSource::Trace` records. [`lsp_server_trace_log_name`].
15//!
16//! The inverse helpers ([`parse_lsp_server_log_name`],
17//! [`parse_lsp_trace_log_name`]) extract the originating
18//! [`InstanceKey`] from a synthetic name so a mode can derive
19//! its identity from its buffer's name alone — no buffer-local
20//! seeding required (B'.7).
21//!
22//! ## Invariant
23//!
24//! Server ids never contain `:`. The parsers split on the first
25//! colon after `*lsp:`; everything between that and the trailing
26//! `*` (or `:trace*`) is the workspace path. Workspaces with
27//! literal colons in their path (rare on POSIX, non-existent on
28//! Windows drive letters with `\\?\C:\…` because the registry
29//! normalises) survive because the workspace segment is the
30//! *remainder* — the parser does not split it further.
31
32use std::path::Path;
33use std::sync::Arc;
34
35use crate::logging::InstanceKey;
36
37/// Synthetic name for the subsystem-wide `*lsp*` buffer.
38///
39/// Created eagerly at App boot so `:b *lsp*` works the moment
40/// the editor starts. The matching major mode is
41/// [`crate::modes::LspLogMode`].
42pub const LSP_SUBSYSTEM_LOG_NAME: &str = "*lsp*";
43
44/// Build the synthetic name for the per-instance LSP log buffer
45/// owned by [`crate::modes::LspServerLogMode`].
46///
47/// Format: `*lsp:<server_id>:<workspace_path>*`. The workspace
48/// path is rendered via `Path::display()` (matches the canonical
49/// form the registry uses for buffer lookups).
50pub fn lsp_server_log_name(instance: &InstanceKey) -> String {
51 format!(
52 "*lsp:{}:{}*",
53 instance.server_id,
54 instance.workspace.display(),
55 )
56}
57
58/// Build the synthetic name for the per-instance LSP trace
59/// buffer owned by [`crate::modes::LspTraceLogMode`].
60///
61/// Format: `*lsp:<server_id>:<workspace_path>:trace*`.
62pub fn lsp_server_trace_log_name(instance: &InstanceKey) -> String {
63 format!(
64 "*lsp:{}:{}:trace*",
65 instance.server_id,
66 instance.workspace.display(),
67 )
68}
69
70/// Recover the [`InstanceKey`] from a per-instance log buffer
71/// name. Returns `None` when `name` does not match the
72/// canonical `*lsp:<server>:<workspace>*` shape.
73///
74/// Rejects the trace variant — a name ending in `:trace*` parses
75/// via [`parse_lsp_trace_log_name`] instead, so the two flavours
76/// stay distinct at the type-of-buffer level.
77pub fn parse_lsp_server_log_name(name: &str) -> Option<InstanceKey> {
78 let body = strip_lsp_wrapping(name)?;
79 if body.ends_with(":trace") {
80 return None;
81 }
82 let (server, workspace) = body.split_once(':')?;
83 if server.is_empty() || workspace.is_empty() {
84 return None;
85 }
86 Some(InstanceKey::new(
87 Arc::<str>::from(server),
88 Arc::<Path>::from(Path::new(workspace)),
89 ))
90}
91
92/// Recover the [`InstanceKey`] from a per-instance trace buffer
93/// name. Returns `None` when `name` does not match
94/// `*lsp:<server>:<workspace>:trace*`.
95pub fn parse_lsp_trace_log_name(name: &str) -> Option<InstanceKey> {
96 let body = strip_lsp_wrapping(name)?;
97 let trimmed = body.strip_suffix(":trace")?;
98 let (server, workspace) = trimmed.split_once(':')?;
99 if server.is_empty() || workspace.is_empty() {
100 return None;
101 }
102 Some(InstanceKey::new(
103 Arc::<str>::from(server),
104 Arc::<Path>::from(Path::new(workspace)),
105 ))
106}
107
108/// Strip the `*lsp:` prefix + closing `*` wrapper. Returns the
109/// inner `<server>:<workspace>[:trace]` body. None if the name
110/// is not LSP-shaped or is the subsystem-wide `*lsp*`.
111fn strip_lsp_wrapping(name: &str) -> Option<&str> {
112 let body = name.strip_prefix("*lsp:")?.strip_suffix('*')?;
113 if body.is_empty() {
114 return None;
115 }
116 Some(body)
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn key(server: &str, workspace: &str) -> InstanceKey {
124 InstanceKey::new(
125 Arc::<str>::from(server),
126 Arc::<Path>::from(Path::new(workspace)),
127 )
128 }
129
130 #[test]
131 fn server_log_name_round_trips() {
132 let k = key("rust", "/home/u/proj");
133 let name = lsp_server_log_name(&k);
134 assert_eq!(name, "*lsp:rust:/home/u/proj*");
135 let parsed = parse_lsp_server_log_name(&name).expect("parse");
136 assert_eq!(parsed, k);
137 }
138
139 #[test]
140 fn trace_log_name_round_trips() {
141 let k = key("python", "/home/u/code");
142 let name = lsp_server_trace_log_name(&k);
143 assert_eq!(name, "*lsp:python:/home/u/code:trace*");
144 let parsed = parse_lsp_trace_log_name(&name).expect("parse");
145 assert_eq!(parsed, k);
146 }
147
148 #[test]
149 fn parsers_reject_subsystem_name() {
150 // `*lsp*` has no body — not a per-instance buffer.
151 assert!(parse_lsp_server_log_name(LSP_SUBSYSTEM_LOG_NAME).is_none());
152 assert!(parse_lsp_trace_log_name(LSP_SUBSYSTEM_LOG_NAME).is_none());
153 }
154
155 #[test]
156 fn server_parser_rejects_trace_variant() {
157 let trace = "*lsp:rust:/p:trace*";
158 assert!(parse_lsp_server_log_name(trace).is_none());
159 }
160
161 #[test]
162 fn trace_parser_rejects_log_variant() {
163 let log = "*lsp:rust:/p*";
164 assert!(parse_lsp_trace_log_name(log).is_none());
165 }
166
167 #[test]
168 fn parsers_reject_garbage_names() {
169 assert!(parse_lsp_server_log_name("*foo*").is_none());
170 assert!(parse_lsp_server_log_name("*lsp:*").is_none());
171 assert!(parse_lsp_server_log_name("*lsp::/p*").is_none());
172 assert!(parse_lsp_trace_log_name("not-lsp").is_none());
173 }
174}