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::services::lsp::manager::detect_language;
23use crate::view::prompt::{Prompt, PromptType};
24
25use super::{uri_to_path, Editor, SemanticTokenRangeRequest};
26
27const SEMANTIC_TOKENS_FULL_DEBOUNCE_MS: u64 = 500;
28const SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS: u64 = 50;
29const SEMANTIC_TOKENS_RANGE_PADDING_LINES: usize = 10;
30
31impl Editor {
32 pub(crate) fn handle_completion_response(
34 &mut self,
35 request_id: u64,
36 items: Vec<lsp_types::CompletionItem>,
37 ) -> AnyhowResult<()> {
38 if self.pending_completion_request != Some(request_id) {
40 tracing::debug!(
41 "Ignoring completion response for outdated request {}",
42 request_id
43 );
44 return Ok(());
45 }
46
47 self.pending_completion_request = None;
48 self.lsp_status.clear();
49
50 if items.is_empty() {
51 tracing::debug!("No completion items received");
52 return Ok(());
53 }
54
55 use crate::primitives::word_navigation::find_completion_word_start;
57 let (word_start, cursor_pos) = {
58 let state = self.active_state();
59 let cursor_pos = state.cursors.primary().position;
60 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
61 (word_start, cursor_pos)
62 };
63 let prefix = if word_start < cursor_pos {
64 self.active_state_mut()
65 .get_text_range(word_start, cursor_pos)
66 .to_lowercase()
67 } else {
68 String::new()
69 };
70
71 let filtered_items: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
73 items.iter().collect()
75 } else {
76 items
78 .iter()
79 .filter(|item| {
80 item.label.to_lowercase().starts_with(&prefix)
81 || item
82 .filter_text
83 .as_ref()
84 .map(|ft| ft.to_lowercase().starts_with(&prefix))
85 .unwrap_or(false)
86 })
87 .collect()
88 };
89
90 if filtered_items.is_empty() {
91 tracing::debug!("No completion items match prefix '{}'", prefix);
92 return Ok(());
93 }
94
95 use crate::view::popup::PopupListItem;
97
98 let popup_items: Vec<PopupListItem> = filtered_items
99 .iter()
100 .map(|item| {
101 let text = item.label.clone();
102 let detail = item.detail.clone();
103 let icon = match item.kind {
104 Some(lsp_types::CompletionItemKind::FUNCTION)
105 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
106 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
107 Some(lsp_types::CompletionItemKind::STRUCT)
108 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
109 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
110 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
111 _ => None,
112 };
113
114 let mut list_item = PopupListItem::new(text);
115 if let Some(detail) = detail {
116 list_item = list_item.with_detail(detail);
117 }
118 if let Some(icon) = icon {
119 list_item = list_item.with_icon(icon);
120 }
121 let data = item
123 .insert_text
124 .clone()
125 .or_else(|| Some(item.label.clone()));
126 if let Some(data) = data {
127 list_item = list_item.with_data(data);
128 }
129 list_item
130 })
131 .collect();
132
133 use crate::model::event::{
135 PopupContentData, PopupData, PopupListItemData, PopupPositionData,
136 };
137 let popup_data = PopupData {
138 title: Some(t!("lsp.popup_completion").to_string()),
139 description: None,
140 transient: false,
141 content: PopupContentData::List {
142 items: popup_items
143 .into_iter()
144 .map(|item| PopupListItemData {
145 text: item.text,
146 detail: item.detail,
147 icon: item.icon,
148 data: item.data,
149 })
150 .collect(),
151 selected: 0,
152 },
153 position: PopupPositionData::BelowCursor,
154 width: 50,
155 max_height: 15,
156 bordered: true,
157 };
158
159 self.completion_items = Some(items);
161
162 self.active_state_mut()
163 .apply(&crate::model::event::Event::ShowPopup { popup: popup_data });
164
165 tracing::info!(
166 "Showing completion popup with {} items",
167 self.completion_items.as_ref().map_or(0, |i| i.len())
168 );
169
170 Ok(())
171 }
172
173 pub(crate) fn handle_goto_definition_response(
175 &mut self,
176 request_id: u64,
177 locations: Vec<lsp_types::Location>,
178 ) -> AnyhowResult<()> {
179 if self.pending_goto_definition_request != Some(request_id) {
181 tracing::debug!(
182 "Ignoring go-to-definition response for outdated request {}",
183 request_id
184 );
185 return Ok(());
186 }
187
188 self.pending_goto_definition_request = None;
189
190 if locations.is_empty() {
191 self.status_message = Some(t!("lsp.no_definition").to_string());
192 return Ok(());
193 }
194
195 let location = &locations[0];
197
198 if let Ok(path) = uri_to_path(&location.uri) {
200 let buffer_id = self.open_file(&path)?;
202
203 let is_library_file = self.is_library_file(&path);
205 if is_library_file {
206 if let Some(state) = self.buffers.get_mut(&buffer_id) {
208 state.editing_disabled = true;
209 }
210 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
211 metadata.read_only = true;
212 }
213 }
214
215 let line = location.range.start.line as usize;
217 let character = location.range.start.character as usize;
218
219 if let Some(state) = self.buffers.get(&buffer_id) {
221 let position = state.buffer.line_col_to_position(line, character);
222
223 let cursor_id = state.cursors.primary_id();
225 let old_position = state.cursors.primary().position;
226 let old_anchor = state.cursors.primary().anchor;
227 let old_sticky_column = state.cursors.primary().sticky_column;
228 let event = crate::model::event::Event::MoveCursor {
229 cursor_id,
230 old_position,
231 new_position: position,
232 old_anchor,
233 new_anchor: None,
234 old_sticky_column,
235 new_sticky_column: 0, };
237
238 if let Some(state) = self.buffers.get_mut(&buffer_id) {
239 state.apply(&event);
240 }
241 }
242
243 self.status_message = Some(
244 t!(
245 "lsp.jumped_to_definition",
246 path = path.display().to_string(),
247 line = line + 1
248 )
249 .to_string(),
250 );
251 } else {
252 self.status_message = Some(t!("lsp.cannot_open_definition").to_string());
253 }
254
255 Ok(())
256 }
257
258 fn is_library_file(&self, path: &std::path::Path) -> bool {
261 super::types::BufferMetadata::is_library_path(path, &self.working_dir)
262 }
263
264 pub fn has_pending_lsp_requests(&self) -> bool {
266 self.pending_completion_request.is_some() || self.pending_goto_definition_request.is_some()
267 }
268
269 pub(crate) fn cancel_pending_lsp_requests(&mut self) {
273 if let Some(request_id) = self.pending_completion_request.take() {
274 tracing::debug!("Canceling pending LSP completion request {}", request_id);
275 self.send_lsp_cancel_request(request_id);
277 self.lsp_status.clear();
278 }
279 if let Some(request_id) = self.pending_goto_definition_request.take() {
280 tracing::debug!(
281 "Canceling pending LSP goto-definition request {}",
282 request_id
283 );
284 self.send_lsp_cancel_request(request_id);
286 self.lsp_status.clear();
287 }
288 }
289
290 fn send_lsp_cancel_request(&mut self, request_id: u64) {
292 let metadata = self.buffer_metadata.get(&self.active_buffer());
294 let file_path = metadata.and_then(|meta| meta.file_path());
295
296 if let Some(path) = file_path {
297 if let Some(language) = detect_language(path, &self.config.languages) {
298 if let Some(lsp) = self.lsp.as_mut() {
299 if let Some(handle) = lsp.get_handle_mut(&language) {
301 if let Err(e) = handle.cancel_request(request_id) {
302 tracing::warn!("Failed to send LSP cancel request: {}", e);
303 } else {
304 tracing::debug!("Sent $/cancelRequest for request_id={}", request_id);
305 }
306 }
307 }
308 }
309 }
310 }
311
312 pub(crate) fn with_lsp_for_buffer<F, R>(&mut self, buffer_id: BufferId, f: F) -> Option<R>
324 where
325 F: FnOnce(&crate::services::lsp::async_handler::LspHandle, &lsp_types::Uri, &str) -> R,
326 {
327 use crate::services::lsp::manager::LspSpawnResult;
328
329 let (uri, _path, language) = {
331 let metadata = self.buffer_metadata.get(&buffer_id)?;
332 if !metadata.lsp_enabled {
333 return None;
334 }
335 let uri = metadata.file_uri()?.clone();
336 let path = metadata.file_path()?.to_path_buf();
337 let language = detect_language(&path, &self.config.languages)?;
338 (uri, path, language)
339 };
340
341 let lsp = self.lsp.as_mut()?;
344 if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
345 return None;
346 }
347
348 let handle_id = lsp.get_handle_mut(&language)?.id();
350
351 let needs_open = {
353 let metadata = self.buffer_metadata.get(&buffer_id)?;
354 !metadata.lsp_opened_with.contains(&handle_id)
355 };
356
357 if needs_open {
358 let text = self.buffers.get(&buffer_id)?.buffer.to_string()?;
360
361 let lsp = self.lsp.as_mut()?;
363 let handle = lsp.get_handle_mut(&language)?;
364 if let Err(e) = handle.did_open(uri.clone(), text, language.clone()) {
365 tracing::warn!("Failed to send didOpen: {}", e);
366 return None;
367 }
368
369 let metadata = self.buffer_metadata.get_mut(&buffer_id)?;
371 metadata.lsp_opened_with.insert(handle_id);
372
373 tracing::debug!(
374 "Sent didOpen for {} to LSP handle {} (language: {})",
375 uri.as_str(),
376 handle_id,
377 language
378 );
379 }
380
381 let lsp = self.lsp.as_mut()?;
383 let handle = lsp.get_handle_mut(&language)?;
384 Some(f(handle, &uri, &language))
385 }
386
387 pub(crate) fn request_completion(&mut self) -> AnyhowResult<()> {
389 let state = self.active_state();
391 let cursor_pos = state.cursors.primary().position;
392
393 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
395 let buffer_id = self.active_buffer();
396 let request_id = self.next_lsp_request_id;
397
398 let sent = self
400 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
401 let result =
402 handle.completion(request_id, uri.clone(), line as u32, character as u32);
403 if result.is_ok() {
404 tracing::info!(
405 "Requested completion at {}:{}:{}",
406 uri.as_str(),
407 line,
408 character
409 );
410 }
411 result.is_ok()
412 })
413 .unwrap_or(false);
414
415 if sent {
416 self.next_lsp_request_id += 1;
417 self.pending_completion_request = Some(request_id);
418 self.lsp_status = "LSP: completion...".to_string();
419 }
420
421 Ok(())
422 }
423
424 pub(crate) fn maybe_trigger_completion(&mut self, c: char) {
434 let path = match self.active_state().buffer.file_path() {
436 Some(p) => p,
437 None => return, };
439
440 let language = match detect_language(path, &self.config.languages) {
441 Some(lang) => lang,
442 None => return, };
444
445 let is_lsp_trigger = self
447 .lsp
448 .as_ref()
449 .map(|lsp| lsp.is_completion_trigger_char(c, &language))
450 .unwrap_or(false);
451
452 let quick_suggestions_enabled = self.config.editor.quick_suggestions;
454 let suggest_on_trigger_chars = self.config.editor.suggest_on_trigger_characters;
455 let is_word_char = c.is_alphanumeric() || c == '_';
456
457 if is_lsp_trigger && suggest_on_trigger_chars {
459 tracing::debug!(
460 "Trigger character '{}' immediately triggers completion for language {}",
461 c,
462 language
463 );
464 self.scheduled_completion_trigger = None;
466 let _ = self.request_completion();
467 return;
468 }
469
470 if quick_suggestions_enabled && is_word_char {
472 let delay_ms = self.config.editor.quick_suggestions_delay_ms;
473 let trigger_time = Instant::now() + Duration::from_millis(delay_ms);
474
475 tracing::debug!(
476 "Scheduling completion trigger in {}ms for language {} (char '{}')",
477 delay_ms,
478 language,
479 c
480 );
481
482 self.scheduled_completion_trigger = Some(trigger_time);
485 }
486 }
487
488 pub(crate) fn request_goto_definition(&mut self) -> AnyhowResult<()> {
490 let state = self.active_state();
492 let cursor_pos = state.cursors.primary().position;
493
494 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
496 let buffer_id = self.active_buffer();
497 let request_id = self.next_lsp_request_id;
498
499 let sent = self
501 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
502 let result =
503 handle.goto_definition(request_id, uri.clone(), line as u32, character as u32);
504 if result.is_ok() {
505 tracing::info!(
506 "Requested go-to-definition at {}:{}:{}",
507 uri.as_str(),
508 line,
509 character
510 );
511 }
512 result.is_ok()
513 })
514 .unwrap_or(false);
515
516 if sent {
517 self.next_lsp_request_id += 1;
518 self.pending_goto_definition_request = Some(request_id);
519 }
520
521 Ok(())
522 }
523
524 pub(crate) fn request_hover(&mut self) -> AnyhowResult<()> {
526 let state = self.active_state();
528 let cursor_pos = state.cursors.primary().position;
529
530 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
532
533 if let Some(pos) = state.buffer.offset_to_position(cursor_pos) {
535 tracing::debug!(
536 "Hover request: cursor_byte={}, line={}, byte_col={}, utf16_col={}",
537 cursor_pos,
538 pos.line,
539 pos.column,
540 character
541 );
542 }
543
544 let buffer_id = self.active_buffer();
545 let request_id = self.next_lsp_request_id;
546
547 let sent = self
549 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
550 let result = handle.hover(request_id, uri.clone(), line as u32, character as u32);
551 if result.is_ok() {
552 tracing::info!(
553 "Requested hover at {}:{}:{} (byte_pos={})",
554 uri.as_str(),
555 line,
556 character,
557 cursor_pos
558 );
559 }
560 result.is_ok()
561 })
562 .unwrap_or(false);
563
564 if sent {
565 self.next_lsp_request_id += 1;
566 self.pending_hover_request = Some(request_id);
567 self.lsp_status = "LSP: hover...".to_string();
568 }
569
570 Ok(())
571 }
572
573 pub(crate) fn request_hover_at_position(&mut self, byte_pos: usize) -> AnyhowResult<()> {
576 let state = self.active_state();
578
579 let (line, character) = state.buffer.position_to_lsp_position(byte_pos);
581
582 if let Some(pos) = state.buffer.offset_to_position(byte_pos) {
584 tracing::trace!(
585 "Mouse hover request: byte_pos={}, line={}, byte_col={}, utf16_col={}",
586 byte_pos,
587 pos.line,
588 pos.column,
589 character
590 );
591 }
592
593 let buffer_id = self.active_buffer();
594 let request_id = self.next_lsp_request_id;
595
596 let sent = self
598 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
599 let result = handle.hover(request_id, uri.clone(), line as u32, character as u32);
600 if result.is_ok() {
601 tracing::trace!(
602 "Mouse hover requested at {}:{}:{} (byte_pos={})",
603 uri.as_str(),
604 line,
605 character,
606 byte_pos
607 );
608 }
609 result.is_ok()
610 })
611 .unwrap_or(false);
612
613 if sent {
614 self.next_lsp_request_id += 1;
615 self.pending_hover_request = Some(request_id);
616 self.lsp_status = "LSP: hover...".to_string();
617 }
618
619 Ok(())
620 }
621
622 pub(crate) fn handle_hover_response(
624 &mut self,
625 request_id: u64,
626 contents: String,
627 is_markdown: bool,
628 range: Option<((u32, u32), (u32, u32))>,
629 ) {
630 if self.pending_hover_request != Some(request_id) {
632 tracing::debug!("Ignoring stale hover response: {}", request_id);
633 return;
634 }
635
636 self.pending_hover_request = None;
637 self.lsp_status.clear();
638
639 if contents.is_empty() {
640 self.set_status_message(t!("lsp.no_hover").to_string());
641 self.hover_symbol_range = None;
642 return;
643 }
644
645 tracing::debug!(
647 "LSP hover content (markdown={}):\n{}",
648 is_markdown,
649 contents
650 );
651
652 if let Some(((start_line, start_char), (end_line, end_char))) = range {
654 let state = self.active_state();
655 let start_byte = state
656 .buffer
657 .lsp_position_to_byte(start_line as usize, start_char as usize);
658 let end_byte = state
659 .buffer
660 .lsp_position_to_byte(end_line as usize, end_char as usize);
661 self.hover_symbol_range = Some((start_byte, end_byte));
662 tracing::debug!(
663 "Hover symbol range: {}..{} (LSP {}:{}..{}:{})",
664 start_byte,
665 end_byte,
666 start_line,
667 start_char,
668 end_line,
669 end_char
670 );
671
672 if let Some(old_handle) = self.hover_symbol_overlay.take() {
674 let remove_event = crate::model::event::Event::RemoveOverlay { handle: old_handle };
675 self.apply_event_to_active_buffer(&remove_event);
676 }
677
678 let event = crate::model::event::Event::AddOverlay {
680 namespace: None,
681 range: start_byte..end_byte,
682 face: crate::model::event::OverlayFace::Background {
683 color: (80, 80, 120), },
685 priority: 90, message: None,
687 extend_to_line_end: false,
688 };
689 self.apply_event_to_active_buffer(&event);
690 if let Some(state) = self.buffers.get(&self.active_buffer()) {
692 self.hover_symbol_overlay = state.overlays.all().last().map(|o| o.handle.clone());
693 }
694 } else {
695 if let Some((hover_byte_pos, _, _, _)) = self.mouse_state.lsp_hover_state {
698 let state = self.active_state();
699 let start_byte = find_word_start(&state.buffer, hover_byte_pos);
700 let end_byte = find_word_end(&state.buffer, hover_byte_pos);
701 if start_byte < end_byte {
702 self.hover_symbol_range = Some((start_byte, end_byte));
703 tracing::debug!(
704 "Hover symbol range (computed from word boundaries): {}..{}",
705 start_byte,
706 end_byte
707 );
708 } else {
709 self.hover_symbol_range = None;
710 }
711 } else {
712 self.hover_symbol_range = None;
713 }
714 }
715
716 use crate::view::popup::{Popup, PopupPosition};
718 use ratatui::style::Style;
719
720 let mut popup = if is_markdown {
722 Popup::markdown(&contents, &self.theme, Some(&self.grammar_registry))
723 } else {
724 let lines: Vec<String> = contents.lines().map(|s| s.to_string()).collect();
726 Popup::text(lines, &self.theme)
727 };
728
729 popup.title = Some(t!("lsp.popup_hover").to_string());
731 popup.transient = true;
732 popup.position = if let Some((x, y)) = self.mouse_hover_screen_position.take() {
734 PopupPosition::Fixed { x, y: y + 1 }
736 } else {
737 PopupPosition::BelowCursor
738 };
739 popup.width = 80;
740 let dynamic_height = (self.terminal_height * 60 / 100).clamp(15, 40);
743 popup.max_height = dynamic_height;
744 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
745 popup.background_style = Style::default().bg(self.theme.popup_bg);
746
747 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
749 state.popups.show(popup);
750 tracing::info!("Showing hover popup (markdown={})", is_markdown);
751 }
752
753 self.mouse_state.lsp_hover_request_sent = true;
756 }
757
758 pub(crate) fn apply_inlay_hints_to_state(
760 state: &mut crate::state::EditorState,
761 hints: &[lsp_types::InlayHint],
762 ) {
763 use crate::view::virtual_text::VirtualTextPosition;
764 use ratatui::style::{Color, Style};
765
766 state.virtual_texts.clear(&mut state.marker_list);
768
769 if hints.is_empty() {
770 return;
771 }
772
773 let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
775
776 for hint in hints {
777 let byte_offset = state.buffer.lsp_position_to_byte(
779 hint.position.line as usize,
780 hint.position.character as usize,
781 );
782
783 let text = match &hint.label {
785 lsp_types::InlayHintLabel::String(s) => s.clone(),
786 lsp_types::InlayHintLabel::LabelParts(parts) => {
787 parts.iter().map(|p| p.value.as_str()).collect::<String>()
788 }
789 };
790
791 if state.buffer.is_empty() {
797 continue;
798 }
799
800 let (byte_offset, position) = if byte_offset >= state.buffer.len() {
801 (
803 state.buffer.len().saturating_sub(1),
804 VirtualTextPosition::AfterChar,
805 )
806 } else {
807 (byte_offset, VirtualTextPosition::BeforeChar)
808 };
809
810 let display_text = text;
812
813 state.virtual_texts.add(
814 &mut state.marker_list,
815 byte_offset,
816 display_text,
817 hint_style,
818 position,
819 0, );
821 }
822
823 tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
824 }
825
826 pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
828 let state = self.active_state();
830 let cursor_pos = state.cursors.primary().position;
831
832 let symbol = {
834 let text = match state.buffer.to_string() {
835 Some(t) => t,
836 None => {
837 self.set_status_message(t!("error.buffer_not_loaded").to_string());
838 return Ok(());
839 }
840 };
841 let bytes = text.as_bytes();
842 let buf_len = bytes.len();
843
844 if cursor_pos <= buf_len {
845 let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
847
848 let mut start = cursor_pos;
850 while start > 0 {
851 start -= 1;
853 while start > 0 && (bytes[start] & 0xC0) == 0x80 {
855 start -= 1;
856 }
857 if let Some(ch) = text[start..].chars().next() {
859 if !is_word_char(ch) {
860 start += ch.len_utf8();
861 break;
862 }
863 } else {
864 break;
865 }
866 }
867
868 let mut end = cursor_pos;
870 while end < buf_len {
871 if let Some(ch) = text[end..].chars().next() {
872 if is_word_char(ch) {
873 end += ch.len_utf8();
874 } else {
875 break;
876 }
877 } else {
878 break;
879 }
880 }
881
882 if start < end {
883 text[start..end].to_string()
884 } else {
885 String::new()
886 }
887 } else {
888 String::new()
889 }
890 };
891
892 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
894 let buffer_id = self.active_buffer();
895 let request_id = self.next_lsp_request_id;
896
897 let sent = self
899 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
900 let result =
901 handle.references(request_id, uri.clone(), line as u32, character as u32);
902 if result.is_ok() {
903 tracing::info!(
904 "Requested find references at {}:{}:{} (byte_pos={})",
905 uri.as_str(),
906 line,
907 character,
908 cursor_pos
909 );
910 }
911 result.is_ok()
912 })
913 .unwrap_or(false);
914
915 if sent {
916 self.next_lsp_request_id += 1;
917 self.pending_references_request = Some(request_id);
918 self.pending_references_symbol = symbol;
919 self.lsp_status = "LSP: finding references...".to_string();
920 }
921
922 Ok(())
923 }
924
925 pub(crate) fn request_signature_help(&mut self) -> AnyhowResult<()> {
927 let state = self.active_state();
929 let cursor_pos = state.cursors.primary().position;
930
931 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
933 let buffer_id = self.active_buffer();
934 let request_id = self.next_lsp_request_id;
935
936 let sent = self
938 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
939 let result =
940 handle.signature_help(request_id, uri.clone(), line as u32, character as u32);
941 if result.is_ok() {
942 tracing::info!(
943 "Requested signature help at {}:{}:{} (byte_pos={})",
944 uri.as_str(),
945 line,
946 character,
947 cursor_pos
948 );
949 }
950 result.is_ok()
951 })
952 .unwrap_or(false);
953
954 if sent {
955 self.next_lsp_request_id += 1;
956 self.pending_signature_help_request = Some(request_id);
957 self.lsp_status = "LSP: signature help...".to_string();
958 }
959
960 Ok(())
961 }
962
963 pub(crate) fn handle_signature_help_response(
965 &mut self,
966 request_id: u64,
967 signature_help: Option<lsp_types::SignatureHelp>,
968 ) {
969 if self.pending_signature_help_request != Some(request_id) {
971 tracing::debug!("Ignoring stale signature help response: {}", request_id);
972 return;
973 }
974
975 self.pending_signature_help_request = None;
976 self.lsp_status.clear();
977
978 let signature_help = match signature_help {
979 Some(help) if !help.signatures.is_empty() => help,
980 _ => {
981 tracing::debug!("No signature help available");
982 return;
983 }
984 };
985
986 let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
988 let signature = match signature_help.signatures.get(active_signature_idx) {
989 Some(sig) => sig,
990 None => return,
991 };
992
993 let mut lines: Vec<String> = Vec::new();
995
996 lines.push(signature.label.clone());
998
999 let active_param = signature_help
1001 .active_parameter
1002 .or(signature.active_parameter)
1003 .unwrap_or(0) as usize;
1004
1005 if let Some(params) = &signature.parameters {
1007 if let Some(param) = params.get(active_param) {
1008 let param_label = match ¶m.label {
1010 lsp_types::ParameterLabel::Simple(s) => s.clone(),
1011 lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1012 let start = offsets[0] as usize;
1014 let end = offsets[1] as usize;
1015 if end <= signature.label.len() {
1016 signature.label[start..end].to_string()
1017 } else {
1018 String::new()
1019 }
1020 }
1021 };
1022
1023 if !param_label.is_empty() {
1024 lines.push(format!("> {}", param_label));
1025 }
1026
1027 if let Some(doc) = ¶m.documentation {
1029 let doc_text = match doc {
1030 lsp_types::Documentation::String(s) => s.clone(),
1031 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1032 };
1033 if !doc_text.is_empty() {
1034 lines.push(String::new());
1035 lines.push(doc_text);
1036 }
1037 }
1038 }
1039 }
1040
1041 if let Some(doc) = &signature.documentation {
1043 let doc_text = match doc {
1044 lsp_types::Documentation::String(s) => s.clone(),
1045 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1046 };
1047 if !doc_text.is_empty() {
1048 if lines.len() > 1 {
1049 lines.push(String::new());
1050 lines.push("---".to_string());
1051 }
1052 lines.push(doc_text);
1053 }
1054 }
1055
1056 use crate::view::popup::{Popup, PopupPosition};
1058 use ratatui::style::Style;
1059
1060 let mut popup = Popup::text(lines, &self.theme);
1061 popup.title = Some(t!("lsp.popup_signature").to_string());
1062 popup.transient = true;
1063 popup.position = PopupPosition::BelowCursor;
1064 popup.width = 60;
1065 popup.max_height = 10;
1066 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1067 popup.background_style = Style::default().bg(self.theme.popup_bg);
1068
1069 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1071 state.popups.show(popup);
1072 tracing::info!(
1073 "Showing signature help popup for {} signatures",
1074 signature_help.signatures.len()
1075 );
1076 }
1077 }
1078
1079 pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1081 let state = self.active_state();
1083 let cursor_pos = state.cursors.primary().position;
1084
1085 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1087
1088 let (start_line, start_char, end_line, end_char) =
1090 if let Some(range) = state.cursors.primary().selection_range() {
1091 let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1092 let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1093 (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1094 } else {
1095 (line as u32, character as u32, line as u32, character as u32)
1096 };
1097
1098 let diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
1101 let buffer_id = self.active_buffer();
1102 let request_id = self.next_lsp_request_id;
1103
1104 let sent = self
1106 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
1107 let result = handle.code_actions(
1108 request_id,
1109 uri.clone(),
1110 start_line,
1111 start_char,
1112 end_line,
1113 end_char,
1114 diagnostics,
1115 );
1116 if result.is_ok() {
1117 tracing::info!(
1118 "Requested code actions at {}:{}:{}-{}:{} (byte_pos={})",
1119 uri.as_str(),
1120 start_line,
1121 start_char,
1122 end_line,
1123 end_char,
1124 cursor_pos
1125 );
1126 }
1127 result.is_ok()
1128 })
1129 .unwrap_or(false);
1130
1131 if sent {
1132 self.next_lsp_request_id += 1;
1133 self.pending_code_actions_request = Some(request_id);
1134 self.lsp_status = "LSP: code actions...".to_string();
1135 }
1136
1137 Ok(())
1138 }
1139
1140 pub(crate) fn handle_code_actions_response(
1142 &mut self,
1143 request_id: u64,
1144 actions: Vec<lsp_types::CodeActionOrCommand>,
1145 ) {
1146 if self.pending_code_actions_request != Some(request_id) {
1148 tracing::debug!("Ignoring stale code actions response: {}", request_id);
1149 return;
1150 }
1151
1152 self.pending_code_actions_request = None;
1153 self.lsp_status.clear();
1154
1155 if actions.is_empty() {
1156 self.set_status_message(t!("lsp.no_code_actions").to_string());
1157 return;
1158 }
1159
1160 let mut lines: Vec<String> = Vec::new();
1162 lines.push(format!("Code Actions ({}):", actions.len()));
1163 lines.push(String::new());
1164
1165 for (i, action) in actions.iter().enumerate() {
1166 let title = match action {
1167 lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1168 lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1169 };
1170 lines.push(format!(" {}. {}", i + 1, title));
1171 }
1172
1173 lines.push(String::new());
1174 lines.push(t!("lsp.code_action_hint").to_string());
1175
1176 use crate::view::popup::{Popup, PopupPosition};
1178 use ratatui::style::Style;
1179
1180 let mut popup = Popup::text(lines, &self.theme);
1181 popup.title = Some(t!("lsp.popup_code_actions").to_string());
1182 popup.position = PopupPosition::BelowCursor;
1183 popup.width = 60;
1184 popup.max_height = 15;
1185 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1186 popup.background_style = Style::default().bg(self.theme.popup_bg);
1187
1188 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1190 state.popups.show(popup);
1191 tracing::info!("Showing code actions popup with {} actions", actions.len());
1192 }
1193
1194 self.set_status_message(
1197 t!("lsp.code_actions_not_implemented", count = actions.len()).to_string(),
1198 );
1199 }
1200
1201 pub(crate) fn handle_references_response(
1203 &mut self,
1204 request_id: u64,
1205 locations: Vec<lsp_types::Location>,
1206 ) -> AnyhowResult<()> {
1207 tracing::info!(
1208 "handle_references_response: received {} locations for request_id={}",
1209 locations.len(),
1210 request_id
1211 );
1212
1213 if self.pending_references_request != Some(request_id) {
1215 tracing::debug!("Ignoring stale references response: {}", request_id);
1216 return Ok(());
1217 }
1218
1219 self.pending_references_request = None;
1220 self.lsp_status.clear();
1221
1222 if locations.is_empty() {
1223 self.set_status_message(t!("lsp.no_references").to_string());
1224 return Ok(());
1225 }
1226
1227 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
1229 .iter()
1230 .map(|loc| {
1231 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
1233 loc.uri.path().as_str().to_string()
1235 } else {
1236 loc.uri.as_str().to_string()
1237 };
1238
1239 crate::services::plugins::hooks::LspLocation {
1240 file,
1241 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
1244 })
1245 .collect();
1246
1247 let count = lsp_locations.len();
1248 let symbol = std::mem::take(&mut self.pending_references_symbol);
1249 self.set_status_message(
1250 t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
1251 );
1252
1253 self.plugin_manager.run_hook(
1255 "lsp_references",
1256 crate::services::plugins::hooks::HookArgs::LspReferences {
1257 symbol: symbol.clone(),
1258 locations: lsp_locations,
1259 },
1260 );
1261
1262 tracing::info!(
1263 "Fired lsp_references hook with {} locations for symbol '{}'",
1264 count,
1265 symbol
1266 );
1267
1268 Ok(())
1269 }
1270
1271 pub(crate) fn apply_lsp_text_edits(
1274 &mut self,
1275 buffer_id: BufferId,
1276 mut edits: Vec<lsp_types::TextEdit>,
1277 ) -> AnyhowResult<usize> {
1278 if edits.is_empty() {
1279 return Ok(0);
1280 }
1281
1282 edits.sort_by(|a, b| {
1284 b.range
1285 .start
1286 .line
1287 .cmp(&a.range.start.line)
1288 .then(b.range.start.character.cmp(&a.range.start.character))
1289 });
1290
1291 let mut batch_events = Vec::new();
1293 let mut changes = 0;
1294
1295 for edit in edits {
1297 let state = self
1298 .buffers
1299 .get_mut(&buffer_id)
1300 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
1301
1302 let start_line = edit.range.start.line as usize;
1304 let start_char = edit.range.start.character as usize;
1305 let end_line = edit.range.end.line as usize;
1306 let end_char = edit.range.end.character as usize;
1307
1308 let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
1309 let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
1310 let buffer_len = state.buffer.len();
1311
1312 let old_text = if start_pos < end_pos && end_pos <= buffer_len {
1314 state.get_text_range(start_pos, end_pos)
1315 } else {
1316 format!(
1317 "<invalid range: start={}, end={}, buffer_len={}>",
1318 start_pos, end_pos, buffer_len
1319 )
1320 };
1321 tracing::debug!(
1322 " Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
1323 start_line, start_char, end_line, end_char,
1324 start_pos, end_pos, old_text, edit.new_text
1325 );
1326
1327 if start_pos < end_pos {
1329 let deleted_text = state.get_text_range(start_pos, end_pos);
1330 let cursor_id = state.cursors.primary_id();
1331 let delete_event = Event::Delete {
1332 range: start_pos..end_pos,
1333 deleted_text,
1334 cursor_id,
1335 };
1336 batch_events.push(delete_event);
1337 }
1338
1339 if !edit.new_text.is_empty() {
1341 let state = self
1342 .buffers
1343 .get(&buffer_id)
1344 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
1345 let cursor_id = state.cursors.primary_id();
1346 let insert_event = Event::Insert {
1347 position: start_pos,
1348 text: edit.new_text.clone(),
1349 cursor_id,
1350 };
1351 batch_events.push(insert_event);
1352 }
1353
1354 changes += 1;
1355 }
1356
1357 if !batch_events.is_empty() {
1359 self.apply_events_to_buffer_as_bulk_edit(
1360 buffer_id,
1361 batch_events,
1362 "LSP Rename".to_string(),
1363 )?;
1364 }
1365
1366 Ok(changes)
1367 }
1368
1369 pub fn handle_rename_response(
1371 &mut self,
1372 _request_id: u64,
1373 result: Result<lsp_types::WorkspaceEdit, String>,
1374 ) -> AnyhowResult<()> {
1375 self.lsp_status.clear();
1376
1377 match result {
1378 Ok(workspace_edit) => {
1379 tracing::debug!(
1381 "Received WorkspaceEdit: changes={:?}, document_changes={:?}",
1382 workspace_edit.changes.as_ref().map(|c| c.len()),
1383 workspace_edit.document_changes.as_ref().map(|dc| match dc {
1384 lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
1385 lsp_types::DocumentChanges::Operations(o) =>
1386 format!("{} operations", o.len()),
1387 })
1388 );
1389
1390 let mut total_changes = 0;
1392
1393 if let Some(changes) = workspace_edit.changes {
1395 for (uri, edits) in changes {
1396 if let Ok(path) = uri_to_path(&uri) {
1397 let buffer_id = self.open_file(&path)?;
1398 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
1399 }
1400 }
1401 }
1402
1403 if let Some(document_changes) = workspace_edit.document_changes {
1406 use lsp_types::DocumentChanges;
1407
1408 let text_edits = match document_changes {
1409 DocumentChanges::Edits(edits) => edits,
1410 DocumentChanges::Operations(ops) => {
1411 ops.into_iter()
1413 .filter_map(|op| {
1414 if let lsp_types::DocumentChangeOperation::Edit(edit) = op {
1415 Some(edit)
1416 } else {
1417 None
1418 }
1419 })
1420 .collect()
1421 }
1422 };
1423
1424 for text_doc_edit in text_edits {
1425 let uri = text_doc_edit.text_document.uri;
1426
1427 if let Ok(path) = uri_to_path(&uri) {
1428 let buffer_id = self.open_file(&path)?;
1429
1430 let edits: Vec<lsp_types::TextEdit> = text_doc_edit
1432 .edits
1433 .into_iter()
1434 .map(|one_of| match one_of {
1435 lsp_types::OneOf::Left(text_edit) => text_edit,
1436 lsp_types::OneOf::Right(annotated) => annotated.text_edit,
1437 })
1438 .collect();
1439
1440 tracing::info!(
1442 "Applying {} edits from rust-analyzer for {:?}:",
1443 edits.len(),
1444 path
1445 );
1446 for (i, edit) in edits.iter().enumerate() {
1447 tracing::info!(
1448 " Edit {}: line {}:{}-{}:{} -> {:?}",
1449 i,
1450 edit.range.start.line,
1451 edit.range.start.character,
1452 edit.range.end.line,
1453 edit.range.end.character,
1454 edit.new_text
1455 );
1456 }
1457
1458 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
1459 }
1460 }
1461 }
1462
1463 self.status_message = Some(t!("lsp.renamed", count = total_changes).to_string());
1464 }
1465 Err(error) => {
1466 if error.contains("content modified") || error.contains("-32801") {
1470 tracing::debug!(
1471 "LSP rename: ContentModified error (expected, ignoring): {}",
1472 error
1473 );
1474 self.status_message = Some(t!("lsp.rename_cancelled").to_string());
1475 } else {
1476 self.status_message = Some(t!("lsp.rename_failed", error = &error).to_string());
1478 }
1479 }
1480 }
1481
1482 Ok(())
1483 }
1484
1485 pub(crate) fn apply_events_to_buffer_as_bulk_edit(
1490 &mut self,
1491 buffer_id: BufferId,
1492 events: Vec<Event>,
1493 description: String,
1494 ) -> AnyhowResult<()> {
1495 use crate::model::event::CursorId;
1496
1497 if events.is_empty() {
1498 return Ok(());
1499 }
1500
1501 let batch_for_lsp = Event::Batch {
1503 events: events.clone(),
1504 description: description.clone(),
1505 };
1506
1507 let original_active = self.active_buffer();
1510 self.split_manager.set_active_buffer_id(buffer_id);
1511 let lsp_changes = self.collect_lsp_changes(&batch_for_lsp);
1512 self.split_manager.set_active_buffer_id(original_active);
1513
1514 let state = self
1515 .buffers
1516 .get_mut(&buffer_id)
1517 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
1518
1519 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = state
1521 .cursors
1522 .iter()
1523 .map(|(id, c)| (id, c.position, c.anchor))
1524 .collect();
1525
1526 let old_tree = state.buffer.snapshot_piece_tree();
1528
1529 let mut edits: Vec<(usize, usize, String)> = Vec::new();
1531 for event in &events {
1532 match event {
1533 Event::Insert { position, text, .. } => {
1534 edits.push((*position, 0, text.clone()));
1535 }
1536 Event::Delete { range, .. } => {
1537 edits.push((range.start, range.len(), String::new()));
1538 }
1539 _ => {}
1540 }
1541 }
1542
1543 edits.sort_by(|a, b| b.0.cmp(&a.0));
1545
1546 let edit_refs: Vec<(usize, usize, &str)> = edits
1548 .iter()
1549 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
1550 .collect();
1551
1552 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
1554
1555 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
1557 for (pos, del_len, text) in &edits {
1558 let delta = text.len() as isize - *del_len as isize;
1559 position_deltas.push((*pos, delta));
1560 }
1561 position_deltas.sort_by_key(|(pos, _)| *pos);
1562
1563 let calc_shift = |original_pos: usize| -> isize {
1564 let mut shift: isize = 0;
1565 for (edit_pos, delta) in &position_deltas {
1566 if *edit_pos < original_pos {
1567 shift += delta;
1568 }
1569 }
1570 shift
1571 };
1572
1573 let buffer_len = state.buffer.len();
1575 let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
1576 .iter()
1577 .map(|(id, pos, anchor)| {
1578 let shift = calc_shift(*pos);
1579 let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
1580 let new_anchor = anchor.map(|a| {
1581 let anchor_shift = calc_shift(a);
1582 ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
1583 });
1584 (*id, new_pos, new_anchor)
1585 })
1586 .collect();
1587
1588 for (cursor_id, new_pos, new_anchor) in &new_cursors {
1590 if let Some(cursor) = state.cursors.get_mut(*cursor_id) {
1591 cursor.position = *new_pos;
1592 cursor.anchor = *new_anchor;
1593 }
1594 }
1595
1596 let new_tree = state.buffer.snapshot_piece_tree();
1598
1599 state.highlighter.invalidate_all();
1601
1602 let bulk_edit = Event::BulkEdit {
1604 old_tree: Some(old_tree),
1605 new_tree: Some(new_tree),
1606 old_cursors,
1607 new_cursors,
1608 description,
1609 };
1610
1611 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
1613 event_log.append(bulk_edit);
1614 }
1615
1616 self.send_lsp_changes_for_buffer(buffer_id, lsp_changes);
1618
1619 Ok(())
1620 }
1621
1622 pub(crate) fn send_lsp_changes_for_buffer(
1624 &mut self,
1625 buffer_id: BufferId,
1626 changes: Vec<TextDocumentContentChangeEvent>,
1627 ) {
1628 if changes.is_empty() {
1629 return;
1630 }
1631
1632 let metadata = match self.buffer_metadata.get(&buffer_id) {
1634 Some(m) => m,
1635 None => {
1636 tracing::debug!(
1637 "send_lsp_changes_for_buffer: no metadata for buffer {:?}",
1638 buffer_id
1639 );
1640 return;
1641 }
1642 };
1643
1644 if !metadata.lsp_enabled {
1645 tracing::debug!("send_lsp_changes_for_buffer: LSP disabled for this buffer");
1646 return;
1647 }
1648
1649 let uri = match metadata.file_uri() {
1651 Some(u) => u.clone(),
1652 None => {
1653 tracing::debug!(
1654 "send_lsp_changes_for_buffer: no URI for buffer (not a file or URI creation failed)"
1655 );
1656 return;
1657 }
1658 };
1659
1660 let path = match metadata.file_path() {
1662 Some(p) => p,
1663 None => {
1664 tracing::debug!("send_lsp_changes_for_buffer: no file path for buffer");
1665 return;
1666 }
1667 };
1668
1669 let language = match detect_language(path, &self.config.languages) {
1670 Some(l) => l,
1671 None => {
1672 tracing::debug!(
1673 "send_lsp_changes_for_buffer: no language detected for {:?}",
1674 path
1675 );
1676 return;
1677 }
1678 };
1679
1680 tracing::trace!(
1681 "send_lsp_changes_for_buffer: sending {} changes to {} in single didChange notification",
1682 changes.len(),
1683 uri.as_str()
1684 );
1685
1686 use crate::services::lsp::manager::LspSpawnResult;
1688 let Some(lsp) = self.lsp.as_mut() else {
1689 tracing::debug!("send_lsp_changes_for_buffer: no LSP manager available");
1690 return;
1691 };
1692
1693 if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
1694 tracing::debug!(
1695 "send_lsp_changes_for_buffer: LSP not running for {} (auto_start disabled)",
1696 language
1697 );
1698 return;
1699 }
1700
1701 let Some(handle) = lsp.get_handle_mut(&language) else {
1703 return;
1704 };
1705 let handle_id = handle.id();
1706
1707 let needs_open = {
1709 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
1710 return;
1711 };
1712 !metadata.lsp_opened_with.contains(&handle_id)
1713 };
1714
1715 if needs_open {
1716 let text = match self
1718 .buffers
1719 .get(&buffer_id)
1720 .and_then(|s| s.buffer.to_string())
1721 {
1722 Some(t) => t,
1723 None => {
1724 tracing::debug!(
1725 "send_lsp_changes_for_buffer: buffer text not available for didOpen"
1726 );
1727 return;
1728 }
1729 };
1730
1731 let Some(lsp) = self.lsp.as_mut() else { return };
1733 let Some(handle) = lsp.get_handle_mut(&language) else {
1734 return;
1735 };
1736 if let Err(e) = handle.did_open(uri.clone(), text, language.clone()) {
1737 tracing::warn!("Failed to send didOpen before didChange: {}", e);
1738 return;
1739 }
1740 tracing::debug!(
1741 "Sent didOpen for {} to LSP handle {} before didChange",
1742 uri.as_str(),
1743 handle_id
1744 );
1745
1746 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
1748 metadata.lsp_opened_with.insert(handle_id);
1749 }
1750 }
1751
1752 let Some(lsp) = self.lsp.as_mut() else { return };
1754 let Some(client) = lsp.get_handle_mut(&language) else {
1755 return;
1756 };
1757 if let Err(e) = client.did_change(uri, changes) {
1758 tracing::warn!("Failed to send didChange to LSP: {}", e);
1759 } else {
1760 tracing::trace!("Successfully sent batched didChange to LSP");
1761 }
1762 }
1763
1764 pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
1766 use crate::primitives::word_navigation::{find_word_end, find_word_start};
1767
1768 let (word_start, word_end) = {
1770 let state = self.active_state();
1771 let cursor_pos = state.cursors.primary().position;
1772
1773 let word_start = find_word_start(&state.buffer, cursor_pos);
1775 let word_end = find_word_end(&state.buffer, cursor_pos);
1776
1777 if word_start >= word_end {
1779 self.status_message = Some(t!("lsp.no_symbol_at_cursor").to_string());
1780 return Ok(());
1781 }
1782
1783 (word_start, word_end)
1784 };
1785
1786 let word_text = self.active_state_mut().get_text_range(word_start, word_end);
1788
1789 let overlay_handle = self.add_overlay(
1791 None,
1792 word_start..word_end,
1793 crate::model::event::OverlayFace::Background {
1794 color: (50, 100, 200), },
1796 100,
1797 Some(t!("lsp.popup_renaming").to_string()),
1798 );
1799
1800 let mut prompt = Prompt::new(
1803 "Rename to: ".to_string(),
1804 PromptType::LspRename {
1805 original_text: word_text.clone(),
1806 start_pos: word_start,
1807 end_pos: word_end,
1808 overlay_handle,
1809 },
1810 );
1811 prompt.set_input(word_text);
1813
1814 self.prompt = Some(prompt);
1815 Ok(())
1816 }
1817
1818 pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
1820 self.remove_overlay(handle.clone());
1821 }
1822
1823 pub(crate) fn perform_lsp_rename(
1825 &mut self,
1826 new_name: String,
1827 original_text: String,
1828 start_pos: usize,
1829 overlay_handle: crate::view::overlay::OverlayHandle,
1830 ) {
1831 self.cancel_rename_overlay(&overlay_handle);
1833
1834 if new_name == original_text {
1836 self.status_message = Some(t!("lsp.name_unchanged").to_string());
1837 return;
1838 }
1839
1840 let rename_pos = start_pos;
1843
1844 let state = self.active_state();
1847 let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
1848 let buffer_id = self.active_buffer();
1849 let request_id = self.next_lsp_request_id;
1850
1851 let sent = self
1853 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
1854 let result = handle.rename(
1855 request_id,
1856 uri.clone(),
1857 line as u32,
1858 character as u32,
1859 new_name.clone(),
1860 );
1861 if result.is_ok() {
1862 tracing::info!(
1863 "Requested rename at {}:{}:{} to '{}'",
1864 uri.as_str(),
1865 line,
1866 character,
1867 new_name
1868 );
1869 }
1870 result.is_ok()
1871 })
1872 .unwrap_or(false);
1873
1874 if sent {
1875 self.next_lsp_request_id += 1;
1876 self.lsp_status = "LSP: rename...".to_string();
1877 } else if self
1878 .buffer_metadata
1879 .get(&buffer_id)
1880 .and_then(|m| m.file_path())
1881 .is_none()
1882 {
1883 self.status_message = Some(t!("lsp.cannot_rename_unsaved").to_string());
1884 }
1885 }
1886
1887 pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
1889 if !self.config.editor.enable_inlay_hints {
1890 return;
1891 }
1892
1893 let buffer_id = self.active_buffer();
1894
1895 let line_count = if let Some(state) = self.buffers.get(&buffer_id) {
1897 state.buffer.line_count().unwrap_or(1000)
1898 } else {
1899 return;
1900 };
1901 let last_line = line_count.saturating_sub(1) as u32;
1902 let request_id = self.next_lsp_request_id;
1903
1904 let sent = self
1906 .with_lsp_for_buffer(buffer_id, |handle, uri, _language| {
1907 let result = handle.inlay_hints(request_id, uri.clone(), 0, 0, last_line, 10000);
1908 if result.is_ok() {
1909 tracing::info!(
1910 "Requested inlay hints for {} (request_id={})",
1911 uri.as_str(),
1912 request_id
1913 );
1914 } else if let Err(e) = &result {
1915 tracing::debug!("Failed to request inlay hints: {}", e);
1916 }
1917 result.is_ok()
1918 })
1919 .unwrap_or(false);
1920
1921 if sent {
1922 self.next_lsp_request_id += 1;
1923 self.pending_inlay_hints_request = Some(request_id);
1924 }
1925 }
1926
1927 pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
1929 if !self.config.editor.enable_semantic_tokens_full {
1930 return;
1931 }
1932
1933 if self.semantic_tokens_in_flight.contains_key(&buffer_id) {
1935 return;
1936 }
1937
1938 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
1939 return;
1940 };
1941 if !metadata.lsp_enabled {
1942 return;
1943 }
1944 let Some(uri) = metadata.file_uri().cloned() else {
1945 return;
1946 };
1947 let Some(path) = metadata.file_path() else {
1948 return;
1949 };
1950 let Some(language) = detect_language(path, &self.config.languages) else {
1951 return;
1952 };
1953
1954 let Some(lsp) = self.lsp.as_mut() else {
1955 return;
1956 };
1957
1958 if !lsp.semantic_tokens_full_supported(&language) {
1959 return;
1960 }
1961 if lsp.semantic_tokens_legend(&language).is_none() {
1962 return;
1963 }
1964
1965 use crate::services::lsp::manager::LspSpawnResult;
1967 if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
1968 return;
1969 }
1970
1971 let Some(state) = self.buffers.get(&buffer_id) else {
1972 return;
1973 };
1974 let buffer_version = state.buffer.version();
1975 if let Some(store) = state.semantic_tokens.as_ref() {
1976 if store.version == buffer_version {
1977 return; }
1979 }
1980
1981 let previous_result_id = state
1982 .semantic_tokens
1983 .as_ref()
1984 .and_then(|store| store.result_id.clone());
1985 let supports_delta = lsp.semantic_tokens_full_delta_supported(&language);
1986 let use_delta = previous_result_id.is_some() && supports_delta;
1987
1988 let Some(handle) = lsp.get_handle_mut(&language) else {
1989 return;
1990 };
1991
1992 let request_id = self.next_lsp_request_id;
1993 self.next_lsp_request_id += 1;
1994
1995 let request_kind = if use_delta {
1996 super::SemanticTokensFullRequestKind::FullDelta
1997 } else {
1998 super::SemanticTokensFullRequestKind::Full
1999 };
2000
2001 let request_result = if use_delta {
2002 handle.semantic_tokens_full_delta(request_id, uri, previous_result_id.unwrap())
2003 } else {
2004 handle.semantic_tokens_full(request_id, uri)
2005 };
2006
2007 match request_result {
2008 Ok(_) => {
2009 self.pending_semantic_token_requests.insert(
2010 request_id,
2011 super::SemanticTokenFullRequest {
2012 buffer_id,
2013 version: buffer_version,
2014 kind: request_kind,
2015 },
2016 );
2017 self.semantic_tokens_in_flight
2018 .insert(buffer_id, (request_id, buffer_version, request_kind));
2019 }
2020 Err(e) => {
2021 tracing::debug!("Failed to request semantic tokens: {}", e);
2022 }
2023 }
2024 }
2025
2026 pub(crate) fn schedule_semantic_tokens_full_refresh(&mut self, buffer_id: BufferId) {
2028 if !self.config.editor.enable_semantic_tokens_full {
2029 return;
2030 }
2031
2032 let next_time = Instant::now() + Duration::from_millis(SEMANTIC_TOKENS_FULL_DEBOUNCE_MS);
2033 self.semantic_tokens_full_debounce
2034 .insert(buffer_id, next_time);
2035 }
2036
2037 pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
2039 if !self.config.editor.enable_semantic_tokens_full {
2040 self.semantic_tokens_full_debounce.remove(&buffer_id);
2041 return;
2042 }
2043
2044 let Some(ready_at) = self.semantic_tokens_full_debounce.get(&buffer_id).copied() else {
2045 return;
2046 };
2047 if Instant::now() < ready_at {
2048 return;
2049 }
2050
2051 self.semantic_tokens_full_debounce.remove(&buffer_id);
2052 self.maybe_request_semantic_tokens(buffer_id);
2053 }
2054
2055 pub(crate) fn maybe_request_semantic_tokens_range(
2057 &mut self,
2058 buffer_id: BufferId,
2059 start_line: usize,
2060 end_line: usize,
2061 ) {
2062 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2063 return;
2064 };
2065 if !metadata.lsp_enabled {
2066 return;
2067 }
2068 let Some(uri) = metadata.file_uri().cloned() else {
2069 return;
2070 };
2071 let Some(path) = metadata.file_path() else {
2072 return;
2073 };
2074 let Some(language) = detect_language(path, &self.config.languages) else {
2075 return;
2076 };
2077
2078 let Some(lsp) = self.lsp.as_mut() else {
2079 return;
2080 };
2081
2082 if !lsp.semantic_tokens_range_supported(&language) {
2083 self.maybe_request_semantic_tokens(buffer_id);
2085 return;
2086 }
2087 if lsp.semantic_tokens_legend(&language).is_none() {
2088 return;
2089 }
2090
2091 use crate::services::lsp::manager::LspSpawnResult;
2093 if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
2094 return;
2095 }
2096
2097 let Some(handle) = lsp.get_handle_mut(&language) else {
2098 return;
2099 };
2100 let Some(state) = self.buffers.get(&buffer_id) else {
2101 return;
2102 };
2103
2104 let buffer_version = state.buffer.version();
2105 let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
2106 let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
2107
2108 if let Some(line_count) = state.buffer.line_count() {
2109 if line_count == 0 {
2110 return;
2111 }
2112 let max_line = line_count.saturating_sub(1);
2113 padded_start = padded_start.min(max_line);
2114 padded_end = padded_end.min(max_line);
2115 }
2116
2117 let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
2118 let end_char = state
2119 .buffer
2120 .get_line(padded_end)
2121 .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
2122 .unwrap_or(0);
2123 let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
2124 state.buffer.lsp_position_to_byte(padded_end, end_char)
2125 } else {
2126 state.buffer.len()
2127 };
2128
2129 if start_byte >= end_byte {
2130 return;
2131 }
2132
2133 let range = start_byte..end_byte;
2134 if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
2135 self.semantic_tokens_range_in_flight.get(&buffer_id)
2136 {
2137 if *in_flight_start == padded_start
2138 && *in_flight_end == padded_end
2139 && *in_flight_version == buffer_version
2140 {
2141 return;
2142 }
2143 if let Err(e) = handle.cancel_request(*in_flight_id) {
2144 tracing::debug!("Failed to cancel semantic token range request: {}", e);
2145 }
2146 self.pending_semantic_token_range_requests
2147 .remove(in_flight_id);
2148 self.semantic_tokens_range_in_flight.remove(&buffer_id);
2149 }
2150
2151 if let Some((applied_start, applied_end, applied_version)) =
2152 self.semantic_tokens_range_applied.get(&buffer_id)
2153 {
2154 if *applied_start == padded_start
2155 && *applied_end == padded_end
2156 && *applied_version == buffer_version
2157 {
2158 return;
2159 }
2160 }
2161
2162 let now = Instant::now();
2163 if let Some((last_start, last_end, last_version, last_time)) =
2164 self.semantic_tokens_range_last_request.get(&buffer_id)
2165 {
2166 if *last_start == padded_start
2167 && *last_end == padded_end
2168 && *last_version == buffer_version
2169 && now.duration_since(*last_time)
2170 < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
2171 {
2172 return;
2173 }
2174 }
2175
2176 let lsp_range = lsp_types::Range {
2177 start: lsp_types::Position {
2178 line: padded_start as u32,
2179 character: 0,
2180 },
2181 end: lsp_types::Position {
2182 line: padded_end as u32,
2183 character: end_char as u32,
2184 },
2185 };
2186
2187 let request_id = self.next_lsp_request_id;
2188 self.next_lsp_request_id += 1;
2189
2190 match handle.semantic_tokens_range(request_id, uri, lsp_range) {
2191 Ok(_) => {
2192 self.pending_semantic_token_range_requests.insert(
2193 request_id,
2194 SemanticTokenRangeRequest {
2195 buffer_id,
2196 version: buffer_version,
2197 range: range.clone(),
2198 start_line: padded_start,
2199 end_line: padded_end,
2200 },
2201 );
2202 self.semantic_tokens_range_in_flight.insert(
2203 buffer_id,
2204 (request_id, padded_start, padded_end, buffer_version),
2205 );
2206 self.semantic_tokens_range_last_request
2207 .insert(buffer_id, (padded_start, padded_end, buffer_version, now));
2208 }
2209 Err(e) => {
2210 tracing::debug!("Failed to request semantic token range: {}", e);
2211 }
2212 }
2213 }
2214}
2215
2216#[cfg(test)]
2217mod tests {
2218 use crate::model::filesystem::StdFileSystem;
2219 use std::sync::Arc;
2220
2221 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
2222 Arc::new(StdFileSystem)
2223 }
2224 use super::Editor;
2225 use crate::model::buffer::Buffer;
2226 use crate::state::EditorState;
2227 use crate::view::virtual_text::VirtualTextPosition;
2228 use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
2229
2230 fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
2231 InlayHint {
2232 position: Position { line, character },
2233 label: InlayHintLabel::String(label.to_string()),
2234 kind,
2235 text_edits: None,
2236 tooltip: None,
2237 padding_left: None,
2238 padding_right: None,
2239 data: None,
2240 }
2241 }
2242
2243 #[test]
2244 fn test_inlay_hint_inserts_before_character() {
2245 let mut state = EditorState::new(
2246 80,
2247 24,
2248 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2249 test_fs(),
2250 );
2251 state.buffer = Buffer::from_str_test("ab");
2252
2253 if !state.buffer.is_empty() {
2254 state.marker_list.adjust_for_insert(0, state.buffer.len());
2255 }
2256
2257 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
2258 Editor::apply_inlay_hints_to_state(&mut state, &hints);
2259
2260 let lookup = state
2261 .virtual_texts
2262 .build_lookup(&state.marker_list, 0, state.buffer.len());
2263 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
2264 assert_eq!(vtexts.len(), 1);
2265 assert_eq!(vtexts[0].text, ": i32");
2266 assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
2267 }
2268
2269 #[test]
2270 fn test_inlay_hint_at_eof_renders_after_last_char() {
2271 let mut state = EditorState::new(
2272 80,
2273 24,
2274 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2275 test_fs(),
2276 );
2277 state.buffer = Buffer::from_str_test("ab");
2278
2279 if !state.buffer.is_empty() {
2280 state.marker_list.adjust_for_insert(0, state.buffer.len());
2281 }
2282
2283 let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
2284 Editor::apply_inlay_hints_to_state(&mut state, &hints);
2285
2286 let lookup = state
2287 .virtual_texts
2288 .build_lookup(&state.marker_list, 0, state.buffer.len());
2289 let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
2290 assert_eq!(vtexts.len(), 1);
2291 assert_eq!(vtexts[0].text, ": i32");
2292 assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
2293 }
2294
2295 #[test]
2296 fn test_inlay_hint_empty_buffer_is_ignored() {
2297 let mut state = EditorState::new(
2298 80,
2299 24,
2300 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2301 test_fs(),
2302 );
2303 state.buffer = Buffer::from_str_test("");
2304
2305 let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
2306 Editor::apply_inlay_hints_to_state(&mut state, &hints);
2307
2308 assert!(state.virtual_texts.is_empty());
2309 }
2310}