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