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