1use std::sync::{Arc, OnceLock};
26
27use lattice_grammar::effect::{Effect, LspRequest};
28use lattice_mode::{
29 ActionContext, ActionHandler, ActionHandlerContribution, BufferStoreHandle, CapabilitySet,
30 DecorationCtx, GutterDecoration, GutterSeverityLevel, Keymap, KeymapEntry, LifecycleFuture,
31 Mode, ModeActivationError, ModeContext, ModeId, ModeKind, ModeRegistry, OptionOverrideSet,
32 Subscription, keymap_entry,
33};
34use lattice_runtime::EventBus;
35
36use crate::supervisor::LspSupervisorHandle;
37
38pub struct LspServerLogMode;
45
46impl LspServerLogMode {
47 pub fn mode_id() -> ModeId {
48 ModeId::new("lsp-server-log-mode")
49 }
50}
51
52impl Mode for LspServerLogMode {
53 type Guard = Option<Subscription>;
54 fn id(&self) -> ModeId {
55 Self::mode_id()
56 }
57 fn kind(&self) -> ModeKind {
58 ModeKind::Major
59 }
60 fn options(&self) -> OptionOverrideSet {
61 lattice_config::overrides! {
62 lattice_config::ReadOnly = true,
63 lattice_config::NoFile = true,
64 }
65 }
66 fn required_capabilities(&self) -> CapabilitySet {
67 CapabilitySet::empty()
68 }
69
70 fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
71 Box::pin(async move {
72 let buffer_id = lattice_core::BufferId(ctx.buffer_id().0 as u32);
73 let Some(store) = ctx.service::<BufferStoreHandle>() else {
74 return Ok(None);
75 };
76 let Some(name) = store.name_for(buffer_id) else {
77 return Ok(None);
78 };
79 let Some(instance) = crate::parse_lsp_server_log_name(&name) else {
80 return Ok(None);
81 };
82 let Some(handle) = store.handle_for(buffer_id) else {
83 return Ok(None);
84 };
85 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
86 return Ok(None);
87 };
88
89 if let Some(logger) = ctx.service::<crate::LspLogger>() {
93 let snap = logger.snapshot_instance(&instance);
94 let mut text = String::new();
95 for record in snap.iter().filter(|r| r.source != crate::LogSource::Trace) {
96 let line = crate::format_log_event_line(
97 record.server_id.as_deref(),
98 crate::log_level_tag(record.level),
99 record.source.tag(),
100 &record.message,
101 );
102 text.push_str(&line);
103 text.push('\n');
104 }
105 if !text.is_empty() {
106 let snapshot = handle.snapshot();
107 let last_line = snapshot.buffer.rope_line_count().saturating_sub(1); let line_text = snapshot.buffer.line(last_line).unwrap_or_default();
109 let pos = lattice_protocol::position::Position::new(
110 last_line,
111 line_text.len() as u32,
112 );
113 let edit = lattice_protocol::edit::Edit::insert(pos, text);
114 let handle_seed = handle.clone();
115 runtime.spawn(async move {
116 let _ = handle_seed.apply_edit_batch(vec![edit]).await;
117 });
118 }
119 }
120
121 let (tx, mut rx) =
122 tokio::sync::mpsc::unbounded_channel::<crate::events::LspLogPushed>();
123 let sub_id = ctx
124 .events()
125 .subscribe_typed::<crate::events::LspLogPushed>(tx);
126 let bus_handle = ctx.events_handle();
127
128 let filter_server = Arc::clone(&instance.server_id);
129 let filter_workspace = Arc::clone(&instance.workspace);
130 runtime.spawn(async move {
131 while let Some(first) = rx.recv().await {
132 let mut batch: Vec<crate::events::LspLogPushed> = vec![first];
133 while let Ok(more) = rx.try_recv() {
134 batch.push(more);
135 }
136 let mut text = String::new();
137 for event in batch.iter().filter(|e| {
138 let id_match = e
139 .server_id
140 .as_ref()
141 .map(|s| Arc::ptr_eq(s, &filter_server) || s == &filter_server)
142 .unwrap_or(false);
143 let ws_match = e
144 .workspace
145 .as_ref()
146 .map(|w| Arc::ptr_eq(w, &filter_workspace) || w == &filter_workspace)
147 .unwrap_or(false);
148 let is_trace = e.level == "trace" || e.source == "trace";
149 id_match && ws_match && !is_trace
150 }) {
151 let line = crate::format_log_event_line(
152 event.server_id.as_deref(),
153 &event.level,
154 &event.source,
155 &event.message,
156 );
157 text.push_str(&line);
158 text.push('\n');
159 }
160 if text.is_empty() {
161 continue;
162 }
163 let snap = handle.snapshot();
164 let last_line = snap.buffer.rope_line_count().saturating_sub(1); let line_text = snap.buffer.line(last_line).unwrap_or_default();
166 let pos = lattice_protocol::position::Position::new(
167 last_line,
168 line_text.len() as u32,
169 );
170 let edit = lattice_protocol::edit::Edit::insert(pos, text);
171 let _ = handle.apply_edit_batch(vec![edit]).await;
172 }
173 });
174
175 Ok(Some(Subscription::new(bus_handle, sub_id)))
176 })
177 }
178}
179
180pub struct LspLogMode;
184
185impl LspLogMode {
186 pub fn mode_id() -> ModeId {
187 ModeId::new("lsp-log-mode")
188 }
189}
190
191impl Mode for LspLogMode {
192 type Guard = Option<Subscription>;
193 fn id(&self) -> ModeId {
194 Self::mode_id()
195 }
196 fn kind(&self) -> ModeKind {
197 ModeKind::Major
198 }
199 fn options(&self) -> OptionOverrideSet {
200 lattice_config::overrides! {
201 lattice_config::ReadOnly = true,
202 lattice_config::NoFile = true,
203 }
204 }
205 fn required_capabilities(&self) -> CapabilitySet {
206 CapabilitySet::empty()
207 }
208
209 fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
210 Box::pin(async move {
211 let buffer_id = lattice_core::BufferId(ctx.buffer_id().0 as u32);
212 let Some(store) = ctx.service::<BufferStoreHandle>() else {
213 return Ok(None);
214 };
215 let Some(handle) = store.handle_for(buffer_id) else {
216 return Ok(None);
217 };
218 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
219 return Ok(None);
220 };
221
222 if let Some(logger) = ctx.service::<crate::LspLogger>() {
226 let snap = logger.snapshot_global();
227 let mut text = String::new();
228 for record in &snap {
229 let line = crate::format_log_event_line(
230 None,
231 crate::log_level_tag(record.level),
232 record.source.tag(),
233 &record.message,
234 );
235 text.push_str(&line);
236 text.push('\n');
237 }
238 if !text.is_empty() {
239 let snapshot = handle.snapshot();
240 let last_line = snapshot.buffer.rope_line_count().saturating_sub(1); let line_text = snapshot.buffer.line(last_line).unwrap_or_default();
242 let pos = lattice_protocol::position::Position::new(
243 last_line,
244 line_text.len() as u32,
245 );
246 let edit = lattice_protocol::edit::Edit::insert(pos, text);
247 let handle_seed = handle.clone();
248 runtime.spawn(async move {
249 let _ = handle_seed.apply_edit_batch(vec![edit]).await;
250 });
251 }
252 }
253
254 let (tx, mut rx) =
255 tokio::sync::mpsc::unbounded_channel::<crate::events::LspLogPushed>();
256 let sub_id = ctx
257 .events()
258 .subscribe_typed::<crate::events::LspLogPushed>(tx);
259 let bus_handle = ctx.events_handle();
260
261 runtime.spawn(async move {
262 while let Some(first) = rx.recv().await {
263 let mut batch: Vec<crate::events::LspLogPushed> = vec![first];
264 while let Ok(more) = rx.try_recv() {
265 batch.push(more);
266 }
267 let mut text = String::new();
268 for event in batch.iter().filter(|e| e.server_id.is_none()) {
269 let line = crate::format_log_event_line(
270 None,
271 &event.level,
272 &event.source,
273 &event.message,
274 );
275 text.push_str(&line);
276 text.push('\n');
277 }
278 if text.is_empty() {
279 continue;
280 }
281 let snap = handle.snapshot();
282 let last_line = snap.buffer.rope_line_count().saturating_sub(1); let line_text = snap.buffer.line(last_line).unwrap_or_default();
284 let pos = lattice_protocol::position::Position::new(
285 last_line,
286 line_text.len() as u32,
287 );
288 let edit = lattice_protocol::edit::Edit::insert(pos, text);
289 let _ = handle.apply_edit_batch(vec![edit]).await;
290 }
291 });
292
293 Ok(Some(Subscription::new(bus_handle, sub_id)))
294 })
295 }
296}
297
298pub struct LspTraceLogMode;
301
302impl LspTraceLogMode {
303 pub fn mode_id() -> ModeId {
304 ModeId::new("lsp-trace-log-mode")
305 }
306}
307
308impl Mode for LspTraceLogMode {
309 type Guard = Option<Subscription>;
310 fn id(&self) -> ModeId {
311 Self::mode_id()
312 }
313 fn kind(&self) -> ModeKind {
314 ModeKind::Major
315 }
316 fn options(&self) -> OptionOverrideSet {
317 lattice_config::overrides! {
318 lattice_config::ReadOnly = true,
319 lattice_config::NoFile = true,
320 }
321 }
322 fn required_capabilities(&self) -> CapabilitySet {
323 CapabilitySet::empty()
324 }
325
326 fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
327 Box::pin(async move {
328 let buffer_id = lattice_core::BufferId(ctx.buffer_id().0 as u32);
329 let Some(store) = ctx.service::<BufferStoreHandle>() else {
330 return Ok(None);
331 };
332 let Some(name) = store.name_for(buffer_id) else {
333 return Ok(None);
334 };
335 let Some(instance) = crate::parse_lsp_trace_log_name(&name) else {
336 return Ok(None);
337 };
338 let Some(handle) = store.handle_for(buffer_id) else {
339 return Ok(None);
340 };
341 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
342 return Ok(None);
343 };
344
345 if let Some(logger) = ctx.service::<crate::LspLogger>() {
346 let snap = logger.snapshot_instance(&instance);
347 let mut text = String::new();
348 for record in snap.iter().filter(|r| {
349 r.source == crate::LogSource::Trace || r.level == crate::LogLevel::Trace
350 }) {
351 let line = crate::format_log_event_line(
352 record.server_id.as_deref(),
353 crate::log_level_tag(record.level),
354 record.source.tag(),
355 &record.message,
356 );
357 text.push_str(&line);
358 text.push('\n');
359 }
360 if !text.is_empty() {
361 let snapshot = handle.snapshot();
362 let last_line = snapshot.buffer.rope_line_count().saturating_sub(1); let line_text = snapshot.buffer.line(last_line).unwrap_or_default();
364 let pos = lattice_protocol::position::Position::new(
365 last_line,
366 line_text.len() as u32,
367 );
368 let edit = lattice_protocol::edit::Edit::insert(pos, text);
369 let handle_seed = handle.clone();
370 runtime.spawn(async move {
371 let _ = handle_seed.apply_edit_batch(vec![edit]).await;
372 });
373 }
374 }
375
376 let (tx, mut rx) =
377 tokio::sync::mpsc::unbounded_channel::<crate::events::LspLogPushed>();
378 let sub_id = ctx
379 .events()
380 .subscribe_typed::<crate::events::LspLogPushed>(tx);
381 let bus_handle = ctx.events_handle();
382
383 let filter_server = Arc::clone(&instance.server_id);
384 let filter_workspace = Arc::clone(&instance.workspace);
385 runtime.spawn(async move {
386 while let Some(first) = rx.recv().await {
387 let mut batch: Vec<crate::events::LspLogPushed> = vec![first];
388 while let Ok(more) = rx.try_recv() {
389 batch.push(more);
390 }
391 let mut text = String::new();
392 for event in batch.iter().filter(|e| {
393 let id_match = e
394 .server_id
395 .as_ref()
396 .map(|s| Arc::ptr_eq(s, &filter_server) || s == &filter_server)
397 .unwrap_or(false);
398 let ws_match = e
399 .workspace
400 .as_ref()
401 .map(|w| Arc::ptr_eq(w, &filter_workspace) || w == &filter_workspace)
402 .unwrap_or(false);
403 let is_trace = e.level == "trace" || e.source == "trace";
404 id_match && ws_match && is_trace
405 }) {
406 let line = crate::format_log_event_line(
407 event.server_id.as_deref(),
408 &event.level,
409 &event.source,
410 &event.message,
411 );
412 text.push_str(&line);
413 text.push('\n');
414 }
415 if text.is_empty() {
416 continue;
417 }
418 let snap = handle.snapshot();
419 let last_line = snap.buffer.rope_line_count().saturating_sub(1); let line_text = snap.buffer.line(last_line).unwrap_or_default();
421 let pos = lattice_protocol::position::Position::new(
422 last_line,
423 line_text.len() as u32,
424 );
425 let edit = lattice_protocol::edit::Edit::insert(pos, text);
426 let _ = handle.apply_edit_batch(vec![edit]).await;
427 }
428 });
429
430 Ok(Some(Subscription::new(bus_handle, sub_id)))
431 })
432 }
433}
434
435pub struct LspModeGuard {
441 bus: Arc<EventBus>,
442 buffer_id: lattice_protocol::ids::DocumentId,
443}
444
445impl Drop for LspModeGuard {
446 fn drop(&mut self) {
447 self.bus
448 .publish_typed(crate::events::LspBufferDetached { id: self.buffer_id });
449 }
450}
451
452fn lsp_mode_keymap_entries() -> &'static [KeymapEntry] {
456 static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
457 ENTRIES.get_or_init(|| {
458 vec![
459 keymap_entry! {
460 mode: Normal, chord: "K",
461 doc: "Show hover documentation",
462 cmd: "action:lsp-hover"
463 },
464 keymap_entry! {
465 mode: Normal, chord: "gd",
466 doc: "Go to definition",
467 cmd: "action:lsp-definition"
468 },
469 keymap_entry! {
470 mode: Normal, chord: "gD",
471 doc: "Go to declaration",
472 cmd: "action:lsp-declaration"
473 },
474 keymap_entry! {
475 mode: Normal, chord: "gy",
476 doc: "Go to type definition",
477 cmd: "action:lsp-type-definition"
478 },
479 keymap_entry! {
480 mode: Normal, chord: "gI",
481 doc: "Go to implementation",
482 cmd: "action:lsp-implementation"
483 },
484 keymap_entry! {
485 mode: Normal, chord: "gr",
486 doc: "Find references",
487 cmd: "action:lsp-references"
488 },
489 keymap_entry! {
490 mode: Normal, chord: "gx",
491 doc: "Follow document link at cursor",
492 cmd: "action:lsp-follow-link"
493 },
494 ]
495 })
496}
497
498pub struct LspMode {
501 sub_modes: Vec<ModeId>,
502}
503
504impl LspMode {
505 pub fn mode_id() -> ModeId {
506 ModeId::new("lsp-mode")
507 }
508
509 pub fn new() -> Self {
510 Self {
511 sub_modes: vec![
512 LspCompletionMode::mode_id(),
513 LspDiagnosticsMode::mode_id(),
514 LspHoverMode::mode_id(),
515 LspSignatureMode::mode_id(),
516 LspFormatMode::mode_id(),
517 LspRenameMode::mode_id(),
518 LspSymbolsMode::mode_id(),
519 LspCodeActionMode::mode_id(),
520 LspNavMode::mode_id(),
521 LspProgressMode::mode_id(),
522 LspDocumentHighlightMode::mode_id(),
523 LspSelectionRangeMode::mode_id(),
524 LspFoldingMode::mode_id(),
525 LspInlayHintMode::mode_id(),
526 LspSemanticTokensMode::mode_id(),
527 ],
528 }
529 }
530}
531
532impl Default for LspMode {
533 fn default() -> Self {
534 Self::new()
535 }
536}
537
538impl Mode for LspMode {
539 type Guard = LspModeGuard;
540 fn id(&self) -> ModeId {
541 Self::mode_id()
542 }
543 fn kind(&self) -> ModeKind {
544 ModeKind::Minor
545 }
546 fn implies(&self) -> &[ModeId] {
547 &self.sub_modes
548 }
549 fn keymap(&self) -> Keymap {
552 Keymap::from_entries(lsp_mode_keymap_entries())
553 }
554 fn action_handlers(&self) -> Vec<ActionHandlerContribution> {
566 fn nav(action_name: &'static str, req: LspRequest) -> ActionHandlerContribution {
567 let handler: ActionHandler =
568 std::sync::Arc::new(move |_ctx: &ActionContext<'_>| -> Option<Effect> {
569 Some(Effect::Lsp(req))
570 });
571 ActionHandlerContribution {
572 action_name,
573 handler,
574 }
575 }
576 vec![
577 nav("action:lsp-hover", LspRequest::Hover),
578 nav("action:lsp-definition", LspRequest::Definition),
579 nav("action:lsp-declaration", LspRequest::Declaration),
580 nav("action:lsp-type-definition", LspRequest::TypeDefinition),
581 nav("action:lsp-implementation", LspRequest::Implementation),
582 nav("action:lsp-references", LspRequest::References),
583 nav("action:lsp-follow-link", LspRequest::FollowLink),
584 ]
585 }
586 fn options(&self) -> OptionOverrideSet {
587 OptionOverrideSet::default()
588 }
589 fn required_capabilities(&self) -> CapabilitySet {
590 CapabilitySet::empty()
591 }
592 fn gutter_decorations(&self, ctx: &DecorationCtx<'_>) -> Vec<GutterDecoration> {
637 let Some(data) = ctx.service::<LspDiagnosticsData>() else {
638 return Vec::new();
639 };
640 let Some(diags) = &data.diagnostics else {
641 return Vec::new();
642 };
643 let Some(ids) = ctx.service::<lattice_mode::BuiltinSignIds>() else {
647 return Vec::new();
648 };
649 let mut per_line: std::collections::HashMap<u32, GutterSeverityLevel> = Default::default();
650 for diag in diags.iter() {
651 let level = match diag.severity {
652 Some(crate::DiagnosticSeverity::ERROR) => GutterSeverityLevel::Error,
653 Some(crate::DiagnosticSeverity::WARNING) => GutterSeverityLevel::Warning,
654 Some(crate::DiagnosticSeverity::INFORMATION) => GutterSeverityLevel::Info,
655 Some(crate::DiagnosticSeverity::HINT) => GutterSeverityLevel::Hint,
656 _ => continue,
657 };
658 per_line
659 .entry(diag.range.start.line)
660 .and_modify(|e| {
661 if level > *e {
662 *e = level;
663 }
664 })
665 .or_insert(level);
666 }
667 per_line
668 .into_iter()
669 .map(|(line, level)| GutterDecoration::Sign {
670 line,
671 sign: ids.for_severity(level),
672 })
673 .collect()
674 }
675
676 fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
677 Box::pin(async move {
678 let buffer_id = lattice_protocol::ids::DocumentId::new(ctx.buffer_id().0);
679 let path_and_text = ctx
684 .service::<BufferStoreHandle>()
685 .and_then(|store| {
686 let core_id = lattice_core::BufferId(ctx.buffer_id().0 as u32);
687 store.handle_for(core_id)
688 })
689 .and_then(|handle| {
690 let path = handle.path()?;
691 let text = handle.text();
692 Some((path, text))
693 });
694
695 if let Some((path, text)) = path_and_text {
696 if let Some(sup) = ctx.service::<LspSupervisorHandle>()
702 && let Err(e) = sup.open_buffer(path.clone(), text).await
703 {
704 return Err(ModeActivationError::LifecycleFailed {
705 mode: LspMode::mode_id(),
706 reason: format!("open_buffer({}) failed: {e}", path.display()),
707 });
708 }
709 }
710 ctx.events()
716 .publish_typed(crate::events::LspBufferAttached { id: buffer_id });
717 Ok(LspModeGuard {
718 bus: ctx.events_handle(),
719 buffer_id,
720 })
721 })
722 }
723}
724
725macro_rules! lsp_sub_mode {
731 ($struct_name:ident, $mode_name:literal) => {
732 pub struct $struct_name;
733
734 impl $struct_name {
735 pub fn mode_id() -> ModeId {
736 ModeId::new($mode_name)
737 }
738 }
739
740 impl Mode for $struct_name {
741 type Guard = ();
742 fn id(&self) -> ModeId {
743 Self::mode_id()
744 }
745 fn kind(&self) -> ModeKind {
746 ModeKind::Minor
747 }
748 fn options(&self) -> OptionOverrideSet {
749 OptionOverrideSet::default()
750 }
751 fn required_capabilities(&self) -> CapabilitySet {
752 CapabilitySet::empty()
753 }
754 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
755 Box::pin(async { Ok(()) })
756 }
757 }
758 };
759}
760
761pub use crate::completion::LspCompletionMode;
764pub trait DiagnosticsQuery: Send + Sync {
777 fn on_line(
781 &self,
782 buffer_id: lattice_protocol::ids::BufferId,
783 line: u32,
784 ) -> Vec<crate::Diagnostic>;
785
786 fn for_uri(&self, _uri: &str) -> Vec<crate::Diagnostic> {
793 Vec::new()
794 }
795
796 fn uris_with_diagnostics(&self) -> Vec<String> {
800 Vec::new()
801 }
802}
803
804pub type DiagnosticsQueryHandle = std::sync::Arc<dyn DiagnosticsQuery>;
807
808#[cfg(test)]
809mod diagnostics_query_for_uri_tests {
810 use super::DiagnosticsQuery;
811 use lattice_protocol::ids::BufferId;
812
813 struct OnlyOnLine;
816 impl DiagnosticsQuery for OnlyOnLine {
817 fn on_line(&self, _b: BufferId, _l: u32) -> Vec<crate::Diagnostic> {
818 Vec::new()
819 }
820 }
821
822 #[test]
823 fn for_uri_methods_are_object_safe_and_default_to_empty() {
824 let q: &dyn DiagnosticsQuery = &OnlyOnLine;
825 assert!(q.for_uri("file:///x.rs").is_empty());
826 assert!(q.uris_with_diagnostics().is_empty());
827 assert!(q.on_line(BufferId::new(0), 0).is_empty());
828 }
829}
830
831pub fn format_diagnostic_popup_lines(diags: &[crate::Diagnostic]) -> Vec<(String, u8)> {
837 use crate::lsp_types::{DiagnosticSeverity, NumberOrString};
838 diags
839 .iter()
840 .map(|d| {
841 let (glyph, rank) = match d.severity {
845 Some(DiagnosticSeverity::ERROR) => ('■', 0u8),
846 Some(DiagnosticSeverity::WARNING) => ('▲', 1),
847 Some(DiagnosticSeverity::INFORMATION) => ('●', 2),
848 _ => ('·', 3),
849 };
850 let msg = d.message.lines().next().unwrap_or("").trim();
851 let mut line = format!("{glyph} {msg}");
852 let mut tags: Vec<String> = Vec::new();
853 if let Some(src) = &d.source {
854 tags.push(src.clone());
855 }
856 if let Some(code) = &d.code {
857 tags.push(match code {
858 NumberOrString::Number(n) => n.to_string(),
859 NumberOrString::String(s) => s.clone(),
860 });
861 }
862 if !tags.is_empty() {
863 line.push_str(&format!(" [{}]", tags.join(":")));
864 }
865 let related = d.related_information.as_ref().map_or(0, |r| r.len());
866 if related > 0 {
867 line.push_str(&format!(" (+{related} related)"));
868 }
869 (line, rank)
870 })
871 .collect()
872}
873
874fn lsp_diagnostics_mode_keymap_entries() -> &'static [KeymapEntry] {
881 static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
882 ENTRIES.get_or_init(|| {
883 vec![
884 keymap_entry! {
885 mode: Normal, chord: "gl",
886 doc: "Show diagnostics on the cursor line in a popup",
887 cmd: "action:lsp-diagnostic-popup"
888 },
889 keymap_entry! {
890 mode: Normal, chord: "]d",
891 doc: "Jump to the next diagnostic (echoes its message)",
892 cmd: "ex:diag-next"
893 },
894 keymap_entry! {
895 mode: Normal, chord: "[d",
896 doc: "Jump to the previous diagnostic (echoes its message)",
897 cmd: "ex:diag-prev"
898 },
899 ]
900 })
901}
902
903pub struct LspDiagnosticsMode;
907
908impl LspDiagnosticsMode {
909 pub fn mode_id() -> ModeId {
910 ModeId::new("lsp-diagnostics-mode")
911 }
912}
913
914impl Mode for LspDiagnosticsMode {
915 type Guard = ();
916 fn id(&self) -> ModeId {
917 Self::mode_id()
918 }
919 fn kind(&self) -> ModeKind {
920 ModeKind::Minor
921 }
922 fn options(&self) -> OptionOverrideSet {
923 OptionOverrideSet::default()
924 }
925 fn required_capabilities(&self) -> CapabilitySet {
926 CapabilitySet::empty()
927 }
928 fn keymap(&self) -> Keymap {
929 Keymap::from_entries(lsp_diagnostics_mode_keymap_entries())
930 }
931 fn action_handlers(&self) -> Vec<ActionHandlerContribution> {
937 let handler: ActionHandler =
938 std::sync::Arc::new(|ctx: &ActionContext<'_>| -> Option<Effect> {
939 let query = ctx.services.get::<DiagnosticsQueryHandle>()?;
940 let diags = query.on_line(ctx.buffer_id, ctx.cursor.line);
941 Some(Effect::ShowDiagnosticsPopup {
942 lines: format_diagnostic_popup_lines(&diags),
943 })
944 });
945 vec![ActionHandlerContribution {
946 action_name: "action:lsp-diagnostic-popup",
947 handler,
948 }]
949 }
950 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
951 Box::pin(async { Ok(()) })
952 }
953}
954lsp_sub_mode!(LspHoverMode, "lsp-hover-mode");
955lsp_sub_mode!(LspSignatureMode, "lsp-signature-mode");
956lsp_sub_mode!(LspFormatMode, "lsp-format-mode");
957lsp_sub_mode!(LspRenameMode, "lsp-rename-mode");
958lsp_sub_mode!(LspSymbolsMode, "lsp-symbols-mode");
959lsp_sub_mode!(LspCodeActionMode, "lsp-code-action-mode");
960lsp_sub_mode!(LspNavMode, "lsp-nav-mode");
961
962pub struct LspDiagnosticsData {
968 pub diagnostics: Option<std::sync::Arc<[crate::Diagnostic]>>,
969}
970
971pub struct LspProgressMode;
977
978impl LspProgressMode {
979 pub fn mode_id() -> ModeId {
980 ModeId::new("lsp-progress-mode")
981 }
982}
983
984impl Mode for LspProgressMode {
985 type Guard = ();
986 fn id(&self) -> ModeId {
987 Self::mode_id()
988 }
989 fn kind(&self) -> ModeKind {
990 ModeKind::Minor
991 }
992 fn options(&self) -> OptionOverrideSet {
993 OptionOverrideSet::default()
994 }
995 fn required_capabilities(&self) -> CapabilitySet {
996 CapabilitySet::empty()
997 }
998 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
999 Box::pin(async { Ok(()) })
1000 }
1001}
1002
1003lsp_sub_mode!(LspDocumentHighlightMode, "lsp-document-highlight-mode");
1004lsp_sub_mode!(LspSelectionRangeMode, "lsp-selection-range-mode");
1005lsp_sub_mode!(LspInlayHintMode, "lsp-inlay-hint-mode");
1006lsp_sub_mode!(LspSemanticTokensMode, "lsp-semantic-tokens-mode");
1007
1008pub struct LspFoldingMode;
1035
1036impl LspFoldingMode {
1037 pub fn mode_id() -> ModeId {
1038 ModeId::new("lsp-folding-mode")
1039 }
1040}
1041
1042impl Mode for LspFoldingMode {
1043 type Guard = ();
1044 fn id(&self) -> ModeId {
1045 Self::mode_id()
1046 }
1047 fn kind(&self) -> ModeKind {
1048 ModeKind::Minor
1049 }
1050 fn options(&self) -> OptionOverrideSet {
1054 lattice_config::overrides! {
1055 lattice_config::FoldMethodOption = lattice_core::FoldMethod::Lsp,
1056 }
1057 }
1058 fn required_capabilities(&self) -> CapabilitySet {
1059 CapabilitySet::empty()
1060 }
1061 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
1063 Box::pin(async { Ok(()) })
1064 }
1065}
1066
1067pub fn register_lsp_log_modes(registry: &mut ModeRegistry) {
1073 registry
1074 .register(LspLogMode)
1075 .expect("lsp-log-mode register");
1076 registry
1077 .register(LspTraceLogMode)
1078 .expect("lsp-trace-log-mode register");
1079 registry
1080 .register(LspServerLogMode)
1081 .expect("lsp-server-log-mode register");
1082 registry
1083 .register(LspMode::new())
1084 .expect("lsp-mode register");
1085 registry
1086 .register(LspDiagnosticsMode)
1087 .expect("lsp-diagnostics-mode register");
1088 registry
1089 .register(LspHoverMode)
1090 .expect("lsp-hover-mode register");
1091 registry
1092 .register(LspSignatureMode)
1093 .expect("lsp-signature-mode register");
1094 registry
1095 .register(LspFormatMode)
1096 .expect("lsp-format-mode register");
1097 registry
1098 .register(LspRenameMode)
1099 .expect("lsp-rename-mode register");
1100 registry
1101 .register(LspSymbolsMode)
1102 .expect("lsp-symbols-mode register");
1103 registry
1104 .register(LspCodeActionMode)
1105 .expect("lsp-code-action-mode register");
1106 registry
1107 .register(LspNavMode)
1108 .expect("lsp-nav-mode register");
1109 registry
1110 .register(LspProgressMode)
1111 .expect("lsp-progress-mode register");
1112 registry
1113 .register(LspDocumentHighlightMode)
1114 .expect("lsp-document-highlight-mode register");
1115 registry
1116 .register(LspSelectionRangeMode)
1117 .expect("lsp-selection-range-mode register");
1118 registry
1119 .register(LspFoldingMode)
1120 .expect("lsp-folding-mode register");
1121 registry
1122 .register(LspInlayHintMode)
1123 .expect("lsp-inlay-hint-mode register");
1124 registry
1125 .register(LspSemanticTokensMode)
1126 .expect("lsp-semantic-tokens-mode register");
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 use super::*;
1132
1133 #[test]
1134 fn each_lsp_mode_has_distinct_id() {
1135 let ids = [
1136 LspLogMode::mode_id(),
1137 LspTraceLogMode::mode_id(),
1138 LspServerLogMode::mode_id(),
1139 LspMode::mode_id(),
1140 LspCompletionMode::mode_id(),
1141 LspDiagnosticsMode::mode_id(),
1142 LspHoverMode::mode_id(),
1143 LspSignatureMode::mode_id(),
1144 LspFormatMode::mode_id(),
1145 LspRenameMode::mode_id(),
1146 LspSymbolsMode::mode_id(),
1147 LspCodeActionMode::mode_id(),
1148 LspNavMode::mode_id(),
1149 LspProgressMode::mode_id(),
1150 LspDocumentHighlightMode::mode_id(),
1151 LspSelectionRangeMode::mode_id(),
1152 LspFoldingMode::mode_id(),
1153 LspInlayHintMode::mode_id(),
1154 LspSemanticTokensMode::mode_id(),
1155 ];
1156 for (i, a) in ids.iter().enumerate() {
1157 for b in &ids[i + 1..] {
1158 assert_ne!(a, b);
1159 }
1160 }
1161 }
1162
1163 #[test]
1164 fn register_lsp_log_modes_populates_registry() {
1165 let mut registry = ModeRegistry::new();
1166 register_lsp_log_modes(&mut registry);
1167 assert!(registry.is_registered(LspLogMode::mode_id()));
1168 assert!(registry.is_registered(LspTraceLogMode::mode_id()));
1169 assert!(registry.is_registered(LspServerLogMode::mode_id()));
1170 assert!(registry.is_registered(LspMode::mode_id()));
1171 assert!(registry.is_registered(LspDiagnosticsMode::mode_id()));
1172 assert!(registry.is_registered(LspHoverMode::mode_id()));
1173 assert!(registry.is_registered(LspSignatureMode::mode_id()));
1174 assert!(registry.is_registered(LspFormatMode::mode_id()));
1175 assert!(registry.is_registered(LspRenameMode::mode_id()));
1176 assert!(registry.is_registered(LspSymbolsMode::mode_id()));
1177 assert!(registry.is_registered(LspCodeActionMode::mode_id()));
1178 assert!(registry.is_registered(LspNavMode::mode_id()));
1179 assert!(registry.is_registered(LspProgressMode::mode_id()));
1180 assert!(registry.is_registered(LspDocumentHighlightMode::mode_id()));
1181 assert!(registry.is_registered(LspSelectionRangeMode::mode_id()));
1182 assert!(registry.is_registered(LspFoldingMode::mode_id()));
1183 assert!(registry.is_registered(LspInlayHintMode::mode_id()));
1184 assert!(registry.is_registered(LspSemanticTokensMode::mode_id()));
1185 }
1186
1187 #[test]
1190 fn lsp_mode_keymap_has_seven_entries() {
1191 let km = LspMode::new().keymap();
1192 assert_eq!(km.entries.len(), 7, "expected exactly 7 LSP nav entries");
1193 }
1194
1195 #[test]
1196 fn lsp_mode_keymap_entries_have_expected_commands() {
1197 let km = LspMode::new().keymap();
1198 let cmds: Vec<&str> = km.entries.iter().filter_map(|e| e.command).collect();
1199 for expected in [
1200 "action:lsp-hover",
1201 "action:lsp-definition",
1202 "action:lsp-declaration",
1203 "action:lsp-type-definition",
1204 "action:lsp-implementation",
1205 "action:lsp-references",
1206 "action:lsp-follow-link",
1207 ] {
1208 assert!(cmds.contains(&expected), "missing command {expected}");
1209 }
1210 }
1211
1212 #[test]
1213 fn lsp_mode_keymap_includes_all_chord_strings() {
1214 let km = LspMode::new().keymap();
1215 let chords: Vec<&str> = km.entries.iter().map(|e| e.chord).collect();
1216 for expected in ["K", "gd", "gD", "gy", "gI", "gr", "gx"] {
1217 assert!(chords.contains(&expected), "missing chord {expected}");
1218 }
1219 }
1220
1221 #[test]
1222 fn lsp_mode_is_minor_with_no_capability_requirements() {
1223 let m = LspMode::new();
1224 assert_eq!(m.kind(), ModeKind::Minor);
1225 assert_eq!(m.required_capabilities(), CapabilitySet::empty());
1226 }
1227
1228 #[test]
1231 fn lsp_mode_action_handlers_map_each_nav_chord_to_its_request() {
1232 let handlers = LspMode::new().action_handlers();
1236 assert_eq!(handlers.len(), 7, "expected one handler per nav chord");
1237
1238 let services = lattice_mode::ServiceRegistry::new();
1239 let events = EventBus::new();
1240 let ctx = ActionContext {
1241 buffer_id: lattice_protocol::ids::BufferId::new(0),
1242 cursor: lattice_protocol::position::Position::ZERO,
1243 selection: None,
1244 services: &services,
1245 events: &events,
1246 prompt_value: None,
1247 args: lattice_grammar::Args::None,
1248 buffer_locals: None,
1249 };
1250
1251 let expected: &[(&str, LspRequest)] = &[
1252 ("action:lsp-hover", LspRequest::Hover),
1253 ("action:lsp-definition", LspRequest::Definition),
1254 ("action:lsp-declaration", LspRequest::Declaration),
1255 ("action:lsp-type-definition", LspRequest::TypeDefinition),
1256 ("action:lsp-implementation", LspRequest::Implementation),
1257 ("action:lsp-references", LspRequest::References),
1258 ("action:lsp-follow-link", LspRequest::FollowLink),
1259 ];
1260 for (name, req) in expected {
1261 let contribution = handlers
1262 .iter()
1263 .find(|c| c.action_name == *name)
1264 .unwrap_or_else(|| panic!("missing handler for {name}"));
1265 match (contribution.handler)(&ctx) {
1266 Some(Effect::Lsp(got)) => {
1267 assert_eq!(got, *req, "handler {name} fired the wrong request")
1268 }
1269 other => panic!("handler {name} returned {other:?}, want Effect::Lsp({req:?})"),
1270 }
1271 }
1272 }
1273
1274 fn diag(
1277 sev: crate::lsp_types::DiagnosticSeverity,
1278 msg: &str,
1279 source: Option<&str>,
1280 code: Option<&str>,
1281 ) -> crate::Diagnostic {
1282 crate::Diagnostic {
1283 range: crate::lsp_types::Range {
1284 start: crate::lsp_types::Position {
1285 line: 0,
1286 character: 0,
1287 },
1288 end: crate::lsp_types::Position {
1289 line: 0,
1290 character: 1,
1291 },
1292 },
1293 severity: Some(sev),
1294 code: code.map(|c| crate::lsp_types::NumberOrString::String(c.to_string())),
1295 code_description: None,
1296 source: source.map(str::to_string),
1297 message: msg.to_string(),
1298 related_information: None,
1299 tags: None,
1300 data: None,
1301 }
1302 }
1303
1304 #[test]
1305 fn lsp_diagnostics_mode_keymap_owns_the_three_chords() {
1306 let km = LspDiagnosticsMode.keymap();
1307 let pairs: Vec<(&str, &str)> = km
1308 .entries
1309 .iter()
1310 .filter_map(|e| e.command.map(|c| (e.chord, c)))
1311 .collect();
1312 assert!(pairs.contains(&("gl", "action:lsp-diagnostic-popup")));
1313 assert!(pairs.contains(&("]d", "ex:diag-next")));
1316 assert!(pairs.contains(&("[d", "ex:diag-prev")));
1317 }
1318
1319 #[test]
1320 fn lsp_diagnostics_mode_contributes_only_the_popup_handler() {
1321 let handlers = LspDiagnosticsMode.action_handlers();
1322 assert_eq!(handlers.len(), 1);
1323 assert_eq!(handlers[0].action_name, "action:lsp-diagnostic-popup");
1324 }
1325
1326 #[test]
1327 fn format_popup_lines_carries_glyph_message_source_code() {
1328 use crate::lsp_types::DiagnosticSeverity;
1329 let lines = format_diagnostic_popup_lines(&[diag(
1330 DiagnosticSeverity::ERROR,
1331 "mismatched types",
1332 Some("rustc"),
1333 Some("E0308"),
1334 )]);
1335 assert_eq!(lines.len(), 1);
1336 assert_eq!(lines[0].1, 0, "error → rank 0");
1337 assert!(lines[0].0.starts_with('■'), "got {:?}", lines[0]);
1338 assert!(lines[0].0.contains("mismatched types"));
1339 assert!(lines[0].0.contains("[rustc:E0308]"));
1340 }
1341
1342 #[test]
1343 fn format_popup_lines_one_per_diagnostic_empty_for_none() {
1344 use crate::lsp_types::DiagnosticSeverity;
1345 assert!(format_diagnostic_popup_lines(&[]).is_empty());
1346 let lines = format_diagnostic_popup_lines(&[
1347 diag(DiagnosticSeverity::WARNING, "unused", None, None),
1348 diag(DiagnosticSeverity::HINT, "consider", None, None),
1349 ]);
1350 assert_eq!(lines.len(), 2);
1351 assert!(lines[0].0.starts_with('▲'));
1352 assert_eq!(lines[0].1, 1, "warning → rank 1");
1353 assert!(lines[1].0.starts_with('·'));
1354 assert_eq!(lines[1].1, 3, "hint → rank 3");
1355 }
1356
1357 #[test]
1358 fn format_popup_lines_appends_related_count() {
1359 use crate::lsp_types::{
1360 DiagnosticRelatedInformation, DiagnosticSeverity, Location, Position, Range, Uri,
1361 };
1362 use std::str::FromStr;
1363 let mut d = diag(DiagnosticSeverity::ERROR, "boom", None, None);
1364 let loc = Location {
1365 uri: Uri::from_str("file:///x.rs").unwrap(),
1366 range: Range {
1367 start: Position {
1368 line: 0,
1369 character: 0,
1370 },
1371 end: Position {
1372 line: 0,
1373 character: 0,
1374 },
1375 },
1376 };
1377 d.related_information = Some(vec![
1378 DiagnosticRelatedInformation {
1379 location: loc.clone(),
1380 message: "a".into(),
1381 },
1382 DiagnosticRelatedInformation {
1383 location: loc,
1384 message: "b".into(),
1385 },
1386 ]);
1387 let lines = format_diagnostic_popup_lines(&[d]);
1388 assert!(lines[0].0.contains("(+2 related)"), "got {:?}", lines[0]);
1389 }
1390
1391 #[tokio::test]
1392 async fn lsp_mode_activates_through_registry_as_minor() {
1393 use crate::completion::register_lsp_completion_mode;
1394 use crate::supervisor::LspSupervisor;
1395 use lattice_mode::{ActiveModes, GuardStoreHandle};
1396 use lattice_protocol::ids::BufferId;
1397 let mut registry = ModeRegistry::new();
1398 register_lsp_log_modes(&mut registry);
1399 let sup = LspSupervisor::new(crate::LspLogger::with_defaults());
1400 let lsp_handle = sup.spawn(&tokio::runtime::Handle::current());
1401 register_lsp_completion_mode(&mut registry, lsp_handle);
1402 let mut active = ActiveModes::new();
1403 let guards = GuardStoreHandle::new();
1404 let cfg = Arc::new(lattice_config::ConfigRegistry::new());
1405 let evt = Arc::new(lattice_runtime::EventBus::new());
1406 let svc = Arc::new(lattice_mode::ServiceRegistry::new());
1407 registry
1408 .activate_minor(
1409 &mut active,
1410 &guards,
1411 &cfg,
1412 &evt,
1413 &svc,
1414 BufferId::new(1),
1415 LspMode::mode_id(),
1416 CapabilitySet::empty(),
1417 )
1418 .expect("activate lsp-mode + sub-mode cascade");
1419 assert!(active.has_minor(LspMode::mode_id()));
1422 assert!(active.has_minor(LspCompletionMode::mode_id()));
1423 assert!(active.has_minor(LspDiagnosticsMode::mode_id()));
1424 assert!(active.has_minor(LspFoldingMode::mode_id()));
1425 }
1426}