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