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 let (popup_bg, popup_border_fg) = {
213 let theme = self.theme();
214 (theme.popup_bg, theme.popup_border_fg)
215 };
216
217 {
218 let buffer_id = self.active_buffer();
219 let state = self
220 .windows
221 .get_mut(&self.active_window)
222 .map(|w| &mut w.buffers)
223 .expect("active window present")
224 .get_mut(&buffer_id)
225 .unwrap();
226 let mut popup_obj =
228 crate::state::convert_popup_data_to_popup(&popup_data, popup_bg, popup_border_fg);
229 popup_obj.accept_key_hint = accept_hint;
230 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
231 popup_obj.focus_key_hint = focus_hint;
232 state.popups.show_or_replace(popup_obj);
233 }
234
235 tracing::info!(
236 "Showing completion popup with {} items",
237 self.active_window_mut()
238 .completion_items
239 .as_ref()
240 .map_or(0, |i| i.len())
241 );
242
243 Ok(())
244 }
245
246 pub(crate) fn handle_goto_definition_response(
248 &mut self,
249 request_id: u64,
250 locations: Vec<lsp_types::Location>,
251 ) -> AnyhowResult<()> {
252 if self.active_window_mut().pending_goto_definition_request != Some(request_id) {
254 tracing::debug!(
255 "Ignoring go-to-definition response for outdated request {}",
256 request_id
257 );
258 return Ok(());
259 }
260
261 self.active_window_mut().pending_goto_definition_request = None;
262
263 if locations.is_empty() {
264 self.active_window_mut().status_message = Some(t!("lsp.no_definition").to_string());
265 return Ok(());
266 }
267
268 let location = &locations[0];
270
271 let wire = crate::app::types::LspUri::from_wire(location.uri.clone());
278 let buffer_id = match self.open_lsp_uri_target(&wire) {
279 Ok(id) => id,
280 Err(e) => {
281 if let Some(confirmation) =
282 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
283 {
284 self.start_large_file_encoding_confirmation(confirmation);
285 } else {
286 self.set_status_message(
287 t!("file.error_opening", error = e.to_string()).to_string(),
288 );
289 }
290 return Ok(());
291 }
292 };
293
294 let line = location.range.start.line as usize;
300 let character = location.range.start.character as usize;
301 let position = self
302 .buffers()
303 .get(&buffer_id)
304 .map(|state| state.buffer.line_col_to_position(line, character));
305
306 if let Some(position) = position {
307 let (cursor_id, old_position, old_anchor, old_sticky_column) = {
308 let cursors = self.active_cursors();
309 let primary = cursors.primary();
310 (
311 cursors.primary_id(),
312 primary.position,
313 primary.anchor,
314 primary.sticky_column,
315 )
316 };
317 let event = crate::model::event::Event::MoveCursor {
318 cursor_id,
319 old_position,
320 new_position: position,
321 old_anchor,
322 new_anchor: None,
323 old_sticky_column,
324 new_sticky_column: 0,
325 };
326
327 let split_id = self
328 .windows
329 .get(&self.active_window)
330 .and_then(|w| w.buffers.splits())
331 .map(|(mgr, _)| mgr)
332 .expect("active window must have a populated split layout")
333 .active_split();
334 self.active_window_mut()
335 .apply_event_to_buffer(buffer_id, split_id, &event);
336 self.active_window_mut()
340 .ensure_active_cursor_visible_for_navigation(true);
341 }
342
343 let display_path = self
344 .buffers()
345 .get(&buffer_id)
346 .and_then(|s| s.buffer.file_path().map(|p| p.display().to_string()))
347 .unwrap_or_default();
348 self.active_window_mut().status_message = Some(
349 t!(
350 "lsp.jumped_to_definition",
351 path = display_path,
352 line = line + 1
353 )
354 .to_string(),
355 );
356
357 Ok(())
358 }
359
360 pub(crate) fn with_lsp_for_buffer<F, R>(
365 &mut self,
366 buffer_id: BufferId,
367 feature: LspFeature,
368 f: F,
369 ) -> Option<R>
370 where
371 F: FnOnce(&LspHandle, &crate::app::types::LspUri, &str) -> R,
372 {
373 use crate::services::lsp::manager::LspSpawnResult;
374
375 let (uri, language, file_path) = {
376 let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
377 if !metadata.lsp_enabled {
378 return None;
379 }
380 let uri = metadata.file_uri()?.clone();
381 let file_path = metadata.file_path().cloned();
382 let language = self
383 .windows
384 .get(&self.active_window)
385 .map(|w| &w.buffers)
386 .expect("active window present")
387 .get(&buffer_id)?
388 .language
389 .clone();
390 (uri, language, file_path)
391 };
392
393 let lsp = self.lsp_mut()?;
394 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
395 return None;
396 }
397
398 self.ensure_did_open_all(buffer_id, &uri, &language)?;
400
401 let lsp = self.lsp_mut()?;
403 let sh = lsp.handle_for_feature_mut(&language, feature)?;
404 Some(f(&sh.handle, &uri, &language))
405 }
406
407 pub(crate) fn with_all_lsp_for_buffer_feature<F, R>(
413 &mut self,
414 buffer_id: BufferId,
415 feature: LspFeature,
416 f: F,
417 ) -> Vec<R>
418 where
419 F: Fn(&LspHandle, &crate::app::types::LspUri, &str) -> R,
420 {
421 use crate::services::lsp::manager::LspSpawnResult;
422
423 let (uri, language, file_path) = match (|| {
424 let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
425 if !metadata.lsp_enabled {
426 return None;
427 }
428 let uri = metadata.file_uri()?.clone();
429 let file_path = metadata.file_path().cloned();
430 let language = self
431 .windows
432 .get(&self.active_window)
433 .map(|w| &w.buffers)
434 .expect("active window present")
435 .get(&buffer_id)?
436 .language
437 .clone();
438 Some((uri, language, file_path))
439 })() {
440 Some(v) => v,
441 None => return Vec::new(),
442 };
443
444 let lsp = match self.lsp_mut() {
445 Some(l) => l,
446 None => return Vec::new(),
447 };
448 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
449 return Vec::new();
450 }
451
452 if self
454 .ensure_did_open_all(buffer_id, &uri, &language)
455 .is_none()
456 {
457 return Vec::new();
458 }
459
460 let lsp = match self.lsp_mut() {
462 Some(l) => l,
463 None => return Vec::new(),
464 };
465 lsp.handles_for_feature_mut(&language, feature)
466 .into_iter()
467 .map(|sh| f(&sh.handle, &uri, &language))
468 .collect()
469 }
470
471 pub(crate) fn with_all_lsp_for_buffer_feature_named<F, R>(
474 &mut self,
475 buffer_id: BufferId,
476 feature: LspFeature,
477 f: F,
478 ) -> Vec<R>
479 where
480 F: Fn(&LspHandle, &crate::app::types::LspUri, &str, &str) -> R,
481 {
482 use crate::services::lsp::manager::LspSpawnResult;
483
484 let (uri, language, file_path) = match (|| {
485 let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
486 if !metadata.lsp_enabled {
487 return None;
488 }
489 let uri = metadata.file_uri()?.clone();
490 let file_path = metadata.file_path().cloned();
491 let language = self
492 .windows
493 .get(&self.active_window)
494 .map(|w| &w.buffers)
495 .expect("active window present")
496 .get(&buffer_id)?
497 .language
498 .clone();
499 Some((uri, language, file_path))
500 })() {
501 Some(v) => v,
502 None => return Vec::new(),
503 };
504
505 let lsp = match self.lsp_mut() {
506 Some(l) => l,
507 None => return Vec::new(),
508 };
509 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
510 return Vec::new();
511 }
512
513 if self
514 .ensure_did_open_all(buffer_id, &uri, &language)
515 .is_none()
516 {
517 return Vec::new();
518 }
519
520 let lsp = match self.lsp_mut() {
521 Some(l) => l,
522 None => return Vec::new(),
523 };
524 lsp.handles_for_feature_mut(&language, feature)
525 .into_iter()
526 .map(|sh| f(&sh.handle, &uri, &language, &sh.name))
527 .collect()
528 }
529
530 fn ensure_did_open_all(
533 &mut self,
534 buffer_id: BufferId,
535 uri: &crate::app::types::LspUri,
536 language: &str,
537 ) -> Option<()> {
538 let lsp = self.lsp_mut()?;
539 let handle_ids: Vec<u64> = lsp
540 .get_handles(language)
541 .iter()
542 .map(|sh| sh.handle.id())
543 .collect();
544
545 let needs_open: Vec<u64> = {
546 let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
547 handle_ids
548 .iter()
549 .filter(|id| !metadata.lsp_opened_with.contains(id))
550 .copied()
551 .collect()
552 };
553
554 if !needs_open.is_empty() {
555 let text = self
556 .windows
557 .get(&self.active_window)
558 .map(|w| &w.buffers)
559 .expect("active window present")
560 .get(&buffer_id)?
561 .buffer
562 .to_string()?;
563 let active_id = self.active_window;
564 let __win = self.windows.get_mut(&active_id)?;
565 let lsp = &mut __win.lsp;
566 for sh in lsp.get_handles_mut(language) {
567 if needs_open.contains(&sh.handle.id()) {
568 if let Err(e) =
569 sh.handle
570 .did_open(uri.as_uri().clone(), text.clone(), language.to_string())
571 {
572 tracing::warn!("Failed to send didOpen to '{}': {}", sh.name, e);
573 continue;
574 }
575 let metadata = __win.buffer_metadata.get_mut(&buffer_id)?;
576 metadata.lsp_opened_with.insert(sh.handle.id());
577 tracing::debug!(
578 "Sent didOpen for {} to LSP handle '{}' (language: {})",
579 uri.as_str(),
580 sh.name,
581 language
582 );
583 }
584 }
585 }
586
587 Some(())
588 }
589
590 pub(crate) fn request_completion(&mut self) {
593 if !self.active_window().pending_completion_requests.is_empty() {
607 let ids: Vec<u64> = self
608 .active_window_mut()
609 .pending_completion_requests
610 .drain()
611 .collect();
612 for request_id in ids {
613 tracing::debug!(
614 "Canceling previous pending LSP completion request {}",
615 request_id
616 );
617 self.active_window_mut().send_lsp_cancel_request(request_id);
618 }
619 }
620 self.active_window_mut().completion_items = None;
621
622 let cursor_pos = self.active_cursors().primary().position;
624 let state = self.active_state();
625
626 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
628 let buffer_id = self.active_buffer();
629
630 let base_request_id = self.active_window_mut().next_lsp_request_id;
632 let counter = std::sync::atomic::AtomicU64::new(0);
634
635 let results = self.with_all_lsp_for_buffer_feature(
636 buffer_id,
637 LspFeature::Completion,
638 |handle, uri, _language| {
639 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
640 let request_id = base_request_id + idx;
641 let result = handle.completion(
642 request_id,
643 uri.as_uri().clone(),
644 line as u32,
645 character as u32,
646 );
647 if result.is_ok() {
648 tracing::info!(
649 "Requested completion at {}:{}:{} (request_id={})",
650 uri.as_str(),
651 line,
652 character,
653 request_id
654 );
655 }
656 (request_id, result.is_ok())
657 },
658 );
659
660 let mut sent_ids = Vec::new();
661 for (request_id, ok) in &results {
662 if *ok {
663 sent_ids.push(*request_id);
664 }
665 }
666 self.active_window_mut().next_lsp_request_id = base_request_id + results.len() as u64;
668
669 if !sent_ids.is_empty() {
670 self.active_window_mut()
671 .pending_completion_requests
672 .extend(sent_ids);
673 } else {
674 self.show_buffer_word_completion_popup();
676 }
677 }
678
679 fn show_buffer_word_completion_popup(&mut self) {
683 let items = self.get_buffer_completion_popup_items();
684 if items.is_empty() {
685 return;
686 }
687
688 let popup_data = crate::app::popup_actions::build_completion_popup_from_items(items, 0);
689 let accept_hint = self.completion_accept_key_hint();
690 let focus_hint = self.popup_focus_key_hint();
691 let (popup_bg, popup_border_fg) = {
692 let theme = self.theme();
693 (theme.popup_bg, theme.popup_border_fg)
694 };
695
696 let buffer_id = self.active_buffer();
697 let state = self
698 .windows
699 .get_mut(&self.active_window)
700 .map(|w| &mut w.buffers)
701 .expect("active window present")
702 .get_mut(&buffer_id)
703 .unwrap();
704 let mut popup_obj =
705 crate::state::convert_popup_data_to_popup(&popup_data, popup_bg, popup_border_fg);
706 popup_obj.accept_key_hint = accept_hint;
707 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
708 popup_obj.focus_key_hint = focus_hint;
709 state.popups.show_or_replace(popup_obj);
710 }
711
712 pub(crate) fn maybe_trigger_completion(&mut self, c: char) {
722 if !self.config.editor.completion_popup_auto_show {
724 return;
725 }
726
727 let language = self.active_state().language.clone();
729
730 let is_lsp_trigger = self
732 .lsp()
733 .as_ref()
734 .map(|lsp| lsp.is_completion_trigger_char(c, &language))
735 .unwrap_or(false);
736
737 let quick_suggestions_enabled = self.config.editor.quick_suggestions;
739 let suggest_on_trigger_chars = self.config.editor.suggest_on_trigger_characters;
740 let is_word_char = c.is_alphanumeric() || c == '_';
741
742 if is_lsp_trigger && suggest_on_trigger_chars {
744 tracing::debug!(
745 "Trigger character '{}' immediately triggers completion for language {}",
746 c,
747 language
748 );
749 self.active_window_mut().scheduled_completion_trigger = None;
751 self.request_completion();
752 return;
753 }
754
755 if quick_suggestions_enabled && is_word_char {
757 let delay_ms = self.config.editor.quick_suggestions_delay_ms;
758 let trigger_time = Instant::now() + Duration::from_millis(delay_ms);
759
760 tracing::debug!(
761 "Scheduling completion trigger in {}ms for language {} (char '{}')",
762 delay_ms,
763 language,
764 c
765 );
766
767 self.active_window_mut().scheduled_completion_trigger = Some(trigger_time);
770 } else {
771 self.active_window_mut().scheduled_completion_trigger = None;
775 }
776 }
777
778 pub(crate) fn request_goto_definition(&mut self) -> AnyhowResult<()> {
780 let cursor_pos = self.active_cursors().primary().position;
782 let state = self.active_state();
783
784 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
786 let buffer_id = self.active_buffer();
787 let request_id = self.active_window_mut().next_lsp_request_id;
788
789 let sent = self
791 .with_lsp_for_buffer(
792 buffer_id,
793 LspFeature::Definition,
794 |handle, uri, _language| {
795 let result = handle.goto_definition(
796 request_id,
797 uri.as_uri().clone(),
798 line as u32,
799 character as u32,
800 );
801 if result.is_ok() {
802 tracing::info!(
803 "Requested go-to-definition at {}:{}:{}",
804 uri.as_str(),
805 line,
806 character
807 );
808 }
809 result.is_ok()
810 },
811 )
812 .unwrap_or(false);
813
814 if sent {
815 self.active_window_mut().next_lsp_request_id += 1;
816 self.active_window_mut().pending_goto_definition_request = Some(request_id);
817 }
818
819 Ok(())
820 }
821
822 pub fn request_hover(&mut self) -> AnyhowResult<()> {
824 let cursor_pos = self.active_cursors().primary().position;
826 let state = self.active_state();
827
828 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
830
831 if let Some(pos) = state.buffer.offset_to_position(cursor_pos) {
833 tracing::debug!(
834 "Hover request: cursor_byte={}, line={}, byte_col={}, utf16_col={}",
835 cursor_pos,
836 pos.line,
837 pos.column,
838 character
839 );
840 }
841
842 let buffer_id = self.active_buffer();
843 let request_id = self.active_window_mut().next_lsp_request_id;
844
845 let sent = self
847 .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
848 let result = handle.hover(
849 request_id,
850 uri.as_uri().clone(),
851 line as u32,
852 character as u32,
853 );
854 if result.is_ok() {
855 tracing::info!(
856 "Requested hover at {}:{}:{} (byte_pos={})",
857 uri.as_str(),
858 line,
859 character,
860 cursor_pos
861 );
862 }
863 result.is_ok()
864 })
865 .unwrap_or(false);
866
867 if sent {
868 self.active_window_mut().next_lsp_request_id += 1;
869 self.active_window_mut().hover.record_request(
870 request_id,
871 line as u32,
872 character as u32,
873 );
874 }
875
876 Ok(())
877 }
878
879 pub(crate) fn request_hover_at_position(&mut self, byte_pos: usize) -> AnyhowResult<bool> {
884 let state = self.active_state();
886
887 let (line, character) = state.buffer.position_to_lsp_position(byte_pos);
889
890 if let Some(pos) = state.buffer.offset_to_position(byte_pos) {
892 tracing::trace!(
893 "Mouse hover request: byte_pos={}, line={}, byte_col={}, utf16_col={}",
894 byte_pos,
895 pos.line,
896 pos.column,
897 character
898 );
899 }
900
901 let buffer_id = self.active_buffer();
902 let request_id = self.active_window_mut().next_lsp_request_id;
903
904 let sent = self
906 .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
907 let result = handle.hover(
908 request_id,
909 uri.as_uri().clone(),
910 line as u32,
911 character as u32,
912 );
913 if result.is_ok() {
914 tracing::trace!(
915 "Mouse hover requested at {}:{}:{} (byte_pos={})",
916 uri.as_str(),
917 line,
918 character,
919 byte_pos
920 );
921 }
922 result.is_ok()
923 })
924 .unwrap_or(false);
925
926 if sent {
927 self.active_window_mut().next_lsp_request_id += 1;
928 self.active_window_mut().hover.record_request(
929 request_id,
930 line as u32,
931 character as u32,
932 );
933 }
934
935 Ok(sent)
936 }
937
938 pub(crate) fn handle_hover_response(
940 &mut self,
941 request_id: u64,
942 contents: String,
943 is_markdown: bool,
944 range: Option<((u32, u32), (u32, u32))>,
945 ) {
946 let Some(position) = self.active_window_mut().hover.claim_pending(request_id) else {
950 tracing::debug!("Ignoring stale hover response: {}", request_id);
951 return;
952 };
953
954 if self.is_lsp_status_popup_open() {
957 tracing::debug!("Suppressing hover response: LSP status popup is open");
958 self.active_window_mut().hover.set_symbol_range(None);
959 return;
960 }
961 let hover_lsp_position = Some(position);
962
963 let diagnostic_lines = hover_lsp_position
968 .map(|pos| self.compose_hover_diagnostic_lines(pos))
969 .unwrap_or_default();
970
971 if contents.is_empty() && diagnostic_lines.is_empty() {
972 self.set_status_message(t!("lsp.no_hover").to_string());
973 self.active_window_mut().hover.set_symbol_range(None);
974 return;
975 }
976
977 tracing::debug!(
979 "LSP hover content (markdown={}):\n{}",
980 is_markdown,
981 contents
982 );
983
984 if let Some(((start_line, start_char), (end_line, end_char))) = range {
986 let state = self.active_state();
987 let start_byte = state
988 .buffer
989 .lsp_position_to_byte(start_line as usize, start_char as usize);
990 let end_byte = state
991 .buffer
992 .lsp_position_to_byte(end_line as usize, end_char as usize);
993 self.active_window_mut()
994 .hover
995 .set_symbol_range(Some((start_byte, end_byte)));
996 tracing::debug!(
997 "Hover symbol range: {}..{} (LSP {}:{}..{}:{})",
998 start_byte,
999 end_byte,
1000 start_line,
1001 start_char,
1002 end_line,
1003 end_char
1004 );
1005
1006 if let Some(old_handle) = self.active_window_mut().hover.take_symbol_overlay() {
1008 let remove_event = crate::model::event::Event::RemoveOverlay { handle: old_handle };
1009 self.apply_event_to_active_buffer(&remove_event);
1010 }
1011
1012 let event = crate::model::event::Event::AddOverlay {
1014 namespace: None,
1015 range: start_byte..end_byte,
1016 face: crate::model::event::OverlayFace::Background {
1017 color: (80, 80, 120), },
1019 priority: 90, message: None,
1021 extend_to_line_end: false,
1022 url: None,
1023 };
1024 self.apply_event_to_active_buffer(&event);
1025 if let Some(state) = self
1027 .windows
1028 .get(&self.active_window)
1029 .map(|w| &w.buffers)
1030 .expect("active window present")
1031 .get(&self.active_buffer())
1032 {
1033 if let Some(handle) = state.overlays.all().last().map(|o| o.handle.clone()) {
1034 self.active_window_mut().hover.set_symbol_overlay(handle);
1035 }
1036 }
1037 } else {
1038 let computed_range = if let Some((hover_byte_pos, _, _, _)) =
1041 self.active_window_mut().mouse_state.lsp_hover_state
1042 {
1043 let state = self.active_state();
1044 let start_byte = find_word_start(&state.buffer, hover_byte_pos);
1045 let end_byte = find_word_end(&state.buffer, hover_byte_pos);
1046 if start_byte < end_byte {
1047 tracing::debug!(
1048 "Hover symbol range (computed from word boundaries): {}..{}",
1049 start_byte,
1050 end_byte
1051 );
1052 Some((start_byte, end_byte))
1053 } else {
1054 None
1055 }
1056 } else {
1057 None
1058 };
1059 self.active_window_mut()
1060 .hover
1061 .set_symbol_range(computed_range);
1062 }
1063
1064 use crate::view::markdown::{parse_markdown, StyledLine};
1075 use crate::view::popup::{Popup, PopupContent, PopupPosition};
1076 use ratatui::style::Style;
1077 use unicode_width::UnicodeWidthStr;
1078
1079 let hover_lines: Vec<StyledLine> = if contents.is_empty() {
1080 Vec::new()
1081 } else if is_markdown {
1082 parse_markdown(
1083 &contents,
1084 &*self.theme.read().unwrap(),
1085 Some(&self.grammar_registry),
1086 )
1087 } else {
1088 contents
1089 .lines()
1090 .map(|s| {
1091 let mut sl = StyledLine::new();
1092 sl.push(
1093 s.to_string(),
1094 Style::default().fg(self.theme.read().unwrap().popup_text_fg),
1095 );
1096 sl
1097 })
1098 .collect()
1099 };
1100
1101 let has_diagnostic = !diagnostic_lines.is_empty();
1102 let mut all_lines: Vec<StyledLine> = Vec::new();
1103 all_lines.extend(diagnostic_lines);
1104 if has_diagnostic && !hover_lines.is_empty() {
1105 let mut sep = StyledLine::new();
1109 sep.push(
1110 "─".repeat(12),
1111 Style::default().fg(self.theme.read().unwrap().popup_border_fg),
1112 );
1113 all_lines.push(sep);
1114 }
1115 all_lines.extend(hover_lines);
1116
1117 while all_lines
1119 .last()
1120 .map(|l| l.spans.iter().all(|s| s.text.trim().is_empty()))
1121 .unwrap_or(false)
1122 {
1123 all_lines.pop();
1124 }
1125
1126 let content_width: usize = all_lines
1131 .iter()
1132 .map(|l| {
1133 l.spans
1134 .iter()
1135 .map(|s| UnicodeWidthStr::width(s.text.as_str()))
1136 .sum::<usize>()
1137 })
1138 .max()
1139 .unwrap_or(0);
1140 let popup_width = (content_width as u16 + 4).clamp(30, 80);
1141 let dynamic_height = (self.terminal_height * 60 / 100).clamp(15, 40);
1142
1143 let mut popup = Popup::text(Vec::new(), &*self.theme.read().unwrap());
1145 popup.content = PopupContent::Markdown(all_lines);
1146 popup.title = Some(t!("lsp.popup_hover").to_string());
1147 popup.transient = true;
1148 popup.position = if let Some((x, y)) = self.active_window_mut().hover.take_screen_position()
1149 {
1150 PopupPosition::Fixed { x, y: y + 1 }
1151 } else {
1152 PopupPosition::BelowCursor
1153 };
1154 popup.width = popup_width;
1155 popup.max_height = dynamic_height;
1156 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1157 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1158 popup.focus_key_hint = self.popup_focus_key_hint();
1159
1160 let __buffer_id = self.active_buffer();
1164 if let Some(state) = self
1165 .windows
1166 .get_mut(&self.active_window)
1167 .map(|w| &mut w.buffers)
1168 .expect("active window present")
1169 .get_mut(&__buffer_id)
1170 {
1171 while state.popups.top().is_some_and(|p| p.transient) {
1172 state.popups.hide();
1173 }
1174 state.popups.show(popup);
1175 tracing::info!("Showing hover popup (markdown={})", is_markdown);
1176 }
1177
1178 self.active_window_mut().mouse_state.lsp_hover_request_sent = true;
1181 }
1182
1183 fn compose_hover_diagnostic_lines(
1194 &self,
1195 lsp_pos: (u32, u32),
1196 ) -> Vec<crate::view::markdown::StyledLine> {
1197 use crate::view::markdown::StyledLine;
1198 use lsp_types::DiagnosticSeverity;
1199 use ratatui::style::{Modifier, Style};
1200
1201 let buffer_id = self.active_buffer();
1202 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
1203 return Vec::new();
1204 };
1205 let Some(uri) = metadata.file_uri() else {
1206 return Vec::new();
1207 };
1208 let Some(diagnostics) = self.get_stored_diagnostics().get(uri.as_str()) else {
1209 return Vec::new();
1210 };
1211
1212 let (hover_line, hover_char) = lsp_pos;
1213 let overlapping: Vec<&lsp_types::Diagnostic> = diagnostics
1214 .iter()
1215 .filter(|d| lsp_range_contains(&d.range, hover_line, hover_char))
1216 .collect();
1217
1218 if overlapping.is_empty() {
1219 return Vec::new();
1220 }
1221
1222 let mut out: Vec<StyledLine> = Vec::new();
1223 for (idx, diag) in overlapping.iter().enumerate() {
1224 if idx > 0 {
1225 out.push(StyledLine::new());
1226 }
1227
1228 let (label, marker, severity_color) = match diag.severity {
1229 Some(DiagnosticSeverity::ERROR) => {
1230 ("Error", "✖", self.theme.read().unwrap().diagnostic_error_fg)
1231 }
1232 Some(DiagnosticSeverity::WARNING) => (
1233 "Warning",
1234 "⚠",
1235 self.theme.read().unwrap().diagnostic_warning_fg,
1236 ),
1237 Some(DiagnosticSeverity::INFORMATION) => {
1238 ("Info", "ℹ", self.theme.read().unwrap().diagnostic_info_fg)
1239 }
1240 Some(DiagnosticSeverity::HINT) => {
1241 ("Hint", "ℹ", self.theme.read().unwrap().diagnostic_hint_fg)
1242 }
1243 _ => ("Diagnostic", "•", self.theme.read().unwrap().popup_text_fg),
1244 };
1245
1246 let header_style = Style::default()
1247 .fg(severity_color)
1248 .add_modifier(Modifier::BOLD);
1249 let mut header = StyledLine::new();
1250 header.push(format!("{} {}", marker, label), header_style);
1251 if let Some(source) = diag.source.as_deref().filter(|s| !s.is_empty()) {
1252 header.push(
1255 format!(" ({})", source),
1256 Style::default()
1257 .fg(self.theme.read().unwrap().tab_inactive_fg)
1258 .add_modifier(Modifier::ITALIC),
1259 );
1260 }
1261 out.push(header);
1262
1263 for message_line in diag.message.lines() {
1267 let mut line = StyledLine::new();
1268 line.push(
1269 message_line.to_string(),
1270 Style::default().fg(self.theme.read().unwrap().popup_text_fg),
1271 );
1272 out.push(line);
1273 }
1274 }
1275 out
1276 }
1277
1278 #[doc(hidden)]
1280 pub fn apply_inlay_hints_to_state(
1281 state: &mut crate::state::EditorState,
1282 hints: &[lsp_types::InlayHint],
1283 ) {
1284 use crate::view::virtual_text::VirtualTextPosition;
1285 use ratatui::style::{Color, Style};
1286
1287 state.virtual_texts.clear(&mut state.marker_list);
1289
1290 if hints.is_empty() {
1291 return;
1292 }
1293
1294 let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
1300 let hint_fg_theme_key = Some("editor.line_number_fg".to_string());
1301
1302 for hint in hints {
1303 let byte_offset = state.buffer.lsp_position_to_byte(
1305 hint.position.line as usize,
1306 hint.position.character as usize,
1307 );
1308
1309 let text = match &hint.label {
1311 lsp_types::InlayHintLabel::String(s) => s.clone(),
1312 lsp_types::InlayHintLabel::LabelParts(parts) => {
1313 parts.iter().map(|p| p.value.as_str()).collect::<String>()
1314 }
1315 };
1316
1317 if state.buffer.is_empty() {
1323 continue;
1324 }
1325
1326 let buf_len = state.buffer.len();
1335 let byte_here = if byte_offset < buf_len {
1336 state
1337 .buffer
1338 .slice_bytes(byte_offset..byte_offset + 1)
1339 .first()
1340 .copied()
1341 } else {
1342 None
1343 };
1344 let at_line_break = matches!(byte_here, Some(b'\n' | b'\r'));
1345
1346 let (byte_offset, position) = if byte_offset >= buf_len {
1347 (buf_len.saturating_sub(1), VirtualTextPosition::AfterChar)
1350 } else if at_line_break && byte_offset > 0 {
1351 (byte_offset - 1, VirtualTextPosition::AfterChar)
1355 } else {
1356 (byte_offset, VirtualTextPosition::BeforeChar)
1357 };
1358
1359 let display_text = text;
1361
1362 state.virtual_texts.add_with_theme_keys(
1363 &mut state.marker_list,
1364 byte_offset,
1365 display_text,
1366 hint_style,
1367 hint_fg_theme_key.clone(),
1368 None,
1369 position,
1370 0, );
1372 }
1373
1374 tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
1375 }
1376
1377 pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
1379 use crate::primitives::word_navigation::{find_word_end, find_word_start};
1380
1381 let cursor_pos = self.active_cursors().primary().position;
1382 let (line, character, symbol) = {
1383 let state = self.active_state();
1384 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1385 let word_start = find_word_start(&state.buffer, cursor_pos);
1386 let word_end = find_word_end(&state.buffer, cursor_pos);
1387 let symbol = String::from_utf8_lossy(&state.buffer.slice_bytes(word_start..word_end))
1388 .into_owned();
1389 (line, character, symbol)
1390 };
1391
1392 let buffer_id = self.active_buffer();
1393 let request_id = self.active_window_mut().next_lsp_request_id;
1394
1395 let sent = self
1397 .with_lsp_for_buffer(
1398 buffer_id,
1399 LspFeature::References,
1400 |handle, uri, _language| {
1401 let result = handle.references(
1402 request_id,
1403 uri.as_uri().clone(),
1404 line as u32,
1405 character as u32,
1406 );
1407 if result.is_ok() {
1408 tracing::info!(
1409 "Requested find references at {}:{}:{} (byte_pos={})",
1410 uri.as_str(),
1411 line,
1412 character,
1413 cursor_pos
1414 );
1415 }
1416 result.is_ok()
1417 },
1418 )
1419 .unwrap_or(false);
1420
1421 if sent {
1422 self.active_window_mut().next_lsp_request_id += 1;
1423 self.active_window_mut().pending_references_request = Some(request_id);
1424 self.active_window_mut().pending_references_symbol = symbol;
1425 }
1426
1427 Ok(())
1428 }
1429
1430 pub(crate) fn request_implementation(&mut self) -> AnyhowResult<()> {
1432 use crate::primitives::word_navigation::{find_word_end, find_word_start};
1433
1434 let cursor_pos = self.active_cursors().primary().position;
1435 let (line, character, symbol) = {
1436 let state = self.active_state();
1437 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1438 let word_start = find_word_start(&state.buffer, cursor_pos);
1439 let word_end = find_word_end(&state.buffer, cursor_pos);
1440 let symbol = String::from_utf8_lossy(&state.buffer.slice_bytes(word_start..word_end))
1441 .into_owned();
1442 (line, character, symbol)
1443 };
1444
1445 let buffer_id = self.active_buffer();
1446 let request_id = self.active_window_mut().next_lsp_request_id;
1447
1448 let sent = self
1450 .with_lsp_for_buffer(
1451 buffer_id,
1452 LspFeature::Implementation,
1453 |handle, uri, _language| {
1454 let result = handle.implementation(
1455 request_id,
1456 uri.as_uri().clone(),
1457 line as u32,
1458 character as u32,
1459 );
1460 if result.is_ok() {
1461 tracing::info!(
1462 "Requested go-to-implementation at {}:{}:{} (byte_pos={})",
1463 uri.as_str(),
1464 line,
1465 character,
1466 cursor_pos
1467 );
1468 }
1469 result.is_ok()
1470 },
1471 )
1472 .unwrap_or(false);
1473
1474 if sent {
1475 self.active_window_mut().next_lsp_request_id += 1;
1476 self.active_window_mut().pending_implementation_request = Some(request_id);
1477 self.active_window_mut().pending_implementation_symbol = symbol;
1478 }
1479
1480 Ok(())
1481 }
1482
1483 pub(crate) fn request_signature_help(&mut self) {
1485 let cursor_pos = self.active_cursors().primary().position;
1487 let state = self.active_state();
1488
1489 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1491 let buffer_id = self.active_buffer();
1492 let request_id = self.active_window_mut().next_lsp_request_id;
1493
1494 let sent = self
1496 .with_lsp_for_buffer(
1497 buffer_id,
1498 LspFeature::SignatureHelp,
1499 |handle, uri, _language| {
1500 let result = handle.signature_help(
1501 request_id,
1502 uri.as_uri().clone(),
1503 line as u32,
1504 character as u32,
1505 );
1506 if result.is_ok() {
1507 tracing::info!(
1508 "Requested signature help at {}:{}:{} (byte_pos={})",
1509 uri.as_str(),
1510 line,
1511 character,
1512 cursor_pos
1513 );
1514 }
1515 result.is_ok()
1516 },
1517 )
1518 .unwrap_or(false);
1519
1520 if sent {
1521 self.active_window_mut().next_lsp_request_id += 1;
1522 self.active_window_mut().pending_signature_help_request = Some(request_id);
1523 }
1524 }
1525
1526 pub(crate) fn handle_signature_help_response(
1528 &mut self,
1529 request_id: u64,
1530 signature_help: Option<lsp_types::SignatureHelp>,
1531 ) {
1532 if self.active_window_mut().pending_signature_help_request != Some(request_id) {
1534 tracing::debug!("Ignoring stale signature help response: {}", request_id);
1535 return;
1536 }
1537
1538 self.active_window_mut().pending_signature_help_request = None;
1539 let signature_help = match signature_help {
1540 Some(help) if !help.signatures.is_empty() => help,
1541 _ => {
1542 tracing::debug!("No signature help available");
1543 return;
1544 }
1545 };
1546
1547 let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
1549 let signature = match signature_help.signatures.get(active_signature_idx) {
1550 Some(sig) => sig,
1551 None => return,
1552 };
1553
1554 let mut content = String::new();
1556
1557 content.push_str(&signature.label);
1559 content.push('\n');
1560
1561 let active_param = signature_help
1563 .active_parameter
1564 .or(signature.active_parameter)
1565 .unwrap_or(0) as usize;
1566
1567 if let Some(params) = &signature.parameters {
1569 if let Some(param) = params.get(active_param) {
1570 let param_label = match ¶m.label {
1572 lsp_types::ParameterLabel::Simple(s) => s.clone(),
1573 lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1574 let start = offsets[0] as usize;
1576 let end = offsets[1] as usize;
1577 if end <= signature.label.len() {
1578 signature.label[start..end].to_string()
1579 } else {
1580 String::new()
1581 }
1582 }
1583 };
1584
1585 if !param_label.is_empty() {
1586 content.push_str(&format!("\n> {}\n", param_label));
1587 }
1588
1589 if let Some(doc) = ¶m.documentation {
1591 let doc_text = match doc {
1592 lsp_types::Documentation::String(s) => s.clone(),
1593 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1594 };
1595 if !doc_text.is_empty() {
1596 content.push('\n');
1597 content.push_str(&doc_text);
1598 content.push('\n');
1599 }
1600 }
1601 }
1602 }
1603
1604 if let Some(doc) = &signature.documentation {
1606 let doc_text = match doc {
1607 lsp_types::Documentation::String(s) => s.clone(),
1608 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1609 };
1610 if !doc_text.is_empty() {
1611 content.push_str("\n---\n\n");
1612 content.push_str(&space_doc_paragraphs(&doc_text));
1613 }
1614 }
1615
1616 use crate::view::popup::{Popup, PopupPosition};
1618 use ratatui::style::Style;
1619
1620 let mut popup = Popup::markdown(
1621 &content,
1622 &*self.theme.read().unwrap(),
1623 Some(&self.grammar_registry),
1624 );
1625 popup.title = Some(t!("lsp.popup_signature").to_string());
1626 popup.transient = true;
1627 popup.position = PopupPosition::BelowCursor;
1628 popup.width = 60;
1629 popup.max_height = 20;
1630 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1631 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1632 popup.focus_key_hint = self.popup_focus_key_hint();
1633
1634 let __buffer_id = self.active_buffer();
1636 if let Some(state) = self
1637 .windows
1638 .get_mut(&self.active_window)
1639 .map(|w| &mut w.buffers)
1640 .expect("active window present")
1641 .get_mut(&__buffer_id)
1642 {
1643 state.popups.show(popup);
1644 tracing::info!(
1645 "Showing signature help popup for {} signatures",
1646 signature_help.signatures.len()
1647 );
1648 }
1649 }
1650
1651 pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1654 if !self
1663 .active_window()
1664 .pending_code_actions_requests
1665 .is_empty()
1666 {
1667 let ids: Vec<u64> = self
1668 .active_window_mut()
1669 .pending_code_actions_requests
1670 .drain()
1671 .collect();
1672 for request_id in ids {
1673 tracing::debug!(
1674 "Canceling previous pending LSP code actions request {}",
1675 request_id
1676 );
1677 self.active_window_mut().send_lsp_cancel_request(request_id);
1678 }
1679 }
1680 self.active_window_mut()
1681 .pending_code_actions_server_names
1682 .clear();
1683 self.active_window_mut().pending_code_actions = None;
1684
1685 let cursor_pos = self.active_cursors().primary().position;
1687 let selection_range = self.active_cursors().primary().selection_range();
1688 let state = self.active_state();
1689
1690 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1692
1693 let (start_line, start_char, end_line, end_char) = if let Some(range) = selection_range {
1695 let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1696 let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1697 (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1698 } else {
1699 (line as u32, character as u32, line as u32, character as u32)
1700 };
1701
1702 let buffer_id = self.active_buffer();
1703
1704 let diagnostics: Vec<lsp_types::Diagnostic> = {
1710 let window = self.active_window();
1711 window
1712 .buffer_metadata
1713 .get(&buffer_id)
1714 .and_then(|m| m.file_uri())
1715 .and_then(|uri| window.stored_diagnostics.get(uri.as_str()))
1716 .map(|diags| {
1717 diags
1718 .iter()
1719 .filter(|d| {
1720 lsp_range_overlaps(&d.range, start_line, start_char, end_line, end_char)
1721 })
1722 .cloned()
1723 .collect()
1724 })
1725 .unwrap_or_default()
1726 };
1727
1728 let base_request_id = self.active_window_mut().next_lsp_request_id;
1730 let counter = std::sync::atomic::AtomicU64::new(0);
1731
1732 let results = self.with_all_lsp_for_buffer_feature_named(
1733 buffer_id,
1734 LspFeature::CodeAction,
1735 |handle, uri, _language, server_name| {
1736 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1737 let request_id = base_request_id + idx;
1738 let result = handle.code_actions(
1739 request_id,
1740 uri.as_uri().clone(),
1741 start_line,
1742 start_char,
1743 end_line,
1744 end_char,
1745 diagnostics.clone(),
1746 );
1747 if result.is_ok() {
1748 tracing::info!(
1749 "Requested code actions at {}:{}:{}-{}:{} (byte_pos={}, request_id={}, server={})",
1750 uri.as_str(),
1751 start_line,
1752 start_char,
1753 end_line,
1754 end_char,
1755 cursor_pos,
1756 request_id,
1757 server_name
1758 );
1759 }
1760 (request_id, result.is_ok(), server_name.to_string())
1761 },
1762 );
1763
1764 let mut sent_ids = Vec::new();
1765 for (request_id, ok, server_name) in &results {
1766 if *ok {
1767 sent_ids.push(*request_id);
1768 self.active_window_mut()
1769 .pending_code_actions_server_names
1770 .insert(*request_id, server_name.clone());
1771 }
1772 }
1773 self.active_window_mut().next_lsp_request_id = base_request_id + results.len() as u64;
1775
1776 if !sent_ids.is_empty() {
1777 self.active_window_mut()
1780 .pending_code_actions_requests
1781 .extend(sent_ids);
1782 }
1783
1784 Ok(())
1785 }
1786
1787 pub(crate) fn handle_code_actions_response(
1791 &mut self,
1792 request_id: u64,
1793 actions: Vec<lsp_types::CodeActionOrCommand>,
1794 ) {
1795 if !self
1797 .active_window_mut()
1798 .pending_code_actions_requests
1799 .remove(&request_id)
1800 {
1801 tracing::debug!("Ignoring stale code actions response: {}", request_id);
1802 return;
1803 }
1804
1805 let server_name = self
1807 .active_window_mut()
1808 .pending_code_actions_server_names
1809 .remove(&request_id)
1810 .unwrap_or_default();
1811
1812 if actions.is_empty() {
1813 if self
1815 .active_window()
1816 .pending_code_actions_requests
1817 .is_empty()
1818 && self
1819 .active_window_mut()
1820 .pending_code_actions
1821 .as_ref()
1822 .is_none_or(|a| a.is_empty())
1823 {
1824 self.set_status_message(t!("lsp.no_code_actions").to_string());
1825 }
1826 return;
1827 }
1828
1829 let tagged_actions: Vec<(String, lsp_types::CodeActionOrCommand)> = actions
1831 .into_iter()
1832 .map(|a| (server_name.clone(), a))
1833 .collect();
1834
1835 match &mut self.active_window_mut().pending_code_actions {
1836 Some(existing) => {
1837 existing.extend(tagged_actions);
1838 tracing::debug!("Extended code actions, now {} total", existing.len());
1839 }
1840 None => {
1841 self.active_window_mut().pending_code_actions = Some(tagged_actions);
1842 }
1843 }
1844
1845 use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1847 use ratatui::style::Style;
1848
1849 let items: Vec<PopupListItem> = {
1850 let all_actions = self.active_window().pending_code_actions.as_ref().unwrap();
1851 let multiple_servers = {
1852 let mut names = std::collections::HashSet::new();
1853 for (name, _) in all_actions {
1854 names.insert(name.as_str());
1855 }
1856 names.len() > 1
1857 };
1858 all_actions
1859 .iter()
1860 .enumerate()
1861 .map(|(i, (srv_name, action))| {
1862 let title = match action {
1863 lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1864 lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1865 };
1866 let kind = match action {
1867 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1868 ca.kind.as_ref().map(|k| k.as_str().to_string())
1869 }
1870 _ => None,
1871 };
1872 let detail = if multiple_servers && !srv_name.is_empty() {
1873 match kind {
1874 Some(k) => Some(format!("[{}] {}", srv_name, k)),
1875 None => Some(format!("[{}]", srv_name)),
1876 }
1877 } else {
1878 kind
1879 };
1880 PopupListItem {
1881 text: format!("{}. {}", i + 1, title),
1882 detail,
1883 icon: None,
1884 data: Some(i.to_string()),
1885 disabled: false,
1886 }
1887 })
1888 .collect()
1889 };
1890
1891 let mut popup = Popup::list(items, &*self.theme.read().unwrap());
1892 popup.kind = crate::view::popup::PopupKind::Action;
1893 popup.title = Some(t!("lsp.popup_code_actions").to_string());
1894 popup.position = PopupPosition::BelowCursor;
1895 popup.width = 60;
1896 popup.max_height = 15;
1897 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1898 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1899 popup.resolver = crate::view::popup::PopupResolver::CodeAction;
1903 popup.focused = true;
1909
1910 let __buffer_id = self.active_buffer();
1912 let action_count = self
1913 .active_window()
1914 .pending_code_actions
1915 .as_ref()
1916 .map_or(0, |v| v.len());
1917 if let Some(state) = self
1918 .windows
1919 .get_mut(&self.active_window)
1920 .map(|w| &mut w.buffers)
1921 .expect("active window present")
1922 .get_mut(&__buffer_id)
1923 {
1924 state.popups.show_or_replace(popup);
1925 tracing::info!("Showing code actions popup with {} actions", action_count);
1926 }
1927 }
1928
1929 pub(crate) fn execute_code_action(&mut self, index: usize) {
1931 let action = match &self.active_window_mut().pending_code_actions {
1932 Some(actions) => actions.get(index).map(|(_, a)| a.clone()),
1933 None => None,
1934 };
1935
1936 let Some(action) = action else {
1937 tracing::warn!("Code action index {} out of range", index);
1938 return;
1939 };
1940
1941 match action {
1942 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1943 if ca.edit.is_none()
1946 && ca.command.is_none()
1947 && ca.data.is_some()
1948 && self.active_window().server_supports_code_action_resolve()
1949 {
1950 tracing::info!(
1951 "Code action '{}' needs resolve, sending codeAction/resolve",
1952 ca.title
1953 );
1954 self.send_code_action_resolve(ca);
1955 return;
1956 }
1957 self.execute_resolved_code_action(ca);
1958 }
1959 lsp_types::CodeActionOrCommand::Command(cmd) => {
1960 self.send_execute_command(cmd);
1961 }
1962 }
1963 }
1964
1965 pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1967 let title = ca.title.clone();
1968
1969 if let Some(edit) = ca.edit {
1971 match self.apply_workspace_edit(edit) {
1972 Ok(n) => {
1973 self.set_status_message(
1974 t!("lsp.code_action_applied", title = &title, count = n).to_string(),
1975 );
1976 }
1977 Err(e) => {
1978 self.set_status_message(format!("Code action failed: {e}"));
1979 return;
1980 }
1981 }
1982 }
1983
1984 if let Some(cmd) = ca.command {
1986 self.send_execute_command(cmd);
1987 }
1988 }
1989
1990 fn send_execute_command(&mut self, cmd: lsp_types::Command) {
1992 tracing::info!("Executing LSP command: {} ({})", cmd.title, cmd.command);
1993 self.set_status_message(
1994 t!(
1995 "lsp.code_action_applied",
1996 title = &cmd.title,
1997 count = 0_usize
1998 )
1999 .to_string(),
2000 );
2001
2002 let language = match self
2004 .buffers()
2005 .get(&self.active_buffer())
2006 .map(|s| s.language.clone())
2007 {
2008 Some(l) => l,
2009 None => return,
2010 };
2011
2012 let __active_id = self.active_window;
2013
2014 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
2015 for sh in lsp.get_handles_mut(&language) {
2016 if let Err(e) = sh
2017 .handle
2018 .execute_command(cmd.command.clone(), cmd.arguments.clone())
2019 {
2020 tracing::warn!("Failed to send executeCommand to '{}': {}", sh.name, e);
2021 }
2022 }
2023 }
2024 }
2025
2026 fn send_code_action_resolve(&mut self, action: lsp_types::CodeAction) {
2028 let language = match self
2029 .buffers()
2030 .get(&self.active_buffer())
2031 .map(|s| s.language.clone())
2032 {
2033 Some(l) => l,
2034 None => return,
2035 };
2036
2037 self.active_window_mut().next_lsp_request_id += 1;
2038 let request_id = self.active_window_mut().next_lsp_request_id;
2039
2040 let __active_id = self.active_window;
2041
2042 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
2043 for sh in lsp.get_handles_mut(&language) {
2044 if let Err(e) = sh.handle.code_action_resolve(request_id, action.clone()) {
2045 tracing::warn!("Failed to send codeAction/resolve to '{}': {}", sh.name, e);
2046 }
2047 }
2048 }
2049 }
2050
2051 pub(crate) fn handle_completion_resolved(&mut self, item: lsp_types::CompletionItem) {
2053 if let Some(additional_edits) = item.additional_text_edits {
2054 if !additional_edits.is_empty() {
2055 tracing::info!(
2056 "Applying {} additional text edits from completion resolve",
2057 additional_edits.len()
2058 );
2059 let buffer_id = self.active_buffer();
2060 if let Err(e) = self.apply_lsp_text_edits(buffer_id, additional_edits) {
2061 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
2062 }
2063 }
2064 }
2065 }
2066
2067 pub(crate) fn apply_formatting_edits(
2069 &mut self,
2070 uri: &str,
2071 edits: Vec<lsp_types::TextEdit>,
2072 ) -> AnyhowResult<usize> {
2073 let buffer_id = self
2075 .active_window()
2076 .buffer_metadata
2077 .iter()
2078 .find(|(_, meta)| meta.file_uri().map(|u| u.as_str() == uri).unwrap_or(false))
2079 .map(|(id, _)| *id);
2080
2081 if let Some(buffer_id) = buffer_id {
2082 let count = self.apply_lsp_text_edits(buffer_id, edits)?;
2083 self.set_status_message(format!("Formatted ({} edits)", count));
2084 Ok(count)
2085 } else {
2086 tracing::warn!("Cannot apply formatting: no buffer for URI {}", uri);
2087 Ok(0)
2088 }
2089 }
2090
2091 pub(crate) fn request_formatting(&mut self) {
2093 let buffer_id = self.active_buffer();
2094 let metadata = match self.active_window().buffer_metadata.get(&buffer_id) {
2095 Some(m) if m.lsp_enabled => m,
2096 _ => {
2097 self.set_status_message("LSP not available for this buffer".to_string());
2098 return;
2099 }
2100 };
2101
2102 let uri = match metadata.file_uri() {
2103 Some(u) => u.clone(),
2104 None => return,
2105 };
2106
2107 let language = match self
2108 .windows
2109 .get(&self.active_window)
2110 .map(|w| &w.buffers)
2111 .expect("active window present")
2112 .get(&buffer_id)
2113 .map(|s| s.language.clone())
2114 {
2115 Some(l) => l,
2116 None => return,
2117 };
2118
2119 let tab_size = self.config.editor.tab_size as u32;
2120 let insert_spaces = !self.config.editor.use_tabs;
2121
2122 self.active_window_mut().next_lsp_request_id += 1;
2123 let request_id = self.active_window_mut().next_lsp_request_id;
2124
2125 let __active_id = self.active_window;
2126
2127 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
2128 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Format) {
2129 if let Err(e) = sh.handle.document_formatting(
2130 request_id,
2131 uri.as_uri().clone(),
2132 tab_size,
2133 insert_spaces,
2134 ) {
2135 tracing::warn!("Failed to request formatting: {}", e);
2136 }
2137 } else {
2138 self.set_status_message("Formatting not supported by LSP server".to_string());
2139 }
2140 }
2141 }
2142
2143 pub(crate) fn handle_references_response(
2145 &mut self,
2146 request_id: u64,
2147 locations: Vec<lsp_types::Location>,
2148 ) -> AnyhowResult<()> {
2149 tracing::info!(
2150 "handle_references_response: received {} locations for request_id={}",
2151 locations.len(),
2152 request_id
2153 );
2154
2155 if self.active_window_mut().pending_references_request != Some(request_id) {
2157 tracing::debug!("Ignoring stale references response: {}", request_id);
2158 return Ok(());
2159 }
2160
2161 self.active_window_mut().pending_references_request = None;
2162 if locations.is_empty() {
2163 self.set_status_message(t!("lsp.no_references").to_string());
2164 return Ok(());
2165 }
2166
2167 let translation = self.authority().path_translation.clone();
2174 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
2175 .iter()
2176 .map(|loc| {
2177 let wire = crate::app::types::LspUri::from_wire(loc.uri.clone());
2178 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
2183 wire.to_host_path(translation.as_ref())
2184 .map(|p| p.to_string_lossy().into_owned())
2185 .unwrap_or_else(|| loc.uri.path().as_str().to_string())
2186 } else {
2187 loc.uri.as_str().to_string()
2188 };
2189
2190 crate::services::plugins::hooks::LspLocation {
2191 file,
2192 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
2195 })
2196 .collect();
2197
2198 let count = lsp_locations.len();
2199 let symbol = std::mem::take(&mut self.active_window_mut().pending_references_symbol);
2200 self.set_status_message(
2201 t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
2202 );
2203
2204 self.plugin_manager.read().unwrap().run_hook(
2206 "lsp_references",
2207 crate::services::plugins::hooks::HookArgs::LspReferences {
2208 symbol: symbol.clone(),
2209 locations: lsp_locations,
2210 },
2211 );
2212
2213 tracing::info!(
2214 "Fired lsp_references hook with {} locations for symbol '{}'",
2215 count,
2216 symbol
2217 );
2218
2219 Ok(())
2220 }
2221
2222 pub(crate) fn handle_implementation_response(
2224 &mut self,
2225 request_id: u64,
2226 locations: Vec<lsp_types::Location>,
2227 ) -> AnyhowResult<()> {
2228 tracing::info!(
2229 "handle_implementation_response: received {} locations for request_id={}",
2230 locations.len(),
2231 request_id
2232 );
2233
2234 if self.active_window_mut().pending_implementation_request != Some(request_id) {
2236 tracing::debug!("Ignoring stale implementation response: {}", request_id);
2237 return Ok(());
2238 }
2239
2240 self.active_window_mut().pending_implementation_request = None;
2241 if locations.is_empty() {
2242 self.set_status_message(t!("lsp.no_implementation").to_string());
2243 return Ok(());
2244 }
2245
2246 let translation = self.authority().path_translation.clone();
2252 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
2253 .iter()
2254 .map(|loc| {
2255 let wire = crate::app::types::LspUri::from_wire(loc.uri.clone());
2256 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
2257 wire.to_host_path(translation.as_ref())
2258 .map(|p| p.to_string_lossy().into_owned())
2259 .unwrap_or_else(|| loc.uri.path().as_str().to_string())
2260 } else {
2261 loc.uri.as_str().to_string()
2262 };
2263
2264 crate::services::plugins::hooks::LspLocation {
2265 file,
2266 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
2269 })
2270 .collect();
2271
2272 let count = lsp_locations.len();
2273 let symbol = std::mem::take(&mut self.active_window_mut().pending_implementation_symbol);
2274 self.set_status_message(
2275 t!("lsp.found_implementations", count = count, symbol = &symbol).to_string(),
2276 );
2277
2278 self.plugin_manager.read().unwrap().run_hook(
2280 "lsp_implementation",
2281 crate::services::plugins::hooks::HookArgs::LspImplementation {
2282 symbol: symbol.clone(),
2283 locations: lsp_locations,
2284 },
2285 );
2286
2287 tracing::info!(
2288 "Fired lsp_implementation hook with {} locations for symbol '{}'",
2289 count,
2290 symbol
2291 );
2292
2293 Ok(())
2294 }
2295
2296 pub(crate) fn apply_lsp_text_edits(
2299 &mut self,
2300 buffer_id: BufferId,
2301 mut edits: Vec<lsp_types::TextEdit>,
2302 ) -> AnyhowResult<usize> {
2303 if edits.is_empty() {
2304 return Ok(0);
2305 }
2306
2307 edits.sort_by(|a, b| {
2309 b.range
2310 .start
2311 .line
2312 .cmp(&a.range.start.line)
2313 .then(b.range.start.character.cmp(&a.range.start.character))
2314 });
2315
2316 let mut batch_events = Vec::new();
2318 let mut changes = 0;
2319
2320 let cursor_id = {
2322 let split_id = self
2323 .split_manager_mut()
2324 .splits_for_buffer(buffer_id)
2325 .into_iter()
2326 .next()
2327 .unwrap_or_else(|| {
2328 self.windows
2329 .get(&self.active_window)
2330 .and_then(|w| w.buffers.splits())
2331 .map(|(mgr, _)| mgr)
2332 .expect("active window must have a populated split layout")
2333 .active_split()
2334 });
2335 self.windows
2336 .get(&self.active_window)
2337 .and_then(|w| w.buffers.splits())
2338 .map(|(_, vs)| vs)
2339 .expect("active window must have a populated split layout")
2340 .get(&split_id)
2341 .map(|vs| vs.cursors.primary_id())
2342 .unwrap_or_else(|| self.active_cursors().primary_id())
2343 };
2344
2345 for edit in edits {
2347 let state = self
2348 .buffers_mut()
2349 .get_mut(&buffer_id)
2350 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2351
2352 let start_line = edit.range.start.line as usize;
2354 let start_char = edit.range.start.character as usize;
2355 let end_line = edit.range.end.line as usize;
2356 let end_char = edit.range.end.character as usize;
2357
2358 let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
2359 let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
2360 let buffer_len = state.buffer.len();
2361
2362 let old_text = if start_pos < end_pos && end_pos <= buffer_len {
2364 state.get_text_range(start_pos, end_pos)
2365 } else {
2366 format!(
2367 "<invalid range: start={}, end={}, buffer_len={}>",
2368 start_pos, end_pos, buffer_len
2369 )
2370 };
2371 tracing::debug!(
2372 " Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
2373 start_line, start_char, end_line, end_char,
2374 start_pos, end_pos, old_text, edit.new_text
2375 );
2376
2377 if start_pos < end_pos {
2379 let deleted_text = state.get_text_range(start_pos, end_pos);
2380 let delete_event = Event::Delete {
2381 range: start_pos..end_pos,
2382 deleted_text,
2383 cursor_id,
2384 };
2385 batch_events.push(delete_event);
2386 }
2387
2388 if !edit.new_text.is_empty() {
2390 let insert_event = Event::Insert {
2391 position: start_pos,
2392 text: edit.new_text.clone(),
2393 cursor_id,
2394 };
2395 batch_events.push(insert_event);
2396 }
2397
2398 changes += 1;
2399 }
2400
2401 if !batch_events.is_empty() {
2403 self.apply_events_to_buffer_as_bulk_edit(
2404 buffer_id,
2405 batch_events,
2406 "LSP Rename".to_string(),
2407 )?;
2408 }
2409
2410 Ok(changes)
2411 }
2412
2413 fn apply_text_document_edit(
2419 &mut self,
2420 text_doc_edit: lsp_types::TextDocumentEdit,
2421 ) -> AnyhowResult<usize> {
2422 let uri = crate::app::types::LspUri::from_wire(text_doc_edit.text_document.uri);
2425
2426 if let Some(expected_version) = text_doc_edit.text_document.version {
2429 if let Ok(path) =
2430 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2431 {
2432 if let Some(lsp) = self.lsp() {
2433 let language = self
2434 .buffers()
2435 .get(&self.active_buffer())
2436 .map(|s| s.language.clone())
2437 .unwrap_or_default();
2438 for sh in lsp.get_handles(&language) {
2439 if let Some(current_version) = sh.handle.document_version(&path) {
2440 if (expected_version as i64) != current_version {
2441 tracing::warn!(
2442 "Rejecting stale TextDocumentEdit for {:?}: \
2443 server version {} != our version {}",
2444 path,
2445 expected_version,
2446 current_version,
2447 );
2448 return Ok(0);
2449 }
2450 }
2451 }
2452 }
2453 }
2454 }
2455
2456 if let Ok(path) =
2457 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2458 {
2459 let buffer_id = match self.open_file(&path) {
2460 Ok(id) => id,
2461 Err(e) => {
2462 if let Some(confirmation) =
2463 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2464 {
2465 self.start_large_file_encoding_confirmation(confirmation);
2466 } else {
2467 self.set_status_message(
2468 t!("file.error_opening", error = e.to_string()).to_string(),
2469 );
2470 }
2471 return Ok(0);
2472 }
2473 };
2474
2475 let edits: Vec<lsp_types::TextEdit> = text_doc_edit
2476 .edits
2477 .into_iter()
2478 .map(|one_of| match one_of {
2479 lsp_types::OneOf::Left(text_edit) => text_edit,
2480 lsp_types::OneOf::Right(annotated) => annotated.text_edit,
2481 })
2482 .collect();
2483
2484 tracing::info!("Applying {} edits for {:?}:", edits.len(), path);
2485 for (i, edit) in edits.iter().enumerate() {
2486 tracing::info!(
2487 " Edit {}: line {}:{}-{}:{} -> {:?}",
2488 i,
2489 edit.range.start.line,
2490 edit.range.start.character,
2491 edit.range.end.line,
2492 edit.range.end.character,
2493 edit.new_text
2494 );
2495 }
2496
2497 self.apply_lsp_text_edits(buffer_id, edits)
2498 } else {
2499 Ok(0)
2500 }
2501 }
2502
2503 fn apply_resource_operation(&mut self, op: lsp_types::ResourceOp) -> AnyhowResult<()> {
2505 let translation = self.authority().path_translation.clone();
2510 let to_host = |uri: &lsp_types::Uri| -> std::path::PathBuf {
2511 crate::app::types::LspUri::from_wire(uri.clone())
2512 .to_host_path(translation.as_ref())
2513 .unwrap_or_else(|| std::path::PathBuf::from(uri.path().as_str()))
2514 };
2515 match op {
2516 lsp_types::ResourceOp::Create(create) => {
2517 let path = to_host(&create.uri);
2518 let overwrite = create
2519 .options
2520 .as_ref()
2521 .and_then(|o| o.overwrite)
2522 .unwrap_or(false);
2523 let ignore_if_exists = create
2524 .options
2525 .as_ref()
2526 .and_then(|o| o.ignore_if_exists)
2527 .unwrap_or(false);
2528
2529 if path.exists() {
2530 if ignore_if_exists {
2531 tracing::debug!("CreateFile: {:?} already exists, ignoring", path);
2532 return Ok(());
2533 }
2534 if !overwrite {
2535 tracing::warn!("CreateFile: {:?} already exists and overwrite=false", path);
2536 return Ok(());
2537 }
2538 }
2539
2540 if let Some(parent) = path.parent() {
2542 std::fs::create_dir_all(parent)?;
2543 }
2544 std::fs::write(&path, "")?;
2545 tracing::info!("CreateFile: created {:?}", path);
2546
2547 if let Err(e) = self.open_file(&path) {
2549 tracing::warn!("CreateFile: failed to open created file {:?}: {}", path, e);
2550 }
2551 }
2552 lsp_types::ResourceOp::Rename(rename) => {
2553 let old_path = to_host(&rename.old_uri);
2554 let new_path = to_host(&rename.new_uri);
2555 let overwrite = rename
2556 .options
2557 .as_ref()
2558 .and_then(|o| o.overwrite)
2559 .unwrap_or(false);
2560 let ignore_if_exists = rename
2561 .options
2562 .as_ref()
2563 .and_then(|o| o.ignore_if_exists)
2564 .unwrap_or(false);
2565
2566 if new_path.exists() {
2567 if ignore_if_exists {
2568 tracing::debug!("RenameFile: {:?} already exists, ignoring", new_path);
2569 return Ok(());
2570 }
2571 if !overwrite {
2572 tracing::warn!(
2573 "RenameFile: {:?} already exists and overwrite=false",
2574 new_path
2575 );
2576 return Ok(());
2577 }
2578 }
2579
2580 if let Some(parent) = new_path.parent() {
2582 std::fs::create_dir_all(parent)?;
2583 }
2584 std::fs::rename(&old_path, &new_path)?;
2585 tracing::info!("RenameFile: {:?} -> {:?}", old_path, new_path);
2586 }
2587 lsp_types::ResourceOp::Delete(delete) => {
2588 let path = to_host(&delete.uri);
2589 let recursive = delete
2590 .options
2591 .as_ref()
2592 .and_then(|o| o.recursive)
2593 .unwrap_or(false);
2594 let ignore_if_not_exists = delete
2595 .options
2596 .as_ref()
2597 .and_then(|o| o.ignore_if_not_exists)
2598 .unwrap_or(false);
2599
2600 if !path.exists() {
2601 if ignore_if_not_exists {
2602 tracing::debug!("DeleteFile: {:?} does not exist, ignoring", path);
2603 return Ok(());
2604 }
2605 tracing::warn!("DeleteFile: {:?} does not exist", path);
2606 return Ok(());
2607 }
2608
2609 if path.is_dir() && recursive {
2610 std::fs::remove_dir_all(&path)?;
2611 } else if path.is_file() {
2612 std::fs::remove_file(&path)?;
2613 }
2614 tracing::info!("DeleteFile: deleted {:?}", path);
2615 }
2616 }
2617 Ok(())
2618 }
2619
2620 pub(crate) fn apply_workspace_edit(
2624 &mut self,
2625 workspace_edit: lsp_types::WorkspaceEdit,
2626 ) -> AnyhowResult<usize> {
2627 tracing::debug!(
2628 "Applying WorkspaceEdit: changes={:?}, document_changes={:?}",
2629 workspace_edit.changes.as_ref().map(|c| c.len()),
2630 workspace_edit.document_changes.as_ref().map(|dc| match dc {
2631 lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
2632 lsp_types::DocumentChanges::Operations(o) => format!("{} operations", o.len()),
2633 })
2634 );
2635
2636 let mut total_changes = 0;
2637
2638 if let Some(changes) = workspace_edit.changes {
2640 for (uri, edits) in changes {
2641 let uri = crate::app::types::LspUri::from_wire(uri);
2642 if let Ok(path) =
2643 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2644 {
2645 let buffer_id = match self.open_file(&path) {
2646 Ok(id) => id,
2647 Err(e) => {
2648 if let Some(confirmation) = e.downcast_ref::<
2649 crate::model::buffer::LargeFileEncodingConfirmation,
2650 >() {
2651 self.start_large_file_encoding_confirmation(confirmation);
2652 } else {
2653 self.set_status_message(
2654 t!("file.error_opening", error = e.to_string())
2655 .to_string(),
2656 );
2657 }
2658 return Ok(0);
2659 }
2660 };
2661 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
2662 }
2663 }
2664 }
2665
2666 if let Some(document_changes) = workspace_edit.document_changes {
2668 use lsp_types::DocumentChanges;
2669
2670 match document_changes {
2671 DocumentChanges::Edits(edits) => {
2672 for text_doc_edit in edits {
2673 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2674 }
2675 }
2676 DocumentChanges::Operations(ops) => {
2677 for op in ops {
2680 match op {
2681 lsp_types::DocumentChangeOperation::Edit(text_doc_edit) => {
2682 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2683 }
2684 lsp_types::DocumentChangeOperation::Op(resource_op) => {
2685 self.apply_resource_operation(resource_op)?;
2686 total_changes += 1;
2687 }
2688 }
2689 }
2690 }
2691 }
2692 }
2693
2694 Ok(total_changes)
2695 }
2696
2697 pub fn handle_rename_response(
2699 &mut self,
2700 _request_id: u64,
2701 result: Result<lsp_types::WorkspaceEdit, String>,
2702 ) -> AnyhowResult<()> {
2703 match result {
2704 Ok(workspace_edit) => {
2705 let total_changes = self.apply_workspace_edit(workspace_edit)?;
2706 self.active_window_mut().status_message =
2707 Some(t!("lsp.renamed", count = total_changes).to_string());
2708 }
2709 Err(error) => {
2710 if error.contains("content modified") || error.contains("-32801") {
2712 tracing::debug!(
2713 "LSP rename: ContentModified error (expected, ignoring): {}",
2714 error
2715 );
2716 self.active_window_mut().status_message =
2717 Some(t!("lsp.rename_cancelled").to_string());
2718 } else {
2719 self.active_window_mut().status_message =
2720 Some(t!("lsp.rename_failed", error = &error).to_string());
2721 }
2722 }
2723 }
2724
2725 Ok(())
2726 }
2727
2728 pub(crate) fn apply_events_to_buffer_as_bulk_edit(
2733 &mut self,
2734 buffer_id: BufferId,
2735 events: Vec<Event>,
2736 description: String,
2737 ) -> AnyhowResult<()> {
2738 use crate::model::event::CursorId;
2739
2740 if events.is_empty() {
2741 return Ok(());
2742 }
2743
2744 let batch_for_lsp = Event::Batch {
2746 events: events.clone(),
2747 description: description.clone(),
2748 };
2749
2750 let original_active = self.active_buffer();
2762 self.windows
2763 .get_mut(&self.active_window)
2764 .and_then(|w| w.split_manager_mut())
2765 .expect("active window must have a populated split layout")
2766 .set_active_buffer_id(buffer_id);
2767 let lsp_changes = self.active_window().collect_lsp_changes(&batch_for_lsp);
2768 self.windows
2769 .get_mut(&self.active_window)
2770 .and_then(|w| w.split_manager_mut())
2771 .expect("active window must have a populated split layout")
2772 .set_active_buffer_id(original_active);
2773
2774 let split_id_for_cursors = self
2777 .split_manager_mut()
2778 .splits_for_buffer(buffer_id)
2779 .into_iter()
2780 .next()
2781 .unwrap_or_else(|| {
2782 self.windows
2783 .get(&self.active_window)
2784 .and_then(|w| w.buffers.splits())
2785 .map(|(mgr, _)| mgr)
2786 .expect("active window must have a populated split layout")
2787 .active_split()
2788 });
2789 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2790 .windows
2791 .get(&self.active_window)
2792 .and_then(|w| w.buffers.splits())
2793 .map(|(_, vs)| vs)
2794 .expect("active window must have a populated split layout")
2795 .get(&split_id_for_cursors)
2796 .and_then(|vs| vs.keyed_states.get(&buffer_id))
2797 .map(|bvs| {
2798 bvs.cursors
2799 .iter()
2800 .map(|(id, c)| (id, c.position, c.anchor))
2801 .collect()
2802 })
2803 .unwrap_or_default();
2804
2805 let __win = self
2812 .windows
2813 .get_mut(&self.active_window)
2814 .expect("active window must exist");
2815 let bulk_edit = __win
2816 .buffers
2817 .with_buffer_and_view_states(buffer_id, |state, vs_map| -> AnyhowResult<Event> {
2818 let old_snapshot = state.buffer.snapshot_buffer_state();
2820
2821 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2823 for event in &events {
2824 match event {
2825 Event::Insert { position, text, .. } => {
2826 edits.push((*position, 0, text.clone()));
2827 }
2828 Event::Delete { range, .. } => {
2829 edits.push((range.start, range.len(), String::new()));
2830 }
2831 _ => {}
2832 }
2833 }
2834
2835 edits.sort_by(|a, b| b.0.cmp(&a.0));
2837
2838 let edit_refs: Vec<(usize, usize, &str)> = edits
2840 .iter()
2841 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2842 .collect();
2843
2844 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2846
2847 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2849
2850 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2852 for (pos, del_len, text) in &edits {
2853 let delta = text.len() as isize - *del_len as isize;
2854 position_deltas.push((*pos, delta));
2855 }
2856 position_deltas.sort_by_key(|(pos, _)| *pos);
2857
2858 let calc_shift = |original_pos: usize| -> isize {
2859 let mut shift: isize = 0;
2860 for (edit_pos, delta) in &position_deltas {
2861 if *edit_pos < original_pos {
2862 shift += delta;
2863 }
2864 }
2865 shift
2866 };
2867
2868 let buffer_len = state.buffer.len();
2870 let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
2871 .iter()
2872 .map(|(id, pos, anchor)| {
2873 let shift = calc_shift(*pos);
2874 let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
2875 let new_anchor = anchor.map(|a| {
2876 let anchor_shift = calc_shift(a);
2877 ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
2878 });
2879 (*id, new_pos, new_anchor)
2880 })
2881 .collect();
2882
2883 let new_snapshot = state.buffer.snapshot_buffer_state();
2885
2886 state.highlighter.invalidate_all();
2888
2889 if let Some(vs) = vs_map.get_mut(&split_id_for_cursors) {
2891 if let Some(bvs) = vs.keyed_states.get_mut(&buffer_id) {
2892 for (cursor_id, new_pos, new_anchor) in &new_cursors {
2893 if let Some(cursor) = bvs.cursors.get_mut(*cursor_id) {
2894 cursor.position = *new_pos;
2895 cursor.anchor = *new_anchor;
2896 }
2897 }
2898 }
2899 }
2900
2901 let edit_lengths: Vec<(usize, usize, usize)> = {
2904 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2905 for (pos, del_len, text) in &edits {
2906 if let Some(last) = lengths.last_mut() {
2907 if last.0 == *pos {
2908 last.1 += del_len;
2909 last.2 += text.len();
2910 continue;
2911 }
2912 }
2913 lengths.push((*pos, *del_len, text.len()));
2914 }
2915 lengths
2916 };
2917
2918 for &(pos, del_len, ins_len) in &edit_lengths {
2920 if del_len > 0 && ins_len > 0 {
2921 if ins_len > del_len {
2922 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2923 state.margins.adjust_for_insert(pos, ins_len - del_len);
2924 } else if del_len > ins_len {
2925 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2926 state.margins.adjust_for_delete(pos, del_len - ins_len);
2927 }
2928 } else if del_len > 0 {
2929 state.marker_list.adjust_for_delete(pos, del_len);
2930 state.margins.adjust_for_delete(pos, del_len);
2931 } else if ins_len > 0 {
2932 state.marker_list.adjust_for_insert(pos, ins_len);
2933 state.margins.adjust_for_insert(pos, ins_len);
2934 }
2935 }
2936
2937 Ok(Event::BulkEdit {
2938 old_snapshot: Some(old_snapshot),
2939 new_snapshot: Some(new_snapshot),
2940 old_cursors,
2941 new_cursors,
2942 description,
2943 edits: edit_lengths,
2944 displaced_markers,
2945 })
2946 })
2947 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))??;
2948
2949 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
2951 event_log.append(bulk_edit);
2952 }
2953
2954 self.active_window_mut()
2956 .send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2957
2958 Ok(())
2959 }
2960
2961 pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2963 if self.active_window().server_supports_prepare_rename() {
2965 self.active_window_mut().send_prepare_rename();
2966 return Ok(());
2967 }
2968
2969 self.show_rename_prompt()
2970 }
2971
2972 pub(crate) fn handle_prepare_rename_response(
2974 &mut self,
2975 result: Result<serde_json::Value, String>,
2976 ) {
2977 match result {
2978 Ok(value) if !value.is_null() => {
2979 if let Err(e) = self.show_rename_prompt() {
2981 self.set_status_message(format!("Rename failed: {e}"));
2982 }
2983 }
2984 Ok(_) => {
2985 self.set_status_message("Cannot rename at this position".to_string());
2986 }
2987 Err(e) => {
2988 self.set_status_message(format!("Cannot rename: {e}"));
2989 }
2990 }
2991 }
2992
2993 fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2996 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2997
2998 let cursor_pos = self.active_cursors().primary().position;
3000 let (word_start, word_end) = {
3001 let state = self.active_state();
3002
3003 let word_start = find_word_start(&state.buffer, cursor_pos);
3005 let word_end = find_word_end(&state.buffer, cursor_pos);
3006
3007 if word_start >= word_end {
3009 self.active_window_mut().status_message =
3010 Some(t!("lsp.no_symbol_at_cursor").to_string());
3011 return Ok(());
3012 }
3013
3014 (word_start, word_end)
3015 };
3016
3017 let word_text = self.active_state_mut().get_text_range(word_start, word_end);
3019
3020 let overlay_handle = self.add_overlay(
3022 None,
3023 word_start..word_end,
3024 crate::model::event::OverlayFace::Background {
3025 color: (50, 100, 200), },
3027 100,
3028 Some(t!("lsp.popup_renaming").to_string()),
3029 );
3030
3031 let mut prompt = Prompt::new(
3034 "Rename to: ".to_string(),
3035 PromptType::LspRename {
3036 original_text: word_text.clone(),
3037 start_pos: word_start,
3038 end_pos: word_end,
3039 overlay_handle,
3040 },
3041 );
3042 prompt.set_input(word_text);
3044
3045 self.active_window_mut().prompt = Some(prompt);
3046 Ok(())
3047 }
3048
3049 pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
3051 self.remove_overlay(handle.clone());
3052 }
3053
3054 pub(crate) fn perform_lsp_rename(
3056 &mut self,
3057 new_name: String,
3058 original_text: String,
3059 start_pos: usize,
3060 overlay_handle: crate::view::overlay::OverlayHandle,
3061 ) {
3062 self.cancel_rename_overlay(&overlay_handle);
3064
3065 if new_name == original_text {
3067 self.active_window_mut().status_message = Some(t!("lsp.name_unchanged").to_string());
3068 return;
3069 }
3070
3071 let rename_pos = start_pos;
3074
3075 let state = self.active_state();
3078 let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
3079 let buffer_id = self.active_buffer();
3080 let request_id = self.active_window_mut().next_lsp_request_id;
3081
3082 let sent = self
3084 .with_lsp_for_buffer(buffer_id, LspFeature::Rename, |handle, uri, _language| {
3085 let result = handle.rename(
3086 request_id,
3087 uri.as_uri().clone(),
3088 line as u32,
3089 character as u32,
3090 new_name.clone(),
3091 );
3092 if result.is_ok() {
3093 tracing::info!(
3094 "Requested rename at {}:{}:{} to '{}'",
3095 uri.as_str(),
3096 line,
3097 character,
3098 new_name
3099 );
3100 }
3101 result.is_ok()
3102 })
3103 .unwrap_or(false);
3104
3105 if sent {
3106 self.active_window_mut().next_lsp_request_id += 1;
3107 } else if self
3108 .active_window()
3109 .buffer_metadata
3110 .get(&buffer_id)
3111 .and_then(|m| m.file_path())
3112 .is_none()
3113 {
3114 self.active_window_mut().status_message =
3115 Some(t!("lsp.cannot_rename_unsaved").to_string());
3116 }
3117 }
3118
3119 pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
3121 let buffer_id = self.active_buffer();
3122 self.request_inlay_hints_for_buffer(buffer_id);
3123 }
3124
3125 pub(crate) fn request_inlay_hints_for_buffer(&mut self, buffer_id: BufferId) {
3127 if !self.config.editor.enable_inlay_hints {
3128 return;
3129 }
3130
3131 let (line_count, version) = if let Some(state) = self
3135 .windows
3136 .get(&self.active_window)
3137 .map(|w| &w.buffers)
3138 .expect("active window present")
3139 .get(&buffer_id)
3140 {
3141 (
3142 state.buffer.line_count().unwrap_or(1000),
3143 state.buffer.version(),
3144 )
3145 } else {
3146 return;
3147 };
3148 let last_line = line_count.saturating_sub(1) as u32;
3149 let request_id = self.active_window_mut().next_lsp_request_id;
3150
3151 let sent = self
3153 .with_lsp_for_buffer(
3154 buffer_id,
3155 LspFeature::InlayHints,
3156 |handle, uri, _language| {
3157 let result = handle.inlay_hints(
3158 request_id,
3159 uri.as_uri().clone(),
3160 0,
3161 0,
3162 last_line,
3163 10000,
3164 );
3165 if result.is_ok() {
3166 tracing::info!(
3167 "Requested inlay hints for {} (request_id={})",
3168 uri.as_str(),
3169 request_id
3170 );
3171 } else if let Err(e) = &result {
3172 tracing::debug!("Failed to request inlay hints: {}", e);
3173 }
3174 result.is_ok()
3175 },
3176 )
3177 .unwrap_or(false);
3178
3179 if sent {
3180 self.active_window_mut().next_lsp_request_id += 1;
3181 self.active_window_mut()
3182 .pending_inlay_hints_requests
3183 .insert(request_id, super::InlayHintsRequest { buffer_id, version });
3184 }
3185 }
3186
3187 pub(crate) fn maybe_request_folding_ranges_debounced(&mut self, buffer_id: BufferId) {
3189 let Some(ready_at) = self
3190 .active_window()
3191 .folding_ranges_debounce
3192 .get(&buffer_id)
3193 .copied()
3194 else {
3195 return;
3196 };
3197 if Instant::now() < ready_at {
3198 return;
3199 }
3200
3201 self.active_window_mut()
3202 .folding_ranges_debounce
3203 .remove(&buffer_id);
3204 self.request_folding_ranges_for_buffer(buffer_id);
3205 }
3206
3207 pub(crate) fn request_folding_ranges_for_buffer(&mut self, buffer_id: BufferId) {
3209 if self
3210 .active_window_mut()
3211 .folding_ranges_in_flight
3212 .contains_key(&buffer_id)
3213 {
3214 return;
3215 }
3216
3217 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3218 return;
3219 };
3220 if !metadata.lsp_enabled {
3221 return;
3222 }
3223 let Some(uri) = metadata.file_uri().cloned() else {
3224 return;
3225 };
3226 let file_path = metadata.file_path().cloned();
3227
3228 let Some(language) = self
3229 .windows
3230 .get(&self.active_window)
3231 .map(|w| &w.buffers)
3232 .expect("active window present")
3233 .get(&buffer_id)
3234 .map(|s| s.language.clone())
3235 else {
3236 return;
3237 };
3238
3239 let __active_id = self.active_window;
3240 let __buffer_version_for_request = self
3243 .windows
3244 .get(&__active_id)
3245 .and_then(|w| w.buffers.get(&buffer_id))
3246 .map(|s| s.buffer.version())
3247 .unwrap_or(0);
3248
3249 let Some(__win) = self.windows.get_mut(&__active_id) else {
3250 return;
3251 };
3252 let __next_id = &mut __win.next_lsp_request_id;
3253 let __pending_folding = &mut __win.pending_folding_range_requests;
3254 let __folding_in_flight = &mut __win.folding_ranges_in_flight;
3255 let lsp = &mut __win.lsp;
3256
3257 if !lsp.folding_ranges_supported(&language) {
3258 return;
3259 }
3260
3261 use crate::services::lsp::manager::LspSpawnResult;
3263 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3264 return;
3265 }
3266
3267 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::FoldingRange) else {
3268 return;
3269 };
3270 let handle = &mut sh.handle;
3271
3272 let request_id = {
3273 let id = *__next_id;
3274 *__next_id += 1;
3275 id
3276 };
3277 let buffer_version = __buffer_version_for_request;
3278 let _ = __folding_in_flight;
3279
3280 match handle.folding_ranges(request_id, uri.as_uri().clone()) {
3281 Ok(()) => {
3282 __pending_folding.insert(
3283 request_id,
3284 super::FoldingRangeRequest {
3285 buffer_id,
3286 version: buffer_version,
3287 },
3288 );
3289 __folding_in_flight.insert(buffer_id, (request_id, buffer_version));
3290 }
3291 Err(e) => {
3292 tracing::debug!("Failed to request folding ranges: {}", e);
3293 }
3294 }
3295 }
3296
3297 pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
3299 if !self.config.editor.enable_semantic_tokens_full {
3300 return;
3301 }
3302
3303 if self
3305 .active_window_mut()
3306 .semantic_tokens_in_flight
3307 .contains_key(&buffer_id)
3308 {
3309 return;
3310 }
3311
3312 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3313 return;
3314 };
3315 if !metadata.lsp_enabled {
3316 return;
3317 }
3318 let Some(uri) = metadata.file_uri().cloned() else {
3319 return;
3320 };
3321 let file_path_for_spawn = metadata.file_path().cloned();
3322 let Some(language) = self
3324 .windows
3325 .get(&self.active_window)
3326 .map(|w| &w.buffers)
3327 .expect("active window present")
3328 .get(&buffer_id)
3329 .map(|s| s.language.clone())
3330 else {
3331 return;
3332 };
3333
3334 let __active_id = self.active_window;
3335 let Some((buffer_version, existing_version, previous_result_id)) = self
3338 .windows
3339 .get(&__active_id)
3340 .and_then(|w| w.buffers.get(&buffer_id))
3341 .map(|state| {
3342 (
3343 state.buffer.version(),
3344 state.semantic_tokens.as_ref().map(|s| s.version),
3345 state
3346 .semantic_tokens
3347 .as_ref()
3348 .and_then(|s| s.result_id.clone()),
3349 )
3350 })
3351 else {
3352 return;
3353 };
3354 if Some(buffer_version) == existing_version {
3355 return; }
3357
3358 let Some(__win) = self.windows.get_mut(&__active_id) else {
3359 return;
3360 };
3361 let __next_id = &mut __win.next_lsp_request_id;
3362 let __pending_st = &mut __win.pending_semantic_token_requests;
3363 let __st_in_flight = &mut __win.semantic_tokens_in_flight;
3364 let lsp = &mut __win.lsp;
3365
3366 use crate::services::lsp::manager::LspSpawnResult;
3368 if lsp.try_spawn(&language, file_path_for_spawn.as_deref()) != LspSpawnResult::Spawned {
3369 return;
3370 }
3371
3372 if !lsp.semantic_tokens_full_supported(&language) {
3374 return;
3375 }
3376 if lsp.semantic_tokens_legend(&language).is_none() {
3377 return;
3378 }
3379
3380 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3381 return;
3382 };
3383 let supports_delta = sh.capabilities.semantic_tokens_full_delta;
3385 let use_delta = previous_result_id.is_some() && supports_delta;
3386 let handle = &mut sh.handle;
3387
3388 let request_id = {
3389 let id = *__next_id;
3390 *__next_id += 1;
3391 id
3392 };
3393
3394 let request_kind = if use_delta {
3395 super::SemanticTokensFullRequestKind::FullDelta
3396 } else {
3397 super::SemanticTokensFullRequestKind::Full
3398 };
3399
3400 let request_result = if use_delta {
3401 handle.semantic_tokens_full_delta(
3402 request_id,
3403 uri.as_uri().clone(),
3404 previous_result_id.unwrap(),
3405 )
3406 } else {
3407 handle.semantic_tokens_full(request_id, uri.as_uri().clone())
3408 };
3409
3410 match request_result {
3411 Ok(_) => {
3412 __pending_st.insert(
3413 request_id,
3414 super::SemanticTokenFullRequest {
3415 buffer_id,
3416 version: buffer_version,
3417 kind: request_kind,
3418 },
3419 );
3420 __st_in_flight.insert(buffer_id, (request_id, buffer_version, request_kind));
3421 }
3422 Err(e) => {
3423 tracing::debug!("Failed to request semantic tokens: {}", e);
3424 }
3425 }
3426 }
3427
3428 pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
3430 if !self.config.editor.enable_semantic_tokens_full {
3431 self.active_window_mut()
3432 .semantic_tokens_full_debounce
3433 .remove(&buffer_id);
3434 return;
3435 }
3436
3437 let Some(ready_at) = self
3438 .active_window()
3439 .semantic_tokens_full_debounce
3440 .get(&buffer_id)
3441 .copied()
3442 else {
3443 return;
3444 };
3445 if Instant::now() < ready_at {
3446 return;
3447 }
3448
3449 self.active_window_mut()
3450 .semantic_tokens_full_debounce
3451 .remove(&buffer_id);
3452 self.maybe_request_semantic_tokens(buffer_id);
3453 }
3454
3455 pub(crate) fn maybe_request_semantic_tokens_range(
3457 &mut self,
3458 buffer_id: BufferId,
3459 start_line: usize,
3460 end_line: usize,
3461 ) {
3462 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3463 return;
3464 };
3465 if !metadata.lsp_enabled {
3466 return;
3467 }
3468 let Some(uri) = metadata.file_uri().cloned() else {
3469 return;
3470 };
3471 let file_path = metadata.file_path().cloned();
3472 let Some(language) = self
3474 .windows
3475 .get(&self.active_window)
3476 .map(|w| &w.buffers)
3477 .expect("active window present")
3478 .get(&buffer_id)
3479 .map(|s| s.language.clone())
3480 else {
3481 return;
3482 };
3483
3484 let __active_id = self.active_window;
3485 let __win = self
3488 .windows
3489 .get_mut(&__active_id)
3490 .expect("active window must exist");
3491 let __next_id = &mut __win.next_lsp_request_id;
3492 let __pending_st_range = &mut __win.pending_semantic_token_range_requests;
3493 let __st_range_in_flight = &mut __win.semantic_tokens_range_in_flight;
3494 let __st_range_last = &mut __win.semantic_tokens_range_last_request;
3495 let __st_range_applied = &__win.semantic_tokens_range_applied;
3496 let lsp = &mut __win.lsp;
3497 let __buffers_ref: &crate::app::window::WindowBuffers = &__win.buffers;
3498
3499 use crate::services::lsp::manager::LspSpawnResult;
3501 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3502 return;
3503 }
3504
3505 if !lsp.semantic_tokens_range_supported(&language) {
3506 self.maybe_request_semantic_tokens(buffer_id);
3508 return;
3509 }
3510 if lsp.semantic_tokens_legend(&language).is_none() {
3511 return;
3512 }
3513
3514 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3515 return;
3516 };
3517 if !sh.capabilities.semantic_tokens_range {
3520 return;
3521 }
3522 let handle = &mut sh.handle;
3523 let Some(state) = __buffers_ref.get(&buffer_id) else {
3524 return;
3525 };
3526
3527 let buffer_version = state.buffer.version();
3528 let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3529 let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3530
3531 if let Some(line_count) = state.buffer.line_count() {
3532 if line_count == 0 {
3533 return;
3534 }
3535 let max_line = line_count.saturating_sub(1);
3536 padded_start = padded_start.min(max_line);
3537 padded_end = padded_end.min(max_line);
3538 }
3539
3540 let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
3541 let end_char = state
3542 .buffer
3543 .get_line(padded_end)
3544 .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
3545 .unwrap_or(0);
3546 let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
3547 state.buffer.lsp_position_to_byte(padded_end, end_char)
3548 } else {
3549 state.buffer.len()
3550 };
3551
3552 if start_byte >= end_byte {
3553 return;
3554 }
3555
3556 let range = start_byte..end_byte;
3557 if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
3558 __st_range_in_flight.get(&buffer_id).copied()
3559 {
3560 if in_flight_start == padded_start
3561 && in_flight_end == padded_end
3562 && in_flight_version == buffer_version
3563 {
3564 return;
3565 }
3566 if let Err(e) = handle.cancel_request(in_flight_id) {
3567 tracing::debug!("Failed to cancel semantic token range request: {}", e);
3568 }
3569 __pending_st_range.remove(&in_flight_id);
3570 __st_range_in_flight.remove(&buffer_id);
3571 }
3572
3573 if let Some((applied_start, applied_end, applied_version)) =
3574 __st_range_applied.get(&buffer_id).copied()
3575 {
3576 if applied_start == padded_start
3577 && applied_end == padded_end
3578 && applied_version == buffer_version
3579 {
3580 return;
3581 }
3582 }
3583
3584 let now = Instant::now();
3585 if let Some((last_start, last_end, last_version, last_time)) =
3586 __st_range_last.get(&buffer_id).copied()
3587 {
3588 if last_start == padded_start
3589 && last_end == padded_end
3590 && last_version == buffer_version
3591 && now.duration_since(last_time)
3592 < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
3593 {
3594 return;
3595 }
3596 }
3597
3598 let lsp_range = lsp_types::Range {
3599 start: lsp_types::Position {
3600 line: padded_start as u32,
3601 character: 0,
3602 },
3603 end: lsp_types::Position {
3604 line: padded_end as u32,
3605 character: end_char as u32,
3606 },
3607 };
3608
3609 let request_id = {
3610 let id = *__next_id;
3611 *__next_id += 1;
3612 id
3613 };
3614 let _ = __st_range_applied;
3615
3616 match handle.semantic_tokens_range(request_id, uri.as_uri().clone(), lsp_range) {
3617 Ok(_) => {
3618 __pending_st_range.insert(
3619 request_id,
3620 SemanticTokenRangeRequest {
3621 buffer_id,
3622 version: buffer_version,
3623 range: range.clone(),
3624 start_line: padded_start,
3625 end_line: padded_end,
3626 },
3627 );
3628 __st_range_in_flight.insert(
3629 buffer_id,
3630 (request_id, padded_start, padded_end, buffer_version),
3631 );
3632 __st_range_last.insert(buffer_id, (padded_start, padded_end, buffer_version, now));
3633 }
3634 Err(e) => {
3635 tracing::debug!("Failed to request semantic token range: {}", e);
3636 }
3637 }
3638 }
3639}
3640
3641#[cfg(test)]
3642mod tests {
3643 use crate::model::filesystem::StdFileSystem;
3644 use std::sync::Arc;
3645
3646 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
3647 Arc::new(StdFileSystem)
3648 }
3649 use super::{lsp_range_contains, lsp_range_overlaps, Editor};
3650
3651 fn range(sl: u32, sc: u32, el: u32, ec: u32) -> lsp_types::Range {
3652 lsp_types::Range {
3653 start: lsp_types::Position {
3654 line: sl,
3655 character: sc,
3656 },
3657 end: lsp_types::Position {
3658 line: el,
3659 character: ec,
3660 },
3661 }
3662 }
3663
3664 #[test]
3665 fn test_lsp_range_contains_inclusive_start_exclusive_end() {
3666 let r = range(3, 10, 3, 20);
3667 assert!(!lsp_range_contains(&r, 3, 9));
3669 assert!(!lsp_range_contains(&r, 2, 50));
3670 assert!(lsp_range_contains(&r, 3, 10));
3672 assert!(lsp_range_contains(&r, 3, 15));
3674 assert!(lsp_range_contains(&r, 3, 19));
3676 assert!(!lsp_range_contains(&r, 3, 20));
3678 assert!(!lsp_range_contains(&r, 3, 21));
3680 assert!(!lsp_range_contains(&r, 4, 0));
3681 }
3682
3683 #[test]
3684 fn test_lsp_range_contains_multiline() {
3685 let r = range(2, 5, 4, 3);
3686 assert!(!lsp_range_contains(&r, 1, 100));
3688 assert!(!lsp_range_contains(&r, 2, 4));
3690 assert!(lsp_range_contains(&r, 2, 5));
3692 assert!(lsp_range_contains(&r, 3, 0));
3694 assert!(lsp_range_contains(&r, 3, 9999));
3695 assert!(lsp_range_contains(&r, 4, 2));
3697 assert!(!lsp_range_contains(&r, 4, 3));
3699 assert!(!lsp_range_contains(&r, 5, 0));
3701 }
3702
3703 #[test]
3704 fn test_lsp_range_contains_zero_length_matches_anchor_only() {
3705 let r = range(7, 4, 7, 4);
3707 assert!(lsp_range_contains(&r, 7, 4));
3708 assert!(!lsp_range_contains(&r, 7, 3));
3709 assert!(!lsp_range_contains(&r, 7, 5));
3710 assert!(!lsp_range_contains(&r, 6, 4));
3711 assert!(!lsp_range_contains(&r, 8, 4));
3712 }
3713
3714 #[test]
3715 fn test_lsp_range_overlaps_point_cursor_in_diagnostic_range() {
3716 let diag = range(3, 10, 3, 20);
3719 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 10));
3721 assert!(lsp_range_overlaps(&diag, 3, 15, 3, 15));
3723 assert!(!lsp_range_overlaps(&diag, 3, 20, 3, 20));
3725 assert!(!lsp_range_overlaps(&diag, 3, 9, 3, 9));
3727 assert!(!lsp_range_overlaps(&diag, 4, 15, 4, 15));
3729 }
3730
3731 #[test]
3732 fn test_lsp_range_overlaps_selection_intersects_diagnostic() {
3733 let diag = range(3, 10, 3, 20);
3735 assert!(lsp_range_overlaps(&diag, 3, 12, 3, 18));
3737 assert!(lsp_range_overlaps(&diag, 3, 5, 3, 15));
3739 assert!(lsp_range_overlaps(&diag, 3, 15, 3, 25));
3741 assert!(lsp_range_overlaps(&diag, 3, 0, 3, 30));
3743 assert!(!lsp_range_overlaps(&diag, 3, 0, 3, 10));
3745 assert!(!lsp_range_overlaps(&diag, 3, 20, 3, 30));
3747 assert!(!lsp_range_overlaps(&diag, 4, 0, 4, 100));
3749 }
3750
3751 #[test]
3752 fn test_lsp_range_overlaps_point_diagnostic_within_selection() {
3753 let diag = range(3, 10, 3, 10);
3755 assert!(lsp_range_overlaps(&diag, 3, 5, 3, 15));
3757 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 15));
3759 assert!(!lsp_range_overlaps(&diag, 3, 0, 3, 10));
3761 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 10));
3763 assert!(!lsp_range_overlaps(&diag, 3, 9, 3, 9));
3765 }
3766
3767 #[test]
3768 fn test_lsp_range_overlaps_multiline() {
3769 let diag = range(2, 5, 4, 3);
3771 assert!(lsp_range_overlaps(&diag, 3, 0, 3, 0));
3773 assert!(lsp_range_overlaps(&diag, 1, 0, 2, 6));
3775 assert!(!lsp_range_overlaps(&diag, 4, 3, 4, 10));
3777 assert!(!lsp_range_overlaps(&diag, 5, 0, 5, 10));
3779 }
3780
3781 use crate::model::buffer::Buffer;
3782 use crate::state::EditorState;
3783 use crate::view::virtual_text::VirtualTextPosition;
3784 use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
3785
3786 fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
3787 InlayHint {
3788 position: Position { line, character },
3789 label: InlayHintLabel::String(label.to_string()),
3790 kind,
3791 text_edits: None,
3792 tooltip: None,
3793 padding_left: None,
3794 padding_right: None,
3795 data: None,
3796 }
3797 }
3798
3799 #[test]
3800 fn test_inlay_hint_inserts_before_character() {
3801 let mut state = EditorState::new(
3802 80,
3803 24,
3804 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3805 test_fs(),
3806 );
3807 state.buffer = Buffer::from_str_test("ab");
3808
3809 if !state.buffer.is_empty() {
3810 state.marker_list.adjust_for_insert(0, state.buffer.len());
3811 }
3812
3813 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3814 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3815
3816 let lookup = state
3817 .virtual_texts
3818 .build_lookup(&state.marker_list, 0, state.buffer.len());
3819 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3820 assert_eq!(vtexts.len(), 1);
3821 assert_eq!(vtexts[0].text, ": i32");
3822 assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
3823 }
3824
3825 #[test]
3826 fn test_inlay_hint_at_eof_renders_after_last_char() {
3827 let mut state = EditorState::new(
3828 80,
3829 24,
3830 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3831 test_fs(),
3832 );
3833 state.buffer = Buffer::from_str_test("ab");
3834
3835 if !state.buffer.is_empty() {
3836 state.marker_list.adjust_for_insert(0, state.buffer.len());
3837 }
3838
3839 let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
3840 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3841
3842 let lookup = state
3843 .virtual_texts
3844 .build_lookup(&state.marker_list, 0, state.buffer.len());
3845 let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
3846 assert_eq!(vtexts.len(), 1);
3847 assert_eq!(vtexts[0].text, ": i32");
3848 assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
3849 }
3850
3851 #[test]
3852 fn test_inlay_hint_empty_buffer_is_ignored() {
3853 let mut state = EditorState::new(
3854 80,
3855 24,
3856 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3857 test_fs(),
3858 );
3859 state.buffer = Buffer::from_str_test("");
3860
3861 let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
3862 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3863
3864 assert!(state.virtual_texts.is_empty());
3865 }
3866
3867 #[test]
3868 fn test_inlay_hint_uses_theme_key_for_foreground() {
3869 let mut state = EditorState::new(
3872 80,
3873 24,
3874 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3875 test_fs(),
3876 );
3877 state.buffer = Buffer::from_str_test("ab");
3878
3879 if !state.buffer.is_empty() {
3880 state.marker_list.adjust_for_insert(0, state.buffer.len());
3881 }
3882
3883 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3884 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3885
3886 let lookup = state
3887 .virtual_texts
3888 .build_lookup(&state.marker_list, 0, state.buffer.len());
3889 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3890 assert_eq!(
3891 vtexts[0].fg_theme_key.as_deref(),
3892 Some("editor.line_number_fg")
3893 );
3894 assert_eq!(vtexts[0].bg_theme_key, None);
3895 }
3896
3897 #[test]
3898 fn test_inlay_hint_removed_when_its_range_is_deleted() {
3899 let mut state = EditorState::new(
3906 80,
3907 24,
3908 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3909 test_fs(),
3910 );
3911 state.buffer = Buffer::from_str_test("let x = 42;");
3912 state.marker_list.adjust_for_insert(0, state.buffer.len());
3913
3914 let hints = vec![make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE))];
3916 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3917 assert_eq!(state.virtual_texts.len(), 1);
3918
3919 let removed = state
3922 .virtual_texts
3923 .remove_in_range(&mut state.marker_list, 4, 10);
3924 assert_eq!(removed, 1, "hint inside deleted range must be removed");
3925 assert!(state.virtual_texts.is_empty());
3926 }
3927
3928 #[test]
3929 fn test_marker_delete_after_repeat_clear_recreate() {
3930 use crate::model::marker::MarkerList;
3936 use crate::view::virtual_text::{VirtualTextManager, VirtualTextPosition};
3937 use ratatui::style::Style;
3938
3939 let mut markers = MarkerList::new();
3940 let mut vtexts = VirtualTextManager::new();
3941
3942 let positions = [200usize, 401, 602, 803, 1205, 1406];
3944 for &p in &positions {
3945 vtexts.add(
3946 &mut markers,
3947 p,
3948 format!("hint-at-{p}"),
3949 Style::default(),
3950 VirtualTextPosition::BeforeChar,
3951 0,
3952 );
3953 }
3954
3955 for _ in 0..3 {
3958 vtexts.clear(&mut markers);
3959 for &p in &positions {
3960 vtexts.add(
3961 &mut markers,
3962 p,
3963 format!("hint-at-{p}"),
3964 Style::default(),
3965 VirtualTextPosition::BeforeChar,
3966 0,
3967 );
3968 }
3969 }
3970
3971 let removed = vtexts.remove_in_range(&mut markers, 1005, 1206);
3973 assert_eq!(
3974 removed, 1,
3975 "exactly one marker inside [1005, 1206) should be removed"
3976 );
3977 markers.adjust_for_delete(1005, 201);
3978
3979 let lookup = vtexts.build_lookup(&markers, 0, 10_000);
3980 let mut positions: Vec<usize> = lookup.keys().copied().collect();
3981 positions.sort();
3982 assert_eq!(
3983 positions,
3984 vec![200, 401, 602, 803, 1205],
3985 "after delete+adjust, expected marker byte positions {:?}, got {:?}",
3986 vec![200, 401, 602, 803, 1205],
3987 positions
3988 );
3989 }
3990
3991 #[test]
3992 fn test_marker_delete_then_adjust_preserves_last_marker_position() {
3993 use crate::model::marker::MarkerList;
4007
4008 let mut markers = MarkerList::new();
4009 let m0 = markers.create(200, false);
4010 let m1 = markers.create(401, false);
4011 let m2 = markers.create(602, false);
4012 let m3 = markers.create(803, false);
4013 let m5 = markers.create(1205, false);
4014 let m6 = markers.create(1406, false);
4015
4016 markers.delete(m5);
4018
4019 markers.adjust_for_delete(1005, 201);
4021
4022 assert_eq!(markers.get_position(m0), Some(200), "m0 unchanged");
4023 assert_eq!(markers.get_position(m1), Some(401), "m1 unchanged");
4024 assert_eq!(markers.get_position(m2), Some(602), "m2 unchanged");
4025 assert_eq!(markers.get_position(m3), Some(803), "m3 unchanged");
4026 assert_eq!(
4027 markers.get_position(m6),
4028 Some(1205),
4029 "m6 must shift from 1406 to 1205 (1406 - 201), not be clamped to delete-start 1005"
4030 );
4031 }
4032
4033 #[test]
4034 fn test_inlay_hint_outside_deletion_survives() {
4035 let mut state = EditorState::new(
4037 80,
4038 24,
4039 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
4040 test_fs(),
4041 );
4042 state.buffer = Buffer::from_str_test("let x = 42; let y = 0;");
4043 state.marker_list.adjust_for_insert(0, state.buffer.len());
4044
4045 let hints = vec![
4046 make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE)), make_hint(0, 17, ": i32", Some(InlayHintKind::TYPE)), ];
4049 Editor::apply_inlay_hints_to_state(&mut state, &hints);
4050 assert_eq!(state.virtual_texts.len(), 2);
4051
4052 let removed = state
4053 .virtual_texts
4054 .remove_in_range(&mut state.marker_list, 4, 10);
4055 assert_eq!(removed, 1);
4056 assert_eq!(state.virtual_texts.len(), 1);
4057 }
4058
4059 #[test]
4060 fn test_space_doc_paragraphs_inserts_blank_lines() {
4061 use super::space_doc_paragraphs;
4062
4063 let input = "sep\n description.\nend\n another.";
4065 let result = space_doc_paragraphs(input);
4066 assert_eq!(result, "sep\n\n description.\n\nend\n\n another.");
4067 }
4068
4069 #[test]
4070 fn test_space_doc_paragraphs_preserves_existing_blank_lines() {
4071 use super::space_doc_paragraphs;
4072
4073 let input = "First paragraph.\n\nSecond paragraph.";
4075 let result = space_doc_paragraphs(input);
4076 assert_eq!(result, "First paragraph.\n\nSecond paragraph.");
4077 }
4078
4079 #[test]
4080 fn test_space_doc_paragraphs_plain_text() {
4081 use super::space_doc_paragraphs;
4082
4083 let input = "Just a single line of docs.";
4084 let result = space_doc_paragraphs(input);
4085 assert_eq!(result, "Just a single line of docs.");
4086 }
4087}