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()) {
1075 state.popups.show(popup);
1076 tracing::info!("Showing hover popup (markdown={})", is_markdown);
1077 }
1078
1079 self.mouse_state.lsp_hover_request_sent = true;
1082 }
1083
1084 fn compose_hover_diagnostic_lines(
1095 &self,
1096 lsp_pos: (u32, u32),
1097 ) -> Vec<crate::view::markdown::StyledLine> {
1098 use crate::view::markdown::StyledLine;
1099 use lsp_types::DiagnosticSeverity;
1100 use ratatui::style::{Modifier, Style};
1101
1102 let buffer_id = self.active_buffer();
1103 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
1104 return Vec::new();
1105 };
1106 let Some(uri) = metadata.file_uri() else {
1107 return Vec::new();
1108 };
1109 let Some(diagnostics) = self.get_stored_diagnostics().get(uri.as_str()) else {
1110 return Vec::new();
1111 };
1112
1113 let (hover_line, hover_char) = lsp_pos;
1114 let overlapping: Vec<&lsp_types::Diagnostic> = diagnostics
1115 .iter()
1116 .filter(|d| lsp_range_contains(&d.range, hover_line, hover_char))
1117 .collect();
1118
1119 if overlapping.is_empty() {
1120 return Vec::new();
1121 }
1122
1123 let mut out: Vec<StyledLine> = Vec::new();
1124 for (idx, diag) in overlapping.iter().enumerate() {
1125 if idx > 0 {
1126 out.push(StyledLine::new());
1127 }
1128
1129 let (label, marker, severity_color) = match diag.severity {
1130 Some(DiagnosticSeverity::ERROR) => ("Error", "✖", self.theme.diagnostic_error_fg),
1131 Some(DiagnosticSeverity::WARNING) => {
1132 ("Warning", "⚠", self.theme.diagnostic_warning_fg)
1133 }
1134 Some(DiagnosticSeverity::INFORMATION) => {
1135 ("Info", "ℹ", self.theme.diagnostic_info_fg)
1136 }
1137 Some(DiagnosticSeverity::HINT) => ("Hint", "ℹ", self.theme.diagnostic_hint_fg),
1138 _ => ("Diagnostic", "•", self.theme.popup_text_fg),
1139 };
1140
1141 let header_style = Style::default()
1142 .fg(severity_color)
1143 .add_modifier(Modifier::BOLD);
1144 let mut header = StyledLine::new();
1145 header.push(format!("{} {}", marker, label), header_style);
1146 if let Some(source) = diag.source.as_deref().filter(|s| !s.is_empty()) {
1147 header.push(
1150 format!(" ({})", source),
1151 Style::default()
1152 .fg(self.theme.tab_inactive_fg)
1153 .add_modifier(Modifier::ITALIC),
1154 );
1155 }
1156 out.push(header);
1157
1158 for message_line in diag.message.lines() {
1162 let mut line = StyledLine::new();
1163 line.push(
1164 message_line.to_string(),
1165 Style::default().fg(self.theme.popup_text_fg),
1166 );
1167 out.push(line);
1168 }
1169 }
1170 out
1171 }
1172
1173 #[doc(hidden)]
1175 pub fn apply_inlay_hints_to_state(
1176 state: &mut crate::state::EditorState,
1177 hints: &[lsp_types::InlayHint],
1178 ) {
1179 use crate::view::virtual_text::VirtualTextPosition;
1180 use ratatui::style::{Color, Style};
1181
1182 state.virtual_texts.clear(&mut state.marker_list);
1184
1185 if hints.is_empty() {
1186 return;
1187 }
1188
1189 let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
1195 let hint_fg_theme_key = Some("editor.line_number_fg".to_string());
1196
1197 for hint in hints {
1198 let byte_offset = state.buffer.lsp_position_to_byte(
1200 hint.position.line as usize,
1201 hint.position.character as usize,
1202 );
1203
1204 let text = match &hint.label {
1206 lsp_types::InlayHintLabel::String(s) => s.clone(),
1207 lsp_types::InlayHintLabel::LabelParts(parts) => {
1208 parts.iter().map(|p| p.value.as_str()).collect::<String>()
1209 }
1210 };
1211
1212 if state.buffer.is_empty() {
1218 continue;
1219 }
1220
1221 let buf_len = state.buffer.len();
1230 let byte_here = if byte_offset < buf_len {
1231 state
1232 .buffer
1233 .slice_bytes(byte_offset..byte_offset + 1)
1234 .first()
1235 .copied()
1236 } else {
1237 None
1238 };
1239 let at_line_break = matches!(byte_here, Some(b'\n' | b'\r'));
1240
1241 let (byte_offset, position) = if byte_offset >= buf_len {
1242 (buf_len.saturating_sub(1), VirtualTextPosition::AfterChar)
1245 } else if at_line_break && byte_offset > 0 {
1246 (byte_offset - 1, VirtualTextPosition::AfterChar)
1250 } else {
1251 (byte_offset, VirtualTextPosition::BeforeChar)
1252 };
1253
1254 let display_text = text;
1256
1257 state.virtual_texts.add_with_theme_keys(
1258 &mut state.marker_list,
1259 byte_offset,
1260 display_text,
1261 hint_style,
1262 hint_fg_theme_key.clone(),
1263 None,
1264 position,
1265 0, );
1267 }
1268
1269 tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
1270 }
1271
1272 pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
1274 use crate::primitives::word_navigation::{find_word_end, find_word_start};
1275
1276 let cursor_pos = self.active_cursors().primary().position;
1277 let (line, character, symbol) = {
1278 let state = self.active_state();
1279 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1280 let word_start = find_word_start(&state.buffer, cursor_pos);
1281 let word_end = find_word_end(&state.buffer, cursor_pos);
1282 let symbol = String::from_utf8_lossy(&state.buffer.slice_bytes(word_start..word_end))
1283 .into_owned();
1284 (line, character, symbol)
1285 };
1286
1287 let buffer_id = self.active_buffer();
1288 let request_id = self.next_lsp_request_id;
1289
1290 let sent = self
1292 .with_lsp_for_buffer(
1293 buffer_id,
1294 LspFeature::References,
1295 |handle, uri, _language| {
1296 let result = handle.references(
1297 request_id,
1298 uri.as_uri().clone(),
1299 line as u32,
1300 character as u32,
1301 );
1302 if result.is_ok() {
1303 tracing::info!(
1304 "Requested find references at {}:{}:{} (byte_pos={})",
1305 uri.as_str(),
1306 line,
1307 character,
1308 cursor_pos
1309 );
1310 }
1311 result.is_ok()
1312 },
1313 )
1314 .unwrap_or(false);
1315
1316 if sent {
1317 self.next_lsp_request_id += 1;
1318 self.pending_references_request = Some(request_id);
1319 self.pending_references_symbol = symbol;
1320 }
1321
1322 Ok(())
1323 }
1324
1325 pub(crate) fn request_signature_help(&mut self) {
1327 let cursor_pos = self.active_cursors().primary().position;
1329 let state = self.active_state();
1330
1331 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1333 let buffer_id = self.active_buffer();
1334 let request_id = self.next_lsp_request_id;
1335
1336 let sent = self
1338 .with_lsp_for_buffer(
1339 buffer_id,
1340 LspFeature::SignatureHelp,
1341 |handle, uri, _language| {
1342 let result = handle.signature_help(
1343 request_id,
1344 uri.as_uri().clone(),
1345 line as u32,
1346 character as u32,
1347 );
1348 if result.is_ok() {
1349 tracing::info!(
1350 "Requested signature help at {}:{}:{} (byte_pos={})",
1351 uri.as_str(),
1352 line,
1353 character,
1354 cursor_pos
1355 );
1356 }
1357 result.is_ok()
1358 },
1359 )
1360 .unwrap_or(false);
1361
1362 if sent {
1363 self.next_lsp_request_id += 1;
1364 self.pending_signature_help_request = Some(request_id);
1365 }
1366 }
1367
1368 pub(crate) fn handle_signature_help_response(
1370 &mut self,
1371 request_id: u64,
1372 signature_help: Option<lsp_types::SignatureHelp>,
1373 ) {
1374 if self.pending_signature_help_request != Some(request_id) {
1376 tracing::debug!("Ignoring stale signature help response: {}", request_id);
1377 return;
1378 }
1379
1380 self.pending_signature_help_request = None;
1381 let signature_help = match signature_help {
1382 Some(help) if !help.signatures.is_empty() => help,
1383 _ => {
1384 tracing::debug!("No signature help available");
1385 return;
1386 }
1387 };
1388
1389 let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
1391 let signature = match signature_help.signatures.get(active_signature_idx) {
1392 Some(sig) => sig,
1393 None => return,
1394 };
1395
1396 let mut content = String::new();
1398
1399 content.push_str(&signature.label);
1401 content.push('\n');
1402
1403 let active_param = signature_help
1405 .active_parameter
1406 .or(signature.active_parameter)
1407 .unwrap_or(0) as usize;
1408
1409 if let Some(params) = &signature.parameters {
1411 if let Some(param) = params.get(active_param) {
1412 let param_label = match ¶m.label {
1414 lsp_types::ParameterLabel::Simple(s) => s.clone(),
1415 lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1416 let start = offsets[0] as usize;
1418 let end = offsets[1] as usize;
1419 if end <= signature.label.len() {
1420 signature.label[start..end].to_string()
1421 } else {
1422 String::new()
1423 }
1424 }
1425 };
1426
1427 if !param_label.is_empty() {
1428 content.push_str(&format!("\n> {}\n", param_label));
1429 }
1430
1431 if let Some(doc) = ¶m.documentation {
1433 let doc_text = match doc {
1434 lsp_types::Documentation::String(s) => s.clone(),
1435 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1436 };
1437 if !doc_text.is_empty() {
1438 content.push('\n');
1439 content.push_str(&doc_text);
1440 content.push('\n');
1441 }
1442 }
1443 }
1444 }
1445
1446 if let Some(doc) = &signature.documentation {
1448 let doc_text = match doc {
1449 lsp_types::Documentation::String(s) => s.clone(),
1450 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1451 };
1452 if !doc_text.is_empty() {
1453 content.push_str("\n---\n\n");
1454 content.push_str(&space_doc_paragraphs(&doc_text));
1455 }
1456 }
1457
1458 use crate::view::popup::{Popup, PopupPosition};
1460 use ratatui::style::Style;
1461
1462 let mut popup = Popup::markdown(&content, &self.theme, Some(&self.grammar_registry));
1463 popup.title = Some(t!("lsp.popup_signature").to_string());
1464 popup.transient = true;
1465 popup.position = PopupPosition::BelowCursor;
1466 popup.width = 60;
1467 popup.max_height = 20;
1468 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1469 popup.background_style = Style::default().bg(self.theme.popup_bg);
1470 popup.focus_key_hint = self.popup_focus_key_hint();
1471
1472 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1474 state.popups.show(popup);
1475 tracing::info!(
1476 "Showing signature help popup for {} signatures",
1477 signature_help.signatures.len()
1478 );
1479 }
1480 }
1481
1482 pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1485 if !self.pending_code_actions_requests.is_empty() {
1494 let ids: Vec<u64> = self.pending_code_actions_requests.drain().collect();
1495 for request_id in ids {
1496 tracing::debug!(
1497 "Canceling previous pending LSP code actions request {}",
1498 request_id
1499 );
1500 self.send_lsp_cancel_request(request_id);
1501 }
1502 }
1503 self.pending_code_actions_server_names.clear();
1504 self.pending_code_actions = None;
1505
1506 let cursor_pos = self.active_cursors().primary().position;
1508 let selection_range = self.active_cursors().primary().selection_range();
1509 let state = self.active_state();
1510
1511 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1513
1514 let (start_line, start_char, end_line, end_char) = if let Some(range) = selection_range {
1516 let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1517 let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1518 (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1519 } else {
1520 (line as u32, character as u32, line as u32, character as u32)
1521 };
1522
1523 let diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
1526 let buffer_id = self.active_buffer();
1527
1528 let base_request_id = self.next_lsp_request_id;
1530 let counter = std::sync::atomic::AtomicU64::new(0);
1531
1532 let results = self.with_all_lsp_for_buffer_feature_named(
1533 buffer_id,
1534 LspFeature::CodeAction,
1535 |handle, uri, _language, server_name| {
1536 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1537 let request_id = base_request_id + idx;
1538 let result = handle.code_actions(
1539 request_id,
1540 uri.as_uri().clone(),
1541 start_line,
1542 start_char,
1543 end_line,
1544 end_char,
1545 diagnostics.clone(),
1546 );
1547 if result.is_ok() {
1548 tracing::info!(
1549 "Requested code actions at {}:{}:{}-{}:{} (byte_pos={}, request_id={}, server={})",
1550 uri.as_str(),
1551 start_line,
1552 start_char,
1553 end_line,
1554 end_char,
1555 cursor_pos,
1556 request_id,
1557 server_name
1558 );
1559 }
1560 (request_id, result.is_ok(), server_name.to_string())
1561 },
1562 );
1563
1564 let mut sent_ids = Vec::new();
1565 for (request_id, ok, server_name) in &results {
1566 if *ok {
1567 sent_ids.push(*request_id);
1568 self.pending_code_actions_server_names
1569 .insert(*request_id, server_name.clone());
1570 }
1571 }
1572 self.next_lsp_request_id = base_request_id + results.len() as u64;
1574
1575 if !sent_ids.is_empty() {
1576 self.pending_code_actions_requests.extend(sent_ids);
1579 }
1580
1581 Ok(())
1582 }
1583
1584 pub(crate) fn handle_code_actions_response(
1588 &mut self,
1589 request_id: u64,
1590 actions: Vec<lsp_types::CodeActionOrCommand>,
1591 ) {
1592 if !self.pending_code_actions_requests.remove(&request_id) {
1594 tracing::debug!("Ignoring stale code actions response: {}", request_id);
1595 return;
1596 }
1597
1598 let server_name = self
1600 .pending_code_actions_server_names
1601 .remove(&request_id)
1602 .unwrap_or_default();
1603
1604 if actions.is_empty() {
1605 if self.pending_code_actions_requests.is_empty()
1607 && self
1608 .pending_code_actions
1609 .as_ref()
1610 .is_none_or(|a| a.is_empty())
1611 {
1612 self.set_status_message(t!("lsp.no_code_actions").to_string());
1613 }
1614 return;
1615 }
1616
1617 let tagged_actions: Vec<(String, lsp_types::CodeActionOrCommand)> = actions
1619 .into_iter()
1620 .map(|a| (server_name.clone(), a))
1621 .collect();
1622
1623 match &mut self.pending_code_actions {
1624 Some(existing) => {
1625 existing.extend(tagged_actions);
1626 tracing::debug!("Extended code actions, now {} total", existing.len());
1627 }
1628 None => {
1629 self.pending_code_actions = Some(tagged_actions);
1630 }
1631 }
1632
1633 use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1635 use ratatui::style::Style;
1636
1637 let all_actions = self.pending_code_actions.as_ref().unwrap();
1639 let multiple_servers = {
1640 let mut names = std::collections::HashSet::new();
1641 for (name, _) in all_actions {
1642 names.insert(name.as_str());
1643 }
1644 names.len() > 1
1645 };
1646
1647 let items: Vec<PopupListItem> = all_actions
1648 .iter()
1649 .enumerate()
1650 .map(|(i, (srv_name, action))| {
1651 let title = match action {
1652 lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1653 lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1654 };
1655 let kind = match action {
1656 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1657 ca.kind.as_ref().map(|k| k.as_str().to_string())
1658 }
1659 _ => None,
1660 };
1661 let detail = if multiple_servers && !srv_name.is_empty() {
1663 match kind {
1664 Some(k) => Some(format!("[{}] {}", srv_name, k)),
1665 None => Some(format!("[{}]", srv_name)),
1666 }
1667 } else {
1668 kind
1669 };
1670 PopupListItem {
1671 text: format!("{}. {}", i + 1, title),
1672 detail,
1673 icon: None,
1674 data: Some(i.to_string()),
1675 disabled: false,
1676 }
1677 })
1678 .collect();
1679
1680 let mut popup = Popup::list(items, &self.theme);
1681 popup.kind = crate::view::popup::PopupKind::Action;
1682 popup.title = Some(t!("lsp.popup_code_actions").to_string());
1683 popup.position = PopupPosition::BelowCursor;
1684 popup.width = 60;
1685 popup.max_height = 15;
1686 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1687 popup.background_style = Style::default().bg(self.theme.popup_bg);
1688 popup.resolver = crate::view::popup::PopupResolver::CodeAction;
1692 popup.focused = true;
1698
1699 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1701 state.popups.show_or_replace(popup);
1702 tracing::info!(
1703 "Showing code actions popup with {} actions",
1704 all_actions.len()
1705 );
1706 }
1707 }
1708
1709 pub(crate) fn execute_code_action(&mut self, index: usize) {
1711 let action = match &self.pending_code_actions {
1712 Some(actions) => actions.get(index).map(|(_, a)| a.clone()),
1713 None => None,
1714 };
1715
1716 let Some(action) = action else {
1717 tracing::warn!("Code action index {} out of range", index);
1718 return;
1719 };
1720
1721 match action {
1722 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1723 if ca.edit.is_none()
1726 && ca.command.is_none()
1727 && ca.data.is_some()
1728 && self.server_supports_code_action_resolve()
1729 {
1730 tracing::info!(
1731 "Code action '{}' needs resolve, sending codeAction/resolve",
1732 ca.title
1733 );
1734 self.send_code_action_resolve(ca);
1735 return;
1736 }
1737 self.execute_resolved_code_action(ca);
1738 }
1739 lsp_types::CodeActionOrCommand::Command(cmd) => {
1740 self.send_execute_command(cmd);
1741 }
1742 }
1743 }
1744
1745 pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1747 let title = ca.title.clone();
1748
1749 if let Some(edit) = ca.edit {
1751 match self.apply_workspace_edit(edit) {
1752 Ok(n) => {
1753 self.set_status_message(
1754 t!("lsp.code_action_applied", title = &title, count = n).to_string(),
1755 );
1756 }
1757 Err(e) => {
1758 self.set_status_message(format!("Code action failed: {e}"));
1759 return;
1760 }
1761 }
1762 }
1763
1764 if let Some(cmd) = ca.command {
1766 self.send_execute_command(cmd);
1767 }
1768 }
1769
1770 fn send_execute_command(&mut self, cmd: lsp_types::Command) {
1772 tracing::info!("Executing LSP command: {} ({})", cmd.title, cmd.command);
1773 self.set_status_message(
1774 t!(
1775 "lsp.code_action_applied",
1776 title = &cmd.title,
1777 count = 0_usize
1778 )
1779 .to_string(),
1780 );
1781
1782 let language = match self
1784 .buffers
1785 .get(&self.active_buffer())
1786 .map(|s| s.language.clone())
1787 {
1788 Some(l) => l,
1789 None => return,
1790 };
1791
1792 if let Some(lsp) = &mut self.lsp {
1793 for sh in lsp.get_handles_mut(&language) {
1794 if let Err(e) = sh
1795 .handle
1796 .execute_command(cmd.command.clone(), cmd.arguments.clone())
1797 {
1798 tracing::warn!("Failed to send executeCommand to '{}': {}", sh.name, e);
1799 }
1800 }
1801 }
1802 }
1803
1804 fn send_code_action_resolve(&mut self, action: lsp_types::CodeAction) {
1806 let language = match self
1807 .buffers
1808 .get(&self.active_buffer())
1809 .map(|s| s.language.clone())
1810 {
1811 Some(l) => l,
1812 None => return,
1813 };
1814
1815 self.next_lsp_request_id += 1;
1816 let request_id = self.next_lsp_request_id;
1817
1818 if let Some(lsp) = &mut self.lsp {
1819 for sh in lsp.get_handles_mut(&language) {
1820 if let Err(e) = sh.handle.code_action_resolve(request_id, action.clone()) {
1821 tracing::warn!("Failed to send codeAction/resolve to '{}': {}", sh.name, e);
1822 }
1823 }
1824 }
1825 }
1826
1827 fn server_supports_code_action_resolve(&self) -> bool {
1829 let language = match self
1830 .buffers
1831 .get(&self.active_buffer())
1832 .map(|s| s.language.clone())
1833 {
1834 Some(l) => l,
1835 None => return false,
1836 };
1837
1838 if let Some(lsp) = &self.lsp {
1839 for sh in lsp.get_handles(&language) {
1840 if sh.capabilities.code_action_resolve {
1841 return true;
1842 }
1843 }
1844 }
1845 false
1846 }
1847
1848 pub(crate) fn server_supports_completion_resolve(&self) -> bool {
1850 let language = match self
1851 .buffers
1852 .get(&self.active_buffer())
1853 .map(|s| s.language.clone())
1854 {
1855 Some(l) => l,
1856 None => return false,
1857 };
1858
1859 if let Some(lsp) = &self.lsp {
1860 for sh in lsp.get_handles(&language) {
1861 if sh.capabilities.completion_resolve {
1862 return true;
1863 }
1864 }
1865 }
1866 false
1867 }
1868
1869 pub(crate) fn send_completion_resolve(&mut self, item: lsp_types::CompletionItem) {
1871 let language = match self
1872 .buffers
1873 .get(&self.active_buffer())
1874 .map(|s| s.language.clone())
1875 {
1876 Some(l) => l,
1877 None => return,
1878 };
1879
1880 self.next_lsp_request_id += 1;
1881 let request_id = self.next_lsp_request_id;
1882
1883 if let Some(lsp) = &mut self.lsp {
1884 for sh in lsp.get_handles_mut(&language) {
1885 if sh.capabilities.completion_resolve {
1886 if let Err(e) = sh.handle.completion_resolve(request_id, item.clone()) {
1887 tracing::warn!(
1888 "Failed to send completionItem/resolve to '{}': {}",
1889 sh.name,
1890 e
1891 );
1892 }
1893 return;
1894 }
1895 }
1896 }
1897 }
1898
1899 pub(crate) fn handle_completion_resolved(&mut self, item: lsp_types::CompletionItem) {
1901 if let Some(additional_edits) = item.additional_text_edits {
1902 if !additional_edits.is_empty() {
1903 tracing::info!(
1904 "Applying {} additional text edits from completion resolve",
1905 additional_edits.len()
1906 );
1907 let buffer_id = self.active_buffer();
1908 if let Err(e) = self.apply_lsp_text_edits(buffer_id, additional_edits) {
1909 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
1910 }
1911 }
1912 }
1913 }
1914
1915 pub(crate) fn apply_formatting_edits(
1917 &mut self,
1918 uri: &str,
1919 edits: Vec<lsp_types::TextEdit>,
1920 ) -> AnyhowResult<usize> {
1921 let buffer_id = self
1923 .buffer_metadata
1924 .iter()
1925 .find(|(_, meta)| meta.file_uri().map(|u| u.as_str() == uri).unwrap_or(false))
1926 .map(|(id, _)| *id);
1927
1928 if let Some(buffer_id) = buffer_id {
1929 let count = self.apply_lsp_text_edits(buffer_id, edits)?;
1930 self.set_status_message(format!("Formatted ({} edits)", count));
1931 Ok(count)
1932 } else {
1933 tracing::warn!("Cannot apply formatting: no buffer for URI {}", uri);
1934 Ok(0)
1935 }
1936 }
1937
1938 pub(crate) fn request_formatting(&mut self) {
1940 let buffer_id = self.active_buffer();
1941 let metadata = match self.buffer_metadata.get(&buffer_id) {
1942 Some(m) if m.lsp_enabled => m,
1943 _ => {
1944 self.set_status_message("LSP not available for this buffer".to_string());
1945 return;
1946 }
1947 };
1948
1949 let uri = match metadata.file_uri() {
1950 Some(u) => u.clone(),
1951 None => return,
1952 };
1953
1954 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
1955 Some(l) => l,
1956 None => return,
1957 };
1958
1959 let tab_size = self.config.editor.tab_size as u32;
1960 let insert_spaces = !self.config.editor.use_tabs;
1961
1962 self.next_lsp_request_id += 1;
1963 let request_id = self.next_lsp_request_id;
1964
1965 if let Some(lsp) = &mut self.lsp {
1966 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Format) {
1967 if let Err(e) = sh.handle.document_formatting(
1968 request_id,
1969 uri.as_uri().clone(),
1970 tab_size,
1971 insert_spaces,
1972 ) {
1973 tracing::warn!("Failed to request formatting: {}", e);
1974 }
1975 } else {
1976 self.set_status_message("Formatting not supported by LSP server".to_string());
1977 }
1978 }
1979 }
1980
1981 pub(crate) fn handle_references_response(
1983 &mut self,
1984 request_id: u64,
1985 locations: Vec<lsp_types::Location>,
1986 ) -> AnyhowResult<()> {
1987 tracing::info!(
1988 "handle_references_response: received {} locations for request_id={}",
1989 locations.len(),
1990 request_id
1991 );
1992
1993 if self.pending_references_request != Some(request_id) {
1995 tracing::debug!("Ignoring stale references response: {}", request_id);
1996 return Ok(());
1997 }
1998
1999 self.pending_references_request = None;
2000 if locations.is_empty() {
2001 self.set_status_message(t!("lsp.no_references").to_string());
2002 return Ok(());
2003 }
2004
2005 let translation = self.authority.path_translation.clone();
2012 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
2013 .iter()
2014 .map(|loc| {
2015 let wire = crate::app::types::LspUri::from_wire(loc.uri.clone());
2016 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
2021 wire.to_host_path(translation.as_ref())
2022 .map(|p| p.to_string_lossy().into_owned())
2023 .unwrap_or_else(|| loc.uri.path().as_str().to_string())
2024 } else {
2025 loc.uri.as_str().to_string()
2026 };
2027
2028 crate::services::plugins::hooks::LspLocation {
2029 file,
2030 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
2033 })
2034 .collect();
2035
2036 let count = lsp_locations.len();
2037 let symbol = std::mem::take(&mut self.pending_references_symbol);
2038 self.set_status_message(
2039 t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
2040 );
2041
2042 self.plugin_manager.run_hook(
2044 "lsp_references",
2045 crate::services::plugins::hooks::HookArgs::LspReferences {
2046 symbol: symbol.clone(),
2047 locations: lsp_locations,
2048 },
2049 );
2050
2051 tracing::info!(
2052 "Fired lsp_references hook with {} locations for symbol '{}'",
2053 count,
2054 symbol
2055 );
2056
2057 Ok(())
2058 }
2059
2060 pub(crate) fn apply_lsp_text_edits(
2063 &mut self,
2064 buffer_id: BufferId,
2065 mut edits: Vec<lsp_types::TextEdit>,
2066 ) -> AnyhowResult<usize> {
2067 if edits.is_empty() {
2068 return Ok(0);
2069 }
2070
2071 edits.sort_by(|a, b| {
2073 b.range
2074 .start
2075 .line
2076 .cmp(&a.range.start.line)
2077 .then(b.range.start.character.cmp(&a.range.start.character))
2078 });
2079
2080 let mut batch_events = Vec::new();
2082 let mut changes = 0;
2083
2084 let cursor_id = {
2086 let split_id = self
2087 .split_manager
2088 .splits_for_buffer(buffer_id)
2089 .into_iter()
2090 .next()
2091 .unwrap_or_else(|| self.split_manager.active_split());
2092 self.split_view_states
2093 .get(&split_id)
2094 .map(|vs| vs.cursors.primary_id())
2095 .unwrap_or_else(|| self.active_cursors().primary_id())
2096 };
2097
2098 for edit in edits {
2100 let state = self
2101 .buffers
2102 .get_mut(&buffer_id)
2103 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2104
2105 let start_line = edit.range.start.line as usize;
2107 let start_char = edit.range.start.character as usize;
2108 let end_line = edit.range.end.line as usize;
2109 let end_char = edit.range.end.character as usize;
2110
2111 let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
2112 let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
2113 let buffer_len = state.buffer.len();
2114
2115 let old_text = if start_pos < end_pos && end_pos <= buffer_len {
2117 state.get_text_range(start_pos, end_pos)
2118 } else {
2119 format!(
2120 "<invalid range: start={}, end={}, buffer_len={}>",
2121 start_pos, end_pos, buffer_len
2122 )
2123 };
2124 tracing::debug!(
2125 " Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
2126 start_line, start_char, end_line, end_char,
2127 start_pos, end_pos, old_text, edit.new_text
2128 );
2129
2130 if start_pos < end_pos {
2132 let deleted_text = state.get_text_range(start_pos, end_pos);
2133 let delete_event = Event::Delete {
2134 range: start_pos..end_pos,
2135 deleted_text,
2136 cursor_id,
2137 };
2138 batch_events.push(delete_event);
2139 }
2140
2141 if !edit.new_text.is_empty() {
2143 let insert_event = Event::Insert {
2144 position: start_pos,
2145 text: edit.new_text.clone(),
2146 cursor_id,
2147 };
2148 batch_events.push(insert_event);
2149 }
2150
2151 changes += 1;
2152 }
2153
2154 if !batch_events.is_empty() {
2156 self.apply_events_to_buffer_as_bulk_edit(
2157 buffer_id,
2158 batch_events,
2159 "LSP Rename".to_string(),
2160 )?;
2161 }
2162
2163 Ok(changes)
2164 }
2165
2166 fn apply_text_document_edit(
2172 &mut self,
2173 text_doc_edit: lsp_types::TextDocumentEdit,
2174 ) -> AnyhowResult<usize> {
2175 let uri = crate::app::types::LspUri::from_wire(text_doc_edit.text_document.uri);
2178
2179 if let Some(expected_version) = text_doc_edit.text_document.version {
2182 if let Ok(path) =
2183 super::lsp_uri_to_host_path(&uri, self.authority.path_translation.as_ref())
2184 {
2185 if let Some(lsp) = &self.lsp {
2186 let language = self
2187 .buffers
2188 .get(&self.active_buffer())
2189 .map(|s| s.language.clone())
2190 .unwrap_or_default();
2191 for sh in lsp.get_handles(&language) {
2192 if let Some(current_version) = sh.handle.document_version(&path) {
2193 if (expected_version as i64) != current_version {
2194 tracing::warn!(
2195 "Rejecting stale TextDocumentEdit for {:?}: \
2196 server version {} != our version {}",
2197 path,
2198 expected_version,
2199 current_version,
2200 );
2201 return Ok(0);
2202 }
2203 }
2204 }
2205 }
2206 }
2207 }
2208
2209 if let Ok(path) =
2210 super::lsp_uri_to_host_path(&uri, self.authority.path_translation.as_ref())
2211 {
2212 let buffer_id = match self.open_file(&path) {
2213 Ok(id) => id,
2214 Err(e) => {
2215 if let Some(confirmation) =
2216 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2217 {
2218 self.start_large_file_encoding_confirmation(confirmation);
2219 } else {
2220 self.set_status_message(
2221 t!("file.error_opening", error = e.to_string()).to_string(),
2222 );
2223 }
2224 return Ok(0);
2225 }
2226 };
2227
2228 let edits: Vec<lsp_types::TextEdit> = text_doc_edit
2229 .edits
2230 .into_iter()
2231 .map(|one_of| match one_of {
2232 lsp_types::OneOf::Left(text_edit) => text_edit,
2233 lsp_types::OneOf::Right(annotated) => annotated.text_edit,
2234 })
2235 .collect();
2236
2237 tracing::info!("Applying {} edits for {:?}:", edits.len(), path);
2238 for (i, edit) in edits.iter().enumerate() {
2239 tracing::info!(
2240 " Edit {}: line {}:{}-{}:{} -> {:?}",
2241 i,
2242 edit.range.start.line,
2243 edit.range.start.character,
2244 edit.range.end.line,
2245 edit.range.end.character,
2246 edit.new_text
2247 );
2248 }
2249
2250 self.apply_lsp_text_edits(buffer_id, edits)
2251 } else {
2252 Ok(0)
2253 }
2254 }
2255
2256 fn apply_resource_operation(&mut self, op: lsp_types::ResourceOp) -> AnyhowResult<()> {
2258 let translation = self.authority.path_translation.clone();
2263 let to_host = |uri: &lsp_types::Uri| -> std::path::PathBuf {
2264 crate::app::types::LspUri::from_wire(uri.clone())
2265 .to_host_path(translation.as_ref())
2266 .unwrap_or_else(|| std::path::PathBuf::from(uri.path().as_str()))
2267 };
2268 match op {
2269 lsp_types::ResourceOp::Create(create) => {
2270 let path = to_host(&create.uri);
2271 let overwrite = create
2272 .options
2273 .as_ref()
2274 .and_then(|o| o.overwrite)
2275 .unwrap_or(false);
2276 let ignore_if_exists = create
2277 .options
2278 .as_ref()
2279 .and_then(|o| o.ignore_if_exists)
2280 .unwrap_or(false);
2281
2282 if path.exists() {
2283 if ignore_if_exists {
2284 tracing::debug!("CreateFile: {:?} already exists, ignoring", path);
2285 return Ok(());
2286 }
2287 if !overwrite {
2288 tracing::warn!("CreateFile: {:?} already exists and overwrite=false", path);
2289 return Ok(());
2290 }
2291 }
2292
2293 if let Some(parent) = path.parent() {
2295 std::fs::create_dir_all(parent)?;
2296 }
2297 std::fs::write(&path, "")?;
2298 tracing::info!("CreateFile: created {:?}", path);
2299
2300 if let Err(e) = self.open_file(&path) {
2302 tracing::warn!("CreateFile: failed to open created file {:?}: {}", path, e);
2303 }
2304 }
2305 lsp_types::ResourceOp::Rename(rename) => {
2306 let old_path = to_host(&rename.old_uri);
2307 let new_path = to_host(&rename.new_uri);
2308 let overwrite = rename
2309 .options
2310 .as_ref()
2311 .and_then(|o| o.overwrite)
2312 .unwrap_or(false);
2313 let ignore_if_exists = rename
2314 .options
2315 .as_ref()
2316 .and_then(|o| o.ignore_if_exists)
2317 .unwrap_or(false);
2318
2319 if new_path.exists() {
2320 if ignore_if_exists {
2321 tracing::debug!("RenameFile: {:?} already exists, ignoring", new_path);
2322 return Ok(());
2323 }
2324 if !overwrite {
2325 tracing::warn!(
2326 "RenameFile: {:?} already exists and overwrite=false",
2327 new_path
2328 );
2329 return Ok(());
2330 }
2331 }
2332
2333 if let Some(parent) = new_path.parent() {
2335 std::fs::create_dir_all(parent)?;
2336 }
2337 std::fs::rename(&old_path, &new_path)?;
2338 tracing::info!("RenameFile: {:?} -> {:?}", old_path, new_path);
2339 }
2340 lsp_types::ResourceOp::Delete(delete) => {
2341 let path = to_host(&delete.uri);
2342 let recursive = delete
2343 .options
2344 .as_ref()
2345 .and_then(|o| o.recursive)
2346 .unwrap_or(false);
2347 let ignore_if_not_exists = delete
2348 .options
2349 .as_ref()
2350 .and_then(|o| o.ignore_if_not_exists)
2351 .unwrap_or(false);
2352
2353 if !path.exists() {
2354 if ignore_if_not_exists {
2355 tracing::debug!("DeleteFile: {:?} does not exist, ignoring", path);
2356 return Ok(());
2357 }
2358 tracing::warn!("DeleteFile: {:?} does not exist", path);
2359 return Ok(());
2360 }
2361
2362 if path.is_dir() && recursive {
2363 std::fs::remove_dir_all(&path)?;
2364 } else if path.is_file() {
2365 std::fs::remove_file(&path)?;
2366 }
2367 tracing::info!("DeleteFile: deleted {:?}", path);
2368 }
2369 }
2370 Ok(())
2371 }
2372
2373 pub(crate) fn apply_workspace_edit(
2377 &mut self,
2378 workspace_edit: lsp_types::WorkspaceEdit,
2379 ) -> AnyhowResult<usize> {
2380 tracing::debug!(
2381 "Applying WorkspaceEdit: changes={:?}, document_changes={:?}",
2382 workspace_edit.changes.as_ref().map(|c| c.len()),
2383 workspace_edit.document_changes.as_ref().map(|dc| match dc {
2384 lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
2385 lsp_types::DocumentChanges::Operations(o) => format!("{} operations", o.len()),
2386 })
2387 );
2388
2389 let mut total_changes = 0;
2390
2391 if let Some(changes) = workspace_edit.changes {
2393 for (uri, edits) in changes {
2394 let uri = crate::app::types::LspUri::from_wire(uri);
2395 if let Ok(path) =
2396 super::lsp_uri_to_host_path(&uri, self.authority.path_translation.as_ref())
2397 {
2398 let buffer_id = match self.open_file(&path) {
2399 Ok(id) => id,
2400 Err(e) => {
2401 if let Some(confirmation) = e.downcast_ref::<
2402 crate::model::buffer::LargeFileEncodingConfirmation,
2403 >() {
2404 self.start_large_file_encoding_confirmation(confirmation);
2405 } else {
2406 self.set_status_message(
2407 t!("file.error_opening", error = e.to_string())
2408 .to_string(),
2409 );
2410 }
2411 return Ok(0);
2412 }
2413 };
2414 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
2415 }
2416 }
2417 }
2418
2419 if let Some(document_changes) = workspace_edit.document_changes {
2421 use lsp_types::DocumentChanges;
2422
2423 match document_changes {
2424 DocumentChanges::Edits(edits) => {
2425 for text_doc_edit in edits {
2426 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2427 }
2428 }
2429 DocumentChanges::Operations(ops) => {
2430 for op in ops {
2433 match op {
2434 lsp_types::DocumentChangeOperation::Edit(text_doc_edit) => {
2435 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2436 }
2437 lsp_types::DocumentChangeOperation::Op(resource_op) => {
2438 self.apply_resource_operation(resource_op)?;
2439 total_changes += 1;
2440 }
2441 }
2442 }
2443 }
2444 }
2445 }
2446
2447 Ok(total_changes)
2448 }
2449
2450 pub fn handle_rename_response(
2452 &mut self,
2453 _request_id: u64,
2454 result: Result<lsp_types::WorkspaceEdit, String>,
2455 ) -> AnyhowResult<()> {
2456 match result {
2457 Ok(workspace_edit) => {
2458 let total_changes = self.apply_workspace_edit(workspace_edit)?;
2459 self.status_message = Some(t!("lsp.renamed", count = total_changes).to_string());
2460 }
2461 Err(error) => {
2462 if error.contains("content modified") || error.contains("-32801") {
2464 tracing::debug!(
2465 "LSP rename: ContentModified error (expected, ignoring): {}",
2466 error
2467 );
2468 self.status_message = Some(t!("lsp.rename_cancelled").to_string());
2469 } else {
2470 self.status_message = Some(t!("lsp.rename_failed", error = &error).to_string());
2471 }
2472 }
2473 }
2474
2475 Ok(())
2476 }
2477
2478 pub(crate) fn apply_events_to_buffer_as_bulk_edit(
2483 &mut self,
2484 buffer_id: BufferId,
2485 events: Vec<Event>,
2486 description: String,
2487 ) -> AnyhowResult<()> {
2488 use crate::model::event::CursorId;
2489
2490 if events.is_empty() {
2491 return Ok(());
2492 }
2493
2494 let batch_for_lsp = Event::Batch {
2496 events: events.clone(),
2497 description: description.clone(),
2498 };
2499
2500 let original_active = self.active_buffer();
2512 self.split_manager.set_active_buffer_id(buffer_id);
2513 let lsp_changes = self.collect_lsp_changes(&batch_for_lsp);
2514 self.split_manager.set_active_buffer_id(original_active);
2515
2516 let split_id_for_cursors = self
2519 .split_manager
2520 .splits_for_buffer(buffer_id)
2521 .into_iter()
2522 .next()
2523 .unwrap_or_else(|| self.split_manager.active_split());
2524 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2525 .split_view_states
2526 .get(&split_id_for_cursors)
2527 .and_then(|vs| vs.keyed_states.get(&buffer_id))
2528 .map(|bvs| {
2529 bvs.cursors
2530 .iter()
2531 .map(|(id, c)| (id, c.position, c.anchor))
2532 .collect()
2533 })
2534 .unwrap_or_default();
2535
2536 let state = self
2537 .buffers
2538 .get_mut(&buffer_id)
2539 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2540
2541 let old_snapshot = state.buffer.snapshot_buffer_state();
2543
2544 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2546 for event in &events {
2547 match event {
2548 Event::Insert { position, text, .. } => {
2549 edits.push((*position, 0, text.clone()));
2550 }
2551 Event::Delete { range, .. } => {
2552 edits.push((range.start, range.len(), String::new()));
2553 }
2554 _ => {}
2555 }
2556 }
2557
2558 edits.sort_by(|a, b| b.0.cmp(&a.0));
2560
2561 let edit_refs: Vec<(usize, usize, &str)> = edits
2563 .iter()
2564 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2565 .collect();
2566
2567 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2569
2570 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2572
2573 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2575 for (pos, del_len, text) in &edits {
2576 let delta = text.len() as isize - *del_len as isize;
2577 position_deltas.push((*pos, delta));
2578 }
2579 position_deltas.sort_by_key(|(pos, _)| *pos);
2580
2581 let calc_shift = |original_pos: usize| -> isize {
2582 let mut shift: isize = 0;
2583 for (edit_pos, delta) in &position_deltas {
2584 if *edit_pos < original_pos {
2585 shift += delta;
2586 }
2587 }
2588 shift
2589 };
2590
2591 let buffer_len = state.buffer.len();
2593 let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
2594 .iter()
2595 .map(|(id, pos, anchor)| {
2596 let shift = calc_shift(*pos);
2597 let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
2598 let new_anchor = anchor.map(|a| {
2599 let anchor_shift = calc_shift(a);
2600 ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
2601 });
2602 (*id, new_pos, new_anchor)
2603 })
2604 .collect();
2605
2606 let new_snapshot = state.buffer.snapshot_buffer_state();
2608
2609 state.highlighter.invalidate_all();
2611
2612 if let Some(vs) = self.split_view_states.get_mut(&split_id_for_cursors) {
2614 if let Some(bvs) = vs.keyed_states.get_mut(&buffer_id) {
2615 for (cursor_id, new_pos, new_anchor) in &new_cursors {
2616 if let Some(cursor) = bvs.cursors.get_mut(*cursor_id) {
2617 cursor.position = *new_pos;
2618 cursor.anchor = *new_anchor;
2619 }
2620 }
2621 }
2622 }
2623
2624 let edit_lengths: Vec<(usize, usize, usize)> = {
2627 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2628 for (pos, del_len, text) in &edits {
2629 if let Some(last) = lengths.last_mut() {
2630 if last.0 == *pos {
2631 last.1 += del_len;
2632 last.2 += text.len();
2633 continue;
2634 }
2635 }
2636 lengths.push((*pos, *del_len, text.len()));
2637 }
2638 lengths
2639 };
2640
2641 for &(pos, del_len, ins_len) in &edit_lengths {
2643 if del_len > 0 && ins_len > 0 {
2644 if ins_len > del_len {
2645 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2646 state.margins.adjust_for_insert(pos, ins_len - del_len);
2647 } else if del_len > ins_len {
2648 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2649 state.margins.adjust_for_delete(pos, del_len - ins_len);
2650 }
2651 } else if del_len > 0 {
2652 state.marker_list.adjust_for_delete(pos, del_len);
2653 state.margins.adjust_for_delete(pos, del_len);
2654 } else if ins_len > 0 {
2655 state.marker_list.adjust_for_insert(pos, ins_len);
2656 state.margins.adjust_for_insert(pos, ins_len);
2657 }
2658 }
2659
2660 let bulk_edit = Event::BulkEdit {
2662 old_snapshot: Some(old_snapshot),
2663 new_snapshot: Some(new_snapshot),
2664 old_cursors,
2665 new_cursors,
2666 description,
2667 edits: edit_lengths,
2668 displaced_markers,
2669 };
2670
2671 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
2673 event_log.append(bulk_edit);
2674 }
2675
2676 self.send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2678
2679 Ok(())
2680 }
2681
2682 pub(crate) fn send_lsp_changes_for_buffer(
2684 &mut self,
2685 buffer_id: BufferId,
2686 changes: Vec<TextDocumentContentChangeEvent>,
2687 ) {
2688 if changes.is_empty() {
2689 return;
2690 }
2691
2692 let metadata = match self.buffer_metadata.get(&buffer_id) {
2694 Some(m) => m,
2695 None => {
2696 tracing::debug!(
2697 "send_lsp_changes_for_buffer: no metadata for buffer {:?}",
2698 buffer_id
2699 );
2700 return;
2701 }
2702 };
2703
2704 if !metadata.lsp_enabled {
2705 tracing::debug!("send_lsp_changes_for_buffer: LSP disabled for this buffer");
2706 return;
2707 }
2708
2709 let uri = match metadata.file_uri() {
2711 Some(u) => u.clone(),
2712 None => {
2713 tracing::debug!(
2714 "send_lsp_changes_for_buffer: no URI for buffer (not a file or URI creation failed)"
2715 );
2716 return;
2717 }
2718 };
2719 let file_path = metadata.file_path().cloned();
2720
2721 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
2723 Some(l) => l,
2724 None => {
2725 tracing::debug!(
2726 "send_lsp_changes_for_buffer: no buffer state for {:?}",
2727 buffer_id
2728 );
2729 return;
2730 }
2731 };
2732
2733 tracing::trace!(
2734 "send_lsp_changes_for_buffer: sending {} changes to {} in single didChange notification",
2735 changes.len(),
2736 uri.as_str()
2737 );
2738
2739 use crate::services::lsp::manager::LspSpawnResult;
2741 let Some(lsp) = self.lsp.as_mut() else {
2742 tracing::debug!("send_lsp_changes_for_buffer: no LSP manager available");
2743 return;
2744 };
2745
2746 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
2747 tracing::debug!(
2748 "send_lsp_changes_for_buffer: LSP not running for {} (auto_start disabled)",
2749 language
2750 );
2751 return;
2752 }
2753
2754 let handles_needing_open: Vec<_> = {
2756 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2757 return;
2758 };
2759 lsp.get_handles(&language)
2760 .into_iter()
2761 .filter(|sh| !metadata.lsp_opened_with.contains(&sh.handle.id()))
2762 .map(|sh| (sh.name.clone(), sh.handle.id()))
2763 .collect()
2764 };
2765
2766 if !handles_needing_open.is_empty() {
2767 let text = match self
2769 .buffers
2770 .get(&buffer_id)
2771 .and_then(|s| s.buffer.to_string())
2772 {
2773 Some(t) => t,
2774 None => {
2775 tracing::debug!(
2776 "send_lsp_changes_for_buffer: buffer text not available for didOpen"
2777 );
2778 return;
2779 }
2780 };
2781
2782 let Some(lsp) = self.lsp.as_mut() else { return };
2784 for sh in lsp.get_handles_mut(&language) {
2785 if handles_needing_open
2786 .iter()
2787 .any(|(_, id)| *id == sh.handle.id())
2788 {
2789 if let Err(e) =
2790 sh.handle
2791 .did_open(uri.as_uri().clone(), text.clone(), language.clone())
2792 {
2793 tracing::warn!(
2794 "Failed to send didOpen to '{}' before didChange: {}",
2795 sh.name,
2796 e
2797 );
2798 } else {
2799 tracing::debug!(
2800 "Sent didOpen for {} to LSP handle '{}' before didChange",
2801 uri.as_str(),
2802 sh.name
2803 );
2804 }
2805 }
2806 }
2807
2808 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
2810 for (_, handle_id) in &handles_needing_open {
2811 metadata.lsp_opened_with.insert(*handle_id);
2812 }
2813 }
2814
2815 return;
2819 }
2820
2821 let Some(lsp) = self.lsp.as_mut() else { return };
2823 let mut any_sent = false;
2824 for sh in lsp.get_handles_mut(&language) {
2825 if let Err(e) = sh.handle.did_change(uri.as_uri().clone(), changes.clone()) {
2826 tracing::warn!("Failed to send didChange to '{}': {}", sh.name, e);
2827 } else {
2828 any_sent = true;
2829 }
2830 }
2831 if any_sent {
2832 tracing::trace!("Successfully sent batched didChange to LSP");
2833
2834 if let Some(state) = self.buffers.get(&buffer_id) {
2837 if let Some(path) = state.buffer.file_path() {
2838 crate::services::lsp::diagnostics::invalidate_cache_for_file(
2839 &path.to_string_lossy(),
2840 );
2841 }
2842 }
2843
2844 self.scheduled_diagnostic_pull = Some((
2846 buffer_id,
2847 std::time::Instant::now() + std::time::Duration::from_millis(1000),
2848 ));
2849
2850 if self.config.editor.enable_inlay_hints {
2855 self.scheduled_inlay_hints_request = Some((
2856 buffer_id,
2857 std::time::Instant::now()
2858 + std::time::Duration::from_millis(INLAY_HINTS_DEBOUNCE_MS),
2859 ));
2860 }
2861 }
2862 }
2863
2864 pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2866 if self.server_supports_prepare_rename() {
2868 self.send_prepare_rename();
2869 return Ok(());
2870 }
2871
2872 self.show_rename_prompt()
2873 }
2874
2875 pub(crate) fn handle_prepare_rename_response(
2877 &mut self,
2878 result: Result<serde_json::Value, String>,
2879 ) {
2880 match result {
2881 Ok(value) if !value.is_null() => {
2882 if let Err(e) = self.show_rename_prompt() {
2884 self.set_status_message(format!("Rename failed: {e}"));
2885 }
2886 }
2887 Ok(_) => {
2888 self.set_status_message("Cannot rename at this position".to_string());
2889 }
2890 Err(e) => {
2891 self.set_status_message(format!("Cannot rename: {e}"));
2892 }
2893 }
2894 }
2895
2896 fn server_supports_prepare_rename(&self) -> bool {
2898 let language = match self
2899 .buffers
2900 .get(&self.active_buffer())
2901 .map(|s| s.language.clone())
2902 {
2903 Some(l) => l,
2904 None => return false,
2905 };
2906
2907 if let Some(lsp) = &self.lsp {
2908 for sh in lsp.get_handles(&language) {
2909 if sh.capabilities.rename {
2910 return true;
2913 }
2914 }
2915 }
2916 false
2917 }
2918
2919 fn send_prepare_rename(&mut self) {
2921 let cursor_pos = self.active_cursors().primary().position;
2922 let (line, character) = self
2923 .active_state()
2924 .buffer
2925 .position_to_lsp_position(cursor_pos);
2926
2927 let buffer_id = self.active_buffer();
2928 let metadata = match self.buffer_metadata.get(&buffer_id) {
2929 Some(m) if m.lsp_enabled => m,
2930 _ => return,
2931 };
2932 let uri = match metadata.file_uri() {
2933 Some(u) => u.clone(),
2934 None => return,
2935 };
2936 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
2937 Some(l) => l,
2938 None => return,
2939 };
2940
2941 self.next_lsp_request_id += 1;
2942 let request_id = self.next_lsp_request_id;
2943
2944 if let Some(lsp) = &mut self.lsp {
2945 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Rename) {
2946 if let Err(e) = sh.handle.prepare_rename(
2947 request_id,
2948 uri.as_uri().clone(),
2949 line as u32,
2950 character as u32,
2951 ) {
2952 tracing::warn!("Failed to send prepareRename: {}", e);
2953 }
2954 }
2955 }
2956 }
2957
2958 fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2960 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2961
2962 let cursor_pos = self.active_cursors().primary().position;
2964 let (word_start, word_end) = {
2965 let state = self.active_state();
2966
2967 let word_start = find_word_start(&state.buffer, cursor_pos);
2969 let word_end = find_word_end(&state.buffer, cursor_pos);
2970
2971 if word_start >= word_end {
2973 self.status_message = Some(t!("lsp.no_symbol_at_cursor").to_string());
2974 return Ok(());
2975 }
2976
2977 (word_start, word_end)
2978 };
2979
2980 let word_text = self.active_state_mut().get_text_range(word_start, word_end);
2982
2983 let overlay_handle = self.add_overlay(
2985 None,
2986 word_start..word_end,
2987 crate::model::event::OverlayFace::Background {
2988 color: (50, 100, 200), },
2990 100,
2991 Some(t!("lsp.popup_renaming").to_string()),
2992 );
2993
2994 let mut prompt = Prompt::new(
2997 "Rename to: ".to_string(),
2998 PromptType::LspRename {
2999 original_text: word_text.clone(),
3000 start_pos: word_start,
3001 end_pos: word_end,
3002 overlay_handle,
3003 },
3004 );
3005 prompt.set_input(word_text);
3007
3008 self.prompt = Some(prompt);
3009 Ok(())
3010 }
3011
3012 pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
3014 self.remove_overlay(handle.clone());
3015 }
3016
3017 pub(crate) fn perform_lsp_rename(
3019 &mut self,
3020 new_name: String,
3021 original_text: String,
3022 start_pos: usize,
3023 overlay_handle: crate::view::overlay::OverlayHandle,
3024 ) {
3025 self.cancel_rename_overlay(&overlay_handle);
3027
3028 if new_name == original_text {
3030 self.status_message = Some(t!("lsp.name_unchanged").to_string());
3031 return;
3032 }
3033
3034 let rename_pos = start_pos;
3037
3038 let state = self.active_state();
3041 let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
3042 let buffer_id = self.active_buffer();
3043 let request_id = self.next_lsp_request_id;
3044
3045 let sent = self
3047 .with_lsp_for_buffer(buffer_id, LspFeature::Rename, |handle, uri, _language| {
3048 let result = handle.rename(
3049 request_id,
3050 uri.as_uri().clone(),
3051 line as u32,
3052 character as u32,
3053 new_name.clone(),
3054 );
3055 if result.is_ok() {
3056 tracing::info!(
3057 "Requested rename at {}:{}:{} to '{}'",
3058 uri.as_str(),
3059 line,
3060 character,
3061 new_name
3062 );
3063 }
3064 result.is_ok()
3065 })
3066 .unwrap_or(false);
3067
3068 if sent {
3069 self.next_lsp_request_id += 1;
3070 } else if self
3071 .buffer_metadata
3072 .get(&buffer_id)
3073 .and_then(|m| m.file_path())
3074 .is_none()
3075 {
3076 self.status_message = Some(t!("lsp.cannot_rename_unsaved").to_string());
3077 }
3078 }
3079
3080 pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
3082 let buffer_id = self.active_buffer();
3083 self.request_inlay_hints_for_buffer(buffer_id);
3084 }
3085
3086 pub(crate) fn request_inlay_hints_for_buffer(&mut self, buffer_id: BufferId) {
3088 if !self.config.editor.enable_inlay_hints {
3089 return;
3090 }
3091
3092 let (line_count, version) = if let Some(state) = self.buffers.get(&buffer_id) {
3096 (
3097 state.buffer.line_count().unwrap_or(1000),
3098 state.buffer.version(),
3099 )
3100 } else {
3101 return;
3102 };
3103 let last_line = line_count.saturating_sub(1) as u32;
3104 let request_id = self.next_lsp_request_id;
3105
3106 let sent = self
3108 .with_lsp_for_buffer(
3109 buffer_id,
3110 LspFeature::InlayHints,
3111 |handle, uri, _language| {
3112 let result = handle.inlay_hints(
3113 request_id,
3114 uri.as_uri().clone(),
3115 0,
3116 0,
3117 last_line,
3118 10000,
3119 );
3120 if result.is_ok() {
3121 tracing::info!(
3122 "Requested inlay hints for {} (request_id={})",
3123 uri.as_str(),
3124 request_id
3125 );
3126 } else if let Err(e) = &result {
3127 tracing::debug!("Failed to request inlay hints: {}", e);
3128 }
3129 result.is_ok()
3130 },
3131 )
3132 .unwrap_or(false);
3133
3134 if sent {
3135 self.next_lsp_request_id += 1;
3136 self.pending_inlay_hints_requests
3137 .insert(request_id, super::InlayHintsRequest { buffer_id, version });
3138 }
3139 }
3140
3141 pub(crate) fn schedule_folding_ranges_refresh(&mut self, buffer_id: BufferId) {
3143 let next_time = Instant::now() + Duration::from_millis(FOLDING_RANGES_DEBOUNCE_MS);
3144 self.folding_ranges_debounce.insert(buffer_id, next_time);
3145 }
3146
3147 pub(crate) fn maybe_request_folding_ranges_debounced(&mut self, buffer_id: BufferId) {
3149 let Some(ready_at) = self.folding_ranges_debounce.get(&buffer_id).copied() else {
3150 return;
3151 };
3152 if Instant::now() < ready_at {
3153 return;
3154 }
3155
3156 self.folding_ranges_debounce.remove(&buffer_id);
3157 self.request_folding_ranges_for_buffer(buffer_id);
3158 }
3159
3160 pub(crate) fn request_folding_ranges_for_buffer(&mut self, buffer_id: BufferId) {
3162 if self.folding_ranges_in_flight.contains_key(&buffer_id) {
3163 return;
3164 }
3165
3166 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
3167 return;
3168 };
3169 if !metadata.lsp_enabled {
3170 return;
3171 }
3172 let Some(uri) = metadata.file_uri().cloned() else {
3173 return;
3174 };
3175 let file_path = metadata.file_path().cloned();
3176
3177 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
3178 return;
3179 };
3180
3181 let Some(lsp) = self.lsp.as_mut() else {
3182 return;
3183 };
3184
3185 if !lsp.folding_ranges_supported(&language) {
3186 return;
3187 }
3188
3189 use crate::services::lsp::manager::LspSpawnResult;
3191 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3192 return;
3193 }
3194
3195 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::FoldingRange) else {
3196 return;
3197 };
3198 let handle = &mut sh.handle;
3199
3200 let request_id = self.next_lsp_request_id;
3201 self.next_lsp_request_id += 1;
3202 let buffer_version = self
3203 .buffers
3204 .get(&buffer_id)
3205 .map(|s| s.buffer.version())
3206 .unwrap_or(0);
3207
3208 match handle.folding_ranges(request_id, uri.as_uri().clone()) {
3209 Ok(()) => {
3210 self.pending_folding_range_requests.insert(
3211 request_id,
3212 super::FoldingRangeRequest {
3213 buffer_id,
3214 version: buffer_version,
3215 },
3216 );
3217 self.folding_ranges_in_flight
3218 .insert(buffer_id, (request_id, buffer_version));
3219 }
3220 Err(e) => {
3221 tracing::debug!("Failed to request folding ranges: {}", e);
3222 }
3223 }
3224 }
3225
3226 pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
3228 if !self.config.editor.enable_semantic_tokens_full {
3229 return;
3230 }
3231
3232 if self.semantic_tokens_in_flight.contains_key(&buffer_id) {
3234 return;
3235 }
3236
3237 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
3238 return;
3239 };
3240 if !metadata.lsp_enabled {
3241 return;
3242 }
3243 let Some(uri) = metadata.file_uri().cloned() else {
3244 return;
3245 };
3246 let file_path_for_spawn = metadata.file_path().cloned();
3247 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
3249 return;
3250 };
3251
3252 let Some(lsp) = self.lsp.as_mut() else {
3253 return;
3254 };
3255
3256 use crate::services::lsp::manager::LspSpawnResult;
3258 if lsp.try_spawn(&language, file_path_for_spawn.as_deref()) != LspSpawnResult::Spawned {
3259 return;
3260 }
3261
3262 if !lsp.semantic_tokens_full_supported(&language) {
3264 return;
3265 }
3266 if lsp.semantic_tokens_legend(&language).is_none() {
3267 return;
3268 }
3269
3270 let Some(state) = self.buffers.get(&buffer_id) else {
3271 return;
3272 };
3273 let buffer_version = state.buffer.version();
3274 if let Some(store) = state.semantic_tokens.as_ref() {
3275 if store.version == buffer_version {
3276 return; }
3278 }
3279
3280 let previous_result_id = state
3281 .semantic_tokens
3282 .as_ref()
3283 .and_then(|store| store.result_id.clone());
3284
3285 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3286 return;
3287 };
3288 let supports_delta = sh.capabilities.semantic_tokens_full_delta;
3290 let use_delta = previous_result_id.is_some() && supports_delta;
3291 let handle = &mut sh.handle;
3292
3293 let request_id = self.next_lsp_request_id;
3294 self.next_lsp_request_id += 1;
3295
3296 let request_kind = if use_delta {
3297 super::SemanticTokensFullRequestKind::FullDelta
3298 } else {
3299 super::SemanticTokensFullRequestKind::Full
3300 };
3301
3302 let request_result = if use_delta {
3303 handle.semantic_tokens_full_delta(
3304 request_id,
3305 uri.as_uri().clone(),
3306 previous_result_id.unwrap(),
3307 )
3308 } else {
3309 handle.semantic_tokens_full(request_id, uri.as_uri().clone())
3310 };
3311
3312 match request_result {
3313 Ok(_) => {
3314 self.pending_semantic_token_requests.insert(
3315 request_id,
3316 super::SemanticTokenFullRequest {
3317 buffer_id,
3318 version: buffer_version,
3319 kind: request_kind,
3320 },
3321 );
3322 self.semantic_tokens_in_flight
3323 .insert(buffer_id, (request_id, buffer_version, request_kind));
3324 }
3325 Err(e) => {
3326 tracing::debug!("Failed to request semantic tokens: {}", e);
3327 }
3328 }
3329 }
3330
3331 pub(crate) fn schedule_semantic_tokens_full_refresh(&mut self, buffer_id: BufferId) {
3333 if !self.config.editor.enable_semantic_tokens_full {
3334 return;
3335 }
3336
3337 let next_time = Instant::now() + Duration::from_millis(SEMANTIC_TOKENS_FULL_DEBOUNCE_MS);
3338 self.semantic_tokens_full_debounce
3339 .insert(buffer_id, next_time);
3340 }
3341
3342 pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
3344 if !self.config.editor.enable_semantic_tokens_full {
3345 self.semantic_tokens_full_debounce.remove(&buffer_id);
3346 return;
3347 }
3348
3349 let Some(ready_at) = self.semantic_tokens_full_debounce.get(&buffer_id).copied() else {
3350 return;
3351 };
3352 if Instant::now() < ready_at {
3353 return;
3354 }
3355
3356 self.semantic_tokens_full_debounce.remove(&buffer_id);
3357 self.maybe_request_semantic_tokens(buffer_id);
3358 }
3359
3360 pub(crate) fn maybe_request_semantic_tokens_range(
3362 &mut self,
3363 buffer_id: BufferId,
3364 start_line: usize,
3365 end_line: usize,
3366 ) {
3367 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
3368 return;
3369 };
3370 if !metadata.lsp_enabled {
3371 return;
3372 }
3373 let Some(uri) = metadata.file_uri().cloned() else {
3374 return;
3375 };
3376 let file_path = metadata.file_path().cloned();
3377 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
3379 return;
3380 };
3381
3382 let Some(lsp) = self.lsp.as_mut() else {
3383 return;
3384 };
3385
3386 use crate::services::lsp::manager::LspSpawnResult;
3388 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3389 return;
3390 }
3391
3392 if !lsp.semantic_tokens_range_supported(&language) {
3393 self.maybe_request_semantic_tokens(buffer_id);
3395 return;
3396 }
3397 if lsp.semantic_tokens_legend(&language).is_none() {
3398 return;
3399 }
3400
3401 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3402 return;
3403 };
3404 if !sh.capabilities.semantic_tokens_range {
3407 return;
3408 }
3409 let handle = &mut sh.handle;
3410 let Some(state) = self.buffers.get(&buffer_id) else {
3411 return;
3412 };
3413
3414 let buffer_version = state.buffer.version();
3415 let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3416 let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3417
3418 if let Some(line_count) = state.buffer.line_count() {
3419 if line_count == 0 {
3420 return;
3421 }
3422 let max_line = line_count.saturating_sub(1);
3423 padded_start = padded_start.min(max_line);
3424 padded_end = padded_end.min(max_line);
3425 }
3426
3427 let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
3428 let end_char = state
3429 .buffer
3430 .get_line(padded_end)
3431 .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
3432 .unwrap_or(0);
3433 let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
3434 state.buffer.lsp_position_to_byte(padded_end, end_char)
3435 } else {
3436 state.buffer.len()
3437 };
3438
3439 if start_byte >= end_byte {
3440 return;
3441 }
3442
3443 let range = start_byte..end_byte;
3444 if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
3445 self.semantic_tokens_range_in_flight.get(&buffer_id)
3446 {
3447 if *in_flight_start == padded_start
3448 && *in_flight_end == padded_end
3449 && *in_flight_version == buffer_version
3450 {
3451 return;
3452 }
3453 if let Err(e) = handle.cancel_request(*in_flight_id) {
3454 tracing::debug!("Failed to cancel semantic token range request: {}", e);
3455 }
3456 self.pending_semantic_token_range_requests
3457 .remove(in_flight_id);
3458 self.semantic_tokens_range_in_flight.remove(&buffer_id);
3459 }
3460
3461 if let Some((applied_start, applied_end, applied_version)) =
3462 self.semantic_tokens_range_applied.get(&buffer_id)
3463 {
3464 if *applied_start == padded_start
3465 && *applied_end == padded_end
3466 && *applied_version == buffer_version
3467 {
3468 return;
3469 }
3470 }
3471
3472 let now = Instant::now();
3473 if let Some((last_start, last_end, last_version, last_time)) =
3474 self.semantic_tokens_range_last_request.get(&buffer_id)
3475 {
3476 if *last_start == padded_start
3477 && *last_end == padded_end
3478 && *last_version == buffer_version
3479 && now.duration_since(*last_time)
3480 < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
3481 {
3482 return;
3483 }
3484 }
3485
3486 let lsp_range = lsp_types::Range {
3487 start: lsp_types::Position {
3488 line: padded_start as u32,
3489 character: 0,
3490 },
3491 end: lsp_types::Position {
3492 line: padded_end as u32,
3493 character: end_char as u32,
3494 },
3495 };
3496
3497 let request_id = self.next_lsp_request_id;
3498 self.next_lsp_request_id += 1;
3499
3500 match handle.semantic_tokens_range(request_id, uri.as_uri().clone(), lsp_range) {
3501 Ok(_) => {
3502 self.pending_semantic_token_range_requests.insert(
3503 request_id,
3504 SemanticTokenRangeRequest {
3505 buffer_id,
3506 version: buffer_version,
3507 range: range.clone(),
3508 start_line: padded_start,
3509 end_line: padded_end,
3510 },
3511 );
3512 self.semantic_tokens_range_in_flight.insert(
3513 buffer_id,
3514 (request_id, padded_start, padded_end, buffer_version),
3515 );
3516 self.semantic_tokens_range_last_request
3517 .insert(buffer_id, (padded_start, padded_end, buffer_version, now));
3518 }
3519 Err(e) => {
3520 tracing::debug!("Failed to request semantic token range: {}", e);
3521 }
3522 }
3523 }
3524}
3525
3526#[cfg(test)]
3527mod tests {
3528 use crate::model::filesystem::StdFileSystem;
3529 use std::sync::Arc;
3530
3531 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
3532 Arc::new(StdFileSystem)
3533 }
3534 use super::{lsp_range_contains, Editor};
3535
3536 fn range(sl: u32, sc: u32, el: u32, ec: u32) -> lsp_types::Range {
3537 lsp_types::Range {
3538 start: lsp_types::Position {
3539 line: sl,
3540 character: sc,
3541 },
3542 end: lsp_types::Position {
3543 line: el,
3544 character: ec,
3545 },
3546 }
3547 }
3548
3549 #[test]
3550 fn test_lsp_range_contains_inclusive_start_exclusive_end() {
3551 let r = range(3, 10, 3, 20);
3552 assert!(!lsp_range_contains(&r, 3, 9));
3554 assert!(!lsp_range_contains(&r, 2, 50));
3555 assert!(lsp_range_contains(&r, 3, 10));
3557 assert!(lsp_range_contains(&r, 3, 15));
3559 assert!(lsp_range_contains(&r, 3, 19));
3561 assert!(!lsp_range_contains(&r, 3, 20));
3563 assert!(!lsp_range_contains(&r, 3, 21));
3565 assert!(!lsp_range_contains(&r, 4, 0));
3566 }
3567
3568 #[test]
3569 fn test_lsp_range_contains_multiline() {
3570 let r = range(2, 5, 4, 3);
3571 assert!(!lsp_range_contains(&r, 1, 100));
3573 assert!(!lsp_range_contains(&r, 2, 4));
3575 assert!(lsp_range_contains(&r, 2, 5));
3577 assert!(lsp_range_contains(&r, 3, 0));
3579 assert!(lsp_range_contains(&r, 3, 9999));
3580 assert!(lsp_range_contains(&r, 4, 2));
3582 assert!(!lsp_range_contains(&r, 4, 3));
3584 assert!(!lsp_range_contains(&r, 5, 0));
3586 }
3587
3588 #[test]
3589 fn test_lsp_range_contains_zero_length_matches_anchor_only() {
3590 let r = range(7, 4, 7, 4);
3592 assert!(lsp_range_contains(&r, 7, 4));
3593 assert!(!lsp_range_contains(&r, 7, 3));
3594 assert!(!lsp_range_contains(&r, 7, 5));
3595 assert!(!lsp_range_contains(&r, 6, 4));
3596 assert!(!lsp_range_contains(&r, 8, 4));
3597 }
3598 use crate::model::buffer::Buffer;
3599 use crate::state::EditorState;
3600 use crate::view::virtual_text::VirtualTextPosition;
3601 use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
3602
3603 fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
3604 InlayHint {
3605 position: Position { line, character },
3606 label: InlayHintLabel::String(label.to_string()),
3607 kind,
3608 text_edits: None,
3609 tooltip: None,
3610 padding_left: None,
3611 padding_right: None,
3612 data: None,
3613 }
3614 }
3615
3616 #[test]
3617 fn test_inlay_hint_inserts_before_character() {
3618 let mut state = EditorState::new(
3619 80,
3620 24,
3621 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3622 test_fs(),
3623 );
3624 state.buffer = Buffer::from_str_test("ab");
3625
3626 if !state.buffer.is_empty() {
3627 state.marker_list.adjust_for_insert(0, state.buffer.len());
3628 }
3629
3630 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3631 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3632
3633 let lookup = state
3634 .virtual_texts
3635 .build_lookup(&state.marker_list, 0, state.buffer.len());
3636 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3637 assert_eq!(vtexts.len(), 1);
3638 assert_eq!(vtexts[0].text, ": i32");
3639 assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
3640 }
3641
3642 #[test]
3643 fn test_inlay_hint_at_eof_renders_after_last_char() {
3644 let mut state = EditorState::new(
3645 80,
3646 24,
3647 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3648 test_fs(),
3649 );
3650 state.buffer = Buffer::from_str_test("ab");
3651
3652 if !state.buffer.is_empty() {
3653 state.marker_list.adjust_for_insert(0, state.buffer.len());
3654 }
3655
3656 let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
3657 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3658
3659 let lookup = state
3660 .virtual_texts
3661 .build_lookup(&state.marker_list, 0, state.buffer.len());
3662 let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
3663 assert_eq!(vtexts.len(), 1);
3664 assert_eq!(vtexts[0].text, ": i32");
3665 assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
3666 }
3667
3668 #[test]
3669 fn test_inlay_hint_empty_buffer_is_ignored() {
3670 let mut state = EditorState::new(
3671 80,
3672 24,
3673 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3674 test_fs(),
3675 );
3676 state.buffer = Buffer::from_str_test("");
3677
3678 let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
3679 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3680
3681 assert!(state.virtual_texts.is_empty());
3682 }
3683
3684 #[test]
3685 fn test_inlay_hint_uses_theme_key_for_foreground() {
3686 let mut state = EditorState::new(
3689 80,
3690 24,
3691 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3692 test_fs(),
3693 );
3694 state.buffer = Buffer::from_str_test("ab");
3695
3696 if !state.buffer.is_empty() {
3697 state.marker_list.adjust_for_insert(0, state.buffer.len());
3698 }
3699
3700 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3701 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3702
3703 let lookup = state
3704 .virtual_texts
3705 .build_lookup(&state.marker_list, 0, state.buffer.len());
3706 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3707 assert_eq!(
3708 vtexts[0].fg_theme_key.as_deref(),
3709 Some("editor.line_number_fg")
3710 );
3711 assert_eq!(vtexts[0].bg_theme_key, None);
3712 }
3713
3714 #[test]
3715 fn test_inlay_hint_removed_when_its_range_is_deleted() {
3716 let mut state = EditorState::new(
3723 80,
3724 24,
3725 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3726 test_fs(),
3727 );
3728 state.buffer = Buffer::from_str_test("let x = 42;");
3729 state.marker_list.adjust_for_insert(0, state.buffer.len());
3730
3731 let hints = vec![make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE))];
3733 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3734 assert_eq!(state.virtual_texts.len(), 1);
3735
3736 let removed = state
3739 .virtual_texts
3740 .remove_in_range(&mut state.marker_list, 4, 10);
3741 assert_eq!(removed, 1, "hint inside deleted range must be removed");
3742 assert!(state.virtual_texts.is_empty());
3743 }
3744
3745 #[test]
3746 fn test_marker_delete_after_repeat_clear_recreate() {
3747 use crate::model::marker::MarkerList;
3753 use crate::view::virtual_text::{VirtualTextManager, VirtualTextPosition};
3754 use ratatui::style::Style;
3755
3756 let mut markers = MarkerList::new();
3757 let mut vtexts = VirtualTextManager::new();
3758
3759 let positions = [200usize, 401, 602, 803, 1205, 1406];
3761 for &p in &positions {
3762 vtexts.add(
3763 &mut markers,
3764 p,
3765 format!("hint-at-{p}"),
3766 Style::default(),
3767 VirtualTextPosition::BeforeChar,
3768 0,
3769 );
3770 }
3771
3772 for _ in 0..3 {
3775 vtexts.clear(&mut markers);
3776 for &p in &positions {
3777 vtexts.add(
3778 &mut markers,
3779 p,
3780 format!("hint-at-{p}"),
3781 Style::default(),
3782 VirtualTextPosition::BeforeChar,
3783 0,
3784 );
3785 }
3786 }
3787
3788 let removed = vtexts.remove_in_range(&mut markers, 1005, 1206);
3790 assert_eq!(
3791 removed, 1,
3792 "exactly one marker inside [1005, 1206) should be removed"
3793 );
3794 markers.adjust_for_delete(1005, 201);
3795
3796 let lookup = vtexts.build_lookup(&markers, 0, 10_000);
3797 let mut positions: Vec<usize> = lookup.keys().copied().collect();
3798 positions.sort();
3799 assert_eq!(
3800 positions,
3801 vec![200, 401, 602, 803, 1205],
3802 "after delete+adjust, expected marker byte positions {:?}, got {:?}",
3803 vec![200, 401, 602, 803, 1205],
3804 positions
3805 );
3806 }
3807
3808 #[test]
3809 fn test_marker_delete_then_adjust_preserves_last_marker_position() {
3810 use crate::model::marker::MarkerList;
3824
3825 let mut markers = MarkerList::new();
3826 let m0 = markers.create(200, false);
3827 let m1 = markers.create(401, false);
3828 let m2 = markers.create(602, false);
3829 let m3 = markers.create(803, false);
3830 let m5 = markers.create(1205, false);
3831 let m6 = markers.create(1406, false);
3832
3833 markers.delete(m5);
3835
3836 markers.adjust_for_delete(1005, 201);
3838
3839 assert_eq!(markers.get_position(m0), Some(200), "m0 unchanged");
3840 assert_eq!(markers.get_position(m1), Some(401), "m1 unchanged");
3841 assert_eq!(markers.get_position(m2), Some(602), "m2 unchanged");
3842 assert_eq!(markers.get_position(m3), Some(803), "m3 unchanged");
3843 assert_eq!(
3844 markers.get_position(m6),
3845 Some(1205),
3846 "m6 must shift from 1406 to 1205 (1406 - 201), not be clamped to delete-start 1005"
3847 );
3848 }
3849
3850 #[test]
3851 fn test_inlay_hint_outside_deletion_survives() {
3852 let mut state = EditorState::new(
3854 80,
3855 24,
3856 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3857 test_fs(),
3858 );
3859 state.buffer = Buffer::from_str_test("let x = 42; let y = 0;");
3860 state.marker_list.adjust_for_insert(0, state.buffer.len());
3861
3862 let hints = vec![
3863 make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE)), make_hint(0, 17, ": i32", Some(InlayHintKind::TYPE)), ];
3866 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3867 assert_eq!(state.virtual_texts.len(), 2);
3868
3869 let removed = state
3870 .virtual_texts
3871 .remove_in_range(&mut state.marker_list, 4, 10);
3872 assert_eq!(removed, 1);
3873 assert_eq!(state.virtual_texts.len(), 1);
3874 }
3875
3876 #[test]
3877 fn test_space_doc_paragraphs_inserts_blank_lines() {
3878 use super::space_doc_paragraphs;
3879
3880 let input = "sep\n description.\nend\n another.";
3882 let result = space_doc_paragraphs(input);
3883 assert_eq!(result, "sep\n\n description.\n\nend\n\n another.");
3884 }
3885
3886 #[test]
3887 fn test_space_doc_paragraphs_preserves_existing_blank_lines() {
3888 use super::space_doc_paragraphs;
3889
3890 let input = "First paragraph.\n\nSecond paragraph.";
3892 let result = space_doc_paragraphs(input);
3893 assert_eq!(result, "First paragraph.\n\nSecond paragraph.");
3894 }
3895
3896 #[test]
3897 fn test_space_doc_paragraphs_plain_text() {
3898 use super::space_doc_paragraphs;
3899
3900 let input = "Just a single line of docs.";
3901 let result = space_doc_paragraphs(input);
3902 assert_eq!(result, "Just a single line of docs.");
3903 }
3904}