1mod command;
16mod document;
17mod health;
18mod hover;
19mod manager;
20mod picker;
21mod stage;
22mod window;
23
24use std::{path::PathBuf, sync::Arc};
25
26use {
27 reovim_core::{
28 bind::{CommandRef, KeymapScope},
29 event_bus::{
30 BufferClosed, BufferModified, CursorMoved, EventBus, EventResult, FileOpened,
31 ModeChanged, RequestOpenFileAtPosition, ShutdownEvent,
32 },
33 keys,
34 plugin::{Plugin, PluginContext, PluginId, PluginStateRegistry},
35 },
36 reovim_lsp::{
37 ClientConfig, GotoDefinitionResponse, HoverContents, Location, LspSaturator, MarkedString,
38 },
39 reovim_plugin_microscope::{MicroscopeOpen, PickerRegistry},
40 tokio::sync::mpsc,
41 tracing::{debug, error, info, warn},
42};
43
44pub mod command_id {
46 use reovim_core::command::id::CommandId;
47
48 pub const GOTO_DEFINITION: CommandId = CommandId::new("lsp_goto_definition");
49 pub const GOTO_REFERENCES: CommandId = CommandId::new("lsp_goto_references");
50 pub const SHOW_HOVER: CommandId = CommandId::new("lsp_show_hover");
51}
52
53pub use {
54 document::{DocumentManager, DocumentState},
55 manager::{LspManager, SharedLspManager},
56 stage::LspRenderStage,
57};
58
59pub use command::{
61 LspGotoDefinition, LspGotoDefinitionCommand, LspGotoReferences, LspGotoReferencesCommand,
62 LspHoverDismiss, LspLogOpen, LspShowHover, LspShowHoverCommand,
63};
64
65pub struct LspPlugin {
70 manager: Arc<SharedLspManager>,
71}
72
73impl Default for LspPlugin {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl LspPlugin {
80 #[must_use]
82 pub fn new() -> Self {
83 Self {
84 manager: Arc::new(SharedLspManager::new()),
85 }
86 }
87
88 #[must_use]
90 pub fn manager(&self) -> Arc<SharedLspManager> {
91 Arc::clone(&self.manager)
92 }
93}
94
95impl Plugin for LspPlugin {
96 fn id(&self) -> PluginId {
97 PluginId::new("reovim:lsp")
98 }
99
100 fn name(&self) -> &'static str {
101 "LSP"
102 }
103
104 fn description(&self) -> &'static str {
105 "Language Server Protocol integration for diagnostics and more"
106 }
107
108 fn build(&self, ctx: &mut PluginContext) {
109 let stage = Arc::new(LspRenderStage::new(Arc::clone(&self.manager)));
111 ctx.register_render_stage(stage);
112
113 if let Err(e) = ctx.register_command(LspGotoDefinitionCommand) {
115 error!("Failed to register lsp_goto_definition command: {:?}", e);
116 }
117 if let Err(e) = ctx.register_command(LspGotoReferencesCommand) {
118 error!("Failed to register lsp_goto_references command: {:?}", e);
119 }
120 if let Err(e) = ctx.register_command(LspShowHoverCommand) {
121 error!("Failed to register lsp_show_hover command: {:?}", e);
122 }
123 debug!(
124 "LspPlugin: registered commands - gd={}, gr={}, K={}",
125 command_id::GOTO_DEFINITION.as_str(),
126 command_id::GOTO_REFERENCES.as_str(),
127 command_id::SHOW_HOVER.as_str()
128 );
129
130 let editor_normal = KeymapScope::editor_normal();
132
133 ctx.bind_key_scoped(
135 editor_normal.clone(),
136 keys!['g' 'd'],
137 CommandRef::Registered(command_id::GOTO_DEFINITION),
138 );
139
140 ctx.bind_key_scoped(
142 editor_normal.clone(),
143 keys!['g' 'r'],
144 CommandRef::Registered(command_id::GOTO_REFERENCES),
145 );
146
147 ctx.bind_key_scoped(
149 editor_normal,
150 keys!['K'],
151 CommandRef::Registered(command_id::SHOW_HOVER),
152 );
153
154 info!("LspPlugin: registered render stage and keybindings (gd, gr, K)");
155 }
156
157 fn init_state(&self, registry: &PluginStateRegistry) {
158 registry.register(Arc::clone(&self.manager));
160
161 registry.register_plugin_window(Arc::new(window::HoverPluginWindow::new(Arc::clone(
163 &self.manager,
164 ))));
165
166 let references_picker = Arc::new(picker::LspReferencesPicker::new());
168 registry.register(Arc::clone(&references_picker));
169
170 let definitions_picker = Arc::new(picker::LspDefinitionsPicker::new());
171 registry.register(Arc::clone(&definitions_picker));
172
173 registry.with_mut::<PickerRegistry, _, _>(|picker_registry| {
174 picker_registry.register(references_picker);
175 picker_registry.register(definitions_picker);
176 });
177
178 debug!("LspPlugin: initialized state");
179 }
180
181 #[allow(clippy::too_many_lines)]
182 fn subscribe(&self, bus: &EventBus, state: Arc<PluginStateRegistry>) {
183 let rt_handle = tokio::runtime::Handle::current();
186
187 {
189 use reovim_plugin_health_check::RegisterHealthCheck;
190
191 let manager = Arc::clone(&self.manager);
192 bus.emit(RegisterHealthCheck::new(Arc::new(health::LspHealthCheck::new(manager))));
193 }
194
195 {
197 use reovim_core::{
198 command_line::ExCommandHandler,
199 event_bus::{DynEvent, core_events::RegisterExCommand},
200 };
201
202 bus.emit(RegisterExCommand::new(
203 "LspLog",
204 ExCommandHandler::ZeroArg {
205 event_constructor: || DynEvent::new(command::LspLogOpen),
206 description: "Open the LSP log file",
207 },
208 ));
209 }
210
211 bus.subscribe::<command::LspLogOpen, _>(100, move |_event, ctx| {
213 use reovim_core::event_bus::core_events::RequestOpenFile;
214
215 let data_dir = dirs::data_local_dir()
217 .unwrap_or_else(|| PathBuf::from("."))
218 .join("reovim");
219
220 if let Ok(entries) = std::fs::read_dir(&data_dir) {
222 let mut lsp_logs: Vec<_> = entries
223 .filter_map(std::result::Result::ok)
224 .filter(|e| {
225 e.file_name().to_string_lossy().starts_with("lsp-")
226 && e.file_name().to_string_lossy().ends_with(".log")
227 })
228 .collect();
229
230 lsp_logs.sort_by(|a, b| {
232 b.metadata()
233 .and_then(|m| m.modified())
234 .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
235 .cmp(
236 &a.metadata()
237 .and_then(|m| m.modified())
238 .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
239 )
240 });
241
242 if let Some(latest) = lsp_logs.first() {
243 let log_path = latest.path();
244 info!("Opening LSP log: {}", log_path.display());
245 ctx.emit(RequestOpenFile { path: log_path });
246 } else {
247 warn!("No LSP log files found in {}", data_dir.display());
248 }
249 } else {
250 warn!("Could not read data directory: {}", data_dir.display());
251 }
252
253 EventResult::Handled
254 });
255
256 {
260 let state = Arc::clone(&state);
261 bus.subscribe::<FileOpened, _>(100, move |event, _ctx| {
262 info!(
263 buffer_id = event.buffer_id,
264 path = %event.path,
265 "LSP: FileOpened event received"
266 );
267 state.with_mut::<Arc<SharedLspManager>, _, _>(|manager| {
268 manager.with_mut(|m| {
269 let path = PathBuf::from(&event.path);
270 if let Some(doc) = m.documents.open_document(event.buffer_id, path) {
271 info!(
272 buffer_id = event.buffer_id,
273 language_id = %doc.language_id,
274 "LSP: opened document, scheduling sync for render stage"
275 );
276 m.documents.schedule_immediate_sync(event.buffer_id);
278 }
279 });
280 });
281 EventResult::Handled
282 });
283 }
284
285 {
287 let state = Arc::clone(&state);
288 bus.subscribe::<BufferModified, _>(100, move |event, _ctx| {
289 state.with_mut::<Arc<SharedLspManager>, _, _>(|manager| {
290 manager.with_mut(|m| {
291 if m.documents.has_document(event.buffer_id)
292 && let Some(version) = m.documents.schedule_sync(event.buffer_id)
293 {
294 debug!(
295 buffer_id = event.buffer_id,
296 version = version,
297 "LSP: scheduled sync"
298 );
299 }
300 });
301 });
302 EventResult::Handled
303 });
304 }
305
306 {
308 let state = Arc::clone(&state);
309 bus.subscribe::<BufferClosed, _>(100, move |event, _ctx| {
310 state.with_mut::<Arc<SharedLspManager>, _, _>(|manager| {
311 manager.with_mut(|m| {
312 if let Some(doc) = m.documents.close_document(event.buffer_id) {
313 debug!(
314 buffer_id = event.buffer_id,
315 uri = ?doc.uri,
316 "LSP: closed document"
317 );
318
319 if let Some(handle) = &m.handle {
321 handle.did_close(doc.uri);
322 }
323 }
324 });
325 });
326 EventResult::Handled
327 });
328 }
329
330 {
332 let state = Arc::clone(&state);
333 let event_sender = bus.sender();
334 let rt_handle = rt_handle.clone();
335 bus.subscribe::<LspGotoDefinition, _>(100, move |event, _ctx| {
336 info!(
337 buffer_id = event.buffer_id,
338 line = event.line,
339 column = event.column,
340 "LSP: LspGotoDefinition event received"
341 );
342 let buffer_id = event.buffer_id;
343 let line = event.line;
344 let column = event.column;
345
346 let request_info = state.with::<Arc<SharedLspManager>, _, _>(|manager| {
348 manager.with(|m| {
349 let doc = m.documents.get(buffer_id)?;
350 let handle = m.handle.as_ref()?;
351 let uri = doc.uri.clone();
352 #[allow(clippy::cast_possible_truncation)]
353 let position = reovim_lsp::Position {
354 line: line as u32, character: column as u32, };
357 let rx = handle.goto_definition(uri.clone(), position);
358 Some((uri, position, rx))
359 })
360 });
361
362 if let Some(Some((uri, _position, Some(rx)))) = request_info {
363 info!(
364 buffer_id,
365 line,
366 column,
367 uri = %uri.as_str(),
368 "LSP: goto definition request sent"
369 );
370
371 let sender = event_sender.clone();
374 let state_clone = Arc::clone(&state);
375 rt_handle.spawn(async move {
376 match rx.await {
377 Ok(Ok(Some(response))) => {
378 let locations = extract_all_locations(&response);
380
381 if locations.is_empty() {
382 info!("LSP: no definition location in response");
383 return;
384 }
385
386 if locations.len() == 1 {
387 let loc = &locations[0];
389 info!(
390 uri = %loc.uri.as_str(),
391 line = loc.range.start.line,
392 col = loc.range.start.character,
393 "LSP: navigating to definition"
394 );
395 if let Some(path) = uri_to_path(&loc.uri) {
396 sender.try_send(RequestOpenFileAtPosition {
397 path,
398 line: loc.range.start.line as usize,
399 column: loc.range.start.character as usize,
400 });
401 } else {
402 warn!(uri = %loc.uri.as_str(), "LSP: cannot convert URI to file path");
403 }
404 } else {
405 info!(count = locations.len(), "LSP: multiple definitions found, opening picker");
407
408 state_clone.with::<Arc<picker::LspDefinitionsPicker>, _, _>(
410 |definitions_picker| {
411 definitions_picker.set_definitions(locations);
412 },
413 );
414
415 sender.try_send(MicroscopeOpen::new("lsp_definitions"));
417 }
418 }
419 Ok(Ok(None)) => {
420 info!("LSP: no definition found");
421 }
422 Ok(Err(e)) => {
423 warn!("LSP: goto definition error: {}", e);
424 }
425 Err(_) => {
426 warn!("LSP: goto definition channel closed");
427 }
428 }
429 });
430 } else {
431 debug!(buffer_id, "LSP: goto definition - no document or handle");
432 }
433
434 EventResult::Handled
435 });
436 }
437
438 {
440 let state = Arc::clone(&state);
441 let event_sender = bus.sender();
442 let rt_handle = rt_handle.clone();
443 bus.subscribe::<LspGotoReferences, _>(100, move |event, _ctx| {
444 let buffer_id = event.buffer_id;
445 let line = event.line;
446 let column = event.column;
447
448 let request_info = state.with::<Arc<SharedLspManager>, _, _>(|manager| {
450 manager.with(|m| {
451 let doc = m.documents.get(buffer_id)?;
452 let handle = m.handle.as_ref()?;
453 let uri = doc.uri.clone();
454 #[allow(clippy::cast_possible_truncation)]
455 let position = reovim_lsp::Position {
456 line: line as u32, character: column as u32, };
459 let rx = handle.references(uri.clone(), position, true);
460 Some((uri, position, rx))
461 })
462 });
463
464 if let Some(Some((uri, _position, Some(rx)))) = request_info {
465 info!(
466 buffer_id,
467 line,
468 column,
469 uri = %uri.as_str(),
470 "LSP: goto references request sent"
471 );
472
473 let state_clone = Arc::clone(&state);
476 let sender = event_sender.clone();
477 rt_handle.spawn(async move {
478 match rx.await {
479 Ok(Ok(Some(locations))) => {
480 info!(count = locations.len(), "LSP: references found");
481
482 if locations.is_empty() {
483 info!("LSP: no references to display");
484 return;
485 }
486
487 state_clone.with::<Arc<picker::LspReferencesPicker>, _, _>(
489 |picker| {
490 picker.set_references(locations);
491 },
492 );
493
494 sender.try_send(MicroscopeOpen::new("lsp_references"));
496 }
497 Ok(Ok(None)) => {
498 info!("LSP: no references found");
499 }
500 Ok(Err(e)) => {
501 warn!("LSP: references error: {}", e);
502 }
503 Err(_) => {
504 warn!("LSP: references channel closed");
505 }
506 }
507 });
508 } else {
509 debug!(buffer_id, "LSP: goto references - no document or handle");
510 }
511
512 EventResult::Handled
513 });
514 }
515
516 {
518 let state = Arc::clone(&state);
519 bus.subscribe::<LspShowHover, _>(100, move |event, _ctx| {
521 let buffer_id = event.buffer_id;
522 let line = event.line;
523 let column = event.column;
524
525 let request_info = state.with::<Arc<SharedLspManager>, _, _>(|manager| {
527 manager.with(|m| {
528 let doc = m.documents.get(buffer_id)?;
529 let handle = m.handle.as_ref()?;
530 let uri = doc.uri.clone();
531 #[allow(clippy::cast_possible_truncation)]
532 let position = reovim_lsp::Position {
533 line: line as u32, character: column as u32, };
536 let rx = handle.hover(uri.clone(), position);
537 Some((uri, position, rx))
538 })
539 });
540
541 if let Some(Some((uri, _position, Some(rx)))) = request_info {
542 info!(
543 buffer_id,
544 line,
545 column,
546 uri = %uri.as_str(),
547 "LSP: hover request sent"
548 );
549
550 let state_clone = Arc::clone(&state);
553 rt_handle.spawn(async move {
554 match rx.await {
555 Ok(Ok(Some(hover))) => {
556 let content = extract_hover_text(&hover.contents);
558 if content.is_empty() {
559 info!("LSP: hover response is empty");
560 } else {
561 info!(
562 content_lines = content.lines().count(),
563 "LSP: hover content received"
564 );
565
566 state_clone.with_mut::<Arc<SharedLspManager>, _, _>(
568 |manager| {
569 manager.with_mut(|m| {
570 let snapshot = hover::HoverSnapshot::new(
571 content, line, column, buffer_id,
572 );
573 m.hover_cache.store(snapshot);
574 m.send_render_signal();
576 });
577 },
578 );
579 }
580 }
581 Ok(Ok(None)) => {
582 info!("LSP: no hover info");
583 }
584 Ok(Err(e)) => {
585 warn!("LSP: hover error: {}", e);
586 }
587 Err(_) => {
588 warn!("LSP: hover channel closed");
589 }
590 }
591 });
592 } else {
593 info!(buffer_id, ?request_info, "LSP: hover - no document or handle");
594 }
595
596 EventResult::Handled
597 });
598 }
599
600 {
602 let state = Arc::clone(&state);
603 bus.subscribe::<ShutdownEvent, _>(100, move |_event, _ctx| {
604 state.with_mut::<Arc<SharedLspManager>, _, _>(|manager| {
605 manager.with_mut(|m| {
606 if m.is_running() {
607 info!("LSP: shutting down language server");
608 m.shutdown();
609 }
610 });
611 });
612 EventResult::Handled
613 });
614 }
615
616 {
618 let state = Arc::clone(&state);
619 bus.subscribe::<command::LspHoverDismiss, _>(100, move |_event, _ctx| {
620 state.with_mut::<Arc<SharedLspManager>, _, _>(|manager| {
621 manager.with_mut(|m| {
622 m.hover_cache.clear();
623 });
624 });
625 EventResult::Handled
626 });
627 }
628
629 {
631 let state = Arc::clone(&state);
632 bus.subscribe::<CursorMoved, _>(200, move |_event, _ctx| {
633 let was_active = state
634 .with_mut::<Arc<SharedLspManager>, _, bool>(|manager| {
635 manager.with_mut(|m| {
636 let is_active = m.hover_cache.is_active();
637 if is_active {
638 debug!("CursorMoved: clearing active hover");
639 m.hover_cache.clear();
640 true
641 } else {
642 false
643 }
644 })
645 })
646 .unwrap_or(false);
647
648 if was_active {
650 debug!("CursorMoved: returning NeedsRender to dismiss hover");
651 EventResult::NeedsRender
652 } else {
653 EventResult::Handled
654 }
655 });
656 }
657
658 {
660 let state = Arc::clone(&state);
661 bus.subscribe::<ModeChanged, _>(200, move |_event, _ctx| {
662 let was_active = state
663 .with_mut::<Arc<SharedLspManager>, _, bool>(|manager| {
664 manager.with_mut(|m| {
665 let is_active = m.hover_cache.is_active();
666 if is_active {
667 debug!("ModeChanged: clearing active hover");
668 m.hover_cache.clear();
669 true
670 } else {
671 false
672 }
673 })
674 })
675 .unwrap_or(false);
676
677 if was_active {
679 debug!("ModeChanged: returning NeedsRender to dismiss hover");
680 EventResult::NeedsRender
681 } else {
682 EventResult::Handled
683 }
684 });
685 }
686
687 {
689 use reovim_core::option::{OptionChanged, OptionValue};
690
691 let state = Arc::clone(&state);
692 bus.subscribe::<OptionChanged, _>(100, move |event, _ctx| {
693 let name = event.name.as_str();
694
695 if !name.starts_with("virtual_text") {
697 return EventResult::Handled;
698 }
699
700 state.with_mut::<Arc<SharedLspManager>, _, _>(|manager| {
701 manager.with_mut(|m| {
702 let config = &mut m.virtual_text_config;
703 match name {
704 "virtual_text" => {
705 if let OptionValue::Bool(enabled) = &event.new_value {
706 config.enabled = *enabled;
707 debug!("LSP: virtual_text enabled = {}", enabled);
708 }
709 }
710 "virtual_text_prefix" => {
711 if let OptionValue::String(prefix) = &event.new_value {
712 config.prefix.clone_from(prefix);
713 debug!("LSP: virtual_text prefix = {:?}", prefix);
714 }
715 }
716 "virtual_text_max_length" => {
717 if let OptionValue::Integer(len) = &event.new_value {
718 #[allow(
719 clippy::cast_possible_truncation,
720 clippy::cast_sign_loss
721 )]
722 {
723 config.max_length = (*len).clamp(10, 200) as u16;
724 }
725 debug!("LSP: virtual_text max_length = {}", len);
726 }
727 }
728 "virtual_text_show" => {
729 if let OptionValue::String(mode) = &event.new_value {
730 config.show_mode = match mode.as_str() {
731 "highest" => manager::VirtualTextShowMode::Highest,
732 "all" => manager::VirtualTextShowMode::All,
733 _ => manager::VirtualTextShowMode::First,
734 };
735 debug!("LSP: virtual_text show_mode = {:?}", config.show_mode);
736 }
737 }
738 _ => {}
739 }
740 });
741 });
742
743 EventResult::NeedsRender
744 });
745 }
746 }
747
748 #[allow(clippy::too_many_lines)]
749 fn boot(
750 &self,
751 bus: &EventBus,
752 state: Arc<PluginStateRegistry>,
753 event_tx: Option<tokio::sync::mpsc::Sender<reovim_core::event::RuntimeEvent>>,
754 ) {
755 if let Some(tx) = event_tx {
757 state.with_mut::<Arc<SharedLspManager>, _, _>(|manager| {
758 manager.with_mut(|m| {
759 m.set_event_tx(tx);
760 });
761 });
762 }
763
764 let (progress_tx, mut progress_rx) =
766 mpsc::unbounded_channel::<Box<dyn std::any::Any + Send>>();
767
768 let bus_sender = bus.sender();
771 tokio::spawn(async move {
772 use {
773 reovim_lsp::{LspProgressBegin, LspProgressEnd, LspProgressReport},
774 reovim_plugin_notification::{ProgressComplete, ProgressUpdate},
775 };
776
777 while let Some(event) = progress_rx.recv().await {
778 if let Some(begin) = event.downcast_ref::<LspProgressBegin>() {
780 let mut progress =
781 ProgressUpdate::new(begin.id.clone(), begin.title.clone(), "LSP");
782 if let Some(pct) = begin.percentage {
783 progress = progress.with_progress(pct);
784 }
785 if let Some(msg) = &begin.message {
786 progress = progress.with_detail(msg.clone());
787 }
788 bus_sender.try_send(progress);
789 } else if let Some(report) = event.downcast_ref::<LspProgressReport>() {
790 let mut progress =
791 ProgressUpdate::new(report.id.clone(), report.title.clone(), "LSP");
792 if let Some(pct) = report.percentage {
793 progress = progress.with_progress(pct);
794 }
795 if let Some(msg) = &report.message {
796 progress = progress.with_detail(msg.clone());
797 }
798 bus_sender.try_send(progress);
799 } else if let Some(end) = event.downcast_ref::<LspProgressEnd>() {
800 let mut complete = ProgressComplete::new(end.id.clone());
801 if let Some(msg) = &end.message {
802 complete = complete.with_message(msg.clone());
803 }
804 bus_sender.try_send(complete);
805 }
806 }
807 });
808
809 let state_clone = Arc::clone(&state);
811
812 tokio::spawn(async move {
813 let root_path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
815
816 if !rust_analyzer_available() {
818 warn!("LSP: rust-analyzer not found in PATH, LSP disabled");
819 return;
820 }
821
822 info!(root = %root_path.display(), "LSP: starting rust-analyzer");
823
824 let config = ClientConfig::rust_analyzer(&root_path);
825
826 let (render_tx, _render_rx) = mpsc::channel::<()>(1);
828
829 match LspSaturator::start(config, Some(render_tx), Some(progress_tx)).await {
830 Ok((handle, cache)) => {
831 info!("LSP: rust-analyzer started successfully");
832
833 state_clone.with_mut::<Arc<SharedLspManager>, _, _>(|manager| {
835 manager.with_mut(|m| {
836 m.set_connection(handle, cache);
837
838 let buffer_ids: Vec<usize> = m.documents.buffer_ids();
841 info!(
842 count = buffer_ids.len(),
843 ?buffer_ids,
844 "LSP: checking pending documents on server ready"
845 );
846 for buffer_id in buffer_ids {
847 info!(buffer_id, "LSP: processing buffer_id");
848 let Some(doc) = m.documents.get(buffer_id) else {
849 warn!(buffer_id, "LSP: document not found for buffer_id");
850 continue;
851 };
852 info!(
853 buffer_id,
854 opened = doc.opened,
855 has_handle = m.handle.is_some(),
856 "LSP: checking doc state"
857 );
858 if let (false, Some(handle)) = (doc.opened, &m.handle) {
859 let path = doc.path.clone();
861 let uri = doc.uri.clone();
862 let version = doc.version;
863 let language_id = doc.language_id.clone();
864
865 if let Ok(content) = std::fs::read_to_string(&path) {
866 info!(
867 buffer_id,
868 version,
869 uri = %uri.as_str(),
870 "LSP: sending didOpen on server ready"
871 );
872 handle.did_open(uri, language_id, version, content);
873 m.documents.mark_opened(buffer_id);
874 }
875 }
876 }
877 });
878 });
879 }
880 Err(e) => {
881 error!("LSP: failed to start rust-analyzer: {}", e);
882 }
883 }
884 });
885 }
886}
887
888fn rust_analyzer_available() -> bool {
893 if std::env::var("REOVIM_TEST").is_ok() {
895 return false;
896 }
897
898 std::process::Command::new("rust-analyzer")
899 .arg("--version")
900 .stdout(std::process::Stdio::null())
901 .stderr(std::process::Stdio::null())
902 .status()
903 .is_ok_and(|s| s.success())
904}
905
906fn extract_all_locations(response: &GotoDefinitionResponse) -> Vec<Location> {
910 match response {
911 GotoDefinitionResponse::Scalar(loc) => vec![loc.clone()],
912 GotoDefinitionResponse::Array(locs) => locs.clone(),
913 GotoDefinitionResponse::Link(links) => links
914 .iter()
915 .map(|link| Location {
916 uri: link.target_uri.clone(),
917 range: link.target_selection_range,
918 })
919 .collect(),
920 }
921}
922
923fn uri_to_path(uri: &reovim_lsp::Uri) -> Option<PathBuf> {
925 let uri_str = uri.as_str();
926 uri_str.strip_prefix("file://").map(|path_str| {
927 let decoded = path_str.replace("%20", " ");
929 PathBuf::from(decoded)
930 })
931}
932
933fn extract_hover_text(contents: &HoverContents) -> String {
935 match contents {
936 HoverContents::Scalar(marked) => marked_string_to_text(marked),
937 HoverContents::Array(items) => items
938 .iter()
939 .map(marked_string_to_text)
940 .collect::<Vec<_>>()
941 .join("\n\n"),
942 HoverContents::Markup(markup) => markup.value.clone(),
943 }
944}
945
946fn marked_string_to_text(marked: &MarkedString) -> String {
948 match marked {
949 MarkedString::String(s) => s.clone(),
950 MarkedString::LanguageString(ls) => {
951 format!("```{}\n{}\n```", ls.language, ls.value)
953 }
954 }
955}
956
957#[cfg(test)]
958mod tests {
959 use {
960 super::*,
961 reovim_core::{
962 bind::CommandRef,
963 keys,
964 modd::ModeState,
965 plugin::{Plugin, PluginContext},
966 },
967 };
968
969 #[test]
970 fn test_lsp_keybindings_registered() {
971 let plugin = LspPlugin::new();
972 let mut ctx = PluginContext::new();
973
974 plugin.build(&mut ctx);
976
977 let keymap = ctx.keymap();
979 let scope = KeymapScope::editor_normal();
980 let bindings = keymap.get_scope(&scope);
981
982 assert!(
984 bindings.get(&keys!['g' 'd']).is_some(),
985 "gd keybinding should be registered. Bindings in scope: {:?}",
986 bindings.keys().collect::<Vec<_>>()
987 );
988 assert!(bindings.get(&keys!['g' 'r']).is_some(), "gr keybinding should be registered");
989 assert!(bindings.get(&keys!['K']).is_some(), "K keybinding should be registered");
990 }
991
992 #[test]
993 fn test_lsp_keybindings_lookup_with_mode() {
994 let plugin = LspPlugin::new();
995 let mut ctx = PluginContext::new();
996
997 plugin.build(&mut ctx);
999
1000 let keymap = ctx.keymap();
1002
1003 let mode = ModeState::new();
1005
1006 let goto_def_binding = keymap.lookup_binding(&mode, &keys!['g' 'd']);
1008 assert!(
1009 goto_def_binding.is_some(),
1010 "lookup_binding should find gd. mode.interactor_id={:?}, mode.edit_mode={:?}",
1011 mode.interactor_id,
1012 mode.edit_mode
1013 );
1014 assert!(goto_def_binding.unwrap().command.is_some(), "gd binding should have a command");
1015
1016 let hover_binding = keymap.lookup_binding(&mode, &keys!['K']);
1018 assert!(hover_binding.is_some(), "lookup_binding should find K");
1019 assert!(hover_binding.unwrap().command.is_some(), "K binding should have a command");
1020 }
1021
1022 #[test]
1023 fn test_lsp_commands_registered_and_resolvable() {
1024 let plugin = LspPlugin::new();
1025 let mut ctx = PluginContext::new();
1026
1027 plugin.build(&mut ctx);
1029
1030 let cmd_registry = ctx.command_registry();
1032
1033 let goto_def_cmd = cmd_registry.get(&command_id::GOTO_DEFINITION);
1035 assert!(
1036 goto_def_cmd.is_some(),
1037 "lsp_goto_definition command should be registered. ID = {:?}",
1038 command_id::GOTO_DEFINITION
1039 );
1040 assert_eq!(goto_def_cmd.unwrap().name(), "lsp_goto_definition");
1041
1042 let show_hover_cmd = cmd_registry.get(&command_id::SHOW_HOVER);
1044 assert!(show_hover_cmd.is_some(), "lsp_show_hover command should be registered");
1045 assert_eq!(show_hover_cmd.unwrap().name(), "lsp_show_hover");
1046
1047 let keymap = ctx.keymap();
1049 let mode = ModeState::new();
1050 let binding = keymap.lookup_binding(&mode, &keys!['g' 'd']).unwrap();
1051
1052 match binding.command.as_ref().unwrap() {
1053 CommandRef::Registered(id) => {
1054 assert_eq!(
1055 id.as_str(),
1056 "lsp_goto_definition",
1057 "gd keybinding should point to lsp_goto_definition"
1058 );
1059 assert!(
1061 cmd_registry.get(id).is_some(),
1062 "Command {id} should be resolvable from registry"
1063 );
1064 }
1065 CommandRef::Inline(_) => panic!("gd keybinding should be a Registered command ref"),
1066 }
1067 }
1068}