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