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