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_signature_help(&mut self) {
1432 let cursor_pos = self.active_cursors().primary().position;
1434 let state = self.active_state();
1435
1436 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1438 let buffer_id = self.active_buffer();
1439 let request_id = self.active_window_mut().next_lsp_request_id;
1440
1441 let sent = self
1443 .with_lsp_for_buffer(
1444 buffer_id,
1445 LspFeature::SignatureHelp,
1446 |handle, uri, _language| {
1447 let result = handle.signature_help(
1448 request_id,
1449 uri.as_uri().clone(),
1450 line as u32,
1451 character as u32,
1452 );
1453 if result.is_ok() {
1454 tracing::info!(
1455 "Requested signature help at {}:{}:{} (byte_pos={})",
1456 uri.as_str(),
1457 line,
1458 character,
1459 cursor_pos
1460 );
1461 }
1462 result.is_ok()
1463 },
1464 )
1465 .unwrap_or(false);
1466
1467 if sent {
1468 self.active_window_mut().next_lsp_request_id += 1;
1469 self.active_window_mut().pending_signature_help_request = Some(request_id);
1470 }
1471 }
1472
1473 pub(crate) fn handle_signature_help_response(
1475 &mut self,
1476 request_id: u64,
1477 signature_help: Option<lsp_types::SignatureHelp>,
1478 ) {
1479 if self.active_window_mut().pending_signature_help_request != Some(request_id) {
1481 tracing::debug!("Ignoring stale signature help response: {}", request_id);
1482 return;
1483 }
1484
1485 self.active_window_mut().pending_signature_help_request = None;
1486 let signature_help = match signature_help {
1487 Some(help) if !help.signatures.is_empty() => help,
1488 _ => {
1489 tracing::debug!("No signature help available");
1490 return;
1491 }
1492 };
1493
1494 let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
1496 let signature = match signature_help.signatures.get(active_signature_idx) {
1497 Some(sig) => sig,
1498 None => return,
1499 };
1500
1501 let mut content = String::new();
1503
1504 content.push_str(&signature.label);
1506 content.push('\n');
1507
1508 let active_param = signature_help
1510 .active_parameter
1511 .or(signature.active_parameter)
1512 .unwrap_or(0) as usize;
1513
1514 if let Some(params) = &signature.parameters {
1516 if let Some(param) = params.get(active_param) {
1517 let param_label = match ¶m.label {
1519 lsp_types::ParameterLabel::Simple(s) => s.clone(),
1520 lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1521 let start = offsets[0] as usize;
1523 let end = offsets[1] as usize;
1524 if end <= signature.label.len() {
1525 signature.label[start..end].to_string()
1526 } else {
1527 String::new()
1528 }
1529 }
1530 };
1531
1532 if !param_label.is_empty() {
1533 content.push_str(&format!("\n> {}\n", param_label));
1534 }
1535
1536 if let Some(doc) = ¶m.documentation {
1538 let doc_text = match doc {
1539 lsp_types::Documentation::String(s) => s.clone(),
1540 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1541 };
1542 if !doc_text.is_empty() {
1543 content.push('\n');
1544 content.push_str(&doc_text);
1545 content.push('\n');
1546 }
1547 }
1548 }
1549 }
1550
1551 if let Some(doc) = &signature.documentation {
1553 let doc_text = match doc {
1554 lsp_types::Documentation::String(s) => s.clone(),
1555 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1556 };
1557 if !doc_text.is_empty() {
1558 content.push_str("\n---\n\n");
1559 content.push_str(&space_doc_paragraphs(&doc_text));
1560 }
1561 }
1562
1563 use crate::view::popup::{Popup, PopupPosition};
1565 use ratatui::style::Style;
1566
1567 let mut popup = Popup::markdown(
1568 &content,
1569 &*self.theme.read().unwrap(),
1570 Some(&self.grammar_registry),
1571 );
1572 popup.title = Some(t!("lsp.popup_signature").to_string());
1573 popup.transient = true;
1574 popup.position = PopupPosition::BelowCursor;
1575 popup.width = 60;
1576 popup.max_height = 20;
1577 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1578 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1579 popup.focus_key_hint = self.popup_focus_key_hint();
1580
1581 let __buffer_id = self.active_buffer();
1583 if let Some(state) = self
1584 .windows
1585 .get_mut(&self.active_window)
1586 .map(|w| &mut w.buffers)
1587 .expect("active window present")
1588 .get_mut(&__buffer_id)
1589 {
1590 state.popups.show(popup);
1591 tracing::info!(
1592 "Showing signature help popup for {} signatures",
1593 signature_help.signatures.len()
1594 );
1595 }
1596 }
1597
1598 pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1601 if !self
1610 .active_window()
1611 .pending_code_actions_requests
1612 .is_empty()
1613 {
1614 let ids: Vec<u64> = self
1615 .active_window_mut()
1616 .pending_code_actions_requests
1617 .drain()
1618 .collect();
1619 for request_id in ids {
1620 tracing::debug!(
1621 "Canceling previous pending LSP code actions request {}",
1622 request_id
1623 );
1624 self.active_window_mut().send_lsp_cancel_request(request_id);
1625 }
1626 }
1627 self.active_window_mut()
1628 .pending_code_actions_server_names
1629 .clear();
1630 self.active_window_mut().pending_code_actions = None;
1631
1632 let cursor_pos = self.active_cursors().primary().position;
1634 let selection_range = self.active_cursors().primary().selection_range();
1635 let state = self.active_state();
1636
1637 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1639
1640 let (start_line, start_char, end_line, end_char) = if let Some(range) = selection_range {
1642 let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1643 let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1644 (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1645 } else {
1646 (line as u32, character as u32, line as u32, character as u32)
1647 };
1648
1649 let buffer_id = self.active_buffer();
1650
1651 let diagnostics: Vec<lsp_types::Diagnostic> = {
1657 let window = self.active_window();
1658 window
1659 .buffer_metadata
1660 .get(&buffer_id)
1661 .and_then(|m| m.file_uri())
1662 .and_then(|uri| window.stored_diagnostics.get(uri.as_str()))
1663 .map(|diags| {
1664 diags
1665 .iter()
1666 .filter(|d| {
1667 lsp_range_overlaps(&d.range, start_line, start_char, end_line, end_char)
1668 })
1669 .cloned()
1670 .collect()
1671 })
1672 .unwrap_or_default()
1673 };
1674
1675 let base_request_id = self.active_window_mut().next_lsp_request_id;
1677 let counter = std::sync::atomic::AtomicU64::new(0);
1678
1679 let results = self.with_all_lsp_for_buffer_feature_named(
1680 buffer_id,
1681 LspFeature::CodeAction,
1682 |handle, uri, _language, server_name| {
1683 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1684 let request_id = base_request_id + idx;
1685 let result = handle.code_actions(
1686 request_id,
1687 uri.as_uri().clone(),
1688 start_line,
1689 start_char,
1690 end_line,
1691 end_char,
1692 diagnostics.clone(),
1693 );
1694 if result.is_ok() {
1695 tracing::info!(
1696 "Requested code actions at {}:{}:{}-{}:{} (byte_pos={}, request_id={}, server={})",
1697 uri.as_str(),
1698 start_line,
1699 start_char,
1700 end_line,
1701 end_char,
1702 cursor_pos,
1703 request_id,
1704 server_name
1705 );
1706 }
1707 (request_id, result.is_ok(), server_name.to_string())
1708 },
1709 );
1710
1711 let mut sent_ids = Vec::new();
1712 for (request_id, ok, server_name) in &results {
1713 if *ok {
1714 sent_ids.push(*request_id);
1715 self.active_window_mut()
1716 .pending_code_actions_server_names
1717 .insert(*request_id, server_name.clone());
1718 }
1719 }
1720 self.active_window_mut().next_lsp_request_id = base_request_id + results.len() as u64;
1722
1723 if !sent_ids.is_empty() {
1724 self.active_window_mut()
1727 .pending_code_actions_requests
1728 .extend(sent_ids);
1729 }
1730
1731 Ok(())
1732 }
1733
1734 pub(crate) fn handle_code_actions_response(
1738 &mut self,
1739 request_id: u64,
1740 actions: Vec<lsp_types::CodeActionOrCommand>,
1741 ) {
1742 if !self
1744 .active_window_mut()
1745 .pending_code_actions_requests
1746 .remove(&request_id)
1747 {
1748 tracing::debug!("Ignoring stale code actions response: {}", request_id);
1749 return;
1750 }
1751
1752 let server_name = self
1754 .active_window_mut()
1755 .pending_code_actions_server_names
1756 .remove(&request_id)
1757 .unwrap_or_default();
1758
1759 if actions.is_empty() {
1760 if self
1762 .active_window()
1763 .pending_code_actions_requests
1764 .is_empty()
1765 && self
1766 .active_window_mut()
1767 .pending_code_actions
1768 .as_ref()
1769 .is_none_or(|a| a.is_empty())
1770 {
1771 self.set_status_message(t!("lsp.no_code_actions").to_string());
1772 }
1773 return;
1774 }
1775
1776 let tagged_actions: Vec<(String, lsp_types::CodeActionOrCommand)> = actions
1778 .into_iter()
1779 .map(|a| (server_name.clone(), a))
1780 .collect();
1781
1782 match &mut self.active_window_mut().pending_code_actions {
1783 Some(existing) => {
1784 existing.extend(tagged_actions);
1785 tracing::debug!("Extended code actions, now {} total", existing.len());
1786 }
1787 None => {
1788 self.active_window_mut().pending_code_actions = Some(tagged_actions);
1789 }
1790 }
1791
1792 use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1794 use ratatui::style::Style;
1795
1796 let items: Vec<PopupListItem> = {
1797 let all_actions = self.active_window().pending_code_actions.as_ref().unwrap();
1798 let multiple_servers = {
1799 let mut names = std::collections::HashSet::new();
1800 for (name, _) in all_actions {
1801 names.insert(name.as_str());
1802 }
1803 names.len() > 1
1804 };
1805 all_actions
1806 .iter()
1807 .enumerate()
1808 .map(|(i, (srv_name, action))| {
1809 let title = match action {
1810 lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1811 lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1812 };
1813 let kind = match action {
1814 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1815 ca.kind.as_ref().map(|k| k.as_str().to_string())
1816 }
1817 _ => None,
1818 };
1819 let detail = if multiple_servers && !srv_name.is_empty() {
1820 match kind {
1821 Some(k) => Some(format!("[{}] {}", srv_name, k)),
1822 None => Some(format!("[{}]", srv_name)),
1823 }
1824 } else {
1825 kind
1826 };
1827 PopupListItem {
1828 text: format!("{}. {}", i + 1, title),
1829 detail,
1830 icon: None,
1831 data: Some(i.to_string()),
1832 disabled: false,
1833 }
1834 })
1835 .collect()
1836 };
1837
1838 let mut popup = Popup::list(items, &*self.theme.read().unwrap());
1839 popup.kind = crate::view::popup::PopupKind::Action;
1840 popup.title = Some(t!("lsp.popup_code_actions").to_string());
1841 popup.position = PopupPosition::BelowCursor;
1842 popup.width = 60;
1843 popup.max_height = 15;
1844 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1845 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1846 popup.resolver = crate::view::popup::PopupResolver::CodeAction;
1850 popup.focused = true;
1856
1857 let __buffer_id = self.active_buffer();
1859 let action_count = self
1860 .active_window()
1861 .pending_code_actions
1862 .as_ref()
1863 .map_or(0, |v| v.len());
1864 if let Some(state) = self
1865 .windows
1866 .get_mut(&self.active_window)
1867 .map(|w| &mut w.buffers)
1868 .expect("active window present")
1869 .get_mut(&__buffer_id)
1870 {
1871 state.popups.show_or_replace(popup);
1872 tracing::info!("Showing code actions popup with {} actions", action_count);
1873 }
1874 }
1875
1876 pub(crate) fn execute_code_action(&mut self, index: usize) {
1878 let action = match &self.active_window_mut().pending_code_actions {
1879 Some(actions) => actions.get(index).map(|(_, a)| a.clone()),
1880 None => None,
1881 };
1882
1883 let Some(action) = action else {
1884 tracing::warn!("Code action index {} out of range", index);
1885 return;
1886 };
1887
1888 match action {
1889 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1890 if ca.edit.is_none()
1893 && ca.command.is_none()
1894 && ca.data.is_some()
1895 && self.active_window().server_supports_code_action_resolve()
1896 {
1897 tracing::info!(
1898 "Code action '{}' needs resolve, sending codeAction/resolve",
1899 ca.title
1900 );
1901 self.send_code_action_resolve(ca);
1902 return;
1903 }
1904 self.execute_resolved_code_action(ca);
1905 }
1906 lsp_types::CodeActionOrCommand::Command(cmd) => {
1907 self.send_execute_command(cmd);
1908 }
1909 }
1910 }
1911
1912 pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1914 let title = ca.title.clone();
1915
1916 if let Some(edit) = ca.edit {
1918 match self.apply_workspace_edit(edit) {
1919 Ok(n) => {
1920 self.set_status_message(
1921 t!("lsp.code_action_applied", title = &title, count = n).to_string(),
1922 );
1923 }
1924 Err(e) => {
1925 self.set_status_message(format!("Code action failed: {e}"));
1926 return;
1927 }
1928 }
1929 }
1930
1931 if let Some(cmd) = ca.command {
1933 self.send_execute_command(cmd);
1934 }
1935 }
1936
1937 fn send_execute_command(&mut self, cmd: lsp_types::Command) {
1939 tracing::info!("Executing LSP command: {} ({})", cmd.title, cmd.command);
1940 self.set_status_message(
1941 t!(
1942 "lsp.code_action_applied",
1943 title = &cmd.title,
1944 count = 0_usize
1945 )
1946 .to_string(),
1947 );
1948
1949 let language = match self
1951 .buffers()
1952 .get(&self.active_buffer())
1953 .map(|s| s.language.clone())
1954 {
1955 Some(l) => l,
1956 None => return,
1957 };
1958
1959 let __active_id = self.active_window;
1960
1961 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1962 for sh in lsp.get_handles_mut(&language) {
1963 if let Err(e) = sh
1964 .handle
1965 .execute_command(cmd.command.clone(), cmd.arguments.clone())
1966 {
1967 tracing::warn!("Failed to send executeCommand to '{}': {}", sh.name, e);
1968 }
1969 }
1970 }
1971 }
1972
1973 fn send_code_action_resolve(&mut self, action: lsp_types::CodeAction) {
1975 let language = match self
1976 .buffers()
1977 .get(&self.active_buffer())
1978 .map(|s| s.language.clone())
1979 {
1980 Some(l) => l,
1981 None => return,
1982 };
1983
1984 self.active_window_mut().next_lsp_request_id += 1;
1985 let request_id = self.active_window_mut().next_lsp_request_id;
1986
1987 let __active_id = self.active_window;
1988
1989 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1990 for sh in lsp.get_handles_mut(&language) {
1991 if let Err(e) = sh.handle.code_action_resolve(request_id, action.clone()) {
1992 tracing::warn!("Failed to send codeAction/resolve to '{}': {}", sh.name, e);
1993 }
1994 }
1995 }
1996 }
1997
1998 pub(crate) fn handle_completion_resolved(&mut self, item: lsp_types::CompletionItem) {
2000 if let Some(additional_edits) = item.additional_text_edits {
2001 if !additional_edits.is_empty() {
2002 tracing::info!(
2003 "Applying {} additional text edits from completion resolve",
2004 additional_edits.len()
2005 );
2006 let buffer_id = self.active_buffer();
2007 if let Err(e) = self.apply_lsp_text_edits(buffer_id, additional_edits) {
2008 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
2009 }
2010 }
2011 }
2012 }
2013
2014 pub(crate) fn apply_formatting_edits(
2016 &mut self,
2017 uri: &str,
2018 edits: Vec<lsp_types::TextEdit>,
2019 ) -> AnyhowResult<usize> {
2020 let buffer_id = self
2022 .active_window()
2023 .buffer_metadata
2024 .iter()
2025 .find(|(_, meta)| meta.file_uri().map(|u| u.as_str() == uri).unwrap_or(false))
2026 .map(|(id, _)| *id);
2027
2028 if let Some(buffer_id) = buffer_id {
2029 let count = self.apply_lsp_text_edits(buffer_id, edits)?;
2030 self.set_status_message(format!("Formatted ({} edits)", count));
2031 Ok(count)
2032 } else {
2033 tracing::warn!("Cannot apply formatting: no buffer for URI {}", uri);
2034 Ok(0)
2035 }
2036 }
2037
2038 pub(crate) fn request_formatting(&mut self) {
2040 let buffer_id = self.active_buffer();
2041 let metadata = match self.active_window().buffer_metadata.get(&buffer_id) {
2042 Some(m) if m.lsp_enabled => m,
2043 _ => {
2044 self.set_status_message("LSP not available for this buffer".to_string());
2045 return;
2046 }
2047 };
2048
2049 let uri = match metadata.file_uri() {
2050 Some(u) => u.clone(),
2051 None => return,
2052 };
2053
2054 let language = match self
2055 .windows
2056 .get(&self.active_window)
2057 .map(|w| &w.buffers)
2058 .expect("active window present")
2059 .get(&buffer_id)
2060 .map(|s| s.language.clone())
2061 {
2062 Some(l) => l,
2063 None => return,
2064 };
2065
2066 let tab_size = self.config.editor.tab_size as u32;
2067 let insert_spaces = !self.config.editor.use_tabs;
2068
2069 self.active_window_mut().next_lsp_request_id += 1;
2070 let request_id = self.active_window_mut().next_lsp_request_id;
2071
2072 let __active_id = self.active_window;
2073
2074 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
2075 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Format) {
2076 if let Err(e) = sh.handle.document_formatting(
2077 request_id,
2078 uri.as_uri().clone(),
2079 tab_size,
2080 insert_spaces,
2081 ) {
2082 tracing::warn!("Failed to request formatting: {}", e);
2083 }
2084 } else {
2085 self.set_status_message("Formatting not supported by LSP server".to_string());
2086 }
2087 }
2088 }
2089
2090 pub(crate) fn handle_references_response(
2092 &mut self,
2093 request_id: u64,
2094 locations: Vec<lsp_types::Location>,
2095 ) -> AnyhowResult<()> {
2096 tracing::info!(
2097 "handle_references_response: received {} locations for request_id={}",
2098 locations.len(),
2099 request_id
2100 );
2101
2102 if self.active_window_mut().pending_references_request != Some(request_id) {
2104 tracing::debug!("Ignoring stale references response: {}", request_id);
2105 return Ok(());
2106 }
2107
2108 self.active_window_mut().pending_references_request = None;
2109 if locations.is_empty() {
2110 self.set_status_message(t!("lsp.no_references").to_string());
2111 return Ok(());
2112 }
2113
2114 let translation = self.authority().path_translation.clone();
2121 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
2122 .iter()
2123 .map(|loc| {
2124 let wire = crate::app::types::LspUri::from_wire(loc.uri.clone());
2125 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
2130 wire.to_host_path(translation.as_ref())
2131 .map(|p| p.to_string_lossy().into_owned())
2132 .unwrap_or_else(|| loc.uri.path().as_str().to_string())
2133 } else {
2134 loc.uri.as_str().to_string()
2135 };
2136
2137 crate::services::plugins::hooks::LspLocation {
2138 file,
2139 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
2142 })
2143 .collect();
2144
2145 let count = lsp_locations.len();
2146 let symbol = std::mem::take(&mut self.active_window_mut().pending_references_symbol);
2147 self.set_status_message(
2148 t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
2149 );
2150
2151 self.plugin_manager.read().unwrap().run_hook(
2153 "lsp_references",
2154 crate::services::plugins::hooks::HookArgs::LspReferences {
2155 symbol: symbol.clone(),
2156 locations: lsp_locations,
2157 },
2158 );
2159
2160 tracing::info!(
2161 "Fired lsp_references hook with {} locations for symbol '{}'",
2162 count,
2163 symbol
2164 );
2165
2166 Ok(())
2167 }
2168
2169 pub(crate) fn apply_lsp_text_edits(
2172 &mut self,
2173 buffer_id: BufferId,
2174 mut edits: Vec<lsp_types::TextEdit>,
2175 ) -> AnyhowResult<usize> {
2176 if edits.is_empty() {
2177 return Ok(0);
2178 }
2179
2180 edits.sort_by(|a, b| {
2182 b.range
2183 .start
2184 .line
2185 .cmp(&a.range.start.line)
2186 .then(b.range.start.character.cmp(&a.range.start.character))
2187 });
2188
2189 let mut batch_events = Vec::new();
2191 let mut changes = 0;
2192
2193 let cursor_id = {
2195 let split_id = self
2196 .split_manager_mut()
2197 .splits_for_buffer(buffer_id)
2198 .into_iter()
2199 .next()
2200 .unwrap_or_else(|| {
2201 self.windows
2202 .get(&self.active_window)
2203 .and_then(|w| w.buffers.splits())
2204 .map(|(mgr, _)| mgr)
2205 .expect("active window must have a populated split layout")
2206 .active_split()
2207 });
2208 self.windows
2209 .get(&self.active_window)
2210 .and_then(|w| w.buffers.splits())
2211 .map(|(_, vs)| vs)
2212 .expect("active window must have a populated split layout")
2213 .get(&split_id)
2214 .map(|vs| vs.cursors.primary_id())
2215 .unwrap_or_else(|| self.active_cursors().primary_id())
2216 };
2217
2218 for edit in edits {
2220 let state = self
2221 .buffers_mut()
2222 .get_mut(&buffer_id)
2223 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2224
2225 let start_line = edit.range.start.line as usize;
2227 let start_char = edit.range.start.character as usize;
2228 let end_line = edit.range.end.line as usize;
2229 let end_char = edit.range.end.character as usize;
2230
2231 let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
2232 let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
2233 let buffer_len = state.buffer.len();
2234
2235 let old_text = if start_pos < end_pos && end_pos <= buffer_len {
2237 state.get_text_range(start_pos, end_pos)
2238 } else {
2239 format!(
2240 "<invalid range: start={}, end={}, buffer_len={}>",
2241 start_pos, end_pos, buffer_len
2242 )
2243 };
2244 tracing::debug!(
2245 " Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
2246 start_line, start_char, end_line, end_char,
2247 start_pos, end_pos, old_text, edit.new_text
2248 );
2249
2250 if start_pos < end_pos {
2252 let deleted_text = state.get_text_range(start_pos, end_pos);
2253 let delete_event = Event::Delete {
2254 range: start_pos..end_pos,
2255 deleted_text,
2256 cursor_id,
2257 };
2258 batch_events.push(delete_event);
2259 }
2260
2261 if !edit.new_text.is_empty() {
2263 let insert_event = Event::Insert {
2264 position: start_pos,
2265 text: edit.new_text.clone(),
2266 cursor_id,
2267 };
2268 batch_events.push(insert_event);
2269 }
2270
2271 changes += 1;
2272 }
2273
2274 if !batch_events.is_empty() {
2276 self.apply_events_to_buffer_as_bulk_edit(
2277 buffer_id,
2278 batch_events,
2279 "LSP Rename".to_string(),
2280 )?;
2281 }
2282
2283 Ok(changes)
2284 }
2285
2286 fn apply_text_document_edit(
2292 &mut self,
2293 text_doc_edit: lsp_types::TextDocumentEdit,
2294 ) -> AnyhowResult<usize> {
2295 let uri = crate::app::types::LspUri::from_wire(text_doc_edit.text_document.uri);
2298
2299 if let Some(expected_version) = text_doc_edit.text_document.version {
2302 if let Ok(path) =
2303 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2304 {
2305 if let Some(lsp) = self.lsp() {
2306 let language = self
2307 .buffers()
2308 .get(&self.active_buffer())
2309 .map(|s| s.language.clone())
2310 .unwrap_or_default();
2311 for sh in lsp.get_handles(&language) {
2312 if let Some(current_version) = sh.handle.document_version(&path) {
2313 if (expected_version as i64) != current_version {
2314 tracing::warn!(
2315 "Rejecting stale TextDocumentEdit for {:?}: \
2316 server version {} != our version {}",
2317 path,
2318 expected_version,
2319 current_version,
2320 );
2321 return Ok(0);
2322 }
2323 }
2324 }
2325 }
2326 }
2327 }
2328
2329 if let Ok(path) =
2330 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2331 {
2332 let buffer_id = match self.open_file(&path) {
2333 Ok(id) => id,
2334 Err(e) => {
2335 if let Some(confirmation) =
2336 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2337 {
2338 self.start_large_file_encoding_confirmation(confirmation);
2339 } else {
2340 self.set_status_message(
2341 t!("file.error_opening", error = e.to_string()).to_string(),
2342 );
2343 }
2344 return Ok(0);
2345 }
2346 };
2347
2348 let edits: Vec<lsp_types::TextEdit> = text_doc_edit
2349 .edits
2350 .into_iter()
2351 .map(|one_of| match one_of {
2352 lsp_types::OneOf::Left(text_edit) => text_edit,
2353 lsp_types::OneOf::Right(annotated) => annotated.text_edit,
2354 })
2355 .collect();
2356
2357 tracing::info!("Applying {} edits for {:?}:", edits.len(), path);
2358 for (i, edit) in edits.iter().enumerate() {
2359 tracing::info!(
2360 " Edit {}: line {}:{}-{}:{} -> {:?}",
2361 i,
2362 edit.range.start.line,
2363 edit.range.start.character,
2364 edit.range.end.line,
2365 edit.range.end.character,
2366 edit.new_text
2367 );
2368 }
2369
2370 self.apply_lsp_text_edits(buffer_id, edits)
2371 } else {
2372 Ok(0)
2373 }
2374 }
2375
2376 fn apply_resource_operation(&mut self, op: lsp_types::ResourceOp) -> AnyhowResult<()> {
2378 let translation = self.authority().path_translation.clone();
2383 let to_host = |uri: &lsp_types::Uri| -> std::path::PathBuf {
2384 crate::app::types::LspUri::from_wire(uri.clone())
2385 .to_host_path(translation.as_ref())
2386 .unwrap_or_else(|| std::path::PathBuf::from(uri.path().as_str()))
2387 };
2388 match op {
2389 lsp_types::ResourceOp::Create(create) => {
2390 let path = to_host(&create.uri);
2391 let overwrite = create
2392 .options
2393 .as_ref()
2394 .and_then(|o| o.overwrite)
2395 .unwrap_or(false);
2396 let ignore_if_exists = create
2397 .options
2398 .as_ref()
2399 .and_then(|o| o.ignore_if_exists)
2400 .unwrap_or(false);
2401
2402 if path.exists() {
2403 if ignore_if_exists {
2404 tracing::debug!("CreateFile: {:?} already exists, ignoring", path);
2405 return Ok(());
2406 }
2407 if !overwrite {
2408 tracing::warn!("CreateFile: {:?} already exists and overwrite=false", path);
2409 return Ok(());
2410 }
2411 }
2412
2413 if let Some(parent) = path.parent() {
2415 std::fs::create_dir_all(parent)?;
2416 }
2417 std::fs::write(&path, "")?;
2418 tracing::info!("CreateFile: created {:?}", path);
2419
2420 if let Err(e) = self.open_file(&path) {
2422 tracing::warn!("CreateFile: failed to open created file {:?}: {}", path, e);
2423 }
2424 }
2425 lsp_types::ResourceOp::Rename(rename) => {
2426 let old_path = to_host(&rename.old_uri);
2427 let new_path = to_host(&rename.new_uri);
2428 let overwrite = rename
2429 .options
2430 .as_ref()
2431 .and_then(|o| o.overwrite)
2432 .unwrap_or(false);
2433 let ignore_if_exists = rename
2434 .options
2435 .as_ref()
2436 .and_then(|o| o.ignore_if_exists)
2437 .unwrap_or(false);
2438
2439 if new_path.exists() {
2440 if ignore_if_exists {
2441 tracing::debug!("RenameFile: {:?} already exists, ignoring", new_path);
2442 return Ok(());
2443 }
2444 if !overwrite {
2445 tracing::warn!(
2446 "RenameFile: {:?} already exists and overwrite=false",
2447 new_path
2448 );
2449 return Ok(());
2450 }
2451 }
2452
2453 if let Some(parent) = new_path.parent() {
2455 std::fs::create_dir_all(parent)?;
2456 }
2457 std::fs::rename(&old_path, &new_path)?;
2458 tracing::info!("RenameFile: {:?} -> {:?}", old_path, new_path);
2459 }
2460 lsp_types::ResourceOp::Delete(delete) => {
2461 let path = to_host(&delete.uri);
2462 let recursive = delete
2463 .options
2464 .as_ref()
2465 .and_then(|o| o.recursive)
2466 .unwrap_or(false);
2467 let ignore_if_not_exists = delete
2468 .options
2469 .as_ref()
2470 .and_then(|o| o.ignore_if_not_exists)
2471 .unwrap_or(false);
2472
2473 if !path.exists() {
2474 if ignore_if_not_exists {
2475 tracing::debug!("DeleteFile: {:?} does not exist, ignoring", path);
2476 return Ok(());
2477 }
2478 tracing::warn!("DeleteFile: {:?} does not exist", path);
2479 return Ok(());
2480 }
2481
2482 if path.is_dir() && recursive {
2483 std::fs::remove_dir_all(&path)?;
2484 } else if path.is_file() {
2485 std::fs::remove_file(&path)?;
2486 }
2487 tracing::info!("DeleteFile: deleted {:?}", path);
2488 }
2489 }
2490 Ok(())
2491 }
2492
2493 pub(crate) fn apply_workspace_edit(
2497 &mut self,
2498 workspace_edit: lsp_types::WorkspaceEdit,
2499 ) -> AnyhowResult<usize> {
2500 tracing::debug!(
2501 "Applying WorkspaceEdit: changes={:?}, document_changes={:?}",
2502 workspace_edit.changes.as_ref().map(|c| c.len()),
2503 workspace_edit.document_changes.as_ref().map(|dc| match dc {
2504 lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
2505 lsp_types::DocumentChanges::Operations(o) => format!("{} operations", o.len()),
2506 })
2507 );
2508
2509 let mut total_changes = 0;
2510
2511 if let Some(changes) = workspace_edit.changes {
2513 for (uri, edits) in changes {
2514 let uri = crate::app::types::LspUri::from_wire(uri);
2515 if let Ok(path) =
2516 super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2517 {
2518 let buffer_id = match self.open_file(&path) {
2519 Ok(id) => id,
2520 Err(e) => {
2521 if let Some(confirmation) = e.downcast_ref::<
2522 crate::model::buffer::LargeFileEncodingConfirmation,
2523 >() {
2524 self.start_large_file_encoding_confirmation(confirmation);
2525 } else {
2526 self.set_status_message(
2527 t!("file.error_opening", error = e.to_string())
2528 .to_string(),
2529 );
2530 }
2531 return Ok(0);
2532 }
2533 };
2534 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
2535 }
2536 }
2537 }
2538
2539 if let Some(document_changes) = workspace_edit.document_changes {
2541 use lsp_types::DocumentChanges;
2542
2543 match document_changes {
2544 DocumentChanges::Edits(edits) => {
2545 for text_doc_edit in edits {
2546 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2547 }
2548 }
2549 DocumentChanges::Operations(ops) => {
2550 for op in ops {
2553 match op {
2554 lsp_types::DocumentChangeOperation::Edit(text_doc_edit) => {
2555 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2556 }
2557 lsp_types::DocumentChangeOperation::Op(resource_op) => {
2558 self.apply_resource_operation(resource_op)?;
2559 total_changes += 1;
2560 }
2561 }
2562 }
2563 }
2564 }
2565 }
2566
2567 Ok(total_changes)
2568 }
2569
2570 pub fn handle_rename_response(
2572 &mut self,
2573 _request_id: u64,
2574 result: Result<lsp_types::WorkspaceEdit, String>,
2575 ) -> AnyhowResult<()> {
2576 match result {
2577 Ok(workspace_edit) => {
2578 let total_changes = self.apply_workspace_edit(workspace_edit)?;
2579 self.active_window_mut().status_message =
2580 Some(t!("lsp.renamed", count = total_changes).to_string());
2581 }
2582 Err(error) => {
2583 if error.contains("content modified") || error.contains("-32801") {
2585 tracing::debug!(
2586 "LSP rename: ContentModified error (expected, ignoring): {}",
2587 error
2588 );
2589 self.active_window_mut().status_message =
2590 Some(t!("lsp.rename_cancelled").to_string());
2591 } else {
2592 self.active_window_mut().status_message =
2593 Some(t!("lsp.rename_failed", error = &error).to_string());
2594 }
2595 }
2596 }
2597
2598 Ok(())
2599 }
2600
2601 pub(crate) fn apply_events_to_buffer_as_bulk_edit(
2606 &mut self,
2607 buffer_id: BufferId,
2608 events: Vec<Event>,
2609 description: String,
2610 ) -> AnyhowResult<()> {
2611 use crate::model::event::CursorId;
2612
2613 if events.is_empty() {
2614 return Ok(());
2615 }
2616
2617 let batch_for_lsp = Event::Batch {
2619 events: events.clone(),
2620 description: description.clone(),
2621 };
2622
2623 let original_active = self.active_buffer();
2635 self.windows
2636 .get_mut(&self.active_window)
2637 .and_then(|w| w.split_manager_mut())
2638 .expect("active window must have a populated split layout")
2639 .set_active_buffer_id(buffer_id);
2640 let lsp_changes = self.active_window().collect_lsp_changes(&batch_for_lsp);
2641 self.windows
2642 .get_mut(&self.active_window)
2643 .and_then(|w| w.split_manager_mut())
2644 .expect("active window must have a populated split layout")
2645 .set_active_buffer_id(original_active);
2646
2647 let split_id_for_cursors = self
2650 .split_manager_mut()
2651 .splits_for_buffer(buffer_id)
2652 .into_iter()
2653 .next()
2654 .unwrap_or_else(|| {
2655 self.windows
2656 .get(&self.active_window)
2657 .and_then(|w| w.buffers.splits())
2658 .map(|(mgr, _)| mgr)
2659 .expect("active window must have a populated split layout")
2660 .active_split()
2661 });
2662 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2663 .windows
2664 .get(&self.active_window)
2665 .and_then(|w| w.buffers.splits())
2666 .map(|(_, vs)| vs)
2667 .expect("active window must have a populated split layout")
2668 .get(&split_id_for_cursors)
2669 .and_then(|vs| vs.keyed_states.get(&buffer_id))
2670 .map(|bvs| {
2671 bvs.cursors
2672 .iter()
2673 .map(|(id, c)| (id, c.position, c.anchor))
2674 .collect()
2675 })
2676 .unwrap_or_default();
2677
2678 let __win = self
2685 .windows
2686 .get_mut(&self.active_window)
2687 .expect("active window must exist");
2688 let bulk_edit = __win
2689 .buffers
2690 .with_buffer_and_view_states(buffer_id, |state, vs_map| -> AnyhowResult<Event> {
2691 let old_snapshot = state.buffer.snapshot_buffer_state();
2693
2694 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2696 for event in &events {
2697 match event {
2698 Event::Insert { position, text, .. } => {
2699 edits.push((*position, 0, text.clone()));
2700 }
2701 Event::Delete { range, .. } => {
2702 edits.push((range.start, range.len(), String::new()));
2703 }
2704 _ => {}
2705 }
2706 }
2707
2708 edits.sort_by(|a, b| b.0.cmp(&a.0));
2710
2711 let edit_refs: Vec<(usize, usize, &str)> = edits
2713 .iter()
2714 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2715 .collect();
2716
2717 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2719
2720 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2722
2723 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2725 for (pos, del_len, text) in &edits {
2726 let delta = text.len() as isize - *del_len as isize;
2727 position_deltas.push((*pos, delta));
2728 }
2729 position_deltas.sort_by_key(|(pos, _)| *pos);
2730
2731 let calc_shift = |original_pos: usize| -> isize {
2732 let mut shift: isize = 0;
2733 for (edit_pos, delta) in &position_deltas {
2734 if *edit_pos < original_pos {
2735 shift += delta;
2736 }
2737 }
2738 shift
2739 };
2740
2741 let buffer_len = state.buffer.len();
2743 let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
2744 .iter()
2745 .map(|(id, pos, anchor)| {
2746 let shift = calc_shift(*pos);
2747 let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
2748 let new_anchor = anchor.map(|a| {
2749 let anchor_shift = calc_shift(a);
2750 ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
2751 });
2752 (*id, new_pos, new_anchor)
2753 })
2754 .collect();
2755
2756 let new_snapshot = state.buffer.snapshot_buffer_state();
2758
2759 state.highlighter.invalidate_all();
2761
2762 if let Some(vs) = vs_map.get_mut(&split_id_for_cursors) {
2764 if let Some(bvs) = vs.keyed_states.get_mut(&buffer_id) {
2765 for (cursor_id, new_pos, new_anchor) in &new_cursors {
2766 if let Some(cursor) = bvs.cursors.get_mut(*cursor_id) {
2767 cursor.position = *new_pos;
2768 cursor.anchor = *new_anchor;
2769 }
2770 }
2771 }
2772 }
2773
2774 let edit_lengths: Vec<(usize, usize, usize)> = {
2777 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2778 for (pos, del_len, text) in &edits {
2779 if let Some(last) = lengths.last_mut() {
2780 if last.0 == *pos {
2781 last.1 += del_len;
2782 last.2 += text.len();
2783 continue;
2784 }
2785 }
2786 lengths.push((*pos, *del_len, text.len()));
2787 }
2788 lengths
2789 };
2790
2791 for &(pos, del_len, ins_len) in &edit_lengths {
2793 if del_len > 0 && ins_len > 0 {
2794 if ins_len > del_len {
2795 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2796 state.margins.adjust_for_insert(pos, ins_len - del_len);
2797 } else if del_len > ins_len {
2798 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2799 state.margins.adjust_for_delete(pos, del_len - ins_len);
2800 }
2801 } else if del_len > 0 {
2802 state.marker_list.adjust_for_delete(pos, del_len);
2803 state.margins.adjust_for_delete(pos, del_len);
2804 } else if ins_len > 0 {
2805 state.marker_list.adjust_for_insert(pos, ins_len);
2806 state.margins.adjust_for_insert(pos, ins_len);
2807 }
2808 }
2809
2810 Ok(Event::BulkEdit {
2811 old_snapshot: Some(old_snapshot),
2812 new_snapshot: Some(new_snapshot),
2813 old_cursors,
2814 new_cursors,
2815 description,
2816 edits: edit_lengths,
2817 displaced_markers,
2818 })
2819 })
2820 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))??;
2821
2822 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
2824 event_log.append(bulk_edit);
2825 }
2826
2827 self.active_window_mut()
2829 .send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2830
2831 Ok(())
2832 }
2833
2834 pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2836 if self.active_window().server_supports_prepare_rename() {
2838 self.active_window_mut().send_prepare_rename();
2839 return Ok(());
2840 }
2841
2842 self.show_rename_prompt()
2843 }
2844
2845 pub(crate) fn handle_prepare_rename_response(
2847 &mut self,
2848 result: Result<serde_json::Value, String>,
2849 ) {
2850 match result {
2851 Ok(value) if !value.is_null() => {
2852 if let Err(e) = self.show_rename_prompt() {
2854 self.set_status_message(format!("Rename failed: {e}"));
2855 }
2856 }
2857 Ok(_) => {
2858 self.set_status_message("Cannot rename at this position".to_string());
2859 }
2860 Err(e) => {
2861 self.set_status_message(format!("Cannot rename: {e}"));
2862 }
2863 }
2864 }
2865
2866 fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2869 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2870
2871 let cursor_pos = self.active_cursors().primary().position;
2873 let (word_start, word_end) = {
2874 let state = self.active_state();
2875
2876 let word_start = find_word_start(&state.buffer, cursor_pos);
2878 let word_end = find_word_end(&state.buffer, cursor_pos);
2879
2880 if word_start >= word_end {
2882 self.active_window_mut().status_message =
2883 Some(t!("lsp.no_symbol_at_cursor").to_string());
2884 return Ok(());
2885 }
2886
2887 (word_start, word_end)
2888 };
2889
2890 let word_text = self.active_state_mut().get_text_range(word_start, word_end);
2892
2893 let overlay_handle = self.add_overlay(
2895 None,
2896 word_start..word_end,
2897 crate::model::event::OverlayFace::Background {
2898 color: (50, 100, 200), },
2900 100,
2901 Some(t!("lsp.popup_renaming").to_string()),
2902 );
2903
2904 let mut prompt = Prompt::new(
2907 "Rename to: ".to_string(),
2908 PromptType::LspRename {
2909 original_text: word_text.clone(),
2910 start_pos: word_start,
2911 end_pos: word_end,
2912 overlay_handle,
2913 },
2914 );
2915 prompt.set_input(word_text);
2917
2918 self.active_window_mut().prompt = Some(prompt);
2919 Ok(())
2920 }
2921
2922 pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
2924 self.remove_overlay(handle.clone());
2925 }
2926
2927 pub(crate) fn perform_lsp_rename(
2929 &mut self,
2930 new_name: String,
2931 original_text: String,
2932 start_pos: usize,
2933 overlay_handle: crate::view::overlay::OverlayHandle,
2934 ) {
2935 self.cancel_rename_overlay(&overlay_handle);
2937
2938 if new_name == original_text {
2940 self.active_window_mut().status_message = Some(t!("lsp.name_unchanged").to_string());
2941 return;
2942 }
2943
2944 let rename_pos = start_pos;
2947
2948 let state = self.active_state();
2951 let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
2952 let buffer_id = self.active_buffer();
2953 let request_id = self.active_window_mut().next_lsp_request_id;
2954
2955 let sent = self
2957 .with_lsp_for_buffer(buffer_id, LspFeature::Rename, |handle, uri, _language| {
2958 let result = handle.rename(
2959 request_id,
2960 uri.as_uri().clone(),
2961 line as u32,
2962 character as u32,
2963 new_name.clone(),
2964 );
2965 if result.is_ok() {
2966 tracing::info!(
2967 "Requested rename at {}:{}:{} to '{}'",
2968 uri.as_str(),
2969 line,
2970 character,
2971 new_name
2972 );
2973 }
2974 result.is_ok()
2975 })
2976 .unwrap_or(false);
2977
2978 if sent {
2979 self.active_window_mut().next_lsp_request_id += 1;
2980 } else if self
2981 .active_window()
2982 .buffer_metadata
2983 .get(&buffer_id)
2984 .and_then(|m| m.file_path())
2985 .is_none()
2986 {
2987 self.active_window_mut().status_message =
2988 Some(t!("lsp.cannot_rename_unsaved").to_string());
2989 }
2990 }
2991
2992 pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
2994 let buffer_id = self.active_buffer();
2995 self.request_inlay_hints_for_buffer(buffer_id);
2996 }
2997
2998 pub(crate) fn request_inlay_hints_for_buffer(&mut self, buffer_id: BufferId) {
3000 if !self.config.editor.enable_inlay_hints {
3001 return;
3002 }
3003
3004 let (line_count, version) = if let Some(state) = self
3008 .windows
3009 .get(&self.active_window)
3010 .map(|w| &w.buffers)
3011 .expect("active window present")
3012 .get(&buffer_id)
3013 {
3014 (
3015 state.buffer.line_count().unwrap_or(1000),
3016 state.buffer.version(),
3017 )
3018 } else {
3019 return;
3020 };
3021 let last_line = line_count.saturating_sub(1) as u32;
3022 let request_id = self.active_window_mut().next_lsp_request_id;
3023
3024 let sent = self
3026 .with_lsp_for_buffer(
3027 buffer_id,
3028 LspFeature::InlayHints,
3029 |handle, uri, _language| {
3030 let result = handle.inlay_hints(
3031 request_id,
3032 uri.as_uri().clone(),
3033 0,
3034 0,
3035 last_line,
3036 10000,
3037 );
3038 if result.is_ok() {
3039 tracing::info!(
3040 "Requested inlay hints for {} (request_id={})",
3041 uri.as_str(),
3042 request_id
3043 );
3044 } else if let Err(e) = &result {
3045 tracing::debug!("Failed to request inlay hints: {}", e);
3046 }
3047 result.is_ok()
3048 },
3049 )
3050 .unwrap_or(false);
3051
3052 if sent {
3053 self.active_window_mut().next_lsp_request_id += 1;
3054 self.active_window_mut()
3055 .pending_inlay_hints_requests
3056 .insert(request_id, super::InlayHintsRequest { buffer_id, version });
3057 }
3058 }
3059
3060 pub(crate) fn maybe_request_folding_ranges_debounced(&mut self, buffer_id: BufferId) {
3062 let Some(ready_at) = self
3063 .active_window()
3064 .folding_ranges_debounce
3065 .get(&buffer_id)
3066 .copied()
3067 else {
3068 return;
3069 };
3070 if Instant::now() < ready_at {
3071 return;
3072 }
3073
3074 self.active_window_mut()
3075 .folding_ranges_debounce
3076 .remove(&buffer_id);
3077 self.request_folding_ranges_for_buffer(buffer_id);
3078 }
3079
3080 pub(crate) fn request_folding_ranges_for_buffer(&mut self, buffer_id: BufferId) {
3082 if self
3083 .active_window_mut()
3084 .folding_ranges_in_flight
3085 .contains_key(&buffer_id)
3086 {
3087 return;
3088 }
3089
3090 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3091 return;
3092 };
3093 if !metadata.lsp_enabled {
3094 return;
3095 }
3096 let Some(uri) = metadata.file_uri().cloned() else {
3097 return;
3098 };
3099 let file_path = metadata.file_path().cloned();
3100
3101 let Some(language) = self
3102 .windows
3103 .get(&self.active_window)
3104 .map(|w| &w.buffers)
3105 .expect("active window present")
3106 .get(&buffer_id)
3107 .map(|s| s.language.clone())
3108 else {
3109 return;
3110 };
3111
3112 let __active_id = self.active_window;
3113 let __buffer_version_for_request = self
3116 .windows
3117 .get(&__active_id)
3118 .and_then(|w| w.buffers.get(&buffer_id))
3119 .map(|s| s.buffer.version())
3120 .unwrap_or(0);
3121
3122 let Some(__win) = self.windows.get_mut(&__active_id) else {
3123 return;
3124 };
3125 let __next_id = &mut __win.next_lsp_request_id;
3126 let __pending_folding = &mut __win.pending_folding_range_requests;
3127 let __folding_in_flight = &mut __win.folding_ranges_in_flight;
3128 let lsp = &mut __win.lsp;
3129
3130 if !lsp.folding_ranges_supported(&language) {
3131 return;
3132 }
3133
3134 use crate::services::lsp::manager::LspSpawnResult;
3136 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3137 return;
3138 }
3139
3140 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::FoldingRange) else {
3141 return;
3142 };
3143 let handle = &mut sh.handle;
3144
3145 let request_id = {
3146 let id = *__next_id;
3147 *__next_id += 1;
3148 id
3149 };
3150 let buffer_version = __buffer_version_for_request;
3151 let _ = __folding_in_flight;
3152
3153 match handle.folding_ranges(request_id, uri.as_uri().clone()) {
3154 Ok(()) => {
3155 __pending_folding.insert(
3156 request_id,
3157 super::FoldingRangeRequest {
3158 buffer_id,
3159 version: buffer_version,
3160 },
3161 );
3162 __folding_in_flight.insert(buffer_id, (request_id, buffer_version));
3163 }
3164 Err(e) => {
3165 tracing::debug!("Failed to request folding ranges: {}", e);
3166 }
3167 }
3168 }
3169
3170 pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
3172 if !self.config.editor.enable_semantic_tokens_full {
3173 return;
3174 }
3175
3176 if self
3178 .active_window_mut()
3179 .semantic_tokens_in_flight
3180 .contains_key(&buffer_id)
3181 {
3182 return;
3183 }
3184
3185 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3186 return;
3187 };
3188 if !metadata.lsp_enabled {
3189 return;
3190 }
3191 let Some(uri) = metadata.file_uri().cloned() else {
3192 return;
3193 };
3194 let file_path_for_spawn = metadata.file_path().cloned();
3195 let Some(language) = self
3197 .windows
3198 .get(&self.active_window)
3199 .map(|w| &w.buffers)
3200 .expect("active window present")
3201 .get(&buffer_id)
3202 .map(|s| s.language.clone())
3203 else {
3204 return;
3205 };
3206
3207 let __active_id = self.active_window;
3208 let Some((buffer_version, existing_version, previous_result_id)) = self
3211 .windows
3212 .get(&__active_id)
3213 .and_then(|w| w.buffers.get(&buffer_id))
3214 .map(|state| {
3215 (
3216 state.buffer.version(),
3217 state.semantic_tokens.as_ref().map(|s| s.version),
3218 state
3219 .semantic_tokens
3220 .as_ref()
3221 .and_then(|s| s.result_id.clone()),
3222 )
3223 })
3224 else {
3225 return;
3226 };
3227 if Some(buffer_version) == existing_version {
3228 return; }
3230
3231 let Some(__win) = self.windows.get_mut(&__active_id) else {
3232 return;
3233 };
3234 let __next_id = &mut __win.next_lsp_request_id;
3235 let __pending_st = &mut __win.pending_semantic_token_requests;
3236 let __st_in_flight = &mut __win.semantic_tokens_in_flight;
3237 let lsp = &mut __win.lsp;
3238
3239 use crate::services::lsp::manager::LspSpawnResult;
3241 if lsp.try_spawn(&language, file_path_for_spawn.as_deref()) != LspSpawnResult::Spawned {
3242 return;
3243 }
3244
3245 if !lsp.semantic_tokens_full_supported(&language) {
3247 return;
3248 }
3249 if lsp.semantic_tokens_legend(&language).is_none() {
3250 return;
3251 }
3252
3253 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3254 return;
3255 };
3256 let supports_delta = sh.capabilities.semantic_tokens_full_delta;
3258 let use_delta = previous_result_id.is_some() && supports_delta;
3259 let handle = &mut sh.handle;
3260
3261 let request_id = {
3262 let id = *__next_id;
3263 *__next_id += 1;
3264 id
3265 };
3266
3267 let request_kind = if use_delta {
3268 super::SemanticTokensFullRequestKind::FullDelta
3269 } else {
3270 super::SemanticTokensFullRequestKind::Full
3271 };
3272
3273 let request_result = if use_delta {
3274 handle.semantic_tokens_full_delta(
3275 request_id,
3276 uri.as_uri().clone(),
3277 previous_result_id.unwrap(),
3278 )
3279 } else {
3280 handle.semantic_tokens_full(request_id, uri.as_uri().clone())
3281 };
3282
3283 match request_result {
3284 Ok(_) => {
3285 __pending_st.insert(
3286 request_id,
3287 super::SemanticTokenFullRequest {
3288 buffer_id,
3289 version: buffer_version,
3290 kind: request_kind,
3291 },
3292 );
3293 __st_in_flight.insert(buffer_id, (request_id, buffer_version, request_kind));
3294 }
3295 Err(e) => {
3296 tracing::debug!("Failed to request semantic tokens: {}", e);
3297 }
3298 }
3299 }
3300
3301 pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
3303 if !self.config.editor.enable_semantic_tokens_full {
3304 self.active_window_mut()
3305 .semantic_tokens_full_debounce
3306 .remove(&buffer_id);
3307 return;
3308 }
3309
3310 let Some(ready_at) = self
3311 .active_window()
3312 .semantic_tokens_full_debounce
3313 .get(&buffer_id)
3314 .copied()
3315 else {
3316 return;
3317 };
3318 if Instant::now() < ready_at {
3319 return;
3320 }
3321
3322 self.active_window_mut()
3323 .semantic_tokens_full_debounce
3324 .remove(&buffer_id);
3325 self.maybe_request_semantic_tokens(buffer_id);
3326 }
3327
3328 pub(crate) fn maybe_request_semantic_tokens_range(
3330 &mut self,
3331 buffer_id: BufferId,
3332 start_line: usize,
3333 end_line: usize,
3334 ) {
3335 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3336 return;
3337 };
3338 if !metadata.lsp_enabled {
3339 return;
3340 }
3341 let Some(uri) = metadata.file_uri().cloned() else {
3342 return;
3343 };
3344 let file_path = metadata.file_path().cloned();
3345 let Some(language) = self
3347 .windows
3348 .get(&self.active_window)
3349 .map(|w| &w.buffers)
3350 .expect("active window present")
3351 .get(&buffer_id)
3352 .map(|s| s.language.clone())
3353 else {
3354 return;
3355 };
3356
3357 let __active_id = self.active_window;
3358 let __win = self
3361 .windows
3362 .get_mut(&__active_id)
3363 .expect("active window must exist");
3364 let __next_id = &mut __win.next_lsp_request_id;
3365 let __pending_st_range = &mut __win.pending_semantic_token_range_requests;
3366 let __st_range_in_flight = &mut __win.semantic_tokens_range_in_flight;
3367 let __st_range_last = &mut __win.semantic_tokens_range_last_request;
3368 let __st_range_applied = &__win.semantic_tokens_range_applied;
3369 let lsp = &mut __win.lsp;
3370 let __buffers_ref: &crate::app::window::WindowBuffers = &__win.buffers;
3371
3372 use crate::services::lsp::manager::LspSpawnResult;
3374 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3375 return;
3376 }
3377
3378 if !lsp.semantic_tokens_range_supported(&language) {
3379 self.maybe_request_semantic_tokens(buffer_id);
3381 return;
3382 }
3383 if lsp.semantic_tokens_legend(&language).is_none() {
3384 return;
3385 }
3386
3387 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3388 return;
3389 };
3390 if !sh.capabilities.semantic_tokens_range {
3393 return;
3394 }
3395 let handle = &mut sh.handle;
3396 let Some(state) = __buffers_ref.get(&buffer_id) else {
3397 return;
3398 };
3399
3400 let buffer_version = state.buffer.version();
3401 let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3402 let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3403
3404 if let Some(line_count) = state.buffer.line_count() {
3405 if line_count == 0 {
3406 return;
3407 }
3408 let max_line = line_count.saturating_sub(1);
3409 padded_start = padded_start.min(max_line);
3410 padded_end = padded_end.min(max_line);
3411 }
3412
3413 let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
3414 let end_char = state
3415 .buffer
3416 .get_line(padded_end)
3417 .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
3418 .unwrap_or(0);
3419 let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
3420 state.buffer.lsp_position_to_byte(padded_end, end_char)
3421 } else {
3422 state.buffer.len()
3423 };
3424
3425 if start_byte >= end_byte {
3426 return;
3427 }
3428
3429 let range = start_byte..end_byte;
3430 if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
3431 __st_range_in_flight.get(&buffer_id).copied()
3432 {
3433 if in_flight_start == padded_start
3434 && in_flight_end == padded_end
3435 && in_flight_version == buffer_version
3436 {
3437 return;
3438 }
3439 if let Err(e) = handle.cancel_request(in_flight_id) {
3440 tracing::debug!("Failed to cancel semantic token range request: {}", e);
3441 }
3442 __pending_st_range.remove(&in_flight_id);
3443 __st_range_in_flight.remove(&buffer_id);
3444 }
3445
3446 if let Some((applied_start, applied_end, applied_version)) =
3447 __st_range_applied.get(&buffer_id).copied()
3448 {
3449 if applied_start == padded_start
3450 && applied_end == padded_end
3451 && applied_version == buffer_version
3452 {
3453 return;
3454 }
3455 }
3456
3457 let now = Instant::now();
3458 if let Some((last_start, last_end, last_version, last_time)) =
3459 __st_range_last.get(&buffer_id).copied()
3460 {
3461 if last_start == padded_start
3462 && last_end == padded_end
3463 && last_version == buffer_version
3464 && now.duration_since(last_time)
3465 < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
3466 {
3467 return;
3468 }
3469 }
3470
3471 let lsp_range = lsp_types::Range {
3472 start: lsp_types::Position {
3473 line: padded_start as u32,
3474 character: 0,
3475 },
3476 end: lsp_types::Position {
3477 line: padded_end as u32,
3478 character: end_char as u32,
3479 },
3480 };
3481
3482 let request_id = {
3483 let id = *__next_id;
3484 *__next_id += 1;
3485 id
3486 };
3487 let _ = __st_range_applied;
3488
3489 match handle.semantic_tokens_range(request_id, uri.as_uri().clone(), lsp_range) {
3490 Ok(_) => {
3491 __pending_st_range.insert(
3492 request_id,
3493 SemanticTokenRangeRequest {
3494 buffer_id,
3495 version: buffer_version,
3496 range: range.clone(),
3497 start_line: padded_start,
3498 end_line: padded_end,
3499 },
3500 );
3501 __st_range_in_flight.insert(
3502 buffer_id,
3503 (request_id, padded_start, padded_end, buffer_version),
3504 );
3505 __st_range_last.insert(buffer_id, (padded_start, padded_end, buffer_version, now));
3506 }
3507 Err(e) => {
3508 tracing::debug!("Failed to request semantic token range: {}", e);
3509 }
3510 }
3511 }
3512}
3513
3514#[cfg(test)]
3515mod tests {
3516 use crate::model::filesystem::StdFileSystem;
3517 use std::sync::Arc;
3518
3519 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
3520 Arc::new(StdFileSystem)
3521 }
3522 use super::{lsp_range_contains, lsp_range_overlaps, Editor};
3523
3524 fn range(sl: u32, sc: u32, el: u32, ec: u32) -> lsp_types::Range {
3525 lsp_types::Range {
3526 start: lsp_types::Position {
3527 line: sl,
3528 character: sc,
3529 },
3530 end: lsp_types::Position {
3531 line: el,
3532 character: ec,
3533 },
3534 }
3535 }
3536
3537 #[test]
3538 fn test_lsp_range_contains_inclusive_start_exclusive_end() {
3539 let r = range(3, 10, 3, 20);
3540 assert!(!lsp_range_contains(&r, 3, 9));
3542 assert!(!lsp_range_contains(&r, 2, 50));
3543 assert!(lsp_range_contains(&r, 3, 10));
3545 assert!(lsp_range_contains(&r, 3, 15));
3547 assert!(lsp_range_contains(&r, 3, 19));
3549 assert!(!lsp_range_contains(&r, 3, 20));
3551 assert!(!lsp_range_contains(&r, 3, 21));
3553 assert!(!lsp_range_contains(&r, 4, 0));
3554 }
3555
3556 #[test]
3557 fn test_lsp_range_contains_multiline() {
3558 let r = range(2, 5, 4, 3);
3559 assert!(!lsp_range_contains(&r, 1, 100));
3561 assert!(!lsp_range_contains(&r, 2, 4));
3563 assert!(lsp_range_contains(&r, 2, 5));
3565 assert!(lsp_range_contains(&r, 3, 0));
3567 assert!(lsp_range_contains(&r, 3, 9999));
3568 assert!(lsp_range_contains(&r, 4, 2));
3570 assert!(!lsp_range_contains(&r, 4, 3));
3572 assert!(!lsp_range_contains(&r, 5, 0));
3574 }
3575
3576 #[test]
3577 fn test_lsp_range_contains_zero_length_matches_anchor_only() {
3578 let r = range(7, 4, 7, 4);
3580 assert!(lsp_range_contains(&r, 7, 4));
3581 assert!(!lsp_range_contains(&r, 7, 3));
3582 assert!(!lsp_range_contains(&r, 7, 5));
3583 assert!(!lsp_range_contains(&r, 6, 4));
3584 assert!(!lsp_range_contains(&r, 8, 4));
3585 }
3586
3587 #[test]
3588 fn test_lsp_range_overlaps_point_cursor_in_diagnostic_range() {
3589 let diag = range(3, 10, 3, 20);
3592 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 10));
3594 assert!(lsp_range_overlaps(&diag, 3, 15, 3, 15));
3596 assert!(!lsp_range_overlaps(&diag, 3, 20, 3, 20));
3598 assert!(!lsp_range_overlaps(&diag, 3, 9, 3, 9));
3600 assert!(!lsp_range_overlaps(&diag, 4, 15, 4, 15));
3602 }
3603
3604 #[test]
3605 fn test_lsp_range_overlaps_selection_intersects_diagnostic() {
3606 let diag = range(3, 10, 3, 20);
3608 assert!(lsp_range_overlaps(&diag, 3, 12, 3, 18));
3610 assert!(lsp_range_overlaps(&diag, 3, 5, 3, 15));
3612 assert!(lsp_range_overlaps(&diag, 3, 15, 3, 25));
3614 assert!(lsp_range_overlaps(&diag, 3, 0, 3, 30));
3616 assert!(!lsp_range_overlaps(&diag, 3, 0, 3, 10));
3618 assert!(!lsp_range_overlaps(&diag, 3, 20, 3, 30));
3620 assert!(!lsp_range_overlaps(&diag, 4, 0, 4, 100));
3622 }
3623
3624 #[test]
3625 fn test_lsp_range_overlaps_point_diagnostic_within_selection() {
3626 let diag = range(3, 10, 3, 10);
3628 assert!(lsp_range_overlaps(&diag, 3, 5, 3, 15));
3630 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 15));
3632 assert!(!lsp_range_overlaps(&diag, 3, 0, 3, 10));
3634 assert!(lsp_range_overlaps(&diag, 3, 10, 3, 10));
3636 assert!(!lsp_range_overlaps(&diag, 3, 9, 3, 9));
3638 }
3639
3640 #[test]
3641 fn test_lsp_range_overlaps_multiline() {
3642 let diag = range(2, 5, 4, 3);
3644 assert!(lsp_range_overlaps(&diag, 3, 0, 3, 0));
3646 assert!(lsp_range_overlaps(&diag, 1, 0, 2, 6));
3648 assert!(!lsp_range_overlaps(&diag, 4, 3, 4, 10));
3650 assert!(!lsp_range_overlaps(&diag, 5, 0, 5, 10));
3652 }
3653
3654 use crate::model::buffer::Buffer;
3655 use crate::state::EditorState;
3656 use crate::view::virtual_text::VirtualTextPosition;
3657 use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
3658
3659 fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
3660 InlayHint {
3661 position: Position { line, character },
3662 label: InlayHintLabel::String(label.to_string()),
3663 kind,
3664 text_edits: None,
3665 tooltip: None,
3666 padding_left: None,
3667 padding_right: None,
3668 data: None,
3669 }
3670 }
3671
3672 #[test]
3673 fn test_inlay_hint_inserts_before_character() {
3674 let mut state = EditorState::new(
3675 80,
3676 24,
3677 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3678 test_fs(),
3679 );
3680 state.buffer = Buffer::from_str_test("ab");
3681
3682 if !state.buffer.is_empty() {
3683 state.marker_list.adjust_for_insert(0, state.buffer.len());
3684 }
3685
3686 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3687 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3688
3689 let lookup = state
3690 .virtual_texts
3691 .build_lookup(&state.marker_list, 0, state.buffer.len());
3692 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3693 assert_eq!(vtexts.len(), 1);
3694 assert_eq!(vtexts[0].text, ": i32");
3695 assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
3696 }
3697
3698 #[test]
3699 fn test_inlay_hint_at_eof_renders_after_last_char() {
3700 let mut state = EditorState::new(
3701 80,
3702 24,
3703 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3704 test_fs(),
3705 );
3706 state.buffer = Buffer::from_str_test("ab");
3707
3708 if !state.buffer.is_empty() {
3709 state.marker_list.adjust_for_insert(0, state.buffer.len());
3710 }
3711
3712 let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
3713 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3714
3715 let lookup = state
3716 .virtual_texts
3717 .build_lookup(&state.marker_list, 0, state.buffer.len());
3718 let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
3719 assert_eq!(vtexts.len(), 1);
3720 assert_eq!(vtexts[0].text, ": i32");
3721 assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
3722 }
3723
3724 #[test]
3725 fn test_inlay_hint_empty_buffer_is_ignored() {
3726 let mut state = EditorState::new(
3727 80,
3728 24,
3729 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3730 test_fs(),
3731 );
3732 state.buffer = Buffer::from_str_test("");
3733
3734 let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
3735 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3736
3737 assert!(state.virtual_texts.is_empty());
3738 }
3739
3740 #[test]
3741 fn test_inlay_hint_uses_theme_key_for_foreground() {
3742 let mut state = EditorState::new(
3745 80,
3746 24,
3747 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3748 test_fs(),
3749 );
3750 state.buffer = Buffer::from_str_test("ab");
3751
3752 if !state.buffer.is_empty() {
3753 state.marker_list.adjust_for_insert(0, state.buffer.len());
3754 }
3755
3756 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3757 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3758
3759 let lookup = state
3760 .virtual_texts
3761 .build_lookup(&state.marker_list, 0, state.buffer.len());
3762 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3763 assert_eq!(
3764 vtexts[0].fg_theme_key.as_deref(),
3765 Some("editor.line_number_fg")
3766 );
3767 assert_eq!(vtexts[0].bg_theme_key, None);
3768 }
3769
3770 #[test]
3771 fn test_inlay_hint_removed_when_its_range_is_deleted() {
3772 let mut state = EditorState::new(
3779 80,
3780 24,
3781 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3782 test_fs(),
3783 );
3784 state.buffer = Buffer::from_str_test("let x = 42;");
3785 state.marker_list.adjust_for_insert(0, state.buffer.len());
3786
3787 let hints = vec![make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE))];
3789 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3790 assert_eq!(state.virtual_texts.len(), 1);
3791
3792 let removed = state
3795 .virtual_texts
3796 .remove_in_range(&mut state.marker_list, 4, 10);
3797 assert_eq!(removed, 1, "hint inside deleted range must be removed");
3798 assert!(state.virtual_texts.is_empty());
3799 }
3800
3801 #[test]
3802 fn test_marker_delete_after_repeat_clear_recreate() {
3803 use crate::model::marker::MarkerList;
3809 use crate::view::virtual_text::{VirtualTextManager, VirtualTextPosition};
3810 use ratatui::style::Style;
3811
3812 let mut markers = MarkerList::new();
3813 let mut vtexts = VirtualTextManager::new();
3814
3815 let positions = [200usize, 401, 602, 803, 1205, 1406];
3817 for &p in &positions {
3818 vtexts.add(
3819 &mut markers,
3820 p,
3821 format!("hint-at-{p}"),
3822 Style::default(),
3823 VirtualTextPosition::BeforeChar,
3824 0,
3825 );
3826 }
3827
3828 for _ in 0..3 {
3831 vtexts.clear(&mut markers);
3832 for &p in &positions {
3833 vtexts.add(
3834 &mut markers,
3835 p,
3836 format!("hint-at-{p}"),
3837 Style::default(),
3838 VirtualTextPosition::BeforeChar,
3839 0,
3840 );
3841 }
3842 }
3843
3844 let removed = vtexts.remove_in_range(&mut markers, 1005, 1206);
3846 assert_eq!(
3847 removed, 1,
3848 "exactly one marker inside [1005, 1206) should be removed"
3849 );
3850 markers.adjust_for_delete(1005, 201);
3851
3852 let lookup = vtexts.build_lookup(&markers, 0, 10_000);
3853 let mut positions: Vec<usize> = lookup.keys().copied().collect();
3854 positions.sort();
3855 assert_eq!(
3856 positions,
3857 vec![200, 401, 602, 803, 1205],
3858 "after delete+adjust, expected marker byte positions {:?}, got {:?}",
3859 vec![200, 401, 602, 803, 1205],
3860 positions
3861 );
3862 }
3863
3864 #[test]
3865 fn test_marker_delete_then_adjust_preserves_last_marker_position() {
3866 use crate::model::marker::MarkerList;
3880
3881 let mut markers = MarkerList::new();
3882 let m0 = markers.create(200, false);
3883 let m1 = markers.create(401, false);
3884 let m2 = markers.create(602, false);
3885 let m3 = markers.create(803, false);
3886 let m5 = markers.create(1205, false);
3887 let m6 = markers.create(1406, false);
3888
3889 markers.delete(m5);
3891
3892 markers.adjust_for_delete(1005, 201);
3894
3895 assert_eq!(markers.get_position(m0), Some(200), "m0 unchanged");
3896 assert_eq!(markers.get_position(m1), Some(401), "m1 unchanged");
3897 assert_eq!(markers.get_position(m2), Some(602), "m2 unchanged");
3898 assert_eq!(markers.get_position(m3), Some(803), "m3 unchanged");
3899 assert_eq!(
3900 markers.get_position(m6),
3901 Some(1205),
3902 "m6 must shift from 1406 to 1205 (1406 - 201), not be clamped to delete-start 1005"
3903 );
3904 }
3905
3906 #[test]
3907 fn test_inlay_hint_outside_deletion_survives() {
3908 let mut state = EditorState::new(
3910 80,
3911 24,
3912 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3913 test_fs(),
3914 );
3915 state.buffer = Buffer::from_str_test("let x = 42; let y = 0;");
3916 state.marker_list.adjust_for_insert(0, state.buffer.len());
3917
3918 let hints = vec![
3919 make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE)), make_hint(0, 17, ": i32", Some(InlayHintKind::TYPE)), ];
3922 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3923 assert_eq!(state.virtual_texts.len(), 2);
3924
3925 let removed = state
3926 .virtual_texts
3927 .remove_in_range(&mut state.marker_list, 4, 10);
3928 assert_eq!(removed, 1);
3929 assert_eq!(state.virtual_texts.len(), 1);
3930 }
3931
3932 #[test]
3933 fn test_space_doc_paragraphs_inserts_blank_lines() {
3934 use super::space_doc_paragraphs;
3935
3936 let input = "sep\n description.\nend\n another.";
3938 let result = space_doc_paragraphs(input);
3939 assert_eq!(result, "sep\n\n description.\n\nend\n\n another.");
3940 }
3941
3942 #[test]
3943 fn test_space_doc_paragraphs_preserves_existing_blank_lines() {
3944 use super::space_doc_paragraphs;
3945
3946 let input = "First paragraph.\n\nSecond paragraph.";
3948 let result = space_doc_paragraphs(input);
3949 assert_eq!(result, "First paragraph.\n\nSecond paragraph.");
3950 }
3951
3952 #[test]
3953 fn test_space_doc_paragraphs_plain_text() {
3954 use super::space_doc_paragraphs;
3955
3956 let input = "Just a single line of docs.";
3957 let result = space_doc_paragraphs(input);
3958 assert_eq!(result, "Just a single line of docs.");
3959 }
3960}