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 = __win.lsp.as_mut()?;
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 let hover_lsp_position = Some(position);
904
905 let diagnostic_lines = hover_lsp_position
910 .map(|pos| self.compose_hover_diagnostic_lines(pos))
911 .unwrap_or_default();
912
913 if contents.is_empty() && diagnostic_lines.is_empty() {
914 self.set_status_message(t!("lsp.no_hover").to_string());
915 self.active_window_mut().hover.set_symbol_range(None);
916 return;
917 }
918
919 tracing::debug!(
921 "LSP hover content (markdown={}):\n{}",
922 is_markdown,
923 contents
924 );
925
926 if let Some(((start_line, start_char), (end_line, end_char))) = range {
928 let state = self.active_state();
929 let start_byte = state
930 .buffer
931 .lsp_position_to_byte(start_line as usize, start_char as usize);
932 let end_byte = state
933 .buffer
934 .lsp_position_to_byte(end_line as usize, end_char as usize);
935 self.active_window_mut()
936 .hover
937 .set_symbol_range(Some((start_byte, end_byte)));
938 tracing::debug!(
939 "Hover symbol range: {}..{} (LSP {}:{}..{}:{})",
940 start_byte,
941 end_byte,
942 start_line,
943 start_char,
944 end_line,
945 end_char
946 );
947
948 if let Some(old_handle) = self.active_window_mut().hover.take_symbol_overlay() {
950 let remove_event = crate::model::event::Event::RemoveOverlay { handle: old_handle };
951 self.apply_event_to_active_buffer(&remove_event);
952 }
953
954 let event = crate::model::event::Event::AddOverlay {
956 namespace: None,
957 range: start_byte..end_byte,
958 face: crate::model::event::OverlayFace::Background {
959 color: (80, 80, 120), },
961 priority: 90, message: None,
963 extend_to_line_end: false,
964 url: None,
965 };
966 self.apply_event_to_active_buffer(&event);
967 if let Some(state) = self
969 .windows
970 .get(&self.active_window)
971 .map(|w| &w.buffers)
972 .expect("active window present")
973 .get(&self.active_buffer())
974 {
975 if let Some(handle) = state.overlays.all().last().map(|o| o.handle.clone()) {
976 self.active_window_mut().hover.set_symbol_overlay(handle);
977 }
978 }
979 } else {
980 let computed_range = if let Some((hover_byte_pos, _, _, _)) =
983 self.active_window_mut().mouse_state.lsp_hover_state
984 {
985 let state = self.active_state();
986 let start_byte = find_word_start(&state.buffer, hover_byte_pos);
987 let end_byte = find_word_end(&state.buffer, hover_byte_pos);
988 if start_byte < end_byte {
989 tracing::debug!(
990 "Hover symbol range (computed from word boundaries): {}..{}",
991 start_byte,
992 end_byte
993 );
994 Some((start_byte, end_byte))
995 } else {
996 None
997 }
998 } else {
999 None
1000 };
1001 self.active_window_mut()
1002 .hover
1003 .set_symbol_range(computed_range);
1004 }
1005
1006 use crate::view::markdown::{parse_markdown, StyledLine};
1017 use crate::view::popup::{Popup, PopupContent, PopupPosition};
1018 use ratatui::style::Style;
1019 use unicode_width::UnicodeWidthStr;
1020
1021 let hover_lines: Vec<StyledLine> = if contents.is_empty() {
1022 Vec::new()
1023 } else if is_markdown {
1024 parse_markdown(
1025 &contents,
1026 &*self.theme.read().unwrap(),
1027 Some(&self.grammar_registry),
1028 )
1029 } else {
1030 contents
1031 .lines()
1032 .map(|s| {
1033 let mut sl = StyledLine::new();
1034 sl.push(
1035 s.to_string(),
1036 Style::default().fg(self.theme.read().unwrap().popup_text_fg),
1037 );
1038 sl
1039 })
1040 .collect()
1041 };
1042
1043 let has_diagnostic = !diagnostic_lines.is_empty();
1044 let mut all_lines: Vec<StyledLine> = Vec::new();
1045 all_lines.extend(diagnostic_lines);
1046 if has_diagnostic && !hover_lines.is_empty() {
1047 let mut sep = StyledLine::new();
1051 sep.push(
1052 "─".repeat(12),
1053 Style::default().fg(self.theme.read().unwrap().popup_border_fg),
1054 );
1055 all_lines.push(sep);
1056 }
1057 all_lines.extend(hover_lines);
1058
1059 while all_lines
1061 .last()
1062 .map(|l| l.spans.iter().all(|s| s.text.trim().is_empty()))
1063 .unwrap_or(false)
1064 {
1065 all_lines.pop();
1066 }
1067
1068 let content_width: usize = all_lines
1073 .iter()
1074 .map(|l| {
1075 l.spans
1076 .iter()
1077 .map(|s| UnicodeWidthStr::width(s.text.as_str()))
1078 .sum::<usize>()
1079 })
1080 .max()
1081 .unwrap_or(0);
1082 let popup_width = (content_width as u16 + 4).clamp(30, 80);
1083 let dynamic_height = (self.terminal_height * 60 / 100).clamp(15, 40);
1084
1085 let mut popup = Popup::text(Vec::new(), &*self.theme.read().unwrap());
1087 popup.content = PopupContent::Markdown(all_lines);
1088 popup.title = Some(t!("lsp.popup_hover").to_string());
1089 popup.transient = true;
1090 popup.position = if let Some((x, y)) = self.active_window_mut().hover.take_screen_position()
1091 {
1092 PopupPosition::Fixed { x, y: y + 1 }
1093 } else {
1094 PopupPosition::BelowCursor
1095 };
1096 popup.width = popup_width;
1097 popup.max_height = dynamic_height;
1098 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1099 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1100 popup.focus_key_hint = self.popup_focus_key_hint();
1101
1102 let __buffer_id = self.active_buffer();
1106 if let Some(state) = self
1107 .windows
1108 .get_mut(&self.active_window)
1109 .map(|w| &mut w.buffers)
1110 .expect("active window present")
1111 .get_mut(&__buffer_id)
1112 {
1113 while state.popups.top().is_some_and(|p| p.transient) {
1114 state.popups.hide();
1115 }
1116 state.popups.show(popup);
1117 tracing::info!("Showing hover popup (markdown={})", is_markdown);
1118 }
1119
1120 self.active_window_mut().mouse_state.lsp_hover_request_sent = true;
1123 }
1124
1125 fn compose_hover_diagnostic_lines(
1136 &self,
1137 lsp_pos: (u32, u32),
1138 ) -> Vec<crate::view::markdown::StyledLine> {
1139 use crate::view::markdown::StyledLine;
1140 use lsp_types::DiagnosticSeverity;
1141 use ratatui::style::{Modifier, Style};
1142
1143 let buffer_id = self.active_buffer();
1144 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
1145 return Vec::new();
1146 };
1147 let Some(uri) = metadata.file_uri() else {
1148 return Vec::new();
1149 };
1150 let Some(diagnostics) = self.get_stored_diagnostics().get(uri.as_str()) else {
1151 return Vec::new();
1152 };
1153
1154 let (hover_line, hover_char) = lsp_pos;
1155 let overlapping: Vec<&lsp_types::Diagnostic> = diagnostics
1156 .iter()
1157 .filter(|d| lsp_range_contains(&d.range, hover_line, hover_char))
1158 .collect();
1159
1160 if overlapping.is_empty() {
1161 return Vec::new();
1162 }
1163
1164 let mut out: Vec<StyledLine> = Vec::new();
1165 for (idx, diag) in overlapping.iter().enumerate() {
1166 if idx > 0 {
1167 out.push(StyledLine::new());
1168 }
1169
1170 let (label, marker, severity_color) = match diag.severity {
1171 Some(DiagnosticSeverity::ERROR) => {
1172 ("Error", "✖", self.theme.read().unwrap().diagnostic_error_fg)
1173 }
1174 Some(DiagnosticSeverity::WARNING) => (
1175 "Warning",
1176 "⚠",
1177 self.theme.read().unwrap().diagnostic_warning_fg,
1178 ),
1179 Some(DiagnosticSeverity::INFORMATION) => {
1180 ("Info", "ℹ", self.theme.read().unwrap().diagnostic_info_fg)
1181 }
1182 Some(DiagnosticSeverity::HINT) => {
1183 ("Hint", "ℹ", self.theme.read().unwrap().diagnostic_hint_fg)
1184 }
1185 _ => ("Diagnostic", "•", self.theme.read().unwrap().popup_text_fg),
1186 };
1187
1188 let header_style = Style::default()
1189 .fg(severity_color)
1190 .add_modifier(Modifier::BOLD);
1191 let mut header = StyledLine::new();
1192 header.push(format!("{} {}", marker, label), header_style);
1193 if let Some(source) = diag.source.as_deref().filter(|s| !s.is_empty()) {
1194 header.push(
1197 format!(" ({})", source),
1198 Style::default()
1199 .fg(self.theme.read().unwrap().tab_inactive_fg)
1200 .add_modifier(Modifier::ITALIC),
1201 );
1202 }
1203 out.push(header);
1204
1205 for message_line in diag.message.lines() {
1209 let mut line = StyledLine::new();
1210 line.push(
1211 message_line.to_string(),
1212 Style::default().fg(self.theme.read().unwrap().popup_text_fg),
1213 );
1214 out.push(line);
1215 }
1216 }
1217 out
1218 }
1219
1220 #[doc(hidden)]
1222 pub fn apply_inlay_hints_to_state(
1223 state: &mut crate::state::EditorState,
1224 hints: &[lsp_types::InlayHint],
1225 ) {
1226 use crate::view::virtual_text::VirtualTextPosition;
1227 use ratatui::style::{Color, Style};
1228
1229 state.virtual_texts.clear(&mut state.marker_list);
1231
1232 if hints.is_empty() {
1233 return;
1234 }
1235
1236 let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
1242 let hint_fg_theme_key = Some("editor.line_number_fg".to_string());
1243
1244 for hint in hints {
1245 let byte_offset = state.buffer.lsp_position_to_byte(
1247 hint.position.line as usize,
1248 hint.position.character as usize,
1249 );
1250
1251 let text = match &hint.label {
1253 lsp_types::InlayHintLabel::String(s) => s.clone(),
1254 lsp_types::InlayHintLabel::LabelParts(parts) => {
1255 parts.iter().map(|p| p.value.as_str()).collect::<String>()
1256 }
1257 };
1258
1259 if state.buffer.is_empty() {
1265 continue;
1266 }
1267
1268 let buf_len = state.buffer.len();
1277 let byte_here = if byte_offset < buf_len {
1278 state
1279 .buffer
1280 .slice_bytes(byte_offset..byte_offset + 1)
1281 .first()
1282 .copied()
1283 } else {
1284 None
1285 };
1286 let at_line_break = matches!(byte_here, Some(b'\n' | b'\r'));
1287
1288 let (byte_offset, position) = if byte_offset >= buf_len {
1289 (buf_len.saturating_sub(1), VirtualTextPosition::AfterChar)
1292 } else if at_line_break && byte_offset > 0 {
1293 (byte_offset - 1, VirtualTextPosition::AfterChar)
1297 } else {
1298 (byte_offset, VirtualTextPosition::BeforeChar)
1299 };
1300
1301 let display_text = text;
1303
1304 state.virtual_texts.add_with_theme_keys(
1305 &mut state.marker_list,
1306 byte_offset,
1307 display_text,
1308 hint_style,
1309 hint_fg_theme_key.clone(),
1310 None,
1311 position,
1312 0, );
1314 }
1315
1316 tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
1317 }
1318
1319 pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
1321 use crate::primitives::word_navigation::{find_word_end, find_word_start};
1322
1323 let cursor_pos = self.active_cursors().primary().position;
1324 let (line, character, symbol) = {
1325 let state = self.active_state();
1326 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1327 let word_start = find_word_start(&state.buffer, cursor_pos);
1328 let word_end = find_word_end(&state.buffer, cursor_pos);
1329 let symbol = String::from_utf8_lossy(&state.buffer.slice_bytes(word_start..word_end))
1330 .into_owned();
1331 (line, character, symbol)
1332 };
1333
1334 let buffer_id = self.active_buffer();
1335 let request_id = self.active_window_mut().next_lsp_request_id;
1336
1337 let sent = self
1339 .with_lsp_for_buffer(
1340 buffer_id,
1341 LspFeature::References,
1342 |handle, uri, _language| {
1343 let result = handle.references(
1344 request_id,
1345 uri.as_uri().clone(),
1346 line as u32,
1347 character as u32,
1348 );
1349 if result.is_ok() {
1350 tracing::info!(
1351 "Requested find references at {}:{}:{} (byte_pos={})",
1352 uri.as_str(),
1353 line,
1354 character,
1355 cursor_pos
1356 );
1357 }
1358 result.is_ok()
1359 },
1360 )
1361 .unwrap_or(false);
1362
1363 if sent {
1364 self.active_window_mut().next_lsp_request_id += 1;
1365 self.active_window_mut().pending_references_request = Some(request_id);
1366 self.active_window_mut().pending_references_symbol = symbol;
1367 }
1368
1369 Ok(())
1370 }
1371
1372 pub(crate) fn request_signature_help(&mut self) {
1374 let cursor_pos = self.active_cursors().primary().position;
1376 let state = self.active_state();
1377
1378 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1380 let buffer_id = self.active_buffer();
1381 let request_id = self.active_window_mut().next_lsp_request_id;
1382
1383 let sent = self
1385 .with_lsp_for_buffer(
1386 buffer_id,
1387 LspFeature::SignatureHelp,
1388 |handle, uri, _language| {
1389 let result = handle.signature_help(
1390 request_id,
1391 uri.as_uri().clone(),
1392 line as u32,
1393 character as u32,
1394 );
1395 if result.is_ok() {
1396 tracing::info!(
1397 "Requested signature help at {}:{}:{} (byte_pos={})",
1398 uri.as_str(),
1399 line,
1400 character,
1401 cursor_pos
1402 );
1403 }
1404 result.is_ok()
1405 },
1406 )
1407 .unwrap_or(false);
1408
1409 if sent {
1410 self.active_window_mut().next_lsp_request_id += 1;
1411 self.active_window_mut().pending_signature_help_request = Some(request_id);
1412 }
1413 }
1414
1415 pub(crate) fn handle_signature_help_response(
1417 &mut self,
1418 request_id: u64,
1419 signature_help: Option<lsp_types::SignatureHelp>,
1420 ) {
1421 if self.active_window_mut().pending_signature_help_request != Some(request_id) {
1423 tracing::debug!("Ignoring stale signature help response: {}", request_id);
1424 return;
1425 }
1426
1427 self.active_window_mut().pending_signature_help_request = None;
1428 let signature_help = match signature_help {
1429 Some(help) if !help.signatures.is_empty() => help,
1430 _ => {
1431 tracing::debug!("No signature help available");
1432 return;
1433 }
1434 };
1435
1436 let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
1438 let signature = match signature_help.signatures.get(active_signature_idx) {
1439 Some(sig) => sig,
1440 None => return,
1441 };
1442
1443 let mut content = String::new();
1445
1446 content.push_str(&signature.label);
1448 content.push('\n');
1449
1450 let active_param = signature_help
1452 .active_parameter
1453 .or(signature.active_parameter)
1454 .unwrap_or(0) as usize;
1455
1456 if let Some(params) = &signature.parameters {
1458 if let Some(param) = params.get(active_param) {
1459 let param_label = match ¶m.label {
1461 lsp_types::ParameterLabel::Simple(s) => s.clone(),
1462 lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1463 let start = offsets[0] as usize;
1465 let end = offsets[1] as usize;
1466 if end <= signature.label.len() {
1467 signature.label[start..end].to_string()
1468 } else {
1469 String::new()
1470 }
1471 }
1472 };
1473
1474 if !param_label.is_empty() {
1475 content.push_str(&format!("\n> {}\n", param_label));
1476 }
1477
1478 if let Some(doc) = ¶m.documentation {
1480 let doc_text = match doc {
1481 lsp_types::Documentation::String(s) => s.clone(),
1482 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1483 };
1484 if !doc_text.is_empty() {
1485 content.push('\n');
1486 content.push_str(&doc_text);
1487 content.push('\n');
1488 }
1489 }
1490 }
1491 }
1492
1493 if let Some(doc) = &signature.documentation {
1495 let doc_text = match doc {
1496 lsp_types::Documentation::String(s) => s.clone(),
1497 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1498 };
1499 if !doc_text.is_empty() {
1500 content.push_str("\n---\n\n");
1501 content.push_str(&space_doc_paragraphs(&doc_text));
1502 }
1503 }
1504
1505 use crate::view::popup::{Popup, PopupPosition};
1507 use ratatui::style::Style;
1508
1509 let mut popup = Popup::markdown(
1510 &content,
1511 &*self.theme.read().unwrap(),
1512 Some(&self.grammar_registry),
1513 );
1514 popup.title = Some(t!("lsp.popup_signature").to_string());
1515 popup.transient = true;
1516 popup.position = PopupPosition::BelowCursor;
1517 popup.width = 60;
1518 popup.max_height = 20;
1519 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1520 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1521 popup.focus_key_hint = self.popup_focus_key_hint();
1522
1523 let __buffer_id = self.active_buffer();
1525 if let Some(state) = self
1526 .windows
1527 .get_mut(&self.active_window)
1528 .map(|w| &mut w.buffers)
1529 .expect("active window present")
1530 .get_mut(&__buffer_id)
1531 {
1532 state.popups.show(popup);
1533 tracing::info!(
1534 "Showing signature help popup for {} signatures",
1535 signature_help.signatures.len()
1536 );
1537 }
1538 }
1539
1540 pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1543 if !self
1552 .active_window()
1553 .pending_code_actions_requests
1554 .is_empty()
1555 {
1556 let ids: Vec<u64> = self
1557 .active_window_mut()
1558 .pending_code_actions_requests
1559 .drain()
1560 .collect();
1561 for request_id in ids {
1562 tracing::debug!(
1563 "Canceling previous pending LSP code actions request {}",
1564 request_id
1565 );
1566 self.active_window_mut().send_lsp_cancel_request(request_id);
1567 }
1568 }
1569 self.active_window_mut()
1570 .pending_code_actions_server_names
1571 .clear();
1572 self.active_window_mut().pending_code_actions = None;
1573
1574 let cursor_pos = self.active_cursors().primary().position;
1576 let selection_range = self.active_cursors().primary().selection_range();
1577 let state = self.active_state();
1578
1579 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1581
1582 let (start_line, start_char, end_line, end_char) = if let Some(range) = selection_range {
1584 let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1585 let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1586 (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1587 } else {
1588 (line as u32, character as u32, line as u32, character as u32)
1589 };
1590
1591 let diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
1594 let buffer_id = self.active_buffer();
1595
1596 let base_request_id = self.active_window_mut().next_lsp_request_id;
1598 let counter = std::sync::atomic::AtomicU64::new(0);
1599
1600 let results = self.with_all_lsp_for_buffer_feature_named(
1601 buffer_id,
1602 LspFeature::CodeAction,
1603 |handle, uri, _language, server_name| {
1604 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1605 let request_id = base_request_id + idx;
1606 let result = handle.code_actions(
1607 request_id,
1608 uri.as_uri().clone(),
1609 start_line,
1610 start_char,
1611 end_line,
1612 end_char,
1613 diagnostics.clone(),
1614 );
1615 if result.is_ok() {
1616 tracing::info!(
1617 "Requested code actions at {}:{}:{}-{}:{} (byte_pos={}, request_id={}, server={})",
1618 uri.as_str(),
1619 start_line,
1620 start_char,
1621 end_line,
1622 end_char,
1623 cursor_pos,
1624 request_id,
1625 server_name
1626 );
1627 }
1628 (request_id, result.is_ok(), server_name.to_string())
1629 },
1630 );
1631
1632 let mut sent_ids = Vec::new();
1633 for (request_id, ok, server_name) in &results {
1634 if *ok {
1635 sent_ids.push(*request_id);
1636 self.active_window_mut()
1637 .pending_code_actions_server_names
1638 .insert(*request_id, server_name.clone());
1639 }
1640 }
1641 self.active_window_mut().next_lsp_request_id = base_request_id + results.len() as u64;
1643
1644 if !sent_ids.is_empty() {
1645 self.active_window_mut()
1648 .pending_code_actions_requests
1649 .extend(sent_ids);
1650 }
1651
1652 Ok(())
1653 }
1654
1655 pub(crate) fn handle_code_actions_response(
1659 &mut self,
1660 request_id: u64,
1661 actions: Vec<lsp_types::CodeActionOrCommand>,
1662 ) {
1663 if !self
1665 .active_window_mut()
1666 .pending_code_actions_requests
1667 .remove(&request_id)
1668 {
1669 tracing::debug!("Ignoring stale code actions response: {}", request_id);
1670 return;
1671 }
1672
1673 let server_name = self
1675 .active_window_mut()
1676 .pending_code_actions_server_names
1677 .remove(&request_id)
1678 .unwrap_or_default();
1679
1680 if actions.is_empty() {
1681 if self
1683 .active_window()
1684 .pending_code_actions_requests
1685 .is_empty()
1686 && self
1687 .active_window_mut()
1688 .pending_code_actions
1689 .as_ref()
1690 .is_none_or(|a| a.is_empty())
1691 {
1692 self.set_status_message(t!("lsp.no_code_actions").to_string());
1693 }
1694 return;
1695 }
1696
1697 let tagged_actions: Vec<(String, lsp_types::CodeActionOrCommand)> = actions
1699 .into_iter()
1700 .map(|a| (server_name.clone(), a))
1701 .collect();
1702
1703 match &mut self.active_window_mut().pending_code_actions {
1704 Some(existing) => {
1705 existing.extend(tagged_actions);
1706 tracing::debug!("Extended code actions, now {} total", existing.len());
1707 }
1708 None => {
1709 self.active_window_mut().pending_code_actions = Some(tagged_actions);
1710 }
1711 }
1712
1713 use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1715 use ratatui::style::Style;
1716
1717 let items: Vec<PopupListItem> = {
1718 let all_actions = self.active_window().pending_code_actions.as_ref().unwrap();
1719 let multiple_servers = {
1720 let mut names = std::collections::HashSet::new();
1721 for (name, _) in all_actions {
1722 names.insert(name.as_str());
1723 }
1724 names.len() > 1
1725 };
1726 all_actions
1727 .iter()
1728 .enumerate()
1729 .map(|(i, (srv_name, action))| {
1730 let title = match action {
1731 lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1732 lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1733 };
1734 let kind = match action {
1735 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1736 ca.kind.as_ref().map(|k| k.as_str().to_string())
1737 }
1738 _ => None,
1739 };
1740 let detail = if multiple_servers && !srv_name.is_empty() {
1741 match kind {
1742 Some(k) => Some(format!("[{}] {}", srv_name, k)),
1743 None => Some(format!("[{}]", srv_name)),
1744 }
1745 } else {
1746 kind
1747 };
1748 PopupListItem {
1749 text: format!("{}. {}", i + 1, title),
1750 detail,
1751 icon: None,
1752 data: Some(i.to_string()),
1753 disabled: false,
1754 }
1755 })
1756 .collect()
1757 };
1758
1759 let mut popup = Popup::list(items, &*self.theme.read().unwrap());
1760 popup.kind = crate::view::popup::PopupKind::Action;
1761 popup.title = Some(t!("lsp.popup_code_actions").to_string());
1762 popup.position = PopupPosition::BelowCursor;
1763 popup.width = 60;
1764 popup.max_height = 15;
1765 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1766 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1767 popup.resolver = crate::view::popup::PopupResolver::CodeAction;
1771 popup.focused = true;
1777
1778 let __buffer_id = self.active_buffer();
1780 let action_count = self
1781 .active_window()
1782 .pending_code_actions
1783 .as_ref()
1784 .map_or(0, |v| v.len());
1785 if let Some(state) = self
1786 .windows
1787 .get_mut(&self.active_window)
1788 .map(|w| &mut w.buffers)
1789 .expect("active window present")
1790 .get_mut(&__buffer_id)
1791 {
1792 state.popups.show_or_replace(popup);
1793 tracing::info!("Showing code actions popup with {} actions", action_count);
1794 }
1795 }
1796
1797 pub(crate) fn execute_code_action(&mut self, index: usize) {
1799 let action = match &self.active_window_mut().pending_code_actions {
1800 Some(actions) => actions.get(index).map(|(_, a)| a.clone()),
1801 None => None,
1802 };
1803
1804 let Some(action) = action else {
1805 tracing::warn!("Code action index {} out of range", index);
1806 return;
1807 };
1808
1809 match action {
1810 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1811 if ca.edit.is_none()
1814 && ca.command.is_none()
1815 && ca.data.is_some()
1816 && self.active_window().server_supports_code_action_resolve()
1817 {
1818 tracing::info!(
1819 "Code action '{}' needs resolve, sending codeAction/resolve",
1820 ca.title
1821 );
1822 self.send_code_action_resolve(ca);
1823 return;
1824 }
1825 self.execute_resolved_code_action(ca);
1826 }
1827 lsp_types::CodeActionOrCommand::Command(cmd) => {
1828 self.send_execute_command(cmd);
1829 }
1830 }
1831 }
1832
1833 pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1835 let title = ca.title.clone();
1836
1837 if let Some(edit) = ca.edit {
1839 match self.apply_workspace_edit(edit) {
1840 Ok(n) => {
1841 self.set_status_message(
1842 t!("lsp.code_action_applied", title = &title, count = n).to_string(),
1843 );
1844 }
1845 Err(e) => {
1846 self.set_status_message(format!("Code action failed: {e}"));
1847 return;
1848 }
1849 }
1850 }
1851
1852 if let Some(cmd) = ca.command {
1854 self.send_execute_command(cmd);
1855 }
1856 }
1857
1858 fn send_execute_command(&mut self, cmd: lsp_types::Command) {
1860 tracing::info!("Executing LSP command: {} ({})", cmd.title, cmd.command);
1861 self.set_status_message(
1862 t!(
1863 "lsp.code_action_applied",
1864 title = &cmd.title,
1865 count = 0_usize
1866 )
1867 .to_string(),
1868 );
1869
1870 let language = match self
1872 .buffers()
1873 .get(&self.active_buffer())
1874 .map(|s| s.language.clone())
1875 {
1876 Some(l) => l,
1877 None => return,
1878 };
1879
1880 let __active_id = self.active_window;
1881
1882 if let Some(lsp) = self
1883 .windows
1884 .get_mut(&__active_id)
1885 .and_then(|w| w.lsp.as_mut())
1886 {
1887 for sh in lsp.get_handles_mut(&language) {
1888 if let Err(e) = sh
1889 .handle
1890 .execute_command(cmd.command.clone(), cmd.arguments.clone())
1891 {
1892 tracing::warn!("Failed to send executeCommand to '{}': {}", sh.name, e);
1893 }
1894 }
1895 }
1896 }
1897
1898 fn send_code_action_resolve(&mut self, action: lsp_types::CodeAction) {
1900 let language = match self
1901 .buffers()
1902 .get(&self.active_buffer())
1903 .map(|s| s.language.clone())
1904 {
1905 Some(l) => l,
1906 None => return,
1907 };
1908
1909 self.active_window_mut().next_lsp_request_id += 1;
1910 let request_id = self.active_window_mut().next_lsp_request_id;
1911
1912 let __active_id = self.active_window;
1913
1914 if let Some(lsp) = self
1915 .windows
1916 .get_mut(&__active_id)
1917 .and_then(|w| w.lsp.as_mut())
1918 {
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
2004 .windows
2005 .get_mut(&__active_id)
2006 .and_then(|w| w.lsp.as_mut())
2007 {
2008 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Format) {
2009 if let Err(e) = sh.handle.document_formatting(
2010 request_id,
2011 uri.as_uri().clone(),
2012 tab_size,
2013 insert_spaces,
2014 ) {
2015 tracing::warn!("Failed to request formatting: {}", e);
2016 }
2017 } else {
2018 self.set_status_message("Formatting not supported by LSP server".to_string());
2019 }
2020 }
2021 }
2022
2023 pub(crate) fn handle_references_response(
2025 &mut self,
2026 request_id: u64,
2027 locations: Vec<lsp_types::Location>,
2028 ) -> AnyhowResult<()> {
2029 tracing::info!(
2030 "handle_references_response: received {} locations for request_id={}",
2031 locations.len(),
2032 request_id
2033 );
2034
2035 if self.active_window_mut().pending_references_request != Some(request_id) {
2037 tracing::debug!("Ignoring stale references response: {}", request_id);
2038 return Ok(());
2039 }
2040
2041 self.active_window_mut().pending_references_request = None;
2042 if locations.is_empty() {
2043 self.set_status_message(t!("lsp.no_references").to_string());
2044 return Ok(());
2045 }
2046
2047 let translation = self.authority.path_translation.clone();
2054 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
2055 .iter()
2056 .map(|loc| {
2057 let wire = crate::app::types::LspUri::from_wire(loc.uri.clone());
2058 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
2063 wire.to_host_path(translation.as_ref())
2064 .map(|p| p.to_string_lossy().into_owned())
2065 .unwrap_or_else(|| loc.uri.path().as_str().to_string())
2066 } else {
2067 loc.uri.as_str().to_string()
2068 };
2069
2070 crate::services::plugins::hooks::LspLocation {
2071 file,
2072 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
2075 })
2076 .collect();
2077
2078 let count = lsp_locations.len();
2079 let symbol = std::mem::take(&mut self.active_window_mut().pending_references_symbol);
2080 self.set_status_message(
2081 t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
2082 );
2083
2084 self.plugin_manager.read().unwrap().run_hook(
2086 "lsp_references",
2087 crate::services::plugins::hooks::HookArgs::LspReferences {
2088 symbol: symbol.clone(),
2089 locations: lsp_locations,
2090 },
2091 );
2092
2093 tracing::info!(
2094 "Fired lsp_references hook with {} locations for symbol '{}'",
2095 count,
2096 symbol
2097 );
2098
2099 Ok(())
2100 }
2101
2102 pub(crate) fn apply_lsp_text_edits(
2105 &mut self,
2106 buffer_id: BufferId,
2107 mut edits: Vec<lsp_types::TextEdit>,
2108 ) -> AnyhowResult<usize> {
2109 if edits.is_empty() {
2110 return Ok(0);
2111 }
2112
2113 edits.sort_by(|a, b| {
2115 b.range
2116 .start
2117 .line
2118 .cmp(&a.range.start.line)
2119 .then(b.range.start.character.cmp(&a.range.start.character))
2120 });
2121
2122 let mut batch_events = Vec::new();
2124 let mut changes = 0;
2125
2126 let cursor_id = {
2128 let split_id = self
2129 .split_manager_mut()
2130 .splits_for_buffer(buffer_id)
2131 .into_iter()
2132 .next()
2133 .unwrap_or_else(|| {
2134 self.windows
2135 .get(&self.active_window)
2136 .and_then(|w| w.buffers.splits())
2137 .map(|(mgr, _)| mgr)
2138 .expect("active window must have a populated split layout")
2139 .active_split()
2140 });
2141 self.windows
2142 .get(&self.active_window)
2143 .and_then(|w| w.buffers.splits())
2144 .map(|(_, vs)| vs)
2145 .expect("active window must have a populated split layout")
2146 .get(&split_id)
2147 .map(|vs| vs.cursors.primary_id())
2148 .unwrap_or_else(|| self.active_cursors().primary_id())
2149 };
2150
2151 for edit in edits {
2153 let state = self
2154 .buffers_mut()
2155 .get_mut(&buffer_id)
2156 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2157
2158 let start_line = edit.range.start.line as usize;
2160 let start_char = edit.range.start.character as usize;
2161 let end_line = edit.range.end.line as usize;
2162 let end_char = edit.range.end.character as usize;
2163
2164 let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
2165 let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
2166 let buffer_len = state.buffer.len();
2167
2168 let old_text = if start_pos < end_pos && end_pos <= buffer_len {
2170 state.get_text_range(start_pos, end_pos)
2171 } else {
2172 format!(
2173 "<invalid range: start={}, end={}, buffer_len={}>",
2174 start_pos, end_pos, buffer_len
2175 )
2176 };
2177 tracing::debug!(
2178 " Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
2179 start_line, start_char, end_line, end_char,
2180 start_pos, end_pos, old_text, edit.new_text
2181 );
2182
2183 if start_pos < end_pos {
2185 let deleted_text = state.get_text_range(start_pos, end_pos);
2186 let delete_event = Event::Delete {
2187 range: start_pos..end_pos,
2188 deleted_text,
2189 cursor_id,
2190 };
2191 batch_events.push(delete_event);
2192 }
2193
2194 if !edit.new_text.is_empty() {
2196 let insert_event = Event::Insert {
2197 position: start_pos,
2198 text: edit.new_text.clone(),
2199 cursor_id,
2200 };
2201 batch_events.push(insert_event);
2202 }
2203
2204 changes += 1;
2205 }
2206
2207 if !batch_events.is_empty() {
2209 self.apply_events_to_buffer_as_bulk_edit(
2210 buffer_id,
2211 batch_events,
2212 "LSP Rename".to_string(),
2213 )?;
2214 }
2215
2216 Ok(changes)
2217 }
2218
2219 fn apply_text_document_edit(
2225 &mut self,
2226 text_doc_edit: lsp_types::TextDocumentEdit,
2227 ) -> AnyhowResult<usize> {
2228 let uri = crate::app::types::LspUri::from_wire(text_doc_edit.text_document.uri);
2231
2232 if let Some(expected_version) = text_doc_edit.text_document.version {
2235 if let Ok(path) =
2236 super::lsp_uri_to_host_path(&uri, self.authority.path_translation.as_ref())
2237 {
2238 if let Some(lsp) = self.lsp() {
2239 let language = self
2240 .buffers()
2241 .get(&self.active_buffer())
2242 .map(|s| s.language.clone())
2243 .unwrap_or_default();
2244 for sh in lsp.get_handles(&language) {
2245 if let Some(current_version) = sh.handle.document_version(&path) {
2246 if (expected_version as i64) != current_version {
2247 tracing::warn!(
2248 "Rejecting stale TextDocumentEdit for {:?}: \
2249 server version {} != our version {}",
2250 path,
2251 expected_version,
2252 current_version,
2253 );
2254 return Ok(0);
2255 }
2256 }
2257 }
2258 }
2259 }
2260 }
2261
2262 if let Ok(path) =
2263 super::lsp_uri_to_host_path(&uri, self.authority.path_translation.as_ref())
2264 {
2265 let buffer_id = match self.open_file(&path) {
2266 Ok(id) => id,
2267 Err(e) => {
2268 if let Some(confirmation) =
2269 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2270 {
2271 self.start_large_file_encoding_confirmation(confirmation);
2272 } else {
2273 self.set_status_message(
2274 t!("file.error_opening", error = e.to_string()).to_string(),
2275 );
2276 }
2277 return Ok(0);
2278 }
2279 };
2280
2281 let edits: Vec<lsp_types::TextEdit> = text_doc_edit
2282 .edits
2283 .into_iter()
2284 .map(|one_of| match one_of {
2285 lsp_types::OneOf::Left(text_edit) => text_edit,
2286 lsp_types::OneOf::Right(annotated) => annotated.text_edit,
2287 })
2288 .collect();
2289
2290 tracing::info!("Applying {} edits for {:?}:", edits.len(), path);
2291 for (i, edit) in edits.iter().enumerate() {
2292 tracing::info!(
2293 " Edit {}: line {}:{}-{}:{} -> {:?}",
2294 i,
2295 edit.range.start.line,
2296 edit.range.start.character,
2297 edit.range.end.line,
2298 edit.range.end.character,
2299 edit.new_text
2300 );
2301 }
2302
2303 self.apply_lsp_text_edits(buffer_id, edits)
2304 } else {
2305 Ok(0)
2306 }
2307 }
2308
2309 fn apply_resource_operation(&mut self, op: lsp_types::ResourceOp) -> AnyhowResult<()> {
2311 let translation = self.authority.path_translation.clone();
2316 let to_host = |uri: &lsp_types::Uri| -> std::path::PathBuf {
2317 crate::app::types::LspUri::from_wire(uri.clone())
2318 .to_host_path(translation.as_ref())
2319 .unwrap_or_else(|| std::path::PathBuf::from(uri.path().as_str()))
2320 };
2321 match op {
2322 lsp_types::ResourceOp::Create(create) => {
2323 let path = to_host(&create.uri);
2324 let overwrite = create
2325 .options
2326 .as_ref()
2327 .and_then(|o| o.overwrite)
2328 .unwrap_or(false);
2329 let ignore_if_exists = create
2330 .options
2331 .as_ref()
2332 .and_then(|o| o.ignore_if_exists)
2333 .unwrap_or(false);
2334
2335 if path.exists() {
2336 if ignore_if_exists {
2337 tracing::debug!("CreateFile: {:?} already exists, ignoring", path);
2338 return Ok(());
2339 }
2340 if !overwrite {
2341 tracing::warn!("CreateFile: {:?} already exists and overwrite=false", path);
2342 return Ok(());
2343 }
2344 }
2345
2346 if let Some(parent) = path.parent() {
2348 std::fs::create_dir_all(parent)?;
2349 }
2350 std::fs::write(&path, "")?;
2351 tracing::info!("CreateFile: created {:?}", path);
2352
2353 if let Err(e) = self.open_file(&path) {
2355 tracing::warn!("CreateFile: failed to open created file {:?}: {}", path, e);
2356 }
2357 }
2358 lsp_types::ResourceOp::Rename(rename) => {
2359 let old_path = to_host(&rename.old_uri);
2360 let new_path = to_host(&rename.new_uri);
2361 let overwrite = rename
2362 .options
2363 .as_ref()
2364 .and_then(|o| o.overwrite)
2365 .unwrap_or(false);
2366 let ignore_if_exists = rename
2367 .options
2368 .as_ref()
2369 .and_then(|o| o.ignore_if_exists)
2370 .unwrap_or(false);
2371
2372 if new_path.exists() {
2373 if ignore_if_exists {
2374 tracing::debug!("RenameFile: {:?} already exists, ignoring", new_path);
2375 return Ok(());
2376 }
2377 if !overwrite {
2378 tracing::warn!(
2379 "RenameFile: {:?} already exists and overwrite=false",
2380 new_path
2381 );
2382 return Ok(());
2383 }
2384 }
2385
2386 if let Some(parent) = new_path.parent() {
2388 std::fs::create_dir_all(parent)?;
2389 }
2390 std::fs::rename(&old_path, &new_path)?;
2391 tracing::info!("RenameFile: {:?} -> {:?}", old_path, new_path);
2392 }
2393 lsp_types::ResourceOp::Delete(delete) => {
2394 let path = to_host(&delete.uri);
2395 let recursive = delete
2396 .options
2397 .as_ref()
2398 .and_then(|o| o.recursive)
2399 .unwrap_or(false);
2400 let ignore_if_not_exists = delete
2401 .options
2402 .as_ref()
2403 .and_then(|o| o.ignore_if_not_exists)
2404 .unwrap_or(false);
2405
2406 if !path.exists() {
2407 if ignore_if_not_exists {
2408 tracing::debug!("DeleteFile: {:?} does not exist, ignoring", path);
2409 return Ok(());
2410 }
2411 tracing::warn!("DeleteFile: {:?} does not exist", path);
2412 return Ok(());
2413 }
2414
2415 if path.is_dir() && recursive {
2416 std::fs::remove_dir_all(&path)?;
2417 } else if path.is_file() {
2418 std::fs::remove_file(&path)?;
2419 }
2420 tracing::info!("DeleteFile: deleted {:?}", path);
2421 }
2422 }
2423 Ok(())
2424 }
2425
2426 pub(crate) fn apply_workspace_edit(
2430 &mut self,
2431 workspace_edit: lsp_types::WorkspaceEdit,
2432 ) -> AnyhowResult<usize> {
2433 tracing::debug!(
2434 "Applying WorkspaceEdit: changes={:?}, document_changes={:?}",
2435 workspace_edit.changes.as_ref().map(|c| c.len()),
2436 workspace_edit.document_changes.as_ref().map(|dc| match dc {
2437 lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
2438 lsp_types::DocumentChanges::Operations(o) => format!("{} operations", o.len()),
2439 })
2440 );
2441
2442 let mut total_changes = 0;
2443
2444 if let Some(changes) = workspace_edit.changes {
2446 for (uri, edits) in changes {
2447 let uri = crate::app::types::LspUri::from_wire(uri);
2448 if let Ok(path) =
2449 super::lsp_uri_to_host_path(&uri, self.authority.path_translation.as_ref())
2450 {
2451 let buffer_id = match self.open_file(&path) {
2452 Ok(id) => id,
2453 Err(e) => {
2454 if let Some(confirmation) = e.downcast_ref::<
2455 crate::model::buffer::LargeFileEncodingConfirmation,
2456 >() {
2457 self.start_large_file_encoding_confirmation(confirmation);
2458 } else {
2459 self.set_status_message(
2460 t!("file.error_opening", error = e.to_string())
2461 .to_string(),
2462 );
2463 }
2464 return Ok(0);
2465 }
2466 };
2467 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
2468 }
2469 }
2470 }
2471
2472 if let Some(document_changes) = workspace_edit.document_changes {
2474 use lsp_types::DocumentChanges;
2475
2476 match document_changes {
2477 DocumentChanges::Edits(edits) => {
2478 for text_doc_edit in edits {
2479 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2480 }
2481 }
2482 DocumentChanges::Operations(ops) => {
2483 for op in ops {
2486 match op {
2487 lsp_types::DocumentChangeOperation::Edit(text_doc_edit) => {
2488 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2489 }
2490 lsp_types::DocumentChangeOperation::Op(resource_op) => {
2491 self.apply_resource_operation(resource_op)?;
2492 total_changes += 1;
2493 }
2494 }
2495 }
2496 }
2497 }
2498 }
2499
2500 Ok(total_changes)
2501 }
2502
2503 pub fn handle_rename_response(
2505 &mut self,
2506 _request_id: u64,
2507 result: Result<lsp_types::WorkspaceEdit, String>,
2508 ) -> AnyhowResult<()> {
2509 match result {
2510 Ok(workspace_edit) => {
2511 let total_changes = self.apply_workspace_edit(workspace_edit)?;
2512 self.active_window_mut().status_message =
2513 Some(t!("lsp.renamed", count = total_changes).to_string());
2514 }
2515 Err(error) => {
2516 if error.contains("content modified") || error.contains("-32801") {
2518 tracing::debug!(
2519 "LSP rename: ContentModified error (expected, ignoring): {}",
2520 error
2521 );
2522 self.active_window_mut().status_message =
2523 Some(t!("lsp.rename_cancelled").to_string());
2524 } else {
2525 self.active_window_mut().status_message =
2526 Some(t!("lsp.rename_failed", error = &error).to_string());
2527 }
2528 }
2529 }
2530
2531 Ok(())
2532 }
2533
2534 pub(crate) fn apply_events_to_buffer_as_bulk_edit(
2539 &mut self,
2540 buffer_id: BufferId,
2541 events: Vec<Event>,
2542 description: String,
2543 ) -> AnyhowResult<()> {
2544 use crate::model::event::CursorId;
2545
2546 if events.is_empty() {
2547 return Ok(());
2548 }
2549
2550 let batch_for_lsp = Event::Batch {
2552 events: events.clone(),
2553 description: description.clone(),
2554 };
2555
2556 let original_active = self.active_buffer();
2568 self.windows
2569 .get_mut(&self.active_window)
2570 .and_then(|w| w.split_manager_mut())
2571 .expect("active window must have a populated split layout")
2572 .set_active_buffer_id(buffer_id);
2573 let lsp_changes = self.active_window().collect_lsp_changes(&batch_for_lsp);
2574 self.windows
2575 .get_mut(&self.active_window)
2576 .and_then(|w| w.split_manager_mut())
2577 .expect("active window must have a populated split layout")
2578 .set_active_buffer_id(original_active);
2579
2580 let split_id_for_cursors = self
2583 .split_manager_mut()
2584 .splits_for_buffer(buffer_id)
2585 .into_iter()
2586 .next()
2587 .unwrap_or_else(|| {
2588 self.windows
2589 .get(&self.active_window)
2590 .and_then(|w| w.buffers.splits())
2591 .map(|(mgr, _)| mgr)
2592 .expect("active window must have a populated split layout")
2593 .active_split()
2594 });
2595 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2596 .windows
2597 .get(&self.active_window)
2598 .and_then(|w| w.buffers.splits())
2599 .map(|(_, vs)| vs)
2600 .expect("active window must have a populated split layout")
2601 .get(&split_id_for_cursors)
2602 .and_then(|vs| vs.keyed_states.get(&buffer_id))
2603 .map(|bvs| {
2604 bvs.cursors
2605 .iter()
2606 .map(|(id, c)| (id, c.position, c.anchor))
2607 .collect()
2608 })
2609 .unwrap_or_default();
2610
2611 let __win = self
2618 .windows
2619 .get_mut(&self.active_window)
2620 .expect("active window must exist");
2621 let bulk_edit = __win
2622 .buffers
2623 .with_buffer_and_view_states(buffer_id, |state, vs_map| -> AnyhowResult<Event> {
2624 let old_snapshot = state.buffer.snapshot_buffer_state();
2626
2627 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2629 for event in &events {
2630 match event {
2631 Event::Insert { position, text, .. } => {
2632 edits.push((*position, 0, text.clone()));
2633 }
2634 Event::Delete { range, .. } => {
2635 edits.push((range.start, range.len(), String::new()));
2636 }
2637 _ => {}
2638 }
2639 }
2640
2641 edits.sort_by(|a, b| b.0.cmp(&a.0));
2643
2644 let edit_refs: Vec<(usize, usize, &str)> = edits
2646 .iter()
2647 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2648 .collect();
2649
2650 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2652
2653 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2655
2656 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2658 for (pos, del_len, text) in &edits {
2659 let delta = text.len() as isize - *del_len as isize;
2660 position_deltas.push((*pos, delta));
2661 }
2662 position_deltas.sort_by_key(|(pos, _)| *pos);
2663
2664 let calc_shift = |original_pos: usize| -> isize {
2665 let mut shift: isize = 0;
2666 for (edit_pos, delta) in &position_deltas {
2667 if *edit_pos < original_pos {
2668 shift += delta;
2669 }
2670 }
2671 shift
2672 };
2673
2674 let buffer_len = state.buffer.len();
2676 let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
2677 .iter()
2678 .map(|(id, pos, anchor)| {
2679 let shift = calc_shift(*pos);
2680 let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
2681 let new_anchor = anchor.map(|a| {
2682 let anchor_shift = calc_shift(a);
2683 ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
2684 });
2685 (*id, new_pos, new_anchor)
2686 })
2687 .collect();
2688
2689 let new_snapshot = state.buffer.snapshot_buffer_state();
2691
2692 state.highlighter.invalidate_all();
2694
2695 if let Some(vs) = vs_map.get_mut(&split_id_for_cursors) {
2697 if let Some(bvs) = vs.keyed_states.get_mut(&buffer_id) {
2698 for (cursor_id, new_pos, new_anchor) in &new_cursors {
2699 if let Some(cursor) = bvs.cursors.get_mut(*cursor_id) {
2700 cursor.position = *new_pos;
2701 cursor.anchor = *new_anchor;
2702 }
2703 }
2704 }
2705 }
2706
2707 let edit_lengths: Vec<(usize, usize, usize)> = {
2710 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2711 for (pos, del_len, text) in &edits {
2712 if let Some(last) = lengths.last_mut() {
2713 if last.0 == *pos {
2714 last.1 += del_len;
2715 last.2 += text.len();
2716 continue;
2717 }
2718 }
2719 lengths.push((*pos, *del_len, text.len()));
2720 }
2721 lengths
2722 };
2723
2724 for &(pos, del_len, ins_len) in &edit_lengths {
2726 if del_len > 0 && ins_len > 0 {
2727 if ins_len > del_len {
2728 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2729 state.margins.adjust_for_insert(pos, ins_len - del_len);
2730 } else if del_len > ins_len {
2731 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2732 state.margins.adjust_for_delete(pos, del_len - ins_len);
2733 }
2734 } else if del_len > 0 {
2735 state.marker_list.adjust_for_delete(pos, del_len);
2736 state.margins.adjust_for_delete(pos, del_len);
2737 } else if ins_len > 0 {
2738 state.marker_list.adjust_for_insert(pos, ins_len);
2739 state.margins.adjust_for_insert(pos, ins_len);
2740 }
2741 }
2742
2743 Ok(Event::BulkEdit {
2744 old_snapshot: Some(old_snapshot),
2745 new_snapshot: Some(new_snapshot),
2746 old_cursors,
2747 new_cursors,
2748 description,
2749 edits: edit_lengths,
2750 displaced_markers,
2751 })
2752 })
2753 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))??;
2754
2755 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
2757 event_log.append(bulk_edit);
2758 }
2759
2760 self.active_window_mut()
2762 .send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2763
2764 Ok(())
2765 }
2766
2767 pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2769 if self.active_window().server_supports_prepare_rename() {
2771 self.active_window_mut().send_prepare_rename();
2772 return Ok(());
2773 }
2774
2775 self.show_rename_prompt()
2776 }
2777
2778 pub(crate) fn handle_prepare_rename_response(
2780 &mut self,
2781 result: Result<serde_json::Value, String>,
2782 ) {
2783 match result {
2784 Ok(value) if !value.is_null() => {
2785 if let Err(e) = self.show_rename_prompt() {
2787 self.set_status_message(format!("Rename failed: {e}"));
2788 }
2789 }
2790 Ok(_) => {
2791 self.set_status_message("Cannot rename at this position".to_string());
2792 }
2793 Err(e) => {
2794 self.set_status_message(format!("Cannot rename: {e}"));
2795 }
2796 }
2797 }
2798
2799 fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2802 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2803
2804 let cursor_pos = self.active_cursors().primary().position;
2806 let (word_start, word_end) = {
2807 let state = self.active_state();
2808
2809 let word_start = find_word_start(&state.buffer, cursor_pos);
2811 let word_end = find_word_end(&state.buffer, cursor_pos);
2812
2813 if word_start >= word_end {
2815 self.active_window_mut().status_message =
2816 Some(t!("lsp.no_symbol_at_cursor").to_string());
2817 return Ok(());
2818 }
2819
2820 (word_start, word_end)
2821 };
2822
2823 let word_text = self.active_state_mut().get_text_range(word_start, word_end);
2825
2826 let overlay_handle = self.add_overlay(
2828 None,
2829 word_start..word_end,
2830 crate::model::event::OverlayFace::Background {
2831 color: (50, 100, 200), },
2833 100,
2834 Some(t!("lsp.popup_renaming").to_string()),
2835 );
2836
2837 let mut prompt = Prompt::new(
2840 "Rename to: ".to_string(),
2841 PromptType::LspRename {
2842 original_text: word_text.clone(),
2843 start_pos: word_start,
2844 end_pos: word_end,
2845 overlay_handle,
2846 },
2847 );
2848 prompt.set_input(word_text);
2850
2851 self.active_window_mut().prompt = Some(prompt);
2852 Ok(())
2853 }
2854
2855 pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
2857 self.remove_overlay(handle.clone());
2858 }
2859
2860 pub(crate) fn perform_lsp_rename(
2862 &mut self,
2863 new_name: String,
2864 original_text: String,
2865 start_pos: usize,
2866 overlay_handle: crate::view::overlay::OverlayHandle,
2867 ) {
2868 self.cancel_rename_overlay(&overlay_handle);
2870
2871 if new_name == original_text {
2873 self.active_window_mut().status_message = Some(t!("lsp.name_unchanged").to_string());
2874 return;
2875 }
2876
2877 let rename_pos = start_pos;
2880
2881 let state = self.active_state();
2884 let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
2885 let buffer_id = self.active_buffer();
2886 let request_id = self.active_window_mut().next_lsp_request_id;
2887
2888 let sent = self
2890 .with_lsp_for_buffer(buffer_id, LspFeature::Rename, |handle, uri, _language| {
2891 let result = handle.rename(
2892 request_id,
2893 uri.as_uri().clone(),
2894 line as u32,
2895 character as u32,
2896 new_name.clone(),
2897 );
2898 if result.is_ok() {
2899 tracing::info!(
2900 "Requested rename at {}:{}:{} to '{}'",
2901 uri.as_str(),
2902 line,
2903 character,
2904 new_name
2905 );
2906 }
2907 result.is_ok()
2908 })
2909 .unwrap_or(false);
2910
2911 if sent {
2912 self.active_window_mut().next_lsp_request_id += 1;
2913 } else if self
2914 .active_window()
2915 .buffer_metadata
2916 .get(&buffer_id)
2917 .and_then(|m| m.file_path())
2918 .is_none()
2919 {
2920 self.active_window_mut().status_message =
2921 Some(t!("lsp.cannot_rename_unsaved").to_string());
2922 }
2923 }
2924
2925 pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
2927 let buffer_id = self.active_buffer();
2928 self.request_inlay_hints_for_buffer(buffer_id);
2929 }
2930
2931 pub(crate) fn request_inlay_hints_for_buffer(&mut self, buffer_id: BufferId) {
2933 if !self.config.editor.enable_inlay_hints {
2934 return;
2935 }
2936
2937 let (line_count, version) = if let Some(state) = self
2941 .windows
2942 .get(&self.active_window)
2943 .map(|w| &w.buffers)
2944 .expect("active window present")
2945 .get(&buffer_id)
2946 {
2947 (
2948 state.buffer.line_count().unwrap_or(1000),
2949 state.buffer.version(),
2950 )
2951 } else {
2952 return;
2953 };
2954 let last_line = line_count.saturating_sub(1) as u32;
2955 let request_id = self.active_window_mut().next_lsp_request_id;
2956
2957 let sent = self
2959 .with_lsp_for_buffer(
2960 buffer_id,
2961 LspFeature::InlayHints,
2962 |handle, uri, _language| {
2963 let result = handle.inlay_hints(
2964 request_id,
2965 uri.as_uri().clone(),
2966 0,
2967 0,
2968 last_line,
2969 10000,
2970 );
2971 if result.is_ok() {
2972 tracing::info!(
2973 "Requested inlay hints for {} (request_id={})",
2974 uri.as_str(),
2975 request_id
2976 );
2977 } else if let Err(e) = &result {
2978 tracing::debug!("Failed to request inlay hints: {}", e);
2979 }
2980 result.is_ok()
2981 },
2982 )
2983 .unwrap_or(false);
2984
2985 if sent {
2986 self.active_window_mut().next_lsp_request_id += 1;
2987 self.active_window_mut()
2988 .pending_inlay_hints_requests
2989 .insert(request_id, super::InlayHintsRequest { buffer_id, version });
2990 }
2991 }
2992
2993 pub(crate) fn maybe_request_folding_ranges_debounced(&mut self, buffer_id: BufferId) {
2995 let Some(ready_at) = self
2996 .active_window()
2997 .folding_ranges_debounce
2998 .get(&buffer_id)
2999 .copied()
3000 else {
3001 return;
3002 };
3003 if Instant::now() < ready_at {
3004 return;
3005 }
3006
3007 self.active_window_mut()
3008 .folding_ranges_debounce
3009 .remove(&buffer_id);
3010 self.request_folding_ranges_for_buffer(buffer_id);
3011 }
3012
3013 pub(crate) fn request_folding_ranges_for_buffer(&mut self, buffer_id: BufferId) {
3015 if self
3016 .active_window_mut()
3017 .folding_ranges_in_flight
3018 .contains_key(&buffer_id)
3019 {
3020 return;
3021 }
3022
3023 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3024 return;
3025 };
3026 if !metadata.lsp_enabled {
3027 return;
3028 }
3029 let Some(uri) = metadata.file_uri().cloned() else {
3030 return;
3031 };
3032 let file_path = metadata.file_path().cloned();
3033
3034 let Some(language) = self
3035 .windows
3036 .get(&self.active_window)
3037 .map(|w| &w.buffers)
3038 .expect("active window present")
3039 .get(&buffer_id)
3040 .map(|s| s.language.clone())
3041 else {
3042 return;
3043 };
3044
3045 let __active_id = self.active_window;
3046 let __buffer_version_for_request = self
3049 .windows
3050 .get(&__active_id)
3051 .and_then(|w| w.buffers.get(&buffer_id))
3052 .map(|s| s.buffer.version())
3053 .unwrap_or(0);
3054
3055 let Some(__win) = self.windows.get_mut(&__active_id) else {
3056 return;
3057 };
3058 let __next_id = &mut __win.next_lsp_request_id;
3059 let __pending_folding = &mut __win.pending_folding_range_requests;
3060 let __folding_in_flight = &mut __win.folding_ranges_in_flight;
3061 let Some(lsp) = __win.lsp.as_mut() else {
3062 return;
3063 };
3064
3065 if !lsp.folding_ranges_supported(&language) {
3066 return;
3067 }
3068
3069 use crate::services::lsp::manager::LspSpawnResult;
3071 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3072 return;
3073 }
3074
3075 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::FoldingRange) else {
3076 return;
3077 };
3078 let handle = &mut sh.handle;
3079
3080 let request_id = {
3081 let id = *__next_id;
3082 *__next_id += 1;
3083 id
3084 };
3085 let buffer_version = __buffer_version_for_request;
3086 let _ = __folding_in_flight;
3087
3088 match handle.folding_ranges(request_id, uri.as_uri().clone()) {
3089 Ok(()) => {
3090 __pending_folding.insert(
3091 request_id,
3092 super::FoldingRangeRequest {
3093 buffer_id,
3094 version: buffer_version,
3095 },
3096 );
3097 __folding_in_flight.insert(buffer_id, (request_id, buffer_version));
3098 }
3099 Err(e) => {
3100 tracing::debug!("Failed to request folding ranges: {}", e);
3101 }
3102 }
3103 }
3104
3105 pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
3107 if !self.config.editor.enable_semantic_tokens_full {
3108 return;
3109 }
3110
3111 if self
3113 .active_window_mut()
3114 .semantic_tokens_in_flight
3115 .contains_key(&buffer_id)
3116 {
3117 return;
3118 }
3119
3120 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3121 return;
3122 };
3123 if !metadata.lsp_enabled {
3124 return;
3125 }
3126 let Some(uri) = metadata.file_uri().cloned() else {
3127 return;
3128 };
3129 let file_path_for_spawn = metadata.file_path().cloned();
3130 let Some(language) = self
3132 .windows
3133 .get(&self.active_window)
3134 .map(|w| &w.buffers)
3135 .expect("active window present")
3136 .get(&buffer_id)
3137 .map(|s| s.language.clone())
3138 else {
3139 return;
3140 };
3141
3142 let __active_id = self.active_window;
3143 let Some((buffer_version, existing_version, previous_result_id)) = self
3146 .windows
3147 .get(&__active_id)
3148 .and_then(|w| w.buffers.get(&buffer_id))
3149 .map(|state| {
3150 (
3151 state.buffer.version(),
3152 state.semantic_tokens.as_ref().map(|s| s.version),
3153 state
3154 .semantic_tokens
3155 .as_ref()
3156 .and_then(|s| s.result_id.clone()),
3157 )
3158 })
3159 else {
3160 return;
3161 };
3162 if Some(buffer_version) == existing_version {
3163 return; }
3165
3166 let Some(__win) = self.windows.get_mut(&__active_id) else {
3167 return;
3168 };
3169 let __next_id = &mut __win.next_lsp_request_id;
3170 let __pending_st = &mut __win.pending_semantic_token_requests;
3171 let __st_in_flight = &mut __win.semantic_tokens_in_flight;
3172 let Some(lsp) = __win.lsp.as_mut() else {
3173 return;
3174 };
3175
3176 use crate::services::lsp::manager::LspSpawnResult;
3178 if lsp.try_spawn(&language, file_path_for_spawn.as_deref()) != LspSpawnResult::Spawned {
3179 return;
3180 }
3181
3182 if !lsp.semantic_tokens_full_supported(&language) {
3184 return;
3185 }
3186 if lsp.semantic_tokens_legend(&language).is_none() {
3187 return;
3188 }
3189
3190 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3191 return;
3192 };
3193 let supports_delta = sh.capabilities.semantic_tokens_full_delta;
3195 let use_delta = previous_result_id.is_some() && supports_delta;
3196 let handle = &mut sh.handle;
3197
3198 let request_id = {
3199 let id = *__next_id;
3200 *__next_id += 1;
3201 id
3202 };
3203
3204 let request_kind = if use_delta {
3205 super::SemanticTokensFullRequestKind::FullDelta
3206 } else {
3207 super::SemanticTokensFullRequestKind::Full
3208 };
3209
3210 let request_result = if use_delta {
3211 handle.semantic_tokens_full_delta(
3212 request_id,
3213 uri.as_uri().clone(),
3214 previous_result_id.unwrap(),
3215 )
3216 } else {
3217 handle.semantic_tokens_full(request_id, uri.as_uri().clone())
3218 };
3219
3220 match request_result {
3221 Ok(_) => {
3222 __pending_st.insert(
3223 request_id,
3224 super::SemanticTokenFullRequest {
3225 buffer_id,
3226 version: buffer_version,
3227 kind: request_kind,
3228 },
3229 );
3230 __st_in_flight.insert(buffer_id, (request_id, buffer_version, request_kind));
3231 }
3232 Err(e) => {
3233 tracing::debug!("Failed to request semantic tokens: {}", e);
3234 }
3235 }
3236 }
3237
3238 pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
3240 if !self.config.editor.enable_semantic_tokens_full {
3241 self.active_window_mut()
3242 .semantic_tokens_full_debounce
3243 .remove(&buffer_id);
3244 return;
3245 }
3246
3247 let Some(ready_at) = self
3248 .active_window()
3249 .semantic_tokens_full_debounce
3250 .get(&buffer_id)
3251 .copied()
3252 else {
3253 return;
3254 };
3255 if Instant::now() < ready_at {
3256 return;
3257 }
3258
3259 self.active_window_mut()
3260 .semantic_tokens_full_debounce
3261 .remove(&buffer_id);
3262 self.maybe_request_semantic_tokens(buffer_id);
3263 }
3264
3265 pub(crate) fn maybe_request_semantic_tokens_range(
3267 &mut self,
3268 buffer_id: BufferId,
3269 start_line: usize,
3270 end_line: usize,
3271 ) {
3272 let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3273 return;
3274 };
3275 if !metadata.lsp_enabled {
3276 return;
3277 }
3278 let Some(uri) = metadata.file_uri().cloned() else {
3279 return;
3280 };
3281 let file_path = metadata.file_path().cloned();
3282 let Some(language) = self
3284 .windows
3285 .get(&self.active_window)
3286 .map(|w| &w.buffers)
3287 .expect("active window present")
3288 .get(&buffer_id)
3289 .map(|s| s.language.clone())
3290 else {
3291 return;
3292 };
3293
3294 let __active_id = self.active_window;
3295 let __win = self
3298 .windows
3299 .get_mut(&__active_id)
3300 .expect("active window must exist");
3301 let __next_id = &mut __win.next_lsp_request_id;
3302 let __pending_st_range = &mut __win.pending_semantic_token_range_requests;
3303 let __st_range_in_flight = &mut __win.semantic_tokens_range_in_flight;
3304 let __st_range_last = &mut __win.semantic_tokens_range_last_request;
3305 let __st_range_applied = &__win.semantic_tokens_range_applied;
3306 let Some(lsp) = __win.lsp.as_mut() else {
3307 return;
3308 };
3309 let __buffers_ref: &crate::app::window::WindowBuffers = &__win.buffers;
3310
3311 use crate::services::lsp::manager::LspSpawnResult;
3313 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3314 return;
3315 }
3316
3317 if !lsp.semantic_tokens_range_supported(&language) {
3318 self.maybe_request_semantic_tokens(buffer_id);
3320 return;
3321 }
3322 if lsp.semantic_tokens_legend(&language).is_none() {
3323 return;
3324 }
3325
3326 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3327 return;
3328 };
3329 if !sh.capabilities.semantic_tokens_range {
3332 return;
3333 }
3334 let handle = &mut sh.handle;
3335 let Some(state) = __buffers_ref.get(&buffer_id) else {
3336 return;
3337 };
3338
3339 let buffer_version = state.buffer.version();
3340 let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3341 let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3342
3343 if let Some(line_count) = state.buffer.line_count() {
3344 if line_count == 0 {
3345 return;
3346 }
3347 let max_line = line_count.saturating_sub(1);
3348 padded_start = padded_start.min(max_line);
3349 padded_end = padded_end.min(max_line);
3350 }
3351
3352 let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
3353 let end_char = state
3354 .buffer
3355 .get_line(padded_end)
3356 .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
3357 .unwrap_or(0);
3358 let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
3359 state.buffer.lsp_position_to_byte(padded_end, end_char)
3360 } else {
3361 state.buffer.len()
3362 };
3363
3364 if start_byte >= end_byte {
3365 return;
3366 }
3367
3368 let range = start_byte..end_byte;
3369 if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
3370 __st_range_in_flight.get(&buffer_id).copied()
3371 {
3372 if in_flight_start == padded_start
3373 && in_flight_end == padded_end
3374 && in_flight_version == buffer_version
3375 {
3376 return;
3377 }
3378 if let Err(e) = handle.cancel_request(in_flight_id) {
3379 tracing::debug!("Failed to cancel semantic token range request: {}", e);
3380 }
3381 __pending_st_range.remove(&in_flight_id);
3382 __st_range_in_flight.remove(&buffer_id);
3383 }
3384
3385 if let Some((applied_start, applied_end, applied_version)) =
3386 __st_range_applied.get(&buffer_id).copied()
3387 {
3388 if applied_start == padded_start
3389 && applied_end == padded_end
3390 && applied_version == buffer_version
3391 {
3392 return;
3393 }
3394 }
3395
3396 let now = Instant::now();
3397 if let Some((last_start, last_end, last_version, last_time)) =
3398 __st_range_last.get(&buffer_id).copied()
3399 {
3400 if last_start == padded_start
3401 && last_end == padded_end
3402 && last_version == buffer_version
3403 && now.duration_since(last_time)
3404 < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
3405 {
3406 return;
3407 }
3408 }
3409
3410 let lsp_range = lsp_types::Range {
3411 start: lsp_types::Position {
3412 line: padded_start as u32,
3413 character: 0,
3414 },
3415 end: lsp_types::Position {
3416 line: padded_end as u32,
3417 character: end_char as u32,
3418 },
3419 };
3420
3421 let request_id = {
3422 let id = *__next_id;
3423 *__next_id += 1;
3424 id
3425 };
3426 let _ = __st_range_applied;
3427
3428 match handle.semantic_tokens_range(request_id, uri.as_uri().clone(), lsp_range) {
3429 Ok(_) => {
3430 __pending_st_range.insert(
3431 request_id,
3432 SemanticTokenRangeRequest {
3433 buffer_id,
3434 version: buffer_version,
3435 range: range.clone(),
3436 start_line: padded_start,
3437 end_line: padded_end,
3438 },
3439 );
3440 __st_range_in_flight.insert(
3441 buffer_id,
3442 (request_id, padded_start, padded_end, buffer_version),
3443 );
3444 __st_range_last.insert(buffer_id, (padded_start, padded_end, buffer_version, now));
3445 }
3446 Err(e) => {
3447 tracing::debug!("Failed to request semantic token range: {}", e);
3448 }
3449 }
3450 }
3451}
3452
3453#[cfg(test)]
3454mod tests {
3455 use crate::model::filesystem::StdFileSystem;
3456 use std::sync::Arc;
3457
3458 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
3459 Arc::new(StdFileSystem)
3460 }
3461 use super::{lsp_range_contains, Editor};
3462
3463 fn range(sl: u32, sc: u32, el: u32, ec: u32) -> lsp_types::Range {
3464 lsp_types::Range {
3465 start: lsp_types::Position {
3466 line: sl,
3467 character: sc,
3468 },
3469 end: lsp_types::Position {
3470 line: el,
3471 character: ec,
3472 },
3473 }
3474 }
3475
3476 #[test]
3477 fn test_lsp_range_contains_inclusive_start_exclusive_end() {
3478 let r = range(3, 10, 3, 20);
3479 assert!(!lsp_range_contains(&r, 3, 9));
3481 assert!(!lsp_range_contains(&r, 2, 50));
3482 assert!(lsp_range_contains(&r, 3, 10));
3484 assert!(lsp_range_contains(&r, 3, 15));
3486 assert!(lsp_range_contains(&r, 3, 19));
3488 assert!(!lsp_range_contains(&r, 3, 20));
3490 assert!(!lsp_range_contains(&r, 3, 21));
3492 assert!(!lsp_range_contains(&r, 4, 0));
3493 }
3494
3495 #[test]
3496 fn test_lsp_range_contains_multiline() {
3497 let r = range(2, 5, 4, 3);
3498 assert!(!lsp_range_contains(&r, 1, 100));
3500 assert!(!lsp_range_contains(&r, 2, 4));
3502 assert!(lsp_range_contains(&r, 2, 5));
3504 assert!(lsp_range_contains(&r, 3, 0));
3506 assert!(lsp_range_contains(&r, 3, 9999));
3507 assert!(lsp_range_contains(&r, 4, 2));
3509 assert!(!lsp_range_contains(&r, 4, 3));
3511 assert!(!lsp_range_contains(&r, 5, 0));
3513 }
3514
3515 #[test]
3516 fn test_lsp_range_contains_zero_length_matches_anchor_only() {
3517 let r = range(7, 4, 7, 4);
3519 assert!(lsp_range_contains(&r, 7, 4));
3520 assert!(!lsp_range_contains(&r, 7, 3));
3521 assert!(!lsp_range_contains(&r, 7, 5));
3522 assert!(!lsp_range_contains(&r, 6, 4));
3523 assert!(!lsp_range_contains(&r, 8, 4));
3524 }
3525 use crate::model::buffer::Buffer;
3526 use crate::state::EditorState;
3527 use crate::view::virtual_text::VirtualTextPosition;
3528 use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
3529
3530 fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
3531 InlayHint {
3532 position: Position { line, character },
3533 label: InlayHintLabel::String(label.to_string()),
3534 kind,
3535 text_edits: None,
3536 tooltip: None,
3537 padding_left: None,
3538 padding_right: None,
3539 data: None,
3540 }
3541 }
3542
3543 #[test]
3544 fn test_inlay_hint_inserts_before_character() {
3545 let mut state = EditorState::new(
3546 80,
3547 24,
3548 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3549 test_fs(),
3550 );
3551 state.buffer = Buffer::from_str_test("ab");
3552
3553 if !state.buffer.is_empty() {
3554 state.marker_list.adjust_for_insert(0, state.buffer.len());
3555 }
3556
3557 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3558 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3559
3560 let lookup = state
3561 .virtual_texts
3562 .build_lookup(&state.marker_list, 0, state.buffer.len());
3563 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3564 assert_eq!(vtexts.len(), 1);
3565 assert_eq!(vtexts[0].text, ": i32");
3566 assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
3567 }
3568
3569 #[test]
3570 fn test_inlay_hint_at_eof_renders_after_last_char() {
3571 let mut state = EditorState::new(
3572 80,
3573 24,
3574 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3575 test_fs(),
3576 );
3577 state.buffer = Buffer::from_str_test("ab");
3578
3579 if !state.buffer.is_empty() {
3580 state.marker_list.adjust_for_insert(0, state.buffer.len());
3581 }
3582
3583 let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
3584 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3585
3586 let lookup = state
3587 .virtual_texts
3588 .build_lookup(&state.marker_list, 0, state.buffer.len());
3589 let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
3590 assert_eq!(vtexts.len(), 1);
3591 assert_eq!(vtexts[0].text, ": i32");
3592 assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
3593 }
3594
3595 #[test]
3596 fn test_inlay_hint_empty_buffer_is_ignored() {
3597 let mut state = EditorState::new(
3598 80,
3599 24,
3600 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3601 test_fs(),
3602 );
3603 state.buffer = Buffer::from_str_test("");
3604
3605 let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
3606 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3607
3608 assert!(state.virtual_texts.is_empty());
3609 }
3610
3611 #[test]
3612 fn test_inlay_hint_uses_theme_key_for_foreground() {
3613 let mut state = EditorState::new(
3616 80,
3617 24,
3618 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3619 test_fs(),
3620 );
3621 state.buffer = Buffer::from_str_test("ab");
3622
3623 if !state.buffer.is_empty() {
3624 state.marker_list.adjust_for_insert(0, state.buffer.len());
3625 }
3626
3627 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3628 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3629
3630 let lookup = state
3631 .virtual_texts
3632 .build_lookup(&state.marker_list, 0, state.buffer.len());
3633 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3634 assert_eq!(
3635 vtexts[0].fg_theme_key.as_deref(),
3636 Some("editor.line_number_fg")
3637 );
3638 assert_eq!(vtexts[0].bg_theme_key, None);
3639 }
3640
3641 #[test]
3642 fn test_inlay_hint_removed_when_its_range_is_deleted() {
3643 let mut state = EditorState::new(
3650 80,
3651 24,
3652 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3653 test_fs(),
3654 );
3655 state.buffer = Buffer::from_str_test("let x = 42;");
3656 state.marker_list.adjust_for_insert(0, state.buffer.len());
3657
3658 let hints = vec![make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE))];
3660 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3661 assert_eq!(state.virtual_texts.len(), 1);
3662
3663 let removed = state
3666 .virtual_texts
3667 .remove_in_range(&mut state.marker_list, 4, 10);
3668 assert_eq!(removed, 1, "hint inside deleted range must be removed");
3669 assert!(state.virtual_texts.is_empty());
3670 }
3671
3672 #[test]
3673 fn test_marker_delete_after_repeat_clear_recreate() {
3674 use crate::model::marker::MarkerList;
3680 use crate::view::virtual_text::{VirtualTextManager, VirtualTextPosition};
3681 use ratatui::style::Style;
3682
3683 let mut markers = MarkerList::new();
3684 let mut vtexts = VirtualTextManager::new();
3685
3686 let positions = [200usize, 401, 602, 803, 1205, 1406];
3688 for &p in &positions {
3689 vtexts.add(
3690 &mut markers,
3691 p,
3692 format!("hint-at-{p}"),
3693 Style::default(),
3694 VirtualTextPosition::BeforeChar,
3695 0,
3696 );
3697 }
3698
3699 for _ in 0..3 {
3702 vtexts.clear(&mut markers);
3703 for &p in &positions {
3704 vtexts.add(
3705 &mut markers,
3706 p,
3707 format!("hint-at-{p}"),
3708 Style::default(),
3709 VirtualTextPosition::BeforeChar,
3710 0,
3711 );
3712 }
3713 }
3714
3715 let removed = vtexts.remove_in_range(&mut markers, 1005, 1206);
3717 assert_eq!(
3718 removed, 1,
3719 "exactly one marker inside [1005, 1206) should be removed"
3720 );
3721 markers.adjust_for_delete(1005, 201);
3722
3723 let lookup = vtexts.build_lookup(&markers, 0, 10_000);
3724 let mut positions: Vec<usize> = lookup.keys().copied().collect();
3725 positions.sort();
3726 assert_eq!(
3727 positions,
3728 vec![200, 401, 602, 803, 1205],
3729 "after delete+adjust, expected marker byte positions {:?}, got {:?}",
3730 vec![200, 401, 602, 803, 1205],
3731 positions
3732 );
3733 }
3734
3735 #[test]
3736 fn test_marker_delete_then_adjust_preserves_last_marker_position() {
3737 use crate::model::marker::MarkerList;
3751
3752 let mut markers = MarkerList::new();
3753 let m0 = markers.create(200, false);
3754 let m1 = markers.create(401, false);
3755 let m2 = markers.create(602, false);
3756 let m3 = markers.create(803, false);
3757 let m5 = markers.create(1205, false);
3758 let m6 = markers.create(1406, false);
3759
3760 markers.delete(m5);
3762
3763 markers.adjust_for_delete(1005, 201);
3765
3766 assert_eq!(markers.get_position(m0), Some(200), "m0 unchanged");
3767 assert_eq!(markers.get_position(m1), Some(401), "m1 unchanged");
3768 assert_eq!(markers.get_position(m2), Some(602), "m2 unchanged");
3769 assert_eq!(markers.get_position(m3), Some(803), "m3 unchanged");
3770 assert_eq!(
3771 markers.get_position(m6),
3772 Some(1205),
3773 "m6 must shift from 1406 to 1205 (1406 - 201), not be clamped to delete-start 1005"
3774 );
3775 }
3776
3777 #[test]
3778 fn test_inlay_hint_outside_deletion_survives() {
3779 let mut state = EditorState::new(
3781 80,
3782 24,
3783 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3784 test_fs(),
3785 );
3786 state.buffer = Buffer::from_str_test("let x = 42; let y = 0;");
3787 state.marker_list.adjust_for_insert(0, state.buffer.len());
3788
3789 let hints = vec![
3790 make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE)), make_hint(0, 17, ": i32", Some(InlayHintKind::TYPE)), ];
3793 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3794 assert_eq!(state.virtual_texts.len(), 2);
3795
3796 let removed = state
3797 .virtual_texts
3798 .remove_in_range(&mut state.marker_list, 4, 10);
3799 assert_eq!(removed, 1);
3800 assert_eq!(state.virtual_texts.len(), 1);
3801 }
3802
3803 #[test]
3804 fn test_space_doc_paragraphs_inserts_blank_lines() {
3805 use super::space_doc_paragraphs;
3806
3807 let input = "sep\n description.\nend\n another.";
3809 let result = space_doc_paragraphs(input);
3810 assert_eq!(result, "sep\n\n description.\n\nend\n\n another.");
3811 }
3812
3813 #[test]
3814 fn test_space_doc_paragraphs_preserves_existing_blank_lines() {
3815 use super::space_doc_paragraphs;
3816
3817 let input = "First paragraph.\n\nSecond paragraph.";
3819 let result = space_doc_paragraphs(input);
3820 assert_eq!(result, "First paragraph.\n\nSecond paragraph.");
3821 }
3822
3823 #[test]
3824 fn test_space_doc_paragraphs_plain_text() {
3825 use super::space_doc_paragraphs;
3826
3827 let input = "Just a single line of docs.";
3828 let result = space_doc_paragraphs(input);
3829 assert_eq!(result, "Just a single line of docs.");
3830 }
3831}