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