1use anyhow::Result as AnyhowResult;
14use rust_i18n::t;
15use std::io;
16use std::time::{Duration, Instant};
17
18use crate::model::event::{BufferId, Event};
19use crate::primitives::word_navigation::{find_word_end, find_word_start};
20use crate::view::prompt::{Prompt, PromptType};
21
22use crate::services::lsp::async_handler::LspHandle;
23use crate::types::LspFeature;
24
25use super::{Editor, SemanticTokenRangeRequest};
26
27fn space_doc_paragraphs(text: &str) -> String {
34 text.replace("\n\n", "\x00").replace(['\n', '\x00'], "\n\n")
35}
36
37fn lsp_range_contains(range: &lsp_types::Range, line: u32, character: u32) -> bool {
42 let start = range.start;
43 let end = range.end;
44 if line < start.line || (line == start.line && character < start.character) {
46 return false;
47 }
48 if start.line == end.line && start.character == end.character {
50 return line == start.line && character == start.character;
51 }
52 if line > end.line || (line == end.line && character >= end.character) {
54 return false;
55 }
56 true
57}
58
59fn lsp_range_overlaps(
65 a: &lsp_types::Range,
66 b_start_line: u32,
67 b_start_char: u32,
68 b_end_line: u32,
69 b_end_char: u32,
70) -> bool {
71 let b_is_point = b_start_line == b_end_line && b_start_char == b_end_char;
72 if b_is_point {
73 return lsp_range_contains(a, b_start_line, b_start_char);
74 }
75 let a_is_point = a.start.line == a.end.line && a.start.character == a.end.character;
76 if a_is_point {
77 let (p_line, p_char) = (a.start.line, a.start.character);
78 if p_line < b_start_line || (p_line == b_start_line && p_char < b_start_char) {
79 return false;
80 }
81 if p_line > b_end_line || (p_line == b_end_line && p_char >= b_end_char) {
82 return false;
83 }
84 return true;
85 }
86 if a.end.line < b_start_line || (a.end.line == b_start_line && a.end.character <= b_start_char)
88 {
89 return false;
90 }
91 if a.start.line > b_end_line || (a.start.line == b_end_line && a.start.character >= b_end_char)
93 {
94 return false;
95 }
96 true
97}
98
99const SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS: u64 = 50;
100const SEMANTIC_TOKENS_RANGE_PADDING_LINES: usize = 10;
101
102impl Editor {
103 pub(crate) fn handle_completion_response(
107 &mut self,
108 request_id: u64,
109 items: Vec<lsp_types::CompletionItem>,
110 ) -> AnyhowResult<()> {
111 if !self
113 .active_window_mut()
114 .pending_completion_requests
115 .remove(&request_id)
116 {
117 tracing::debug!(
118 "Ignoring completion response for outdated request {}",
119 request_id
120 );
121 return Ok(());
122 }
123
124 if items.is_empty() {
125 tracing::debug!("No completion items received");
126 if self.active_window().pending_completion_requests.is_empty()
127 && self.active_window().completion_items.is_none()
128 {
129 self.show_buffer_word_completion_popup();
132 }
133 return Ok(());
134 }
135
136 use crate::primitives::word_navigation::find_completion_word_start;
138 let cursor_pos = self.active_cursors().primary().position;
139 let (word_start, cursor_pos) = {
140 let state = self.active_state();
141 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
142 (word_start, cursor_pos)
143 };
144 let prefix = if word_start < cursor_pos {
145 self.active_state_mut()
146 .get_text_range(word_start, cursor_pos)
147 .to_lowercase()
148 } else {
149 String::new()
150 };
151
152 let matches_prefix = |item: &lsp_types::CompletionItem| -> bool {
153 prefix.is_empty()
154 || item.label.to_lowercase().starts_with(&prefix)
155 || item
156 .filter_text
157 .as_ref()
158 .map(|ft| ft.to_lowercase().starts_with(&prefix))
159 .unwrap_or(false)
160 };
161
162 let filtered_items: Vec<&lsp_types::CompletionItem> =
163 items.iter().filter(|item| matches_prefix(item)).collect();
164
165 if filtered_items.is_empty() && self.active_window().completion_items.is_none() {
166 tracing::debug!("No completion items match prefix '{}'", prefix);
167 return Ok(());
168 }
169
170 match &mut self.active_window_mut().completion_items {
172 Some(existing) => {
173 existing.extend(items);
174 tracing::debug!("Extended completion items, now {} total", existing.len());
175 }
176 None => {
177 self.active_window_mut().completion_items = Some(items);
178 }
179 }
180
181 let all_items = self.active_window_mut().completion_items.as_ref().unwrap();
183 let all_filtered: Vec<&lsp_types::CompletionItem> = all_items
184 .iter()
185 .filter(|item| matches_prefix(item))
186 .collect();
187
188 if all_filtered.is_empty() {
189 tracing::debug!("No completion items match prefix '{}'", prefix);
190 return Ok(());
191 }
192
193 let mut all_popup_items =
195 crate::app::popup_actions::lsp_items_to_popup_items(&all_filtered);
196 let buffer_word_items = self.get_buffer_completion_popup_items();
197 let lsp_labels: std::collections::HashSet<String> = all_popup_items
199 .iter()
200 .map(|i| i.text.to_lowercase())
201 .collect();
202 all_popup_items.extend(
203 buffer_word_items
204 .into_iter()
205 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
206 );
207
208 let popup_data =
209 crate::app::popup_actions::build_completion_popup_from_items(all_popup_items, 0);
210 let accept_hint = self.completion_accept_key_hint();
211 let focus_hint = self.popup_focus_key_hint();
212
213 {
214 let buffer_id = self.active_buffer();
215 let state = self
216 .windows
217 .get_mut(&self.active_window)
218 .map(|w| &mut w.buffers)
219 .expect("active window present")
220 .get_mut(&buffer_id)
221 .unwrap();
222 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
224 popup_obj.accept_key_hint = accept_hint;
225 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
226 popup_obj.focus_key_hint = focus_hint;
227 state.popups.show_or_replace(popup_obj);
228 }
229
230 tracing::info!(
231 "Showing completion popup with {} items",
232 self.active_window_mut()
233 .completion_items
234 .as_ref()
235 .map_or(0, |i| i.len())
236 );
237
238 Ok(())
239 }
240
241 pub(crate) fn handle_goto_definition_response(
243 &mut self,
244 request_id: u64,
245 locations: Vec<lsp_types::Location>,
246 ) -> AnyhowResult<()> {
247 if self.active_window_mut().pending_goto_definition_request != Some(request_id) {
249 tracing::debug!(
250 "Ignoring go-to-definition response for outdated request {}",
251 request_id
252 );
253 return Ok(());
254 }
255
256 self.active_window_mut().pending_goto_definition_request = None;
257
258 if locations.is_empty() {
259 self.active_window_mut().status_message = Some(t!("lsp.no_definition").to_string());
260 return Ok(());
261 }
262
263 let location = &locations[0];
265
266 let wire = crate::app::types::LspUri::from_wire(location.uri.clone());
273 let buffer_id = match self.open_lsp_uri_target(&wire) {
274 Ok(id) => id,
275 Err(e) => {
276 if let Some(confirmation) =
277 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
278 {
279 self.start_large_file_encoding_confirmation(confirmation);
280 } else {
281 self.set_status_message(
282 t!("file.error_opening", error = e.to_string()).to_string(),
283 );
284 }
285 return Ok(());
286 }
287 };
288
289 let line = location.range.start.line as usize;
295 let character = location.range.start.character as usize;
296 let position = self
297 .buffers()
298 .get(&buffer_id)
299 .map(|state| state.buffer.line_col_to_position(line, character));
300
301 if let Some(position) = position {
302 let (cursor_id, old_position, old_anchor, old_sticky_column) = {
303 let cursors = self.active_cursors();
304 let primary = cursors.primary();
305 (
306 cursors.primary_id(),
307 primary.position,
308 primary.anchor,
309 primary.sticky_column,
310 )
311 };
312 let event = crate::model::event::Event::MoveCursor {
313 cursor_id,
314 old_position,
315 new_position: position,
316 old_anchor,
317 new_anchor: None,
318 old_sticky_column,
319 new_sticky_column: 0,
320 };
321
322 let split_id = self
323 .windows
324 .get(&self.active_window)
325 .and_then(|w| w.buffers.splits())
326 .map(|(mgr, _)| mgr)
327 .expect("active window must have a populated split layout")
328 .active_split();
329 self.active_window_mut()
330 .apply_event_to_buffer(buffer_id, split_id, &event);
331 self.active_window_mut()
335 .ensure_active_cursor_visible_for_navigation(true);
336 }
337
338 let display_path = self
339 .buffers()
340 .get(&buffer_id)
341 .and_then(|s| s.buffer.file_path().map(|p| p.display().to_string()))
342 .unwrap_or_default();
343 self.active_window_mut().status_message = Some(
344 t!(
345 "lsp.jumped_to_definition",
346 path = display_path,
347 line = line + 1
348 )
349 .to_string(),
350 );
351
352 Ok(())
353 }
354
355 pub(crate) fn with_lsp_for_buffer<F, R>(
360 &mut self,
361 buffer_id: BufferId,
362 feature: LspFeature,
363 f: F,
364 ) -> Option<R>
365 where
366 F: FnOnce(&LspHandle, &crate::app::types::LspUri, &str) -> R,
367 {
368 use crate::services::lsp::manager::LspSpawnResult;
369
370 let (uri, language, file_path) = {
371 let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
372 if !metadata.lsp_enabled {
373 return None;
374 }
375 let uri = metadata.file_uri()?.clone();
376 let file_path = metadata.file_path().cloned();
377 let language = self
378 .windows
379 .get(&self.active_window)
380 .map(|w| &w.buffers)
381 .expect("active window present")
382 .get(&buffer_id)?
383 .language
384 .clone();
385 (uri, language, file_path)
386 };
387
388 let lsp = self.lsp_mut()?;
389 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
390 return None;
391 }
392
393 self.ensure_did_open_all(buffer_id, &uri, &language)?;
395
396 let lsp = self.lsp_mut()?;
398 let sh = lsp.handle_for_feature_mut(&language, feature)?;
399 Some(f(&sh.handle, &uri, &language))
400 }
401
402 pub(crate) fn with_all_lsp_for_buffer_feature<F, R>(
408 &mut self,
409 buffer_id: BufferId,
410 feature: LspFeature,
411 f: F,
412 ) -> Vec<R>
413 where
414 F: Fn(&LspHandle, &crate::app::types::LspUri, &str) -> R,
415 {
416 use crate::services::lsp::manager::LspSpawnResult;
417
418 let (uri, language, file_path) = match (|| {
419 let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
420 if !metadata.lsp_enabled {
421 return None;
422 }
423 let uri = metadata.file_uri()?.clone();
424 let file_path = metadata.file_path().cloned();
425 let language = self
426 .windows
427 .get(&self.active_window)
428 .map(|w| &w.buffers)
429 .expect("active window present")
430 .get(&buffer_id)?
431 .language
432 .clone();
433 Some((uri, language, file_path))
434 })() {
435 Some(v) => v,
436 None => return Vec::new(),
437 };
438
439 let lsp = match self.lsp_mut() {
440 Some(l) => l,
441 None => return Vec::new(),
442 };
443 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
444 return Vec::new();
445 }
446
447 if self
449 .ensure_did_open_all(buffer_id, &uri, &language)
450 .is_none()
451 {
452 return Vec::new();
453 }
454
455 let lsp = match self.lsp_mut() {
457 Some(l) => l,
458 None => return Vec::new(),
459 };
460 lsp.handles_for_feature_mut(&language, feature)
461 .into_iter()
462 .map(|sh| f(&sh.handle, &uri, &language))
463 .collect()
464 }
465
466 pub(crate) fn with_all_lsp_for_buffer_feature_named<F, R>(
469 &mut self,
470 buffer_id: BufferId,
471 feature: LspFeature,
472 f: F,
473 ) -> Vec<R>
474 where
475 F: Fn(&LspHandle, &crate::app::types::LspUri, &str, &str) -> R,
476 {
477 use crate::services::lsp::manager::LspSpawnResult;
478
479 let (uri, language, file_path) = match (|| {
480 let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
481 if !metadata.lsp_enabled {
482 return None;
483 }
484 let uri = metadata.file_uri()?.clone();
485 let file_path = metadata.file_path().cloned();
486 let language = self
487 .windows
488 .get(&self.active_window)
489 .map(|w| &w.buffers)
490 .expect("active window present")
491 .get(&buffer_id)?
492 .language
493 .clone();
494 Some((uri, language, file_path))
495 })() {
496 Some(v) => v,
497 None => return Vec::new(),
498 };
499
500 let lsp = match self.lsp_mut() {
501 Some(l) => l,
502 None => return Vec::new(),
503 };
504 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
505 return Vec::new();
506 }
507
508 if self
509 .ensure_did_open_all(buffer_id, &uri, &language)
510 .is_none()
511 {
512 return Vec::new();
513 }
514
515 let lsp = match self.lsp_mut() {
516 Some(l) => l,
517 None => return Vec::new(),
518 };
519 lsp.handles_for_feature_mut(&language, feature)
520 .into_iter()
521 .map(|sh| f(&sh.handle, &uri, &language, &sh.name))
522 .collect()
523 }
524
525 fn ensure_did_open_all(
528 &mut self,
529 buffer_id: BufferId,
530 uri: &crate::app::types::LspUri,
531 language: &str,
532 ) -> Option<()> {
533 let lsp = self.lsp_mut()?;
534 let handle_ids: Vec<u64> = lsp
535 .get_handles(language)
536 .iter()
537 .map(|sh| sh.handle.id())
538 .collect();
539
540 let needs_open: Vec<u64> = {
541 let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
542 handle_ids
543 .iter()
544 .filter(|id| !metadata.lsp_opened_with.contains(id))
545 .copied()
546 .collect()
547 };
548
549 if !needs_open.is_empty() {
550 let text = self
551 .windows
552 .get(&self.active_window)
553 .map(|w| &w.buffers)
554 .expect("active window present")
555 .get(&buffer_id)?
556 .buffer
557 .to_string()?;
558 let active_id = self.active_window;
559 let __win = self.windows.get_mut(&active_id)?;
560 let lsp = &mut __win.lsp;
561 for sh in lsp.get_handles_mut(language) {
562 if needs_open.contains(&sh.handle.id()) {
563 if let Err(e) =
564 sh.handle
565 .did_open(uri.as_uri().clone(), text.clone(), language.to_string())
566 {
567 tracing::warn!("Failed to send didOpen to '{}': {}", sh.name, e);
568 continue;
569 }
570 let metadata = __win.buffer_metadata.get_mut(&buffer_id)?;
571 metadata.lsp_opened_with.insert(sh.handle.id());
572 tracing::debug!(
573 "Sent didOpen for {} to LSP handle '{}' (language: {})",
574 uri.as_str(),
575 sh.name,
576 language
577 );
578 }
579 }
580 }
581
582 Some(())
583 }
584
585 pub(crate) fn request_completion(&mut self) {
588 if !self.active_window().pending_completion_requests.is_empty() {
602 let ids: Vec<u64> = self
603 .active_window_mut()
604 .pending_completion_requests
605 .drain()
606 .collect();
607 for request_id in ids {
608 tracing::debug!(
609 "Canceling previous pending LSP completion request {}",
610 request_id
611 );
612 self.active_window_mut().send_lsp_cancel_request(request_id);
613 }
614 }
615 self.active_window_mut().completion_items = None;
616
617 let cursor_pos = self.active_cursors().primary().position;
619 let state = self.active_state();
620
621 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
623 let buffer_id = self.active_buffer();
624
625 let base_request_id = self.active_window_mut().next_lsp_request_id;
627 let counter = std::sync::atomic::AtomicU64::new(0);
629
630 let results = self.with_all_lsp_for_buffer_feature(
631 buffer_id,
632 LspFeature::Completion,
633 |handle, uri, _language| {
634 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
635 let request_id = base_request_id + idx;
636 let result = handle.completion(
637 request_id,
638 uri.as_uri().clone(),
639 line as u32,
640 character as u32,
641 );
642 if result.is_ok() {
643 tracing::info!(
644 "Requested completion at {}:{}:{} (request_id={})",
645 uri.as_str(),
646 line,
647 character,
648 request_id
649 );
650 }
651 (request_id, result.is_ok())
652 },
653 );
654
655 let mut sent_ids = Vec::new();
656 for (request_id, ok) in &results {
657 if *ok {
658 sent_ids.push(*request_id);
659 }
660 }
661 self.active_window_mut().next_lsp_request_id = base_request_id + results.len() as u64;
663
664 if !sent_ids.is_empty() {
665 self.active_window_mut()
666 .pending_completion_requests
667 .extend(sent_ids);
668 } else {
669 self.show_buffer_word_completion_popup();
671 }
672 }
673
674 fn show_buffer_word_completion_popup(&mut self) {
678 let items = self.get_buffer_completion_popup_items();
679 if items.is_empty() {
680 return;
681 }
682
683 let popup_data = crate::app::popup_actions::build_completion_popup_from_items(items, 0);
684 let accept_hint = self.completion_accept_key_hint();
685 let focus_hint = self.popup_focus_key_hint();
686
687 let buffer_id = self.active_buffer();
688 let state = self
689 .windows
690 .get_mut(&self.active_window)
691 .map(|w| &mut w.buffers)
692 .expect("active window present")
693 .get_mut(&buffer_id)
694 .unwrap();
695 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
696 popup_obj.accept_key_hint = accept_hint;
697 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
698 popup_obj.focus_key_hint = focus_hint;
699 state.popups.show_or_replace(popup_obj);
700 }
701
702 pub(crate) fn maybe_trigger_completion(&mut self, c: char) {
712 if !self.config.editor.completion_popup_auto_show {
714 return;
715 }
716
717 let language = self.active_state().language.clone();
719
720 let is_lsp_trigger = self
722 .lsp()
723 .as_ref()
724 .map(|lsp| lsp.is_completion_trigger_char(c, &language))
725 .unwrap_or(false);
726
727 let quick_suggestions_enabled = self.config.editor.quick_suggestions;
729 let suggest_on_trigger_chars = self.config.editor.suggest_on_trigger_characters;
730 let is_word_char = c.is_alphanumeric() || c == '_';
731
732 if is_lsp_trigger && suggest_on_trigger_chars {
734 tracing::debug!(
735 "Trigger character '{}' immediately triggers completion for language {}",
736 c,
737 language
738 );
739 self.active_window_mut().scheduled_completion_trigger = None;
741 self.request_completion();
742 return;
743 }
744
745 if quick_suggestions_enabled && is_word_char {
747 let delay_ms = self.config.editor.quick_suggestions_delay_ms;
748 let trigger_time = Instant::now() + Duration::from_millis(delay_ms);
749
750 tracing::debug!(
751 "Scheduling completion trigger in {}ms for language {} (char '{}')",
752 delay_ms,
753 language,
754 c
755 );
756
757 self.active_window_mut().scheduled_completion_trigger = Some(trigger_time);
760 } else {
761 self.active_window_mut().scheduled_completion_trigger = None;
765 }
766 }
767
768 pub(crate) fn request_goto_definition(&mut self) -> AnyhowResult<()> {
770 let cursor_pos = self.active_cursors().primary().position;
772 let state = self.active_state();
773
774 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
776 let buffer_id = self.active_buffer();
777 let request_id = self.active_window_mut().next_lsp_request_id;
778
779 let sent = self
781 .with_lsp_for_buffer(
782 buffer_id,
783 LspFeature::Definition,
784 |handle, uri, _language| {
785 let result = handle.goto_definition(
786 request_id,
787 uri.as_uri().clone(),
788 line as u32,
789 character as u32,
790 );
791 if result.is_ok() {
792 tracing::info!(
793 "Requested go-to-definition at {}:{}:{}",
794 uri.as_str(),
795 line,
796 character
797 );
798 }
799 result.is_ok()
800 },
801 )
802 .unwrap_or(false);
803
804 if sent {
805 self.active_window_mut().next_lsp_request_id += 1;
806 self.active_window_mut().pending_goto_definition_request = Some(request_id);
807 }
808
809 Ok(())
810 }
811
812 pub fn request_hover(&mut self) -> AnyhowResult<()> {
814 let cursor_pos = self.active_cursors().primary().position;
816 let state = self.active_state();
817
818 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
820
821 if let Some(pos) = state.buffer.offset_to_position(cursor_pos) {
823 tracing::debug!(
824 "Hover request: cursor_byte={}, line={}, byte_col={}, utf16_col={}",
825 cursor_pos,
826 pos.line,
827 pos.column,
828 character
829 );
830 }
831
832 let buffer_id = self.active_buffer();
833 let request_id = self.active_window_mut().next_lsp_request_id;
834
835 let sent = self
837 .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
838 let result = handle.hover(
839 request_id,
840 uri.as_uri().clone(),
841 line as u32,
842 character as u32,
843 );
844 if result.is_ok() {
845 tracing::info!(
846 "Requested hover at {}:{}:{} (byte_pos={})",
847 uri.as_str(),
848 line,
849 character,
850 cursor_pos
851 );
852 }
853 result.is_ok()
854 })
855 .unwrap_or(false);
856
857 if sent {
858 self.active_window_mut().next_lsp_request_id += 1;
859 self.active_window_mut().hover.record_request(
860 request_id,
861 line as u32,
862 character as u32,
863 );
864 }
865
866 Ok(())
867 }
868
869 pub(crate) fn request_hover_at_position(&mut self, byte_pos: usize) -> AnyhowResult<bool> {
874 let state = self.active_state();
876
877 let (line, character) = state.buffer.position_to_lsp_position(byte_pos);
879
880 if let Some(pos) = state.buffer.offset_to_position(byte_pos) {
882 tracing::trace!(
883 "Mouse hover request: byte_pos={}, line={}, byte_col={}, utf16_col={}",
884 byte_pos,
885 pos.line,
886 pos.column,
887 character
888 );
889 }
890
891 let buffer_id = self.active_buffer();
892 let request_id = self.active_window_mut().next_lsp_request_id;
893
894 let sent = self
896 .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
897 let result = handle.hover(
898 request_id,
899 uri.as_uri().clone(),
900 line as u32,
901 character as u32,
902 );
903 if result.is_ok() {
904 tracing::trace!(
905 "Mouse hover requested at {}:{}:{} (byte_pos={})",
906 uri.as_str(),
907 line,
908 character,
909 byte_pos
910 );
911 }
912 result.is_ok()
913 })
914 .unwrap_or(false);
915
916 if sent {
917 self.active_window_mut().next_lsp_request_id += 1;
918 self.active_window_mut().hover.record_request(
919 request_id,
920 line as u32,
921 character as u32,
922 );
923 }
924
925 Ok(sent)
926 }
927
928 pub(crate) fn handle_hover_response(
930 &mut self,
931 request_id: u64,
932 contents: String,
933 is_markdown: bool,
934 range: Option<((u32, u32), (u32, u32))>,
935 ) {
936 let Some(position) = self.active_window_mut().hover.claim_pending(request_id) else {
940 tracing::debug!("Ignoring stale hover response: {}", request_id);
941 return;
942 };
943
944 if self.is_lsp_status_popup_open() {
947 tracing::debug!("Suppressing hover response: LSP status popup is open");
948 self.active_window_mut().hover.set_symbol_range(None);
949 return;
950 }
951 let hover_lsp_position = Some(position);
952
953 let diagnostic_lines = hover_lsp_position
958 .map(|pos| self.compose_hover_diagnostic_lines(pos))
959 .unwrap_or_default();
960
961 if contents.is_empty() && diagnostic_lines.is_empty() {
962 self.set_status_message(t!("lsp.no_hover").to_string());
963 self.active_window_mut().hover.set_symbol_range(None);
964 return;
965 }
966
967 tracing::debug!(
969 "LSP hover content (markdown={}):\n{}",
970 is_markdown,
971 contents
972 );
973
974 if let Some(((start_line, start_char), (end_line, end_char))) = range {
976 let state = self.active_state();
977 let start_byte = state
978 .buffer
979 .lsp_position_to_byte(start_line as usize, start_char as usize);
980 let end_byte = state
981 .buffer
982 .lsp_position_to_byte(end_line as usize, end_char as usize);
983 self.active_window_mut()
984 .hover
985 .set_symbol_range(Some((start_byte, end_byte)));
986 tracing::debug!(
987 "Hover symbol range: {}..{} (LSP {}:{}..{}:{})",
988 start_byte,
989 end_byte,
990 start_line,
991 start_char,
992 end_line,
993 end_char
994 );
995
996 if let Some(old_handle) = self.active_window_mut().hover.take_symbol_overlay() {
998 let remove_event = crate::model::event::Event::RemoveOverlay { handle: old_handle };
999 self.apply_event_to_active_buffer(&remove_event);
1000 }
1001
1002 let event = crate::model::event::Event::AddOverlay {
1004 namespace: None,
1005 range: start_byte..end_byte,
1006 face: crate::model::event::OverlayFace::Background {
1007 color: (80, 80, 120), },
1009 priority: 90, message: None,
1011 extend_to_line_end: false,
1012 url: None,
1013 };
1014 self.apply_event_to_active_buffer(&event);
1015 if let Some(state) = self
1017 .windows
1018 .get(&self.active_window)
1019 .map(|w| &w.buffers)
1020 .expect("active window present")
1021 .get(&self.active_buffer())
1022 {
1023 if let Some(handle) = state.overlays.all().last().map(|o| o.handle.clone()) {
1024 self.active_window_mut().hover.set_symbol_overlay(handle);
1025 }
1026 }
1027 } else {
1028 let computed_range = if let Some((hover_byte_pos, _, _, _)) =
1031 self.active_window_mut().mouse_state.lsp_hover_state
1032 {
1033 let state = self.active_state();
1034 let start_byte = find_word_start(&state.buffer, hover_byte_pos);
1035 let end_byte = find_word_end(&state.buffer, hover_byte_pos);
1036 if start_byte < end_byte {
1037 tracing::debug!(
1038 "Hover symbol range (computed from word boundaries): {}..{}",
1039 start_byte,
1040 end_byte
1041 );
1042 Some((start_byte, end_byte))
1043 } else {
1044 None
1045 }
1046 } else {
1047 None
1048 };
1049 self.active_window_mut()
1050 .hover
1051 .set_symbol_range(computed_range);
1052 }
1053
1054 use crate::view::markdown::{parse_markdown, StyledLine};
1065 use crate::view::popup::{Popup, PopupContent, PopupPosition};
1066 use ratatui::style::Style;
1067 use unicode_width::UnicodeWidthStr;
1068
1069 let hover_lines: Vec<StyledLine> = if contents.is_empty() {
1070 Vec::new()
1071 } else if is_markdown {
1072 parse_markdown(
1073 &contents,
1074 &*self.theme.read().unwrap(),
1075 Some(&self.grammar_registry),
1076 )
1077 } else {
1078 contents
1079 .lines()
1080 .map(|s| {
1081 let mut sl = StyledLine::new();
1082 sl.push(
1083 s.to_string(),
1084 Style::default().fg(self.theme.read().unwrap().popup_text_fg),
1085 );
1086 sl
1087 })
1088 .collect()
1089 };
1090
1091 let has_diagnostic = !diagnostic_lines.is_empty();
1092 let mut all_lines: Vec<StyledLine> = Vec::new();
1093 all_lines.extend(diagnostic_lines);
1094 if has_diagnostic && !hover_lines.is_empty() {
1095 let mut sep = StyledLine::new();
1099 sep.push(
1100 "─".repeat(12),
1101 Style::default().fg(self.theme.read().unwrap().popup_border_fg),
1102 );
1103 all_lines.push(sep);
1104 }
1105 all_lines.extend(hover_lines);
1106
1107 while all_lines
1109 .last()
1110 .map(|l| l.spans.iter().all(|s| s.text.trim().is_empty()))
1111 .unwrap_or(false)
1112 {
1113 all_lines.pop();
1114 }
1115
1116 let content_width: usize = all_lines
1121 .iter()
1122 .map(|l| {
1123 l.spans
1124 .iter()
1125 .map(|s| UnicodeWidthStr::width(s.text.as_str()))
1126 .sum::<usize>()
1127 })
1128 .max()
1129 .unwrap_or(0);
1130 let popup_width = (content_width as u16 + 4).clamp(30, 80);
1131 let dynamic_height = (self.terminal_height * 60 / 100).clamp(15, 40);
1132
1133 let mut popup = Popup::text(Vec::new(), &*self.theme.read().unwrap());
1135 popup.content = PopupContent::Markdown(all_lines);
1136 popup.title = Some(t!("lsp.popup_hover").to_string());
1137 popup.transient = true;
1138 popup.position = if let Some((x, y)) = self.active_window_mut().hover.take_screen_position()
1139 {
1140 PopupPosition::Fixed { x, y: y + 1 }
1141 } else {
1142 PopupPosition::BelowCursor
1143 };
1144 popup.width = popup_width;
1145 popup.max_height = dynamic_height;
1146 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1147 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1148 popup.focus_key_hint = self.popup_focus_key_hint();
1149
1150 let __buffer_id = self.active_buffer();
1154 if let Some(state) = self
1155 .windows
1156 .get_mut(&self.active_window)
1157 .map(|w| &mut w.buffers)
1158 .expect("active window present")
1159 .get_mut(&__buffer_id)
1160 {
1161 while state.popups.top().is_some_and(|p| p.transient) {
1162 state.popups.hide();
1163 }
1164 state.popups.show(popup);
1165 tracing::info!("Showing hover popup (markdown={})", is_markdown);
1166 }
1167
1168 self.active_window_mut().mouse_state.lsp_hover_request_sent = true;
1171 }
1172
1173 fn compose_hover_diagnostic_lines(
1184 &self,
1185 lsp_pos: (u32, u32),
1186 ) -> Vec<crate::view::markdown::StyledLine> {
1187 use crate::view::markdown::StyledLine;
1188 use lsp_types::DiagnosticSeverity;
1189 use ratatui::style::{Modifier, Style};
1190
1191 let buffer_id = self.active_buffer();
1192 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
1193 return Vec::new();
1194 };
1195 let Some(uri) = metadata.file_uri() else {
1196 return Vec::new();
1197 };
1198 let Some(diagnostics) = self.get_stored_diagnostics().get(uri.as_str()) else {
1199 return Vec::new();
1200 };
1201
1202 let (hover_line, hover_char) = lsp_pos;
1203 let overlapping: Vec<&lsp_types::Diagnostic> = diagnostics
1204 .iter()
1205 .filter(|d| lsp_range_contains(&d.range, hover_line, hover_char))
1206 .collect();
1207
1208 if overlapping.is_empty() {
1209 return Vec::new();
1210 }
1211
1212 let mut out: Vec<StyledLine> = Vec::new();
1213 for (idx, diag) in overlapping.iter().enumerate() {
1214 if idx > 0 {
1215 out.push(StyledLine::new());
1216 }
1217
1218 let (label, marker, severity_color) = match diag.severity {
1219 Some(DiagnosticSeverity::ERROR) => {
1220 ("Error", "✖", self.theme.read().unwrap().diagnostic_error_fg)
1221 }
1222 Some(DiagnosticSeverity::WARNING) => (
1223 "Warning",
1224 "⚠",
1225 self.theme.read().unwrap().diagnostic_warning_fg,
1226 ),
1227 Some(DiagnosticSeverity::INFORMATION) => {
1228 ("Info", "ℹ", self.theme.read().unwrap().diagnostic_info_fg)
1229 }
1230 Some(DiagnosticSeverity::HINT) => {
1231 ("Hint", "ℹ", self.theme.read().unwrap().diagnostic_hint_fg)
1232 }
1233 _ => ("Diagnostic", "•", self.theme.read().unwrap().popup_text_fg),
1234 };
1235
1236 let header_style = Style::default()
1237 .fg(severity_color)
1238 .add_modifier(Modifier::BOLD);
1239 let mut header = StyledLine::new();
1240 header.push(format!("{} {}", marker, label), header_style);
1241 if let Some(source) = diag.source.as_deref().filter(|s| !s.is_empty()) {
1242 header.push(
1245 format!(" ({})", source),
1246 Style::default()
1247 .fg(self.theme.read().unwrap().tab_inactive_fg)
1248 .add_modifier(Modifier::ITALIC),
1249 );
1250 }
1251 out.push(header);
1252
1253 for message_line in diag.message.lines() {
1257 let mut line = StyledLine::new();
1258 line.push(
1259 message_line.to_string(),
1260 Style::default().fg(self.theme.read().unwrap().popup_text_fg),
1261 );
1262 out.push(line);
1263 }
1264 }
1265 out
1266 }
1267
1268 #[doc(hidden)]
1270 pub fn apply_inlay_hints_to_state(
1271 state: &mut crate::state::EditorState,
1272 hints: &[lsp_types::InlayHint],
1273 ) {
1274 use crate::view::virtual_text::VirtualTextPosition;
1275 use ratatui::style::{Color, Style};
1276
1277 state.virtual_texts.clear(&mut state.marker_list);
1279
1280 if hints.is_empty() {
1281 return;
1282 }
1283
1284 let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
1290 let hint_fg_theme_key = Some("editor.line_number_fg".to_string());
1291
1292 for hint in hints {
1293 let byte_offset = state.buffer.lsp_position_to_byte(
1295 hint.position.line as usize,
1296 hint.position.character as usize,
1297 );
1298
1299 let text = match &hint.label {
1301 lsp_types::InlayHintLabel::String(s) => s.clone(),
1302 lsp_types::InlayHintLabel::LabelParts(parts) => {
1303 parts.iter().map(|p| p.value.as_str()).collect::<String>()
1304 }
1305 };
1306
1307 if state.buffer.is_empty() {
1313 continue;
1314 }
1315
1316 let buf_len = state.buffer.len();
1325 let byte_here = if byte_offset < buf_len {
1326 state
1327 .buffer
1328 .slice_bytes(byte_offset..byte_offset + 1)
1329 .first()
1330 .copied()
1331 } else {
1332 None
1333 };
1334 let at_line_break = matches!(byte_here, Some(b'\n' | b'\r'));
1335
1336 let (byte_offset, position) = if byte_offset >= buf_len {
1337 (buf_len.saturating_sub(1), VirtualTextPosition::AfterChar)
1340 } else if at_line_break && byte_offset > 0 {
1341 (byte_offset - 1, VirtualTextPosition::AfterChar)
1345 } else {
1346 (byte_offset, VirtualTextPosition::BeforeChar)
1347 };
1348
1349 let display_text = text;
1351
1352 state.virtual_texts.add_with_theme_keys(
1353 &mut state.marker_list,
1354 byte_offset,
1355 display_text,
1356 hint_style,
1357 hint_fg_theme_key.clone(),
1358 None,
1359 position,
1360 0, );
1362 }
1363
1364 tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
1365 }
1366
1367 pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
1369 use crate::primitives::word_navigation::{find_word_end, find_word_start};
1370
1371 let cursor_pos = self.active_cursors().primary().position;
1372 let (line, character, symbol) = {
1373 let state = self.active_state();
1374 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1375 let word_start = find_word_start(&state.buffer, cursor_pos);
1376 let word_end = find_word_end(&state.buffer, cursor_pos);
1377 let symbol = String::from_utf8_lossy(&state.buffer.slice_bytes(word_start..word_end))
1378 .into_owned();
1379 (line, character, symbol)
1380 };
1381
1382 let buffer_id = self.active_buffer();
1383 let request_id = self.active_window_mut().next_lsp_request_id;
1384
1385 let sent = self
1387 .with_lsp_for_buffer(
1388 buffer_id,
1389 LspFeature::References,
1390 |handle, uri, _language| {
1391 let result = handle.references(
1392 request_id,
1393 uri.as_uri().clone(),
1394 line as u32,
1395 character as u32,
1396 );
1397 if result.is_ok() {
1398 tracing::info!(
1399 "Requested find references at {}:{}:{} (byte_pos={})",
1400 uri.as_str(),
1401 line,
1402 character,
1403 cursor_pos
1404 );
1405 }
1406 result.is_ok()
1407 },
1408 )
1409 .unwrap_or(false);
1410
1411 if sent {
1412 self.active_window_mut().next_lsp_request_id += 1;
1413 self.active_window_mut().pending_references_request = Some(request_id);
1414 self.active_window_mut().pending_references_symbol = symbol;
1415 }
1416
1417 Ok(())
1418 }
1419
1420 pub(crate) fn request_signature_help(&mut self) {
1422 let cursor_pos = self.active_cursors().primary().position;
1424 let state = self.active_state();
1425
1426 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1428 let buffer_id = self.active_buffer();
1429 let request_id = self.active_window_mut().next_lsp_request_id;
1430
1431 let sent = self
1433 .with_lsp_for_buffer(
1434 buffer_id,
1435 LspFeature::SignatureHelp,
1436 |handle, uri, _language| {
1437 let result = handle.signature_help(
1438 request_id,
1439 uri.as_uri().clone(),
1440 line as u32,
1441 character as u32,
1442 );
1443 if result.is_ok() {
1444 tracing::info!(
1445 "Requested signature help at {}:{}:{} (byte_pos={})",
1446 uri.as_str(),
1447 line,
1448 character,
1449 cursor_pos
1450 );
1451 }
1452 result.is_ok()
1453 },
1454 )
1455 .unwrap_or(false);
1456
1457 if sent {
1458 self.active_window_mut().next_lsp_request_id += 1;
1459 self.active_window_mut().pending_signature_help_request = Some(request_id);
1460 }
1461 }
1462
1463 pub(crate) fn handle_signature_help_response(
1465 &mut self,
1466 request_id: u64,
1467 signature_help: Option<lsp_types::SignatureHelp>,
1468 ) {
1469 if self.active_window_mut().pending_signature_help_request != Some(request_id) {
1471 tracing::debug!("Ignoring stale signature help response: {}", request_id);
1472 return;
1473 }
1474
1475 self.active_window_mut().pending_signature_help_request = None;
1476 let signature_help = match signature_help {
1477 Some(help) if !help.signatures.is_empty() => help,
1478 _ => {
1479 tracing::debug!("No signature help available");
1480 return;
1481 }
1482 };
1483
1484 let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
1486 let signature = match signature_help.signatures.get(active_signature_idx) {
1487 Some(sig) => sig,
1488 None => return,
1489 };
1490
1491 let mut content = String::new();
1493
1494 content.push_str(&signature.label);
1496 content.push('\n');
1497
1498 let active_param = signature_help
1500 .active_parameter
1501 .or(signature.active_parameter)
1502 .unwrap_or(0) as usize;
1503
1504 if let Some(params) = &signature.parameters {
1506 if let Some(param) = params.get(active_param) {
1507 let param_label = match ¶m.label {
1509 lsp_types::ParameterLabel::Simple(s) => s.clone(),
1510 lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1511 let start = offsets[0] as usize;
1513 let end = offsets[1] as usize;
1514 if end <= signature.label.len() {
1515 signature.label[start..end].to_string()
1516 } else {
1517 String::new()
1518 }
1519 }
1520 };
1521
1522 if !param_label.is_empty() {
1523 content.push_str(&format!("\n> {}\n", param_label));
1524 }
1525
1526 if let Some(doc) = ¶m.documentation {
1528 let doc_text = match doc {
1529 lsp_types::Documentation::String(s) => s.clone(),
1530 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1531 };
1532 if !doc_text.is_empty() {
1533 content.push('\n');
1534 content.push_str(&doc_text);
1535 content.push('\n');
1536 }
1537 }
1538 }
1539 }
1540
1541 if let Some(doc) = &signature.documentation {
1543 let doc_text = match doc {
1544 lsp_types::Documentation::String(s) => s.clone(),
1545 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1546 };
1547 if !doc_text.is_empty() {
1548 content.push_str("\n---\n\n");
1549 content.push_str(&space_doc_paragraphs(&doc_text));
1550 }
1551 }
1552
1553 use crate::view::popup::{Popup, PopupPosition};
1555 use ratatui::style::Style;
1556
1557 let mut popup = Popup::markdown(
1558 &content,
1559 &*self.theme.read().unwrap(),
1560 Some(&self.grammar_registry),
1561 );
1562 popup.title = Some(t!("lsp.popup_signature").to_string());
1563 popup.transient = true;
1564 popup.position = PopupPosition::BelowCursor;
1565 popup.width = 60;
1566 popup.max_height = 20;
1567 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1568 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1569 popup.focus_key_hint = self.popup_focus_key_hint();
1570
1571 let __buffer_id = self.active_buffer();
1573 if let Some(state) = self
1574 .windows
1575 .get_mut(&self.active_window)
1576 .map(|w| &mut w.buffers)
1577 .expect("active window present")
1578 .get_mut(&__buffer_id)
1579 {
1580 state.popups.show(popup);
1581 tracing::info!(
1582 "Showing signature help popup for {} signatures",
1583 signature_help.signatures.len()
1584 );
1585 }
1586 }
1587
1588 pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1591 if !self
1600 .active_window()
1601 .pending_code_actions_requests
1602 .is_empty()
1603 {
1604 let ids: Vec<u64> = self
1605 .active_window_mut()
1606 .pending_code_actions_requests
1607 .drain()
1608 .collect();
1609 for request_id in ids {
1610 tracing::debug!(
1611 "Canceling previous pending LSP code actions request {}",
1612 request_id
1613 );
1614 self.active_window_mut().send_lsp_cancel_request(request_id);
1615 }
1616 }
1617 self.active_window_mut()
1618 .pending_code_actions_server_names
1619 .clear();
1620 self.active_window_mut().pending_code_actions = None;
1621
1622 let cursor_pos = self.active_cursors().primary().position;
1624 let selection_range = self.active_cursors().primary().selection_range();
1625 let state = self.active_state();
1626
1627 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1629
1630 let (start_line, start_char, end_line, end_char) = if let Some(range) = selection_range {
1632 let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1633 let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1634 (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1635 } else {
1636 (line as u32, character as u32, line as u32, character as u32)
1637 };
1638
1639 let buffer_id = self.active_buffer();
1640
1641 let diagnostics: Vec<lsp_types::Diagnostic> = {
1647 let window = self.active_window();
1648 window
1649 .buffer_metadata
1650 .get(&buffer_id)
1651 .and_then(|m| m.file_uri())
1652 .and_then(|uri| window.stored_diagnostics.get(uri.as_str()))
1653 .map(|diags| {
1654 diags
1655 .iter()
1656 .filter(|d| {
1657 lsp_range_overlaps(&d.range, start_line, start_char, end_line, end_char)
1658 })
1659 .cloned()
1660 .collect()
1661 })
1662 .unwrap_or_default()
1663 };
1664
1665 let base_request_id = self.active_window_mut().next_lsp_request_id;
1667 let counter = std::sync::atomic::AtomicU64::new(0);
1668
1669 let results = self.with_all_lsp_for_buffer_feature_named(
1670 buffer_id,
1671 LspFeature::CodeAction,
1672 |handle, uri, _language, server_name| {
1673 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1674 let request_id = base_request_id + idx;
1675 let result = handle.code_actions(
1676 request_id,
1677 uri.as_uri().clone(),
1678 start_line,
1679 start_char,
1680 end_line,
1681 end_char,
1682 diagnostics.clone(),
1683 );
1684 if result.is_ok() {
1685 tracing::info!(
1686 "Requested code actions at {}:{}:{}-{}:{} (byte_pos={}, request_id={}, server={})",
1687 uri.as_str(),
1688 start_line,
1689 start_char,
1690 end_line,
1691 end_char,
1692 cursor_pos,
1693 request_id,
1694 server_name
1695 );
1696 }
1697 (request_id, result.is_ok(), server_name.to_string())
1698 },
1699 );
1700
1701 let mut sent_ids = Vec::new();
1702 for (request_id, ok, server_name) in &results {
1703 if *ok {
1704 sent_ids.push(*request_id);
1705 self.active_window_mut()
1706 .pending_code_actions_server_names
1707 .insert(*request_id, server_name.clone());
1708 }
1709 }
1710 self.active_window_mut().next_lsp_request_id = base_request_id + results.len() as u64;
1712
1713 if !sent_ids.is_empty() {
1714 self.active_window_mut()
1717 .pending_code_actions_requests
1718 .extend(sent_ids);
1719 }
1720
1721 Ok(())
1722 }
1723
1724 pub(crate) fn handle_code_actions_response(
1728 &mut self,
1729 request_id: u64,
1730 actions: Vec<lsp_types::CodeActionOrCommand>,
1731 ) {
1732 if !self
1734 .active_window_mut()
1735 .pending_code_actions_requests
1736 .remove(&request_id)
1737 {
1738 tracing::debug!("Ignoring stale code actions response: {}", request_id);
1739 return;
1740 }
1741
1742 let server_name = self
1744 .active_window_mut()
1745 .pending_code_actions_server_names
1746 .remove(&request_id)
1747 .unwrap_or_default();
1748
1749 if actions.is_empty() {
1750 if self
1752 .active_window()
1753 .pending_code_actions_requests
1754 .is_empty()
1755 && self
1756 .active_window_mut()
1757 .pending_code_actions
1758 .as_ref()
1759 .is_none_or(|a| a.is_empty())
1760 {
1761 self.set_status_message(t!("lsp.no_code_actions").to_string());
1762 }
1763 return;
1764 }
1765
1766 let tagged_actions: Vec<(String, lsp_types::CodeActionOrCommand)> = actions
1768 .into_iter()
1769 .map(|a| (server_name.clone(), a))
1770 .collect();
1771
1772 match &mut self.active_window_mut().pending_code_actions {
1773 Some(existing) => {
1774 existing.extend(tagged_actions);
1775 tracing::debug!("Extended code actions, now {} total", existing.len());
1776 }
1777 None => {
1778 self.active_window_mut().pending_code_actions = Some(tagged_actions);
1779 }
1780 }
1781
1782 use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1784 use ratatui::style::Style;
1785
1786 let items: Vec<PopupListItem> = {
1787 let all_actions = self.active_window().pending_code_actions.as_ref().unwrap();
1788 let multiple_servers = {
1789 let mut names = std::collections::HashSet::new();
1790 for (name, _) in all_actions {
1791 names.insert(name.as_str());
1792 }
1793 names.len() > 1
1794 };
1795 all_actions
1796 .iter()
1797 .enumerate()
1798 .map(|(i, (srv_name, action))| {
1799 let title = match action {
1800 lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1801 lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1802 };
1803 let kind = match action {
1804 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1805 ca.kind.as_ref().map(|k| k.as_str().to_string())
1806 }
1807 _ => None,
1808 };
1809 let detail = if multiple_servers && !srv_name.is_empty() {
1810 match kind {
1811 Some(k) => Some(format!("[{}] {}", srv_name, k)),
1812 None => Some(format!("[{}]", srv_name)),
1813 }
1814 } else {
1815 kind
1816 };
1817 PopupListItem {
1818 text: format!("{}. {}", i + 1, title),
1819 detail,
1820 icon: None,
1821 data: Some(i.to_string()),
1822 disabled: false,
1823 }
1824 })
1825 .collect()
1826 };
1827
1828 let mut popup = Popup::list(items, &*self.theme.read().unwrap());
1829 popup.kind = crate::view::popup::PopupKind::Action;
1830 popup.title = Some(t!("lsp.popup_code_actions").to_string());
1831 popup.position = PopupPosition::BelowCursor;
1832 popup.width = 60;
1833 popup.max_height = 15;
1834 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1835 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1836 popup.resolver = crate::view::popup::PopupResolver::CodeAction;
1840 popup.focused = true;
1846
1847 let __buffer_id = self.active_buffer();
1849 let action_count = self
1850 .active_window()
1851 .pending_code_actions
1852 .as_ref()
1853 .map_or(0, |v| v.len());
1854 if let Some(state) = self
1855 .windows
1856 .get_mut(&self.active_window)
1857 .map(|w| &mut w.buffers)
1858 .expect("active window present")
1859 .get_mut(&__buffer_id)
1860 {
1861 state.popups.show_or_replace(popup);
1862 tracing::info!("Showing code actions popup with {} actions", action_count);
1863 }
1864 }
1865
1866 pub(crate) fn execute_code_action(&mut self, index: usize) {
1868 let action = match &self.active_window_mut().pending_code_actions {
1869 Some(actions) => actions.get(index).map(|(_, a)| a.clone()),
1870 None => None,
1871 };
1872
1873 let Some(action) = action else {
1874 tracing::warn!("Code action index {} out of range", index);
1875 return;
1876 };
1877
1878 match action {
1879 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1880 if ca.edit.is_none()
1883 && ca.command.is_none()
1884 && ca.data.is_some()
1885 && self.active_window().server_supports_code_action_resolve()
1886 {
1887 tracing::info!(
1888 "Code action '{}' needs resolve, sending codeAction/resolve",
1889 ca.title
1890 );
1891 self.send_code_action_resolve(ca);
1892 return;
1893 }
1894 self.execute_resolved_code_action(ca);
1895 }
1896 lsp_types::CodeActionOrCommand::Command(cmd) => {
1897 self.send_execute_command(cmd);
1898 }
1899 }
1900 }
1901
1902 pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1904 let title = ca.title.clone();
1905
1906 if let Some(edit) = ca.edit {
1908 match self.apply_workspace_edit(edit) {
1909 Ok(n) => {
1910 self.set_status_message(
1911 t!("lsp.code_action_applied", title = &title, count = n).to_string(),
1912 );
1913 }
1914 Err(e) => {
1915 self.set_status_message(format!("Code action failed: {e}"));
1916 return;
1917 }
1918 }
1919 }
1920
1921 if let Some(cmd) = ca.command {
1923 self.send_execute_command(cmd);
1924 }
1925 }
1926
1927 fn send_execute_command(&mut self, cmd: lsp_types::Command) {
1929 tracing::info!("Executing LSP command: {} ({})", cmd.title, cmd.command);
1930 self.set_status_message(
1931 t!(
1932 "lsp.code_action_applied",
1933 title = &cmd.title,
1934 count = 0_usize
1935 )
1936 .to_string(),
1937 );
1938
1939 let language = match self
1941 .buffers()
1942 .get(&self.active_buffer())
1943 .map(|s| s.language.clone())
1944 {
1945 Some(l) => l,
1946 None => return,
1947 };
1948
1949 let __active_id = self.active_window;
1950
1951 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1952 for sh in lsp.get_handles_mut(&language) {
1953 if let Err(e) = sh
1954 .handle
1955 .execute_command(cmd.command.clone(), cmd.arguments.clone())
1956 {
1957 tracing::warn!("Failed to send executeCommand to '{}': {}", sh.name, e);
1958 }
1959 }
1960 }
1961 }
1962
1963 fn send_code_action_resolve(&mut self, action: lsp_types::CodeAction) {
1965 let language = match self
1966 .buffers()
1967 .get(&self.active_buffer())
1968 .map(|s| s.language.clone())
1969 {
1970 Some(l) => l,
1971 None => return,
1972 };
1973
1974 self.active_window_mut().next_lsp_request_id += 1;
1975 let request_id = self.active_window_mut().next_lsp_request_id;
1976
1977 let __active_id = self.active_window;
1978
1979 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1980 for sh in lsp.get_handles_mut(&language) {
1981 if let Err(e) = sh.handle.code_action_resolve(request_id, action.clone()) {
1982 tracing::warn!("Failed to send codeAction/resolve to '{}': {}", sh.name, e);
1983 }
1984 }
1985 }
1986 }
1987
1988 pub(crate) fn handle_completion_resolved(&mut self, item: lsp_types::CompletionItem) {
1990 if let Some(additional_edits) = item.additional_text_edits {
1991 if !additional_edits.is_empty() {
1992 tracing::info!(
1993 "Applying {} additional text edits from completion resolve",
1994 additional_edits.len()
1995 );
1996 let buffer_id = self.active_buffer();
1997 if let Err(e) = self.apply_lsp_text_edits(buffer_id, additional_edits) {
1998 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
1999 }
2000 }
2001 }
2002 }
2003
2004 pub(crate) fn apply_formatting_edits(
2006 &mut self,
2007 uri: &str,
2008 edits: Vec<lsp_types::TextEdit>,
2009 ) -> AnyhowResult<usize> {
2010 let buffer_id = self
2012 .active_window()
2013 .buffer_metadata
2014 .iter()
2015 .find(|(_, meta)| meta.file_uri().map(|u| u.as_str() == uri).unwrap_or(false))
2016 .map(|(id, _)| *id);
2017
2018 if let Some(buffer_id) = buffer_id {
2019 let count = self.apply_lsp_text_edits(buffer_id, edits)?;
2020 self.set_status_message(format!("Formatted ({} edits)", count));
2021 Ok(count)
2022 } else {
2023 tracing::warn!("Cannot apply formatting: no buffer for URI {}", uri);
2024 Ok(0)
2025 }
2026 }
2027
2028 pub(crate) fn request_formatting(&mut self) {
2030 let buffer_id = self.active_buffer();
2031 let metadata = match self.active_window().buffer_metadata.get(&buffer_id) {
2032 Some(m) if m.lsp_enabled => m,
2033 _ => {
2034 self.set_status_message("LSP not available for this buffer".to_string());
2035 return;
2036 }
2037 };
2038
2039 let uri = match metadata.file_uri() {
2040 Some(u) => u.clone(),
2041 None => return,
2042 };
2043
2044 let language = match self
2045 .windows
2046 .get(&self.active_window)
2047 .map(|w| &w.buffers)
2048 .expect("active window present")
2049 .get(&buffer_id)
2050 .map(|s| s.language.clone())
2051 {
2052 Some(l) => l,
2053 None => return,
2054 };
2055
2056 let tab_size = self.config.editor.tab_size as u32;
2057 let insert_spaces = !self.config.editor.use_tabs;
2058
2059 self.active_window_mut().next_lsp_request_id += 1;
2060 let request_id = self.active_window_mut().next_lsp_request_id;
2061
2062 let __active_id = self.active_window;
2063
2064 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
2065 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Format) {
2066 if let Err(e) = sh.handle.document_formatting(
2067 request_id,
2068 uri.as_uri().clone(),
2069 tab_size,
2070 insert_spaces,
2071 ) {
2072 tracing::warn!("Failed to request formatting: {}", e);
2073 }
2074 } else {
2075 self.set_status_message("Formatting not supported by LSP server".to_string());
2076 }
2077 }
2078 }
2079
2080 pub(crate) fn handle_references_response(
2082 &mut self,
2083 request_id: u64,
2084 locations: Vec<lsp_types::Location>,
2085 ) -> AnyhowResult<()> {
2086 tracing::info!(
2087 "handle_references_response: received {} locations for request_id={}",
2088 locations.len(),
2089 request_id
2090 );
2091
2092 if self.active_window_mut().pending_references_request != Some(request_id) {
2094 tracing::debug!("Ignoring stale references response: {}", request_id);
2095 return Ok(());
2096 }
2097
2098 self.active_window_mut().pending_references_request = None;
2099 if locations.is_empty() {
2100 self.set_status_message(t!("lsp.no_references").to_string());
2101 return Ok(());
2102 }
2103
2104 let translation = self.authority().path_translation.clone();
2111 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
2112 .iter()
2113 .map(|loc| {
2114 let wire = crate::app::types::LspUri::from_wire(loc.uri.clone());
2115 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
2120 wire.to_host_path(translation.as_ref())
2121 .map(|p| p.to_string_lossy().into_owned())
2122 .unwrap_or_else(|| loc.uri.path().as_str().to_string())
2123 } else {
2124 loc.uri.as_str().to_string()
2125 };
2126
2127 crate::services::plugins::hooks::LspLocation {
2128 file,
2129 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
2132 })
2133 .collect();
2134
2135 let count = lsp_locations.len();
2136 let symbol = std::mem::take(&mut self.active_window_mut().pending_references_symbol);
2137 self.set_status_message(
2138 t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
2139 );
2140
2141 self.plugin_manager.read().unwrap().run_hook(
2143 "lsp_references",
2144 crate::services::plugins::hooks::HookArgs::LspReferences {
2145 symbol: symbol.clone(),
2146 locations: lsp_locations,
2147 },
2148 );
2149
2150 tracing::info!(
2151 "Fired lsp_references hook with {} locations for symbol '{}'",
2152 count,
2153 symbol
2154 );
2155
2156 Ok(())
2157 }
2158
2159 pub(crate) fn apply_lsp_text_edits(
2162 &mut self,
2163 buffer_id: BufferId,
2164 mut edits: Vec<lsp_types::TextEdit>,
2165 ) -> AnyhowResult<usize> {
2166 if edits.is_empty() {
2167 return Ok(0);
2168 }
2169
2170 edits.sort_by(|a, b| {
2172 b.range
2173 .start
2174 .line
2175 .cmp(&a.range.start.line)
2176 .then(b.range.start.character.cmp(&a.range.start.character))
2177 });
2178
2179 let mut batch_events = Vec::new();
2181 let mut changes = 0;
2182
2183 let cursor_id = {
2185 let split_id = self
2186 .split_manager_mut()
2187 .splits_for_buffer(buffer_id)
2188 .into_iter()
2189 .next()
2190 .unwrap_or_else(|| {
2191 self.windows
2192 .get(&self.active_window)
2193 .and_then(|w| w.buffers.splits())
2194 .map(|(mgr, _)| mgr)
2195 .expect("active window must have a populated split layout")
2196 .active_split()
2197 });
2198 self.windows
2199 .get(&self.active_window)
2200 .and_then(|w| w.buffers.splits())
2201 .map(|(_, vs)| vs)
2202 .expect("active window must have a populated split layout")
2203 .get(&split_id)
2204 .map(|vs| vs.cursors.primary_id())
2205 .unwrap_or_else(|| self.active_cursors().primary_id())
2206 };
2207
2208 for edit in edits {
2210 let state = self
2211 .buffers_mut()
2212 .get_mut(&buffer_id)
2213 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2214
2215 let start_line = edit.range.start.line as usize;
2217 let start_char = edit.range.start.character as usize;
2218 let end_line = edit.range.end.line as usize;
2219 let end_char = edit.range.end.character as usize;
2220
2221 let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
2222 let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
2223 let buffer_len = state.buffer.len();
2224
2225 let old_text = if start_pos < end_pos && end_pos <= buffer_len {
2227 state.get_text_range(start_pos, end_pos)
2228 } else {
2229 format!(
2230 "<invalid range: start={}, end={}, buffer_len={}>",
2231 start_pos, end_pos, buffer_len
2232 )
2233 };
2234 tracing::debug!(
2235 " Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
2236 start_line, start_char, end_line, end_char,
2237 start_pos, end_pos, old_text, edit.new_text
2238 );
2239
2240 if start_pos < end_pos {
2242 let deleted_text = state.get_text_range(start_pos, end_pos);
2243 let delete_event = Event::Delete {
2244 range: start_pos..end_pos,
2245 deleted_text,
2246 cursor_id,
2247 };
2248 batch_events.push(delete_event);
2249 }
2250
2251 if !edit.new_text.is_empty() {
2253 let insert_event = Event::Insert {
2254 position: start_pos,
2255 text: edit.new_text.clone(),
2256 cursor_id,
2257 };
2258 batch_events.push(insert_event);
2259 }
2260
2261 changes += 1;
2262 }
2263
2264 if !batch_events.is_empty() {
2266 self.apply_events_to_buffer_as_bulk_edit(
2267 buffer_id,
2268 batch_events,
2269 "LSP Rename".to_string(),
2270 )?;
2271 }
2272
2273 Ok(changes)
2274 }
2275
2276 fn apply_text_document_edit(
2282 &mut self,
2283 text_doc_edit: lsp_types::TextDocumentEdit,
2284 ) -> AnyhowResult<usize> {
2285 let uri = crate::app::types::LspUri::from_wire(text_doc_edit.text_document.uri);
2288
2289 if let Some(expected_version) = text_doc_edit.text_document.version {
2292 if let Ok(path) =
2293 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2294 {
2295 if let Some(lsp) = self.lsp() {
2296 let language = self
2297 .buffers()
2298 .get(&self.active_buffer())
2299 .map(|s| s.language.clone())
2300 .unwrap_or_default();
2301 for sh in lsp.get_handles(&language) {
2302 if let Some(current_version) = sh.handle.document_version(&path) {
2303 if (expected_version as i64) != current_version {
2304 tracing::warn!(
2305 "Rejecting stale TextDocumentEdit for {:?}: \
2306 server version {} != our version {}",
2307 path,
2308 expected_version,
2309 current_version,
2310 );
2311 return Ok(0);
2312 }
2313 }
2314 }
2315 }
2316 }
2317 }
2318
2319 if let Ok(path) =
2320 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2321 {
2322 let buffer_id = match self.open_file(&path) {
2323 Ok(id) => id,
2324 Err(e) => {
2325 if let Some(confirmation) =
2326 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2327 {
2328 self.start_large_file_encoding_confirmation(confirmation);
2329 } else {
2330 self.set_status_message(
2331 t!("file.error_opening", error = e.to_string()).to_string(),
2332 );
2333 }
2334 return Ok(0);
2335 }
2336 };
2337
2338 let edits: Vec<lsp_types::TextEdit> = text_doc_edit
2339 .edits
2340 .into_iter()
2341 .map(|one_of| match one_of {
2342 lsp_types::OneOf::Left(text_edit) => text_edit,
2343 lsp_types::OneOf::Right(annotated) => annotated.text_edit,
2344 })
2345 .collect();
2346
2347 tracing::info!("Applying {} edits for {:?}:", edits.len(), path);
2348 for (i, edit) in edits.iter().enumerate() {
2349 tracing::info!(
2350 " Edit {}: line {}:{}-{}:{} -> {:?}",
2351 i,
2352 edit.range.start.line,
2353 edit.range.start.character,
2354 edit.range.end.line,
2355 edit.range.end.character,
2356 edit.new_text
2357 );
2358 }
2359
2360 self.apply_lsp_text_edits(buffer_id, edits)
2361 } else {
2362 Ok(0)
2363 }
2364 }
2365
2366 fn apply_resource_operation(&mut self, op: lsp_types::ResourceOp) -> AnyhowResult<()> {
2368 let translation = self.authority().path_translation.clone();
2373 let to_host = |uri: &lsp_types::Uri| -> std::path::PathBuf {
2374 crate::app::types::LspUri::from_wire(uri.clone())
2375 .to_host_path(translation.as_ref())
2376 .unwrap_or_else(|| std::path::PathBuf::from(uri.path().as_str()))
2377 };
2378 match op {
2379 lsp_types::ResourceOp::Create(create) => {
2380 let path = to_host(&create.uri);
2381 let overwrite = create
2382 .options
2383 .as_ref()
2384 .and_then(|o| o.overwrite)
2385 .unwrap_or(false);
2386 let ignore_if_exists = create
2387 .options
2388 .as_ref()
2389 .and_then(|o| o.ignore_if_exists)
2390 .unwrap_or(false);
2391
2392 if path.exists() {
2393 if ignore_if_exists {
2394 tracing::debug!("CreateFile: {:?} already exists, ignoring", path);
2395 return Ok(());
2396 }
2397 if !overwrite {
2398 tracing::warn!("CreateFile: {:?} already exists and overwrite=false", path);
2399 return Ok(());
2400 }
2401 }
2402
2403 if let Some(parent) = path.parent() {
2405 std::fs::create_dir_all(parent)?;
2406 }
2407 std::fs::write(&path, "")?;
2408 tracing::info!("CreateFile: created {:?}", path);
2409
2410 if let Err(e) = self.open_file(&path) {
2412 tracing::warn!("CreateFile: failed to open created file {:?}: {}", path, e);
2413 }
2414 }
2415 lsp_types::ResourceOp::Rename(rename) => {
2416 let old_path = to_host(&rename.old_uri);
2417 let new_path = to_host(&rename.new_uri);
2418 let overwrite = rename
2419 .options
2420 .as_ref()
2421 .and_then(|o| o.overwrite)
2422 .unwrap_or(false);
2423 let ignore_if_exists = rename
2424 .options
2425 .as_ref()
2426 .and_then(|o| o.ignore_if_exists)
2427 .unwrap_or(false);
2428
2429 if new_path.exists() {
2430 if ignore_if_exists {
2431 tracing::debug!("RenameFile: {:?} already exists, ignoring", new_path);
2432 return Ok(());
2433 }
2434 if !overwrite {
2435 tracing::warn!(
2436 "RenameFile: {:?} already exists and overwrite=false",
2437 new_path
2438 );
2439 return Ok(());
2440 }
2441 }
2442
2443 if let Some(parent) = new_path.parent() {
2445 std::fs::create_dir_all(parent)?;
2446 }
2447 std::fs::rename(&old_path, &new_path)?;
2448 tracing::info!("RenameFile: {:?} -> {:?}", old_path, new_path);
2449 }
2450 lsp_types::ResourceOp::Delete(delete) => {
2451 let path = to_host(&delete.uri);
2452 let recursive = delete
2453 .options
2454 .as_ref()
2455 .and_then(|o| o.recursive)
2456 .unwrap_or(false);
2457 let ignore_if_not_exists = delete
2458 .options
2459 .as_ref()
2460 .and_then(|o| o.ignore_if_not_exists)
2461 .unwrap_or(false);
2462
2463 if !path.exists() {
2464 if ignore_if_not_exists {
2465 tracing::debug!("DeleteFile: {:?} does not exist, ignoring", path);
2466 return Ok(());
2467 }
2468 tracing::warn!("DeleteFile: {:?} does not exist", path);
2469 return Ok(());
2470 }
2471
2472 if path.is_dir() && recursive {
2473 std::fs::remove_dir_all(&path)?;
2474 } else if path.is_file() {
2475 std::fs::remove_file(&path)?;
2476 }
2477 tracing::info!("DeleteFile: deleted {:?}", path);
2478 }
2479 }
2480 Ok(())
2481 }
2482
2483 pub(crate) fn apply_workspace_edit(
2487 &mut self,
2488 workspace_edit: lsp_types::WorkspaceEdit,
2489 ) -> AnyhowResult<usize> {
2490 tracing::debug!(
2491 "Applying WorkspaceEdit: changes={:?}, document_changes={:?}",
2492 workspace_edit.changes.as_ref().map(|c| c.len()),
2493 workspace_edit.document_changes.as_ref().map(|dc| match dc {
2494 lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
2495 lsp_types::DocumentChanges::Operations(o) => format!("{} operations", o.len()),
2496 })
2497 );
2498
2499 let mut total_changes = 0;
2500
2501 if let Some(changes) = workspace_edit.changes {
2503 for (uri, edits) in changes {
2504 let uri = crate::app::types::LspUri::from_wire(uri);
2505 if let Ok(path) =
2506 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2507 {
2508 let buffer_id = match self.open_file(&path) {
2509 Ok(id) => id,
2510 Err(e) => {
2511 if let Some(confirmation) = e.downcast_ref::<
2512 crate::model::buffer::LargeFileEncodingConfirmation,
2513 >() {
2514 self.start_large_file_encoding_confirmation(confirmation);
2515 } else {
2516 self.set_status_message(
2517 t!("file.error_opening", error = e.to_string())
2518 .to_string(),
2519 );
2520 }
2521 return Ok(0);
2522 }
2523 };
2524 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
2525 }
2526 }
2527 }
2528
2529 if let Some(document_changes) = workspace_edit.document_changes {
2531 use lsp_types::DocumentChanges;
2532
2533 match document_changes {
2534 DocumentChanges::Edits(edits) => {
2535 for text_doc_edit in edits {
2536 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2537 }
2538 }
2539 DocumentChanges::Operations(ops) => {
2540 for op in ops {
2543 match op {
2544 lsp_types::DocumentChangeOperation::Edit(text_doc_edit) => {
2545 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2546 }
2547 lsp_types::DocumentChangeOperation::Op(resource_op) => {
2548 self.apply_resource_operation(resource_op)?;
2549 total_changes += 1;
2550 }
2551 }
2552 }
2553 }
2554 }
2555 }
2556
2557 Ok(total_changes)
2558 }
2559
2560 pub fn handle_rename_response(
2562 &mut self,
2563 _request_id: u64,
2564 result: Result<lsp_types::WorkspaceEdit, String>,
2565 ) -> AnyhowResult<()> {
2566 match result {
2567 Ok(workspace_edit) => {
2568 let total_changes = self.apply_workspace_edit(workspace_edit)?;
2569 self.active_window_mut().status_message =
2570 Some(t!("lsp.renamed", count = total_changes).to_string());
2571 }
2572 Err(error) => {
2573 if error.contains("content modified") || error.contains("-32801") {
2575 tracing::debug!(
2576 "LSP rename: ContentModified error (expected, ignoring): {}",
2577 error
2578 );
2579 self.active_window_mut().status_message =
2580 Some(t!("lsp.rename_cancelled").to_string());
2581 } else {
2582 self.active_window_mut().status_message =
2583 Some(t!("lsp.rename_failed", error = &error).to_string());
2584 }
2585 }
2586 }
2587
2588 Ok(())
2589 }
2590
2591 pub(crate) fn apply_events_to_buffer_as_bulk_edit(
2596 &mut self,
2597 buffer_id: BufferId,
2598 events: Vec<Event>,
2599 description: String,
2600 ) -> AnyhowResult<()> {
2601 use crate::model::event::CursorId;
2602
2603 if events.is_empty() {
2604 return Ok(());
2605 }
2606
2607 let batch_for_lsp = Event::Batch {
2609 events: events.clone(),
2610 description: description.clone(),
2611 };
2612
2613 let original_active = self.active_buffer();
2625 self.windows
2626 .get_mut(&self.active_window)
2627 .and_then(|w| w.split_manager_mut())
2628 .expect("active window must have a populated split layout")
2629 .set_active_buffer_id(buffer_id);
2630 let lsp_changes = self.active_window().collect_lsp_changes(&batch_for_lsp);
2631 self.windows
2632 .get_mut(&self.active_window)
2633 .and_then(|w| w.split_manager_mut())
2634 .expect("active window must have a populated split layout")
2635 .set_active_buffer_id(original_active);
2636
2637 let split_id_for_cursors = self
2640 .split_manager_mut()
2641 .splits_for_buffer(buffer_id)
2642 .into_iter()
2643 .next()
2644 .unwrap_or_else(|| {
2645 self.windows
2646 .get(&self.active_window)
2647 .and_then(|w| w.buffers.splits())
2648 .map(|(mgr, _)| mgr)
2649 .expect("active window must have a populated split layout")
2650 .active_split()
2651 });
2652 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2653 .windows
2654 .get(&self.active_window)
2655 .and_then(|w| w.buffers.splits())
2656 .map(|(_, vs)| vs)
2657 .expect("active window must have a populated split layout")
2658 .get(&split_id_for_cursors)
2659 .and_then(|vs| vs.keyed_states.get(&buffer_id))
2660 .map(|bvs| {
2661 bvs.cursors
2662 .iter()
2663 .map(|(id, c)| (id, c.position, c.anchor))
2664 .collect()
2665 })
2666 .unwrap_or_default();
2667
2668 let __win = self
2675 .windows
2676 .get_mut(&self.active_window)
2677 .expect("active window must exist");
2678 let bulk_edit = __win
2679 .buffers
2680 .with_buffer_and_view_states(buffer_id, |state, vs_map| -> AnyhowResult<Event> {
2681 let old_snapshot = state.buffer.snapshot_buffer_state();
2683
2684 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2686 for event in &events {
2687 match event {
2688 Event::Insert { position, text, .. } => {
2689 edits.push((*position, 0, text.clone()));
2690 }
2691 Event::Delete { range, .. } => {
2692 edits.push((range.start, range.len(), String::new()));
2693 }
2694 _ => {}
2695 }
2696 }
2697
2698 edits.sort_by(|a, b| b.0.cmp(&a.0));
2700
2701 let edit_refs: Vec<(usize, usize, &str)> = edits
2703 .iter()
2704 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2705 .collect();
2706
2707 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2709
2710 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2712
2713 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2715 for (pos, del_len, text) in &edits {
2716 let delta = text.len() as isize - *del_len as isize;
2717 position_deltas.push((*pos, delta));
2718 }
2719 position_deltas.sort_by_key(|(pos, _)| *pos);
2720
2721 let calc_shift = |original_pos: usize| -> isize {
2722 let mut shift: isize = 0;
2723 for (edit_pos, delta) in &position_deltas {
2724 if *edit_pos < original_pos {
2725 shift += delta;
2726 }
2727 }
2728 shift
2729 };
2730
2731 let buffer_len = state.buffer.len();
2733 let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
2734 .iter()
2735 .map(|(id, pos, anchor)| {
2736 let shift = calc_shift(*pos);
2737 let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
2738 let new_anchor = anchor.map(|a| {
2739 let anchor_shift = calc_shift(a);
2740 ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
2741 });
2742 (*id, new_pos, new_anchor)
2743 })
2744 .collect();
2745
2746 let new_snapshot = state.buffer.snapshot_buffer_state();
2748
2749 state.highlighter.invalidate_all();
2751
2752 if let Some(vs) = vs_map.get_mut(&split_id_for_cursors) {
2754 if let Some(bvs) = vs.keyed_states.get_mut(&buffer_id) {
2755 for (cursor_id, new_pos, new_anchor) in &new_cursors {
2756 if let Some(cursor) = bvs.cursors.get_mut(*cursor_id) {
2757 cursor.position = *new_pos;
2758 cursor.anchor = *new_anchor;
2759 }
2760 }
2761 }
2762 }
2763
2764 let edit_lengths: Vec<(usize, usize, usize)> = {
2767 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2768 for (pos, del_len, text) in &edits {
2769 if let Some(last) = lengths.last_mut() {
2770 if last.0 == *pos {
2771 last.1 += del_len;
2772 last.2 += text.len();
2773 continue;
2774 }
2775 }
2776 lengths.push((*pos, *del_len, text.len()));
2777 }
2778 lengths
2779 };
2780
2781 for &(pos, del_len, ins_len) in &edit_lengths {
2783 if del_len > 0 && ins_len > 0 {
2784 if ins_len > del_len {
2785 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2786 state.margins.adjust_for_insert(pos, ins_len - del_len);
2787 } else if del_len > ins_len {
2788 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2789 state.margins.adjust_for_delete(pos, del_len - ins_len);
2790 }
2791 } else if del_len > 0 {
2792 state.marker_list.adjust_for_delete(pos, del_len);
2793 state.margins.adjust_for_delete(pos, del_len);
2794 } else if ins_len > 0 {
2795 state.marker_list.adjust_for_insert(pos, ins_len);
2796 state.margins.adjust_for_insert(pos, ins_len);
2797 }
2798 }
2799
2800 Ok(Event::BulkEdit {
2801 old_snapshot: Some(old_snapshot),
2802 new_snapshot: Some(new_snapshot),
2803 old_cursors,
2804 new_cursors,
2805 description,
2806 edits: edit_lengths,
2807 displaced_markers,
2808 })
2809 })
2810 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))??;
2811
2812 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
2814 event_log.append(bulk_edit);
2815 }
2816
2817 self.active_window_mut()
2819 .send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2820
2821 Ok(())
2822 }
2823
2824 pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2826 if self.active_window().server_supports_prepare_rename() {
2828 self.active_window_mut().send_prepare_rename();
2829 return Ok(());
2830 }
2831
2832 self.show_rename_prompt()
2833 }
2834
2835 pub(crate) fn handle_prepare_rename_response(
2837 &mut self,
2838 result: Result<serde_json::Value, String>,
2839 ) {
2840 match result {
2841 Ok(value) if !value.is_null() => {
2842 if let Err(e) = self.show_rename_prompt() {
2844 self.set_status_message(format!("Rename failed: {e}"));
2845 }
2846 }
2847 Ok(_) => {
2848 self.set_status_message("Cannot rename at this position".to_string());
2849 }
2850 Err(e) => {
2851 self.set_status_message(format!("Cannot rename: {e}"));
2852 }
2853 }
2854 }
2855
2856 fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2859 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2860
2861 let cursor_pos = self.active_cursors().primary().position;
2863 let (word_start, word_end) = {
2864 let state = self.active_state();
2865
2866 let word_start = find_word_start(&state.buffer, cursor_pos);
2868 let word_end = find_word_end(&state.buffer, cursor_pos);
2869
2870 if word_start >= word_end {
2872 self.active_window_mut().status_message =
2873 Some(t!("lsp.no_symbol_at_cursor").to_string());
2874 return Ok(());
2875 }
2876
2877 (word_start, word_end)
2878 };
2879
2880 let word_text = self.active_state_mut().get_text_range(word_start, word_end);
2882
2883 let overlay_handle = self.add_overlay(
2885 None,
2886 word_start..word_end,
2887 crate::model::event::OverlayFace::Background {
2888 color: (50, 100, 200), },
2890 100,
2891 Some(t!("lsp.popup_renaming").to_string()),
2892 );
2893
2894 let mut prompt = Prompt::new(
2897 "Rename to: ".to_string(),
2898 PromptType::LspRename {
2899 original_text: word_text.clone(),
2900 start_pos: word_start,
2901 end_pos: word_end,
2902 overlay_handle,
2903 },
2904 );
2905 prompt.set_input(word_text);
2907
2908 self.active_window_mut().prompt = Some(prompt);
2909 Ok(())
2910 }
2911
2912 pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
2914 self.remove_overlay(handle.clone());
2915 }
2916
2917 pub(crate) fn perform_lsp_rename(
2919 &mut self,
2920 new_name: String,
2921 original_text: String,
2922 start_pos: usize,
2923 overlay_handle: crate::view::overlay::OverlayHandle,
2924 ) {
2925 self.cancel_rename_overlay(&overlay_handle);
2927
2928 if new_name == original_text {
2930 self.active_window_mut().status_message = Some(t!("lsp.name_unchanged").to_string());
2931 return;
2932 }
2933
2934 let rename_pos = start_pos;
2937
2938 let state = self.active_state();
2941 let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
2942 let buffer_id = self.active_buffer();
2943 let request_id = self.active_window_mut().next_lsp_request_id;
2944
2945 let sent = self
2947 .with_lsp_for_buffer(buffer_id, LspFeature::Rename, |handle, uri, _language| {
2948 let result = handle.rename(
2949 request_id,
2950 uri.as_uri().clone(),
2951 line as u32,
2952 character as u32,
2953 new_name.clone(),
2954 );
2955 if result.is_ok() {
2956 tracing::info!(
2957 "Requested rename at {}:{}:{} to '{}'",
2958 uri.as_str(),
2959 line,
2960 character,
2961 new_name
2962 );
2963 }
2964 result.is_ok()
2965 })
2966 .unwrap_or(false);
2967
2968 if sent {
2969 self.active_window_mut().next_lsp_request_id += 1;
2970 } else if self
2971 .active_window()
2972 .buffer_metadata
2973 .get(&buffer_id)
2974 .and_then(|m| m.file_path())
2975 .is_none()
2976 {
2977 self.active_window_mut().status_message =
2978 Some(t!("lsp.cannot_rename_unsaved").to_string());
2979 }
2980 }
2981
2982 pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
2984 let buffer_id = self.active_buffer();
2985 self.request_inlay_hints_for_buffer(buffer_id);
2986 }
2987
2988 pub(crate) fn request_inlay_hints_for_buffer(&mut self, buffer_id: BufferId) {
2990 if !self.config.editor.enable_inlay_hints {
2991 return;
2992 }
2993
2994 let (line_count, version) = if let Some(state) = self
2998 .windows
2999 .get(&self.active_window)
3000 .map(|w| &w.buffers)
3001 .expect("active window present")
3002 .get(&buffer_id)
3003 {
3004 (
3005 state.buffer.line_count().unwrap_or(1000),
3006 state.buffer.version(),
3007 )
3008 } else {
3009 return;
3010 };
3011 let last_line = line_count.saturating_sub(1) as u32;
3012 let request_id = self.active_window_mut().next_lsp_request_id;
3013
3014 let sent = self
3016 .with_lsp_for_buffer(
3017 buffer_id,
3018 LspFeature::InlayHints,
3019 |handle, uri, _language| {
3020 let result = handle.inlay_hints(
3021 request_id,
3022 uri.as_uri().clone(),
3023 0,
3024 0,
3025 last_line,
3026 10000,
3027 );
3028 if result.is_ok() {
3029 tracing::info!(
3030 "Requested inlay hints for {} (request_id={})",
3031 uri.as_str(),
3032 request_id
3033 );
3034 } else if let Err(e) = &result {
3035 tracing::debug!("Failed to request inlay hints: {}", e);
3036 }
3037 result.is_ok()
3038 },
3039 )
3040 .unwrap_or(false);
3041
3042 if sent {
3043 self.active_window_mut().next_lsp_request_id += 1;
3044 self.active_window_mut()
3045 .pending_inlay_hints_requests
3046 .insert(request_id, super::InlayHintsRequest { buffer_id, version });
3047 }
3048 }
3049
3050 pub(crate) fn maybe_request_folding_ranges_debounced(&mut self, buffer_id: BufferId) {
3052 let Some(ready_at) = self
3053 .active_window()
3054 .folding_ranges_debounce
3055 .get(&buffer_id)
3056 .copied()
3057 else {
3058 return;
3059 };
3060 if Instant::now() < ready_at {
3061 return;
3062 }
3063
3064 self.active_window_mut()
3065 .folding_ranges_debounce
3066 .remove(&buffer_id);
3067 self.request_folding_ranges_for_buffer(buffer_id);
3068 }
3069
3070 pub(crate) fn request_folding_ranges_for_buffer(&mut self, buffer_id: BufferId) {
3072 if self
3073 .active_window_mut()
3074 .folding_ranges_in_flight
3075 .contains_key(&buffer_id)
3076 {
3077 return;
3078 }
3079
3080 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3081 return;
3082 };
3083 if !metadata.lsp_enabled {
3084 return;
3085 }
3086 let Some(uri) = metadata.file_uri().cloned() else {
3087 return;
3088 };
3089 let file_path = metadata.file_path().cloned();
3090
3091 let Some(language) = self
3092 .windows
3093 .get(&self.active_window)
3094 .map(|w| &w.buffers)
3095 .expect("active window present")
3096 .get(&buffer_id)
3097 .map(|s| s.language.clone())
3098 else {
3099 return;
3100 };
3101
3102 let __active_id = self.active_window;
3103 let __buffer_version_for_request = self
3106 .windows
3107 .get(&__active_id)
3108 .and_then(|w| w.buffers.get(&buffer_id))
3109 .map(|s| s.buffer.version())
3110 .unwrap_or(0);
3111
3112 let Some(__win) = self.windows.get_mut(&__active_id) else {
3113 return;
3114 };
3115 let __next_id = &mut __win.next_lsp_request_id;
3116 let __pending_folding = &mut __win.pending_folding_range_requests;
3117 let __folding_in_flight = &mut __win.folding_ranges_in_flight;
3118 let lsp = &mut __win.lsp;
3119
3120 if !lsp.folding_ranges_supported(&language) {
3121 return;
3122 }
3123
3124 use crate::services::lsp::manager::LspSpawnResult;
3126 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3127 return;
3128 }
3129
3130 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::FoldingRange) else {
3131 return;
3132 };
3133 let handle = &mut sh.handle;
3134
3135 let request_id = {
3136 let id = *__next_id;
3137 *__next_id += 1;
3138 id
3139 };
3140 let buffer_version = __buffer_version_for_request;
3141 let _ = __folding_in_flight;
3142
3143 match handle.folding_ranges(request_id, uri.as_uri().clone()) {
3144 Ok(()) => {
3145 __pending_folding.insert(
3146 request_id,
3147 super::FoldingRangeRequest {
3148 buffer_id,
3149 version: buffer_version,
3150 },
3151 );
3152 __folding_in_flight.insert(buffer_id, (request_id, buffer_version));
3153 }
3154 Err(e) => {
3155 tracing::debug!("Failed to request folding ranges: {}", e);
3156 }
3157 }
3158 }
3159
3160 pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
3162 if !self.config.editor.enable_semantic_tokens_full {
3163 return;
3164 }
3165
3166 if self
3168 .active_window_mut()
3169 .semantic_tokens_in_flight
3170 .contains_key(&buffer_id)
3171 {
3172 return;
3173 }
3174
3175 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3176 return;
3177 };
3178 if !metadata.lsp_enabled {
3179 return;
3180 }
3181 let Some(uri) = metadata.file_uri().cloned() else {
3182 return;
3183 };
3184 let file_path_for_spawn = metadata.file_path().cloned();
3185 let Some(language) = self
3187 .windows
3188 .get(&self.active_window)
3189 .map(|w| &w.buffers)
3190 .expect("active window present")
3191 .get(&buffer_id)
3192 .map(|s| s.language.clone())
3193 else {
3194 return;
3195 };
3196
3197 let __active_id = self.active_window;
3198 let Some((buffer_version, existing_version, previous_result_id)) = self
3201 .windows
3202 .get(&__active_id)
3203 .and_then(|w| w.buffers.get(&buffer_id))
3204 .map(|state| {
3205 (
3206 state.buffer.version(),
3207 state.semantic_tokens.as_ref().map(|s| s.version),
3208 state
3209 .semantic_tokens
3210 .as_ref()
3211 .and_then(|s| s.result_id.clone()),
3212 )
3213 })
3214 else {
3215 return;
3216 };
3217 if Some(buffer_version) == existing_version {
3218 return; }
3220
3221 let Some(__win) = self.windows.get_mut(&__active_id) else {
3222 return;
3223 };
3224 let __next_id = &mut __win.next_lsp_request_id;
3225 let __pending_st = &mut __win.pending_semantic_token_requests;
3226 let __st_in_flight = &mut __win.semantic_tokens_in_flight;
3227 let lsp = &mut __win.lsp;
3228
3229 use crate::services::lsp::manager::LspSpawnResult;
3231 if lsp.try_spawn(&language, file_path_for_spawn.as_deref()) != LspSpawnResult::Spawned {
3232 return;
3233 }
3234
3235 if !lsp.semantic_tokens_full_supported(&language) {
3237 return;
3238 }
3239 if lsp.semantic_tokens_legend(&language).is_none() {
3240 return;
3241 }
3242
3243 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3244 return;
3245 };
3246 let supports_delta = sh.capabilities.semantic_tokens_full_delta;
3248 let use_delta = previous_result_id.is_some() && supports_delta;
3249 let handle = &mut sh.handle;
3250
3251 let request_id = {
3252 let id = *__next_id;
3253 *__next_id += 1;
3254 id
3255 };
3256
3257 let request_kind = if use_delta {
3258 super::SemanticTokensFullRequestKind::FullDelta
3259 } else {
3260 super::SemanticTokensFullRequestKind::Full
3261 };
3262
3263 let request_result = if use_delta {
3264 handle.semantic_tokens_full_delta(
3265 request_id,
3266 uri.as_uri().clone(),
3267 previous_result_id.unwrap(),
3268 )
3269 } else {
3270 handle.semantic_tokens_full(request_id, uri.as_uri().clone())
3271 };
3272
3273 match request_result {
3274 Ok(_) => {
3275 __pending_st.insert(
3276 request_id,
3277 super::SemanticTokenFullRequest {
3278 buffer_id,
3279 version: buffer_version,
3280 kind: request_kind,
3281 },
3282 );
3283 __st_in_flight.insert(buffer_id, (request_id, buffer_version, request_kind));
3284 }
3285 Err(e) => {
3286 tracing::debug!("Failed to request semantic tokens: {}", e);
3287 }
3288 }
3289 }
3290
3291 pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
3293 if !self.config.editor.enable_semantic_tokens_full {
3294 self.active_window_mut()
3295 .semantic_tokens_full_debounce
3296 .remove(&buffer_id);
3297 return;
3298 }
3299
3300 let Some(ready_at) = self
3301 .active_window()
3302 .semantic_tokens_full_debounce
3303 .get(&buffer_id)
3304 .copied()
3305 else {
3306 return;
3307 };
3308 if Instant::now() < ready_at {
3309 return;
3310 }
3311
3312 self.active_window_mut()
3313 .semantic_tokens_full_debounce
3314 .remove(&buffer_id);
3315 self.maybe_request_semantic_tokens(buffer_id);
3316 }
3317
3318 pub(crate) fn maybe_request_semantic_tokens_range(
3320 &mut self,
3321 buffer_id: BufferId,
3322 start_line: usize,
3323 end_line: usize,
3324 ) {
3325 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3326 return;
3327 };
3328 if !metadata.lsp_enabled {
3329 return;
3330 }
3331 let Some(uri) = metadata.file_uri().cloned() else {
3332 return;
3333 };
3334 let file_path = metadata.file_path().cloned();
3335 let Some(language) = self
3337 .windows
3338 .get(&self.active_window)
3339 .map(|w| &w.buffers)
3340 .expect("active window present")
3341 .get(&buffer_id)
3342 .map(|s| s.language.clone())
3343 else {
3344 return;
3345 };
3346
3347 let __active_id = self.active_window;
3348 let __win = self
3351 .windows
3352 .get_mut(&__active_id)
3353 .expect("active window must exist");
3354 let __next_id = &mut __win.next_lsp_request_id;
3355 let __pending_st_range = &mut __win.pending_semantic_token_range_requests;
3356 let __st_range_in_flight = &mut __win.semantic_tokens_range_in_flight;
3357 let __st_range_last = &mut __win.semantic_tokens_range_last_request;
3358 let __st_range_applied = &__win.semantic_tokens_range_applied;
3359 let lsp = &mut __win.lsp;
3360 let __buffers_ref: &crate::app::window::WindowBuffers = &__win.buffers;
3361
3362 use crate::services::lsp::manager::LspSpawnResult;
3364 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3365 return;
3366 }
3367
3368 if !lsp.semantic_tokens_range_supported(&language) {
3369 self.maybe_request_semantic_tokens(buffer_id);
3371 return;
3372 }
3373 if lsp.semantic_tokens_legend(&language).is_none() {
3374 return;
3375 }
3376
3377 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3378 return;
3379 };
3380 if !sh.capabilities.semantic_tokens_range {
3383 return;
3384 }
3385 let handle = &mut sh.handle;
3386 let Some(state) = __buffers_ref.get(&buffer_id) else {
3387 return;
3388 };
3389
3390 let buffer_version = state.buffer.version();
3391 let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3392 let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3393
3394 if let Some(line_count) = state.buffer.line_count() {
3395 if line_count == 0 {
3396 return;
3397 }
3398 let max_line = line_count.saturating_sub(1);
3399 padded_start = padded_start.min(max_line);
3400 padded_end = padded_end.min(max_line);
3401 }
3402
3403 let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
3404 let end_char = state
3405 .buffer
3406 .get_line(padded_end)
3407 .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
3408 .unwrap_or(0);
3409 let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
3410 state.buffer.lsp_position_to_byte(padded_end, end_char)
3411 } else {
3412 state.buffer.len()
3413 };
3414
3415 if start_byte >= end_byte {
3416 return;
3417 }
3418
3419 let range = start_byte..end_byte;
3420 if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
3421 __st_range_in_flight.get(&buffer_id).copied()
3422 {
3423 if in_flight_start == padded_start
3424 && in_flight_end == padded_end
3425 && in_flight_version == buffer_version
3426 {
3427 return;
3428 }
3429 if let Err(e) = handle.cancel_request(in_flight_id) {
3430 tracing::debug!("Failed to cancel semantic token range request: {}", e);
3431 }
3432 __pending_st_range.remove(&in_flight_id);
3433 __st_range_in_flight.remove(&buffer_id);
3434 }
3435
3436 if let Some((applied_start, applied_end, applied_version)) =
3437 __st_range_applied.get(&buffer_id).copied()
3438 {
3439 if applied_start == padded_start
3440 && applied_end == padded_end
3441 && applied_version == buffer_version
3442 {
3443 return;
3444 }
3445 }
3446
3447 let now = Instant::now();
3448 if let Some((last_start, last_end, last_version, last_time)) =
3449 __st_range_last.get(&buffer_id).copied()
3450 {
3451 if last_start == padded_start
3452 && last_end == padded_end
3453 && last_version == buffer_version
3454 && now.duration_since(last_time)
3455 < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
3456 {
3457 return;
3458 }
3459 }
3460
3461 let lsp_range = lsp_types::Range {
3462 start: lsp_types::Position {
3463 line: padded_start as u32,
3464 character: 0,
3465 },
3466 end: lsp_types::Position {
3467 line: padded_end as u32,
3468 character: end_char as u32,
3469 },
3470 };
3471
3472 let request_id = {
3473 let id = *__next_id;
3474 *__next_id += 1;
3475 id
3476 };
3477 let _ = __st_range_applied;
3478
3479 match handle.semantic_tokens_range(request_id, uri.as_uri().clone(), lsp_range) {
3480 Ok(_) => {
3481 __pending_st_range.insert(
3482 request_id,
3483 SemanticTokenRangeRequest {
3484 buffer_id,
3485 version: buffer_version,
3486 range: range.clone(),
3487 start_line: padded_start,
3488 end_line: padded_end,
3489 },
3490 );
3491 __st_range_in_flight.insert(
3492 buffer_id,
3493 (request_id, padded_start, padded_end, buffer_version),
3494 );
3495 __st_range_last.insert(buffer_id, (padded_start, padded_end, buffer_version, now));
3496 }
3497 Err(e) => {
3498 tracing::debug!("Failed to request semantic token range: {}", e);
3499 }
3500 }
3501 }
3502}
3503
3504#[cfg(test)]
3505mod tests {
3506 use crate::model::filesystem::StdFileSystem;
3507 use std::sync::Arc;
3508
3509 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
3510 Arc::new(StdFileSystem)
3511 }
3512 use super::{lsp_range_contains, lsp_range_overlaps, Editor};
3513
3514 fn range(sl: u32, sc: u32, el: u32, ec: u32) -> lsp_types::Range {
3515 lsp_types::Range {
3516 start: lsp_types::Position {
3517 line: sl,
3518 character: sc,
3519 },
3520 end: lsp_types::Position {
3521 line: el,
3522 character: ec,
3523 },
3524 }
3525 }
3526
3527 #[test]
3528 fn test_lsp_range_contains_inclusive_start_exclusive_end() {
3529 let r = range(3, 10, 3, 20);
3530 assert!(!lsp_range_contains(&r, 3, 9));
3532 assert!(!lsp_range_contains(&r, 2, 50));
3533 assert!(lsp_range_contains(&r, 3, 10));
3535 assert!(lsp_range_contains(&r, 3, 15));
3537 assert!(lsp_range_contains(&r, 3, 19));
3539 assert!(!lsp_range_contains(&r, 3, 20));
3541 assert!(!lsp_range_contains(&r, 3, 21));
3543 assert!(!lsp_range_contains(&r, 4, 0));
3544 }
3545
3546 #[test]
3547 fn test_lsp_range_contains_multiline() {
3548 let r = range(2, 5, 4, 3);
3549 assert!(!lsp_range_contains(&r, 1, 100));
3551 assert!(!lsp_range_contains(&r, 2, 4));
3553 assert!(lsp_range_contains(&r, 2, 5));
3555 assert!(lsp_range_contains(&r, 3, 0));
3557 assert!(lsp_range_contains(&r, 3, 9999));
3558 assert!(lsp_range_contains(&r, 4, 2));
3560 assert!(!lsp_range_contains(&r, 4, 3));
3562 assert!(!lsp_range_contains(&r, 5, 0));
3564 }
3565
3566 #[test]
3567 fn test_lsp_range_contains_zero_length_matches_anchor_only() {
3568 let r = range(7, 4, 7, 4);
3570 assert!(lsp_range_contains(&r, 7, 4));
3571 assert!(!lsp_range_contains(&r, 7, 3));
3572 assert!(!lsp_range_contains(&r, 7, 5));
3573 assert!(!lsp_range_contains(&r, 6, 4));
3574 assert!(!lsp_range_contains(&r, 8, 4));
3575 }
3576
3577 #[test]
3578 fn test_lsp_range_overlaps_point_cursor_in_diagnostic_range() {
3579 let diag = range(3, 10, 3, 20);
3582 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 10));
3584 assert!(lsp_range_overlaps(&diag, 3, 15, 3, 15));
3586 assert!(!lsp_range_overlaps(&diag, 3, 20, 3, 20));
3588 assert!(!lsp_range_overlaps(&diag, 3, 9, 3, 9));
3590 assert!(!lsp_range_overlaps(&diag, 4, 15, 4, 15));
3592 }
3593
3594 #[test]
3595 fn test_lsp_range_overlaps_selection_intersects_diagnostic() {
3596 let diag = range(3, 10, 3, 20);
3598 assert!(lsp_range_overlaps(&diag, 3, 12, 3, 18));
3600 assert!(lsp_range_overlaps(&diag, 3, 5, 3, 15));
3602 assert!(lsp_range_overlaps(&diag, 3, 15, 3, 25));
3604 assert!(lsp_range_overlaps(&diag, 3, 0, 3, 30));
3606 assert!(!lsp_range_overlaps(&diag, 3, 0, 3, 10));
3608 assert!(!lsp_range_overlaps(&diag, 3, 20, 3, 30));
3610 assert!(!lsp_range_overlaps(&diag, 4, 0, 4, 100));
3612 }
3613
3614 #[test]
3615 fn test_lsp_range_overlaps_point_diagnostic_within_selection() {
3616 let diag = range(3, 10, 3, 10);
3618 assert!(lsp_range_overlaps(&diag, 3, 5, 3, 15));
3620 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 15));
3622 assert!(!lsp_range_overlaps(&diag, 3, 0, 3, 10));
3624 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 10));
3626 assert!(!lsp_range_overlaps(&diag, 3, 9, 3, 9));
3628 }
3629
3630 #[test]
3631 fn test_lsp_range_overlaps_multiline() {
3632 let diag = range(2, 5, 4, 3);
3634 assert!(lsp_range_overlaps(&diag, 3, 0, 3, 0));
3636 assert!(lsp_range_overlaps(&diag, 1, 0, 2, 6));
3638 assert!(!lsp_range_overlaps(&diag, 4, 3, 4, 10));
3640 assert!(!lsp_range_overlaps(&diag, 5, 0, 5, 10));
3642 }
3643
3644 use crate::model::buffer::Buffer;
3645 use crate::state::EditorState;
3646 use crate::view::virtual_text::VirtualTextPosition;
3647 use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
3648
3649 fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
3650 InlayHint {
3651 position: Position { line, character },
3652 label: InlayHintLabel::String(label.to_string()),
3653 kind,
3654 text_edits: None,
3655 tooltip: None,
3656 padding_left: None,
3657 padding_right: None,
3658 data: None,
3659 }
3660 }
3661
3662 #[test]
3663 fn test_inlay_hint_inserts_before_character() {
3664 let mut state = EditorState::new(
3665 80,
3666 24,
3667 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3668 test_fs(),
3669 );
3670 state.buffer = Buffer::from_str_test("ab");
3671
3672 if !state.buffer.is_empty() {
3673 state.marker_list.adjust_for_insert(0, state.buffer.len());
3674 }
3675
3676 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3677 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3678
3679 let lookup = state
3680 .virtual_texts
3681 .build_lookup(&state.marker_list, 0, state.buffer.len());
3682 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3683 assert_eq!(vtexts.len(), 1);
3684 assert_eq!(vtexts[0].text, ": i32");
3685 assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
3686 }
3687
3688 #[test]
3689 fn test_inlay_hint_at_eof_renders_after_last_char() {
3690 let mut state = EditorState::new(
3691 80,
3692 24,
3693 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3694 test_fs(),
3695 );
3696 state.buffer = Buffer::from_str_test("ab");
3697
3698 if !state.buffer.is_empty() {
3699 state.marker_list.adjust_for_insert(0, state.buffer.len());
3700 }
3701
3702 let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
3703 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3704
3705 let lookup = state
3706 .virtual_texts
3707 .build_lookup(&state.marker_list, 0, state.buffer.len());
3708 let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
3709 assert_eq!(vtexts.len(), 1);
3710 assert_eq!(vtexts[0].text, ": i32");
3711 assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
3712 }
3713
3714 #[test]
3715 fn test_inlay_hint_empty_buffer_is_ignored() {
3716 let mut state = EditorState::new(
3717 80,
3718 24,
3719 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3720 test_fs(),
3721 );
3722 state.buffer = Buffer::from_str_test("");
3723
3724 let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
3725 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3726
3727 assert!(state.virtual_texts.is_empty());
3728 }
3729
3730 #[test]
3731 fn test_inlay_hint_uses_theme_key_for_foreground() {
3732 let mut state = EditorState::new(
3735 80,
3736 24,
3737 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3738 test_fs(),
3739 );
3740 state.buffer = Buffer::from_str_test("ab");
3741
3742 if !state.buffer.is_empty() {
3743 state.marker_list.adjust_for_insert(0, state.buffer.len());
3744 }
3745
3746 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3747 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3748
3749 let lookup = state
3750 .virtual_texts
3751 .build_lookup(&state.marker_list, 0, state.buffer.len());
3752 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3753 assert_eq!(
3754 vtexts[0].fg_theme_key.as_deref(),
3755 Some("editor.line_number_fg")
3756 );
3757 assert_eq!(vtexts[0].bg_theme_key, None);
3758 }
3759
3760 #[test]
3761 fn test_inlay_hint_removed_when_its_range_is_deleted() {
3762 let mut state = EditorState::new(
3769 80,
3770 24,
3771 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3772 test_fs(),
3773 );
3774 state.buffer = Buffer::from_str_test("let x = 42;");
3775 state.marker_list.adjust_for_insert(0, state.buffer.len());
3776
3777 let hints = vec![make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE))];
3779 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3780 assert_eq!(state.virtual_texts.len(), 1);
3781
3782 let removed = state
3785 .virtual_texts
3786 .remove_in_range(&mut state.marker_list, 4, 10);
3787 assert_eq!(removed, 1, "hint inside deleted range must be removed");
3788 assert!(state.virtual_texts.is_empty());
3789 }
3790
3791 #[test]
3792 fn test_marker_delete_after_repeat_clear_recreate() {
3793 use crate::model::marker::MarkerList;
3799 use crate::view::virtual_text::{VirtualTextManager, VirtualTextPosition};
3800 use ratatui::style::Style;
3801
3802 let mut markers = MarkerList::new();
3803 let mut vtexts = VirtualTextManager::new();
3804
3805 let positions = [200usize, 401, 602, 803, 1205, 1406];
3807 for &p in &positions {
3808 vtexts.add(
3809 &mut markers,
3810 p,
3811 format!("hint-at-{p}"),
3812 Style::default(),
3813 VirtualTextPosition::BeforeChar,
3814 0,
3815 );
3816 }
3817
3818 for _ in 0..3 {
3821 vtexts.clear(&mut markers);
3822 for &p in &positions {
3823 vtexts.add(
3824 &mut markers,
3825 p,
3826 format!("hint-at-{p}"),
3827 Style::default(),
3828 VirtualTextPosition::BeforeChar,
3829 0,
3830 );
3831 }
3832 }
3833
3834 let removed = vtexts.remove_in_range(&mut markers, 1005, 1206);
3836 assert_eq!(
3837 removed, 1,
3838 "exactly one marker inside [1005, 1206) should be removed"
3839 );
3840 markers.adjust_for_delete(1005, 201);
3841
3842 let lookup = vtexts.build_lookup(&markers, 0, 10_000);
3843 let mut positions: Vec<usize> = lookup.keys().copied().collect();
3844 positions.sort();
3845 assert_eq!(
3846 positions,
3847 vec![200, 401, 602, 803, 1205],
3848 "after delete+adjust, expected marker byte positions {:?}, got {:?}",
3849 vec![200, 401, 602, 803, 1205],
3850 positions
3851 );
3852 }
3853
3854 #[test]
3855 fn test_marker_delete_then_adjust_preserves_last_marker_position() {
3856 use crate::model::marker::MarkerList;
3870
3871 let mut markers = MarkerList::new();
3872 let m0 = markers.create(200, false);
3873 let m1 = markers.create(401, false);
3874 let m2 = markers.create(602, false);
3875 let m3 = markers.create(803, false);
3876 let m5 = markers.create(1205, false);
3877 let m6 = markers.create(1406, false);
3878
3879 markers.delete(m5);
3881
3882 markers.adjust_for_delete(1005, 201);
3884
3885 assert_eq!(markers.get_position(m0), Some(200), "m0 unchanged");
3886 assert_eq!(markers.get_position(m1), Some(401), "m1 unchanged");
3887 assert_eq!(markers.get_position(m2), Some(602), "m2 unchanged");
3888 assert_eq!(markers.get_position(m3), Some(803), "m3 unchanged");
3889 assert_eq!(
3890 markers.get_position(m6),
3891 Some(1205),
3892 "m6 must shift from 1406 to 1205 (1406 - 201), not be clamped to delete-start 1005"
3893 );
3894 }
3895
3896 #[test]
3897 fn test_inlay_hint_outside_deletion_survives() {
3898 let mut state = EditorState::new(
3900 80,
3901 24,
3902 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3903 test_fs(),
3904 );
3905 state.buffer = Buffer::from_str_test("let x = 42; let y = 0;");
3906 state.marker_list.adjust_for_insert(0, state.buffer.len());
3907
3908 let hints = vec![
3909 make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE)), make_hint(0, 17, ": i32", Some(InlayHintKind::TYPE)), ];
3912 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3913 assert_eq!(state.virtual_texts.len(), 2);
3914
3915 let removed = state
3916 .virtual_texts
3917 .remove_in_range(&mut state.marker_list, 4, 10);
3918 assert_eq!(removed, 1);
3919 assert_eq!(state.virtual_texts.len(), 1);
3920 }
3921
3922 #[test]
3923 fn test_space_doc_paragraphs_inserts_blank_lines() {
3924 use super::space_doc_paragraphs;
3925
3926 let input = "sep\n description.\nend\n another.";
3928 let result = space_doc_paragraphs(input);
3929 assert_eq!(result, "sep\n\n description.\n\nend\n\n another.");
3930 }
3931
3932 #[test]
3933 fn test_space_doc_paragraphs_preserves_existing_blank_lines() {
3934 use super::space_doc_paragraphs;
3935
3936 let input = "First paragraph.\n\nSecond paragraph.";
3938 let result = space_doc_paragraphs(input);
3939 assert_eq!(result, "First paragraph.\n\nSecond paragraph.");
3940 }
3941
3942 #[test]
3943 fn test_space_doc_paragraphs_plain_text() {
3944 use super::space_doc_paragraphs;
3945
3946 let input = "Just a single line of docs.";
3947 let result = space_doc_paragraphs(input);
3948 assert_eq!(result, "Just a single line of docs.");
3949 }
3950}