1use lattice_help::{HelpContent, one_line};
16
17use crate::{
18 Capabilities, DiagnosticSeverity, DiagnosticsLayer, LogRecord, LogSource, LspLogger,
19 LspSupervisor, LspSupervisorHandle, actor::uri_to_path,
20};
21
22pub fn diagnostics_help(layer: &DiagnosticsLayer) -> HelpContent {
33 let snapshot = layer.snapshot();
34 let counts = layer.severity_counts();
35 let mut lines: Vec<String> = Vec::new();
36 if snapshot.is_empty() {
37 lines.push("# Workspace diagnostics".to_string());
38 lines.push(String::new());
39 lines.push("(none)".to_string());
40 return HelpContent::from_lines("diagnostics", lines);
41 }
42 lines.push(format!(
43 "# Workspace diagnostics ({} total: {} errors, {} warnings, {} info, {} hints)",
44 counts.total(),
45 counts.errors,
46 counts.warnings,
47 counts.info,
48 counts.hints
49 ));
50 lines.push(String::new());
51 for (uri, diags) in snapshot {
52 let path = uri_to_path(&uri)
53 .map(|p| p.display().to_string())
54 .unwrap_or_else(|| uri.as_str().to_string());
55 lines.push(format!("## {} ({})", path, diags.len()));
56 lines.push(String::new());
57 for d in diags {
58 let sev = match d.severity {
59 Some(DiagnosticSeverity::ERROR) => "E",
60 Some(DiagnosticSeverity::WARNING) => "W",
61 Some(DiagnosticSeverity::INFORMATION) => "I",
62 Some(DiagnosticSeverity::HINT) => "H",
63 _ => "?",
64 };
65 let line0 = d.range.start.line;
66 let col0 = d.range.start.character;
67 let label = format!(
68 "{}:{}:{} {}",
69 path,
70 line0 + 1,
71 col0 + 1,
72 one_line(&d.message)
73 );
74 let label = lattice_help::escape_link_text(&label);
78 let url = lattice_help::escape_link_text(&format!("{path}:{line0}"));
79 lines.push(format!("[{sev}] [{label}](file:{url})"));
80 }
81 lines.push(String::new());
82 }
83 HelpContent::from_lines("diagnostics", lines)
84}
85
86pub fn lsp_global_log_help(logger: &LspLogger) -> HelpContent {
90 let records = logger.snapshot_global();
91 let mut lines: Vec<String> = Vec::new();
92 lines.push(format!(
93 "# *lsp* (subsystem-wide, {} records)",
94 records.len()
95 ));
96 lines.push(String::new());
97 if records.is_empty() {
98 lines.push("(no records)".to_string());
99 } else {
100 for r in records {
101 lines.push(format_log_record(&r));
102 }
103 }
104 HelpContent::from_lines("lsp", lines)
105}
106
107pub fn lsp_server_log_help(
110 logger: &LspLogger,
111 instance: &crate::logging::InstanceKey,
112) -> HelpContent {
113 let records = logger.snapshot_instance(instance);
114 let body: Vec<&LogRecord> = records
115 .iter()
116 .filter(|r| r.source != LogSource::Trace)
117 .collect();
118 let mut lines: Vec<String> = Vec::new();
119 lines.push(format!(
120 "# *lsp:{}:{}* ({} records, trace excluded)",
121 instance.server_id,
122 instance.workspace.display(),
123 body.len()
124 ));
125 lines.push(String::new());
126 if body.is_empty() {
127 lines.push("(no records)".to_string());
128 } else {
129 for r in body {
130 lines.push(format_log_record(r));
131 }
132 }
133 HelpContent::from_lines(
134 format!(
135 "lsp:{}:{}",
136 instance.server_id,
137 instance.workspace.display()
138 ),
139 lines,
140 )
141}
142
143pub fn lsp_server_trace_help(
148 logger: &LspLogger,
149 instance: &crate::logging::InstanceKey,
150) -> HelpContent {
151 let records = logger.snapshot_instance(instance);
152 let body: Vec<&LogRecord> = records
153 .iter()
154 .filter(|r| r.source == LogSource::Trace)
155 .collect();
156 let mut lines: Vec<String> = Vec::new();
157 let trace_on = logger.is_tracing(instance);
158 lines.push(format!(
159 "# *lsp:{}:{}:trace* ({} records, trace currently {})",
160 instance.server_id,
161 instance.workspace.display(),
162 body.len(),
163 if trace_on { "ON" } else { "OFF" }
164 ));
165 lines.push(String::new());
166 if body.is_empty() {
167 lines.push("(no trace records; toggle with `:lsp-trace <server>`)".to_string());
168 } else {
169 for r in body {
170 lines.push(format_log_record(r));
171 }
172 }
173 HelpContent::from_lines(
174 format!(
175 "lsp:{}:{}:trace",
176 instance.server_id,
177 instance.workspace.display()
178 ),
179 lines,
180 )
181}
182
183pub fn lsp_server_log_listing_help(supervisor: &LspSupervisor) -> HelpContent {
191 let mut actors = supervisor.running_actors();
192 actors.sort_by(|a, b| a.0.1.cmp(&b.0.1).then_with(|| a.0.0.cmp(&b.0.0)));
193 let mut lines: Vec<String> = Vec::new();
194 lines.push(format!(
195 "# :lsp-server-log ({} server actor(s) running)",
196 actors.len()
197 ));
198 lines.push(String::new());
199 if actors.is_empty() {
200 lines.push(
201 "(no LSP servers running; open a file with a matching language to attach. \
202 see :lsp-status for the full overview.)"
203 .to_string(),
204 );
205 return HelpContent::from_lines("lsp-server-log", lines);
206 }
207 lines.push(
208 "Each row links to the per-server log (`*lsp:<server>*`) and trace \
209 (`*lsp:<server>:trace*`). Press `<CR>` on a link to open. Use \
210 `/query` to filter rows by id or workspace path."
211 .to_string(),
212 );
213 lines.push(String::new());
214 for ((workspace, server_id), handle) in &actors {
215 let buffer_count = supervisor.buffer_count_for(&(workspace.clone(), server_id.clone()));
216 let caps = handle.capabilities();
217 let cap_summary = summarise_capabilities(&caps);
218 lines.push(format!("## [{server_id}](exec:lsp-log {server_id})"));
219 lines.push(format!("- workspace: `{}`", workspace.display()));
220 lines.push(format!("- buffers: {buffer_count} attached"));
221 lines.push(format!("- capabilities: {cap_summary}"));
222 lines.push(format!(
223 "- trace: [open / toggle](exec:lsp-trace {server_id})"
224 ));
225 lines.push(String::new());
226 }
227 HelpContent::from_lines("lsp-server-log", lines)
228}
229
230pub fn lsp_status_help(supervisor: &LspSupervisorHandle) -> HelpContent {
233 let actors = supervisor.running_actors();
234 let mut lines: Vec<String> = Vec::new();
235 lines.push(format!(
236 "# :lsp-status ({} server(s), {} attached buffer(s))",
237 actors.len(),
238 supervisor.attached_buffer_count()
239 ));
240 lines.push(String::new());
241 if actors.is_empty() {
242 lines.push(
243 "(no LSP servers running; open a file with a matching language to attach)".to_string(),
244 );
245 } else {
246 for ((workspace, server_id), handle) in actors {
247 let caps = handle.capabilities();
248 lines.push(format!("## {server_id}"));
249 lines.push(format!("- workspace root: `{}`", workspace.display()));
250 lines.push(format!("- position encoding: {:?}", caps.position_encoding));
251 lines.push(format!("- supports hover: {}", caps.supports_hover()));
252 lines.push(format!(
253 "- supports definition: {}",
254 caps.supports_definition()
255 ));
256 lines.push(format!(
257 "- diagnostics subscribers: {}",
258 handle.diagnostics_subscriber_count()
259 ));
260 lines.push(String::new());
261 }
262 }
263 HelpContent::from_lines("lsp-status", lines)
264}
265
266pub fn summarise_capabilities(caps: &Capabilities) -> String {
271 let mut parts: Vec<&str> = Vec::new();
272 if caps.supports_hover() {
273 parts.push("hover");
274 }
275 if caps.supports_definition() {
276 parts.push("definition");
277 }
278 if caps.supports_references() {
279 parts.push("references");
280 }
281 if caps.supports_document_symbol() {
282 parts.push("document-symbol");
283 }
284 if caps.supports_workspace_symbol() {
285 parts.push("workspace-symbol");
286 }
287 if caps.supports_completion() {
288 parts.push("completion");
289 }
290 if parts.is_empty() {
291 "(none advertised)".into()
292 } else {
293 parts.join(", ")
294 }
295}
296
297fn format_log_record(r: &LogRecord) -> String {
301 use std::time::SystemTime;
302 let elapsed = r.timestamp.duration_since(SystemTime::UNIX_EPOCH).ok();
303 let secs = elapsed.map(|d| d.as_secs()).unwrap_or(0);
304 let ms = elapsed.map(|d| d.subsec_millis()).unwrap_or(0);
305 let hh = (secs / 3600) % 24;
306 let mm = (secs / 60) % 60;
307 let ss = secs % 60;
308 format!(
309 "{:02}:{:02}:{:02}.{:03} {} {:>6}: {}",
310 hh,
311 mm,
312 ss,
313 ms,
314 r.level.short(),
315 r.source.tag(),
316 one_line(&r.message)
317 )
318}
319
320#[cfg(test)]
321mod tests {
322 #![allow(clippy::unwrap_used, clippy::panic)]
323 use super::*;
324 use crate::{DiagnosticEvent, LspLogger};
325 use lsp_types::{Diagnostic, Position, Range, Uri};
326 use std::str::FromStr;
327 use std::sync::Arc;
328
329 #[test]
333 fn a_message_with_brackets_keeps_its_row_a_single_link() {
334 let layer = DiagnosticsLayer::new(LspLogger::with_defaults());
335 let message = "expected `[u8; 4]`, found `fn(i32)`";
336 layer.apply(DiagnosticEvent {
337 server_id: Arc::from("rust"),
338 uri: Uri::from_str("file:///x.rs").unwrap(),
339 version: Some(1),
340 diagnostics: Arc::from(
341 vec![Diagnostic {
342 range: Range {
343 start: Position {
344 line: 2,
345 character: 4,
346 },
347 end: Position {
348 line: 2,
349 character: 9,
350 },
351 },
352 severity: Some(DiagnosticSeverity::ERROR),
353 message: message.into(),
354 ..Default::default()
355 }]
356 .into_boxed_slice(),
357 ),
358 });
359
360 let content = diagnostics_help(&layer);
361 let text = content.buffer.content.as_string();
362 let row = text
363 .lines()
364 .find(|l| l.starts_with("[E]"))
365 .unwrap_or_else(|| panic!("an error row:\n{text}"));
366 assert_eq!(
367 row,
368 format!("[E] /x.rs:3:5 {message}"),
369 "the whole label renders, with no markdown left over"
370 );
371 assert!(
372 content
373 .metadata
374 .links
375 .iter()
376 .any(|l| matches!(&l.target, lattice_help::HelpLinkTarget::Source { .. })),
377 "and the row still jumps to the file: {:?}",
378 content.metadata.links
379 );
380 }
381}