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
39const SEMANTIC_TOKENS_FULL_DEBOUNCE_MS: u64 = 500;
40const SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS: u64 = 50;
41const SEMANTIC_TOKENS_RANGE_PADDING_LINES: usize = 10;
42const FOLDING_RANGES_DEBOUNCE_MS: u64 = 300;
43
44impl Editor {
45 pub(crate) fn handle_completion_response(
49 &mut self,
50 request_id: u64,
51 items: Vec<lsp_types::CompletionItem>,
52 ) -> AnyhowResult<()> {
53 if !self.pending_completion_requests.remove(&request_id) {
55 tracing::debug!(
56 "Ignoring completion response for outdated request {}",
57 request_id
58 );
59 return Ok(());
60 }
61
62 if self.pending_completion_requests.is_empty() {
64 self.update_lsp_status_from_server_statuses();
65 }
66
67 if items.is_empty() {
68 tracing::debug!("No completion items received");
69 return Ok(());
70 }
71
72 use crate::primitives::word_navigation::find_completion_word_start;
74 let cursor_pos = self.active_cursors().primary().position;
75 let (word_start, cursor_pos) = {
76 let state = self.active_state();
77 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
78 (word_start, cursor_pos)
79 };
80 let prefix = if word_start < cursor_pos {
81 self.active_state_mut()
82 .get_text_range(word_start, cursor_pos)
83 .to_lowercase()
84 } else {
85 String::new()
86 };
87
88 let filtered_items: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
90 items.iter().collect()
92 } else {
93 items
95 .iter()
96 .filter(|item| {
97 item.label.to_lowercase().starts_with(&prefix)
98 || item
99 .filter_text
100 .as_ref()
101 .map(|ft| ft.to_lowercase().starts_with(&prefix))
102 .unwrap_or(false)
103 })
104 .collect()
105 };
106
107 if filtered_items.is_empty() && self.completion_items.is_none() {
108 tracing::debug!("No completion items match prefix '{}'", prefix);
109 return Ok(());
110 }
111
112 match &mut self.completion_items {
114 Some(existing) => {
115 existing.extend(items);
116 tracing::debug!("Extended completion items, now {} total", existing.len());
117 }
118 None => {
119 self.completion_items = Some(items);
120 }
121 }
122
123 let all_items = self.completion_items.as_ref().unwrap();
125 let all_filtered: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
126 all_items.iter().collect()
127 } else {
128 all_items
129 .iter()
130 .filter(|item| {
131 item.label.to_lowercase().starts_with(&prefix)
132 || item
133 .filter_text
134 .as_ref()
135 .map(|ft| ft.to_lowercase().starts_with(&prefix))
136 .unwrap_or(false)
137 })
138 .collect()
139 };
140
141 if all_filtered.is_empty() {
142 tracing::debug!("No completion items match prefix '{}'", prefix);
143 return Ok(());
144 }
145
146 let mut all_popup_items =
148 crate::app::popup_actions::lsp_items_to_popup_items(&all_filtered);
149 let buffer_word_items = self.get_buffer_completion_popup_items();
150 let lsp_labels: std::collections::HashSet<String> = all_popup_items
152 .iter()
153 .map(|i| i.text.to_lowercase())
154 .collect();
155 all_popup_items.extend(
156 buffer_word_items
157 .into_iter()
158 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
159 );
160
161 let popup_data =
162 crate::app::popup_actions::build_completion_popup_from_items(all_popup_items, 0);
163 let accept_hint = self.completion_accept_key_hint();
164
165 {
166 let buffer_id = self.active_buffer();
167 let state = self.buffers.get_mut(&buffer_id).unwrap();
168 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
170 popup_obj.accept_key_hint = accept_hint;
171 state.popups.show_or_replace(popup_obj);
172 }
173
174 tracing::info!(
175 "Showing completion popup with {} items",
176 self.completion_items.as_ref().map_or(0, |i| i.len())
177 );
178
179 Ok(())
180 }
181
182 pub(crate) fn handle_goto_definition_response(
184 &mut self,
185 request_id: u64,
186 locations: Vec<lsp_types::Location>,
187 ) -> AnyhowResult<()> {
188 if self.pending_goto_definition_request != Some(request_id) {
190 tracing::debug!(
191 "Ignoring go-to-definition response for outdated request {}",
192 request_id
193 );
194 return Ok(());
195 }
196
197 self.pending_goto_definition_request = None;
198
199 if locations.is_empty() {
200 self.status_message = Some(t!("lsp.no_definition").to_string());
201 return Ok(());
202 }
203
204 let location = &locations[0];
206
207 if let Ok(path) = uri_to_path(&location.uri) {
209 let buffer_id = match self.open_file(&path) {
211 Ok(id) => id,
212 Err(e) => {
213 if let Some(confirmation) =
215 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
216 {
217 self.start_large_file_encoding_confirmation(confirmation);
218 } else {
219 self.set_status_message(
220 t!("file.error_opening", error = e.to_string()).to_string(),
221 );
222 }
223 return Ok(());
224 }
225 };
226
227 let line = location.range.start.line as usize;
229 let character = location.range.start.character as usize;
230
231 let position = self
233 .buffers
234 .get(&buffer_id)
235 .map(|state| state.buffer.line_col_to_position(line, character));
236
237 if let Some(position) = position {
238 let (cursor_id, old_position, old_anchor, old_sticky_column) = {
240 let cursors = self.active_cursors();
241 let primary = cursors.primary();
242 (
243 cursors.primary_id(),
244 primary.position,
245 primary.anchor,
246 primary.sticky_column,
247 )
248 };
249 let event = crate::model::event::Event::MoveCursor {
250 cursor_id,
251 old_position,
252 new_position: position,
253 old_anchor,
254 new_anchor: None,
255 old_sticky_column,
256 new_sticky_column: 0, };
258
259 let split_id = self.split_manager.active_split();
260 if let Some(state) = self.buffers.get_mut(&buffer_id) {
261 let cursors = &mut self.split_view_states.get_mut(&split_id).unwrap().cursors;
262 state.apply(cursors, &event);
263 }
264 }
265
266 self.status_message = Some(
267 t!(
268 "lsp.jumped_to_definition",
269 path = path.display().to_string(),
270 line = line + 1
271 )
272 .to_string(),
273 );
274 } else {
275 self.status_message = Some(t!("lsp.cannot_open_definition").to_string());
276 }
277
278 Ok(())
279 }
280
281 pub fn has_pending_lsp_requests(&self) -> bool {
283 !self.pending_completion_requests.is_empty()
284 || self.pending_goto_definition_request.is_some()
285 }
286
287 pub(crate) fn cancel_pending_lsp_requests(&mut self) {
291 self.scheduled_completion_trigger = None;
293 if !self.pending_completion_requests.is_empty() {
294 let ids: Vec<u64> = self.pending_completion_requests.drain().collect();
295 for request_id in ids {
296 tracing::debug!("Canceling pending LSP completion request {}", request_id);
297 self.send_lsp_cancel_request(request_id);
298 }
299 self.update_lsp_status_from_server_statuses();
300 }
301 if let Some(request_id) = self.pending_goto_definition_request.take() {
302 tracing::debug!(
303 "Canceling pending LSP goto-definition request {}",
304 request_id
305 );
306 self.send_lsp_cancel_request(request_id);
308 self.update_lsp_status_from_server_statuses();
309 }
310 }
311
312 fn send_lsp_cancel_request(&mut self, request_id: u64) {
314 let buffer_id = self.active_buffer();
316 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
317 return;
318 };
319
320 if let Some(lsp) = self.lsp.as_mut() {
321 if let Some(handle) = lsp.get_handle_mut(&language) {
323 if let Err(e) = handle.cancel_request(request_id) {
324 tracing::warn!("Failed to send LSP cancel request: {}", e);
325 } else {
326 tracing::debug!("Sent $/cancelRequest for request_id={}", request_id);
327 }
328 }
329 }
330 }
331
332 pub(crate) fn with_lsp_for_buffer<F, R>(
337 &mut self,
338 buffer_id: BufferId,
339 feature: LspFeature,
340 f: F,
341 ) -> Option<R>
342 where
343 F: FnOnce(&LspHandle, &lsp_types::Uri, &str) -> R,
344 {
345 use crate::services::lsp::manager::LspSpawnResult;
346
347 let (uri, language, file_path) = {
348 let metadata = self.buffer_metadata.get(&buffer_id)?;
349 if !metadata.lsp_enabled {
350 return None;
351 }
352 let uri = metadata.file_uri()?.clone();
353 let file_path = metadata.file_path().cloned();
354 let language = self.buffers.get(&buffer_id)?.language.clone();
355 (uri, language, file_path)
356 };
357
358 let lsp = self.lsp.as_mut()?;
359 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
360 return None;
361 }
362
363 self.ensure_did_open_all(buffer_id, &uri, &language)?;
365
366 let lsp = self.lsp.as_mut()?;
368 let sh = lsp.handle_for_feature_mut(&language, feature)?;
369 Some(f(&sh.handle, &uri, &language))
370 }
371
372 pub(crate) fn with_all_lsp_for_buffer_feature<F, R>(
378 &mut self,
379 buffer_id: BufferId,
380 feature: LspFeature,
381 f: F,
382 ) -> Vec<R>
383 where
384 F: Fn(&LspHandle, &lsp_types::Uri, &str) -> R,
385 {
386 use crate::services::lsp::manager::LspSpawnResult;
387
388 let (uri, language, file_path) = match (|| {
389 let metadata = self.buffer_metadata.get(&buffer_id)?;
390 if !metadata.lsp_enabled {
391 return None;
392 }
393 let uri = metadata.file_uri()?.clone();
394 let file_path = metadata.file_path().cloned();
395 let language = self.buffers.get(&buffer_id)?.language.clone();
396 Some((uri, language, file_path))
397 })() {
398 Some(v) => v,
399 None => return Vec::new(),
400 };
401
402 let lsp = match self.lsp.as_mut() {
403 Some(l) => l,
404 None => return Vec::new(),
405 };
406 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
407 return Vec::new();
408 }
409
410 if self
412 .ensure_did_open_all(buffer_id, &uri, &language)
413 .is_none()
414 {
415 return Vec::new();
416 }
417
418 let lsp = match self.lsp.as_mut() {
420 Some(l) => l,
421 None => return Vec::new(),
422 };
423 lsp.handles_for_feature_mut(&language, feature)
424 .into_iter()
425 .map(|sh| f(&sh.handle, &uri, &language))
426 .collect()
427 }
428
429 pub(crate) fn with_all_lsp_for_buffer_feature_named<F, R>(
432 &mut self,
433 buffer_id: BufferId,
434 feature: LspFeature,
435 f: F,
436 ) -> Vec<R>
437 where
438 F: Fn(&LspHandle, &lsp_types::Uri, &str, &str) -> R,
439 {
440 use crate::services::lsp::manager::LspSpawnResult;
441
442 let (uri, language, file_path) = match (|| {
443 let metadata = self.buffer_metadata.get(&buffer_id)?;
444 if !metadata.lsp_enabled {
445 return None;
446 }
447 let uri = metadata.file_uri()?.clone();
448 let file_path = metadata.file_path().cloned();
449 let language = self.buffers.get(&buffer_id)?.language.clone();
450 Some((uri, language, file_path))
451 })() {
452 Some(v) => v,
453 None => return Vec::new(),
454 };
455
456 let lsp = match self.lsp.as_mut() {
457 Some(l) => l,
458 None => return Vec::new(),
459 };
460 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
461 return Vec::new();
462 }
463
464 if self
465 .ensure_did_open_all(buffer_id, &uri, &language)
466 .is_none()
467 {
468 return Vec::new();
469 }
470
471 let lsp = match self.lsp.as_mut() {
472 Some(l) => l,
473 None => return Vec::new(),
474 };
475 lsp.handles_for_feature_mut(&language, feature)
476 .into_iter()
477 .map(|sh| f(&sh.handle, &uri, &language, &sh.name))
478 .collect()
479 }
480
481 fn ensure_did_open_all(
484 &mut self,
485 buffer_id: BufferId,
486 uri: &lsp_types::Uri,
487 language: &str,
488 ) -> Option<()> {
489 let lsp = self.lsp.as_mut()?;
490 let handle_ids: Vec<u64> = lsp
491 .get_handles(language)
492 .iter()
493 .map(|sh| sh.handle.id())
494 .collect();
495
496 let needs_open: Vec<u64> = {
497 let metadata = self.buffer_metadata.get(&buffer_id)?;
498 handle_ids
499 .iter()
500 .filter(|id| !metadata.lsp_opened_with.contains(id))
501 .copied()
502 .collect()
503 };
504
505 if !needs_open.is_empty() {
506 let text = self.buffers.get(&buffer_id)?.buffer.to_string()?;
507 let lsp = self.lsp.as_mut()?;
508 for sh in lsp.get_handles_mut(language) {
509 if needs_open.contains(&sh.handle.id()) {
510 if let Err(e) =
511 sh.handle
512 .did_open(uri.clone(), text.clone(), language.to_string())
513 {
514 tracing::warn!("Failed to send didOpen to '{}': {}", sh.name, e);
515 continue;
516 }
517 let metadata = self.buffer_metadata.get_mut(&buffer_id)?;
518 metadata.lsp_opened_with.insert(sh.handle.id());
519 tracing::debug!(
520 "Sent didOpen for {} to LSP handle '{}' (language: {})",
521 uri.as_str(),
522 sh.name,
523 language
524 );
525 }
526 }
527 }
528
529 Some(())
530 }
531
532 pub(crate) fn request_completion(&mut self) {
535 if !self.pending_completion_requests.is_empty() {
549 let ids: Vec<u64> = self.pending_completion_requests.drain().collect();
550 for request_id in ids {
551 tracing::debug!(
552 "Canceling previous pending LSP completion request {}",
553 request_id
554 );
555 self.send_lsp_cancel_request(request_id);
556 }
557 }
558 self.completion_items = None;
559
560 let cursor_pos = self.active_cursors().primary().position;
562 let state = self.active_state();
563
564 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
566 let buffer_id = self.active_buffer();
567
568 let base_request_id = self.next_lsp_request_id;
570 let counter = std::sync::atomic::AtomicU64::new(0);
572
573 let results = self.with_all_lsp_for_buffer_feature(
574 buffer_id,
575 LspFeature::Completion,
576 |handle, uri, _language| {
577 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
578 let request_id = base_request_id + idx;
579 let result =
580 handle.completion(request_id, uri.clone(), line as u32, character as u32);
581 if result.is_ok() {
582 tracing::info!(
583 "Requested completion at {}:{}:{} (request_id={})",
584 uri.as_str(),
585 line,
586 character,
587 request_id
588 );
589 }
590 (request_id, result.is_ok())
591 },
592 );
593
594 let mut sent_ids = Vec::new();
595 for (request_id, ok) in &results {
596 if *ok {
597 sent_ids.push(*request_id);
598 }
599 }
600 self.next_lsp_request_id = base_request_id + results.len() as u64;
602
603 if !sent_ids.is_empty() {
604 self.pending_completion_requests.extend(sent_ids);
605 self.lsp_status = "LSP: completion...".to_string();
606 } else {
607 self.show_buffer_word_completion_popup();
609 }
610 }
611
612 fn show_buffer_word_completion_popup(&mut self) {
616 let items = self.get_buffer_completion_popup_items();
617 if items.is_empty() {
618 return;
619 }
620
621 let popup_data = crate::app::popup_actions::build_completion_popup_from_items(items, 0);
622 let accept_hint = self.completion_accept_key_hint();
623
624 let buffer_id = self.active_buffer();
625 let state = self.buffers.get_mut(&buffer_id).unwrap();
626 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
627 popup_obj.accept_key_hint = accept_hint;
628 state.popups.show_or_replace(popup_obj);
629 }
630
631 pub(crate) fn maybe_trigger_completion(&mut self, c: char) {
641 if !self.config.editor.completion_popup_auto_show {
643 return;
644 }
645
646 let language = self.active_state().language.clone();
648
649 let is_lsp_trigger = self
651 .lsp
652 .as_ref()
653 .map(|lsp| lsp.is_completion_trigger_char(c, &language))
654 .unwrap_or(false);
655
656 let quick_suggestions_enabled = self.config.editor.quick_suggestions;
658 let suggest_on_trigger_chars = self.config.editor.suggest_on_trigger_characters;
659 let is_word_char = c.is_alphanumeric() || c == '_';
660
661 if is_lsp_trigger && suggest_on_trigger_chars {
663 tracing::debug!(
664 "Trigger character '{}' immediately triggers completion for language {}",
665 c,
666 language
667 );
668 self.scheduled_completion_trigger = None;
670 self.request_completion();
671 return;
672 }
673
674 if quick_suggestions_enabled && is_word_char {
676 let delay_ms = self.config.editor.quick_suggestions_delay_ms;
677 let trigger_time = Instant::now() + Duration::from_millis(delay_ms);
678
679 tracing::debug!(
680 "Scheduling completion trigger in {}ms for language {} (char '{}')",
681 delay_ms,
682 language,
683 c
684 );
685
686 self.scheduled_completion_trigger = Some(trigger_time);
689 } else {
690 self.scheduled_completion_trigger = None;
694 }
695 }
696
697 pub(crate) fn request_goto_definition(&mut self) -> AnyhowResult<()> {
699 let cursor_pos = self.active_cursors().primary().position;
701 let state = self.active_state();
702
703 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
705 let buffer_id = self.active_buffer();
706 let request_id = self.next_lsp_request_id;
707
708 let sent = self
710 .with_lsp_for_buffer(
711 buffer_id,
712 LspFeature::Definition,
713 |handle, uri, _language| {
714 let result = handle.goto_definition(
715 request_id,
716 uri.clone(),
717 line as u32,
718 character as u32,
719 );
720 if result.is_ok() {
721 tracing::info!(
722 "Requested go-to-definition at {}:{}:{}",
723 uri.as_str(),
724 line,
725 character
726 );
727 }
728 result.is_ok()
729 },
730 )
731 .unwrap_or(false);
732
733 if sent {
734 self.next_lsp_request_id += 1;
735 self.pending_goto_definition_request = Some(request_id);
736 }
737
738 Ok(())
739 }
740
741 pub(crate) fn request_hover(&mut self) -> AnyhowResult<()> {
743 let cursor_pos = self.active_cursors().primary().position;
745 let state = self.active_state();
746
747 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
749
750 if let Some(pos) = state.buffer.offset_to_position(cursor_pos) {
752 tracing::debug!(
753 "Hover request: cursor_byte={}, line={}, byte_col={}, utf16_col={}",
754 cursor_pos,
755 pos.line,
756 pos.column,
757 character
758 );
759 }
760
761 let buffer_id = self.active_buffer();
762 let request_id = self.next_lsp_request_id;
763
764 let sent = self
766 .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
767 let result = handle.hover(request_id, uri.clone(), line as u32, character as u32);
768 if result.is_ok() {
769 tracing::info!(
770 "Requested hover at {}:{}:{} (byte_pos={})",
771 uri.as_str(),
772 line,
773 character,
774 cursor_pos
775 );
776 }
777 result.is_ok()
778 })
779 .unwrap_or(false);
780
781 if sent {
782 self.next_lsp_request_id += 1;
783 self.pending_hover_request = Some(request_id);
784 self.lsp_status = "LSP: hover...".to_string();
785 }
786
787 Ok(())
788 }
789
790 pub(crate) fn request_hover_at_position(&mut self, byte_pos: usize) -> AnyhowResult<bool> {
795 let state = self.active_state();
797
798 let (line, character) = state.buffer.position_to_lsp_position(byte_pos);
800
801 if let Some(pos) = state.buffer.offset_to_position(byte_pos) {
803 tracing::trace!(
804 "Mouse hover request: byte_pos={}, line={}, byte_col={}, utf16_col={}",
805 byte_pos,
806 pos.line,
807 pos.column,
808 character
809 );
810 }
811
812 let buffer_id = self.active_buffer();
813 let request_id = self.next_lsp_request_id;
814
815 let sent = self
817 .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
818 let result = handle.hover(request_id, uri.clone(), line as u32, character as u32);
819 if result.is_ok() {
820 tracing::trace!(
821 "Mouse hover requested at {}:{}:{} (byte_pos={})",
822 uri.as_str(),
823 line,
824 character,
825 byte_pos
826 );
827 }
828 result.is_ok()
829 })
830 .unwrap_or(false);
831
832 if sent {
833 self.next_lsp_request_id += 1;
834 self.pending_hover_request = Some(request_id);
835 self.lsp_status = "LSP: hover...".to_string();
836 }
837
838 Ok(sent)
839 }
840
841 pub(crate) fn handle_hover_response(
843 &mut self,
844 request_id: u64,
845 contents: String,
846 is_markdown: bool,
847 range: Option<((u32, u32), (u32, u32))>,
848 ) {
849 if self.pending_hover_request != Some(request_id) {
851 tracing::debug!("Ignoring stale hover response: {}", request_id);
852 return;
853 }
854
855 self.pending_hover_request = None;
856 self.update_lsp_status_from_server_statuses();
857
858 if contents.is_empty() {
859 self.set_status_message(t!("lsp.no_hover").to_string());
860 self.hover_symbol_range = None;
861 return;
862 }
863
864 tracing::debug!(
866 "LSP hover content (markdown={}):\n{}",
867 is_markdown,
868 contents
869 );
870
871 if let Some(((start_line, start_char), (end_line, end_char))) = range {
873 let state = self.active_state();
874 let start_byte = state
875 .buffer
876 .lsp_position_to_byte(start_line as usize, start_char as usize);
877 let end_byte = state
878 .buffer
879 .lsp_position_to_byte(end_line as usize, end_char as usize);
880 self.hover_symbol_range = Some((start_byte, end_byte));
881 tracing::debug!(
882 "Hover symbol range: {}..{} (LSP {}:{}..{}:{})",
883 start_byte,
884 end_byte,
885 start_line,
886 start_char,
887 end_line,
888 end_char
889 );
890
891 if let Some(old_handle) = self.hover_symbol_overlay.take() {
893 let remove_event = crate::model::event::Event::RemoveOverlay { handle: old_handle };
894 self.apply_event_to_active_buffer(&remove_event);
895 }
896
897 let event = crate::model::event::Event::AddOverlay {
899 namespace: None,
900 range: start_byte..end_byte,
901 face: crate::model::event::OverlayFace::Background {
902 color: (80, 80, 120), },
904 priority: 90, message: None,
906 extend_to_line_end: false,
907 url: None,
908 };
909 self.apply_event_to_active_buffer(&event);
910 if let Some(state) = self.buffers.get(&self.active_buffer()) {
912 self.hover_symbol_overlay = state.overlays.all().last().map(|o| o.handle.clone());
913 }
914 } else {
915 if let Some((hover_byte_pos, _, _, _)) = self.mouse_state.lsp_hover_state {
918 let state = self.active_state();
919 let start_byte = find_word_start(&state.buffer, hover_byte_pos);
920 let end_byte = find_word_end(&state.buffer, hover_byte_pos);
921 if start_byte < end_byte {
922 self.hover_symbol_range = Some((start_byte, end_byte));
923 tracing::debug!(
924 "Hover symbol range (computed from word boundaries): {}..{}",
925 start_byte,
926 end_byte
927 );
928 } else {
929 self.hover_symbol_range = None;
930 }
931 } else {
932 self.hover_symbol_range = None;
933 }
934 }
935
936 use crate::view::popup::{Popup, PopupPosition};
938 use ratatui::style::Style;
939
940 let mut popup = if is_markdown {
942 Popup::markdown(&contents, &self.theme, Some(&self.grammar_registry))
943 } else {
944 let lines: Vec<String> = contents.lines().map(|s| s.to_string()).collect();
946 Popup::text(lines, &self.theme)
947 };
948
949 popup.title = Some(t!("lsp.popup_hover").to_string());
951 popup.transient = true;
952 popup.position = if let Some((x, y)) = self.mouse_hover_screen_position.take() {
954 PopupPosition::Fixed { x, y: y + 1 }
956 } else {
957 PopupPosition::BelowCursor
958 };
959 popup.width = 80;
960 let dynamic_height = (self.terminal_height * 60 / 100).clamp(15, 40);
963 popup.max_height = dynamic_height;
964 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
965 popup.background_style = Style::default().bg(self.theme.popup_bg);
966
967 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
969 state.popups.show(popup);
970 tracing::info!("Showing hover popup (markdown={})", is_markdown);
971 }
972
973 self.mouse_state.lsp_hover_request_sent = true;
976 }
977
978 pub(crate) fn apply_inlay_hints_to_state(
980 state: &mut crate::state::EditorState,
981 hints: &[lsp_types::InlayHint],
982 ) {
983 use crate::view::virtual_text::VirtualTextPosition;
984 use ratatui::style::{Color, Style};
985
986 state.virtual_texts.clear(&mut state.marker_list);
988
989 if hints.is_empty() {
990 return;
991 }
992
993 let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
995
996 for hint in hints {
997 let byte_offset = state.buffer.lsp_position_to_byte(
999 hint.position.line as usize,
1000 hint.position.character as usize,
1001 );
1002
1003 let text = match &hint.label {
1005 lsp_types::InlayHintLabel::String(s) => s.clone(),
1006 lsp_types::InlayHintLabel::LabelParts(parts) => {
1007 parts.iter().map(|p| p.value.as_str()).collect::<String>()
1008 }
1009 };
1010
1011 if state.buffer.is_empty() {
1017 continue;
1018 }
1019
1020 let (byte_offset, position) = if byte_offset >= state.buffer.len() {
1021 (
1023 state.buffer.len().saturating_sub(1),
1024 VirtualTextPosition::AfterChar,
1025 )
1026 } else {
1027 (byte_offset, VirtualTextPosition::BeforeChar)
1028 };
1029
1030 let display_text = text;
1032
1033 state.virtual_texts.add(
1034 &mut state.marker_list,
1035 byte_offset,
1036 display_text,
1037 hint_style,
1038 position,
1039 0, );
1041 }
1042
1043 tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
1044 }
1045
1046 pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
1048 let cursor_pos = self.active_cursors().primary().position;
1050 let state = self.active_state();
1051
1052 let symbol = {
1054 let text = match state.buffer.to_string() {
1055 Some(t) => t,
1056 None => {
1057 self.set_status_message(t!("error.buffer_not_loaded").to_string());
1058 return Ok(());
1059 }
1060 };
1061 let bytes = text.as_bytes();
1062 let buf_len = bytes.len();
1063
1064 if cursor_pos <= buf_len {
1065 let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
1067
1068 let mut start = cursor_pos;
1070 while start > 0 {
1071 start -= 1;
1073 while start > 0 && (bytes[start] & 0xC0) == 0x80 {
1075 start -= 1;
1076 }
1077 if let Some(ch) = text[start..].chars().next() {
1079 if !is_word_char(ch) {
1080 start += ch.len_utf8();
1081 break;
1082 }
1083 } else {
1084 break;
1085 }
1086 }
1087
1088 let mut end = cursor_pos;
1090 while end < buf_len {
1091 if let Some(ch) = text[end..].chars().next() {
1092 if is_word_char(ch) {
1093 end += ch.len_utf8();
1094 } else {
1095 break;
1096 }
1097 } else {
1098 break;
1099 }
1100 }
1101
1102 if start < end {
1103 text[start..end].to_string()
1104 } else {
1105 String::new()
1106 }
1107 } else {
1108 String::new()
1109 }
1110 };
1111
1112 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1114 let buffer_id = self.active_buffer();
1115 let request_id = self.next_lsp_request_id;
1116
1117 let sent = self
1119 .with_lsp_for_buffer(
1120 buffer_id,
1121 LspFeature::References,
1122 |handle, uri, _language| {
1123 let result =
1124 handle.references(request_id, uri.clone(), line as u32, character as u32);
1125 if result.is_ok() {
1126 tracing::info!(
1127 "Requested find references at {}:{}:{} (byte_pos={})",
1128 uri.as_str(),
1129 line,
1130 character,
1131 cursor_pos
1132 );
1133 }
1134 result.is_ok()
1135 },
1136 )
1137 .unwrap_or(false);
1138
1139 if sent {
1140 self.next_lsp_request_id += 1;
1141 self.pending_references_request = Some(request_id);
1142 self.pending_references_symbol = symbol;
1143 self.lsp_status = "LSP: finding references...".to_string();
1144 }
1145
1146 Ok(())
1147 }
1148
1149 pub(crate) fn request_signature_help(&mut self) {
1151 let cursor_pos = self.active_cursors().primary().position;
1153 let state = self.active_state();
1154
1155 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1157 let buffer_id = self.active_buffer();
1158 let request_id = self.next_lsp_request_id;
1159
1160 let sent = self
1162 .with_lsp_for_buffer(
1163 buffer_id,
1164 LspFeature::SignatureHelp,
1165 |handle, uri, _language| {
1166 let result = handle.signature_help(
1167 request_id,
1168 uri.clone(),
1169 line as u32,
1170 character as u32,
1171 );
1172 if result.is_ok() {
1173 tracing::info!(
1174 "Requested signature help at {}:{}:{} (byte_pos={})",
1175 uri.as_str(),
1176 line,
1177 character,
1178 cursor_pos
1179 );
1180 }
1181 result.is_ok()
1182 },
1183 )
1184 .unwrap_or(false);
1185
1186 if sent {
1187 self.next_lsp_request_id += 1;
1188 self.pending_signature_help_request = Some(request_id);
1189 self.lsp_status = "LSP: signature help...".to_string();
1190 }
1191 }
1192
1193 pub(crate) fn handle_signature_help_response(
1195 &mut self,
1196 request_id: u64,
1197 signature_help: Option<lsp_types::SignatureHelp>,
1198 ) {
1199 if self.pending_signature_help_request != Some(request_id) {
1201 tracing::debug!("Ignoring stale signature help response: {}", request_id);
1202 return;
1203 }
1204
1205 self.pending_signature_help_request = None;
1206 self.update_lsp_status_from_server_statuses();
1207
1208 let signature_help = match signature_help {
1209 Some(help) if !help.signatures.is_empty() => help,
1210 _ => {
1211 tracing::debug!("No signature help available");
1212 return;
1213 }
1214 };
1215
1216 let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
1218 let signature = match signature_help.signatures.get(active_signature_idx) {
1219 Some(sig) => sig,
1220 None => return,
1221 };
1222
1223 let mut content = String::new();
1225
1226 content.push_str(&signature.label);
1228 content.push('\n');
1229
1230 let active_param = signature_help
1232 .active_parameter
1233 .or(signature.active_parameter)
1234 .unwrap_or(0) as usize;
1235
1236 if let Some(params) = &signature.parameters {
1238 if let Some(param) = params.get(active_param) {
1239 let param_label = match ¶m.label {
1241 lsp_types::ParameterLabel::Simple(s) => s.clone(),
1242 lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1243 let start = offsets[0] as usize;
1245 let end = offsets[1] as usize;
1246 if end <= signature.label.len() {
1247 signature.label[start..end].to_string()
1248 } else {
1249 String::new()
1250 }
1251 }
1252 };
1253
1254 if !param_label.is_empty() {
1255 content.push_str(&format!("\n> {}\n", param_label));
1256 }
1257
1258 if let Some(doc) = ¶m.documentation {
1260 let doc_text = match doc {
1261 lsp_types::Documentation::String(s) => s.clone(),
1262 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1263 };
1264 if !doc_text.is_empty() {
1265 content.push('\n');
1266 content.push_str(&doc_text);
1267 content.push('\n');
1268 }
1269 }
1270 }
1271 }
1272
1273 if let Some(doc) = &signature.documentation {
1275 let doc_text = match doc {
1276 lsp_types::Documentation::String(s) => s.clone(),
1277 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1278 };
1279 if !doc_text.is_empty() {
1280 content.push_str("\n---\n\n");
1281 content.push_str(&space_doc_paragraphs(&doc_text));
1282 }
1283 }
1284
1285 use crate::view::popup::{Popup, PopupPosition};
1287 use ratatui::style::Style;
1288
1289 let mut popup = Popup::markdown(&content, &self.theme, Some(&self.grammar_registry));
1290 popup.title = Some(t!("lsp.popup_signature").to_string());
1291 popup.transient = true;
1292 popup.position = PopupPosition::BelowCursor;
1293 popup.width = 60;
1294 popup.max_height = 20;
1295 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1296 popup.background_style = Style::default().bg(self.theme.popup_bg);
1297
1298 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1300 state.popups.show(popup);
1301 tracing::info!(
1302 "Showing signature help popup for {} signatures",
1303 signature_help.signatures.len()
1304 );
1305 }
1306 }
1307
1308 pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1311 let cursor_pos = self.active_cursors().primary().position;
1313 let selection_range = self.active_cursors().primary().selection_range();
1314 let state = self.active_state();
1315
1316 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1318
1319 let (start_line, start_char, end_line, end_char) = if let Some(range) = selection_range {
1321 let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1322 let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1323 (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1324 } else {
1325 (line as u32, character as u32, line as u32, character as u32)
1326 };
1327
1328 let diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
1331 let buffer_id = self.active_buffer();
1332
1333 let base_request_id = self.next_lsp_request_id;
1335 let counter = std::sync::atomic::AtomicU64::new(0);
1336
1337 let results = self.with_all_lsp_for_buffer_feature_named(
1338 buffer_id,
1339 LspFeature::CodeAction,
1340 |handle, uri, _language, server_name| {
1341 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1342 let request_id = base_request_id + idx;
1343 let result = handle.code_actions(
1344 request_id,
1345 uri.clone(),
1346 start_line,
1347 start_char,
1348 end_line,
1349 end_char,
1350 diagnostics.clone(),
1351 );
1352 if result.is_ok() {
1353 tracing::info!(
1354 "Requested code actions at {}:{}:{}-{}:{} (byte_pos={}, request_id={}, server={})",
1355 uri.as_str(),
1356 start_line,
1357 start_char,
1358 end_line,
1359 end_char,
1360 cursor_pos,
1361 request_id,
1362 server_name
1363 );
1364 }
1365 (request_id, result.is_ok(), server_name.to_string())
1366 },
1367 );
1368
1369 let mut sent_ids = Vec::new();
1370 for (request_id, ok, server_name) in &results {
1371 if *ok {
1372 sent_ids.push(*request_id);
1373 self.pending_code_actions_server_names
1374 .insert(*request_id, server_name.clone());
1375 }
1376 }
1377 self.next_lsp_request_id = base_request_id + results.len() as u64;
1379
1380 if !sent_ids.is_empty() {
1381 self.pending_code_actions = None;
1383 self.pending_code_actions_requests.extend(sent_ids);
1384 self.lsp_status = "LSP: code actions...".to_string();
1385 }
1386
1387 Ok(())
1388 }
1389
1390 pub(crate) fn handle_code_actions_response(
1394 &mut self,
1395 request_id: u64,
1396 actions: Vec<lsp_types::CodeActionOrCommand>,
1397 ) {
1398 if !self.pending_code_actions_requests.remove(&request_id) {
1400 tracing::debug!("Ignoring stale code actions response: {}", request_id);
1401 return;
1402 }
1403
1404 if self.pending_code_actions_requests.is_empty() {
1406 self.update_lsp_status_from_server_statuses();
1407 }
1408
1409 let server_name = self
1411 .pending_code_actions_server_names
1412 .remove(&request_id)
1413 .unwrap_or_default();
1414
1415 if actions.is_empty() {
1416 if self.pending_code_actions_requests.is_empty()
1418 && self
1419 .pending_code_actions
1420 .as_ref()
1421 .is_none_or(|a| a.is_empty())
1422 {
1423 self.set_status_message(t!("lsp.no_code_actions").to_string());
1424 }
1425 return;
1426 }
1427
1428 let tagged_actions: Vec<(String, lsp_types::CodeActionOrCommand)> = actions
1430 .into_iter()
1431 .map(|a| (server_name.clone(), a))
1432 .collect();
1433
1434 match &mut self.pending_code_actions {
1435 Some(existing) => {
1436 existing.extend(tagged_actions);
1437 tracing::debug!("Extended code actions, now {} total", existing.len());
1438 }
1439 None => {
1440 self.pending_code_actions = Some(tagged_actions);
1441 }
1442 }
1443
1444 use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1446 use ratatui::style::Style;
1447
1448 let all_actions = self.pending_code_actions.as_ref().unwrap();
1450 let multiple_servers = {
1451 let mut names = std::collections::HashSet::new();
1452 for (name, _) in all_actions {
1453 names.insert(name.as_str());
1454 }
1455 names.len() > 1
1456 };
1457
1458 let items: Vec<PopupListItem> = all_actions
1459 .iter()
1460 .enumerate()
1461 .map(|(i, (srv_name, action))| {
1462 let title = match action {
1463 lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1464 lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1465 };
1466 let kind = match action {
1467 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1468 ca.kind.as_ref().map(|k| k.as_str().to_string())
1469 }
1470 _ => None,
1471 };
1472 let detail = if multiple_servers && !srv_name.is_empty() {
1474 match kind {
1475 Some(k) => Some(format!("[{}] {}", srv_name, k)),
1476 None => Some(format!("[{}]", srv_name)),
1477 }
1478 } else {
1479 kind
1480 };
1481 PopupListItem {
1482 text: format!("{}. {}", i + 1, title),
1483 detail,
1484 icon: None,
1485 data: Some(i.to_string()),
1486 }
1487 })
1488 .collect();
1489
1490 let mut popup = Popup::list(items, &self.theme);
1491 popup.kind = crate::view::popup::PopupKind::Action;
1492 popup.title = Some(t!("lsp.popup_code_actions").to_string());
1493 popup.position = PopupPosition::BelowCursor;
1494 popup.width = 60;
1495 popup.max_height = 15;
1496 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1497 popup.background_style = Style::default().bg(self.theme.popup_bg);
1498
1499 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1501 state.popups.show_or_replace(popup);
1502 tracing::info!(
1503 "Showing code actions popup with {} actions",
1504 all_actions.len()
1505 );
1506 }
1507 }
1508
1509 pub(crate) fn execute_code_action(&mut self, index: usize) {
1511 let action = match &self.pending_code_actions {
1512 Some(actions) => actions.get(index).map(|(_, a)| a.clone()),
1513 None => None,
1514 };
1515
1516 let Some(action) = action else {
1517 tracing::warn!("Code action index {} out of range", index);
1518 return;
1519 };
1520
1521 match action {
1522 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1523 if ca.edit.is_none()
1526 && ca.command.is_none()
1527 && ca.data.is_some()
1528 && self.server_supports_code_action_resolve()
1529 {
1530 tracing::info!(
1531 "Code action '{}' needs resolve, sending codeAction/resolve",
1532 ca.title
1533 );
1534 self.send_code_action_resolve(ca);
1535 return;
1536 }
1537 self.execute_resolved_code_action(ca);
1538 }
1539 lsp_types::CodeActionOrCommand::Command(cmd) => {
1540 self.send_execute_command(cmd);
1541 }
1542 }
1543 }
1544
1545 pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1547 let title = ca.title.clone();
1548
1549 if let Some(edit) = ca.edit {
1551 match self.apply_workspace_edit(edit) {
1552 Ok(n) => {
1553 self.set_status_message(
1554 t!("lsp.code_action_applied", title = &title, count = n).to_string(),
1555 );
1556 }
1557 Err(e) => {
1558 self.set_status_message(format!("Code action failed: {e}"));
1559 return;
1560 }
1561 }
1562 }
1563
1564 if let Some(cmd) = ca.command {
1566 self.send_execute_command(cmd);
1567 }
1568 }
1569
1570 fn send_execute_command(&mut self, cmd: lsp_types::Command) {
1572 tracing::info!("Executing LSP command: {} ({})", cmd.title, cmd.command);
1573 self.set_status_message(
1574 t!(
1575 "lsp.code_action_applied",
1576 title = &cmd.title,
1577 count = 0_usize
1578 )
1579 .to_string(),
1580 );
1581
1582 let language = match self
1584 .buffers
1585 .get(&self.active_buffer())
1586 .map(|s| s.language.clone())
1587 {
1588 Some(l) => l,
1589 None => return,
1590 };
1591
1592 if let Some(lsp) = &mut self.lsp {
1593 for sh in lsp.get_handles_mut(&language) {
1594 if let Err(e) = sh
1595 .handle
1596 .execute_command(cmd.command.clone(), cmd.arguments.clone())
1597 {
1598 tracing::warn!("Failed to send executeCommand to '{}': {}", sh.name, e);
1599 }
1600 }
1601 }
1602 }
1603
1604 fn send_code_action_resolve(&mut self, action: lsp_types::CodeAction) {
1606 let language = match self
1607 .buffers
1608 .get(&self.active_buffer())
1609 .map(|s| s.language.clone())
1610 {
1611 Some(l) => l,
1612 None => return,
1613 };
1614
1615 self.next_lsp_request_id += 1;
1616 let request_id = self.next_lsp_request_id;
1617
1618 if let Some(lsp) = &mut self.lsp {
1619 for sh in lsp.get_handles_mut(&language) {
1620 if let Err(e) = sh.handle.code_action_resolve(request_id, action.clone()) {
1621 tracing::warn!("Failed to send codeAction/resolve to '{}': {}", sh.name, e);
1622 }
1623 }
1624 }
1625 }
1626
1627 fn server_supports_code_action_resolve(&self) -> bool {
1629 let language = match self
1630 .buffers
1631 .get(&self.active_buffer())
1632 .map(|s| s.language.clone())
1633 {
1634 Some(l) => l,
1635 None => return false,
1636 };
1637
1638 if let Some(lsp) = &self.lsp {
1639 for sh in lsp.get_handles(&language) {
1640 if sh.capabilities.code_action_resolve {
1641 return true;
1642 }
1643 }
1644 }
1645 false
1646 }
1647
1648 pub(crate) fn server_supports_completion_resolve(&self) -> bool {
1650 let language = match self
1651 .buffers
1652 .get(&self.active_buffer())
1653 .map(|s| s.language.clone())
1654 {
1655 Some(l) => l,
1656 None => return false,
1657 };
1658
1659 if let Some(lsp) = &self.lsp {
1660 for sh in lsp.get_handles(&language) {
1661 if sh.capabilities.completion_resolve {
1662 return true;
1663 }
1664 }
1665 }
1666 false
1667 }
1668
1669 pub(crate) fn send_completion_resolve(&mut self, item: lsp_types::CompletionItem) {
1671 let language = match self
1672 .buffers
1673 .get(&self.active_buffer())
1674 .map(|s| s.language.clone())
1675 {
1676 Some(l) => l,
1677 None => return,
1678 };
1679
1680 self.next_lsp_request_id += 1;
1681 let request_id = self.next_lsp_request_id;
1682
1683 if let Some(lsp) = &mut self.lsp {
1684 for sh in lsp.get_handles_mut(&language) {
1685 if sh.capabilities.completion_resolve {
1686 if let Err(e) = sh.handle.completion_resolve(request_id, item.clone()) {
1687 tracing::warn!(
1688 "Failed to send completionItem/resolve to '{}': {}",
1689 sh.name,
1690 e
1691 );
1692 }
1693 return;
1694 }
1695 }
1696 }
1697 }
1698
1699 pub(crate) fn handle_completion_resolved(&mut self, item: lsp_types::CompletionItem) {
1701 if let Some(additional_edits) = item.additional_text_edits {
1702 if !additional_edits.is_empty() {
1703 tracing::info!(
1704 "Applying {} additional text edits from completion resolve",
1705 additional_edits.len()
1706 );
1707 let buffer_id = self.active_buffer();
1708 if let Err(e) = self.apply_lsp_text_edits(buffer_id, additional_edits) {
1709 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
1710 }
1711 }
1712 }
1713 }
1714
1715 pub(crate) fn apply_formatting_edits(
1717 &mut self,
1718 uri: &str,
1719 edits: Vec<lsp_types::TextEdit>,
1720 ) -> AnyhowResult<usize> {
1721 let buffer_id = self
1723 .buffer_metadata
1724 .iter()
1725 .find(|(_, meta)| meta.file_uri().map(|u| u.as_str() == uri).unwrap_or(false))
1726 .map(|(id, _)| *id);
1727
1728 if let Some(buffer_id) = buffer_id {
1729 let count = self.apply_lsp_text_edits(buffer_id, edits)?;
1730 self.set_status_message(format!("Formatted ({} edits)", count));
1731 Ok(count)
1732 } else {
1733 tracing::warn!("Cannot apply formatting: no buffer for URI {}", uri);
1734 Ok(0)
1735 }
1736 }
1737
1738 pub(crate) fn request_formatting(&mut self) {
1740 let buffer_id = self.active_buffer();
1741 let metadata = match self.buffer_metadata.get(&buffer_id) {
1742 Some(m) if m.lsp_enabled => m,
1743 _ => {
1744 self.set_status_message("LSP not available for this buffer".to_string());
1745 return;
1746 }
1747 };
1748
1749 let uri = match metadata.file_uri() {
1750 Some(u) => u.clone(),
1751 None => return,
1752 };
1753
1754 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
1755 Some(l) => l,
1756 None => return,
1757 };
1758
1759 let tab_size = self.config.editor.tab_size as u32;
1760 let insert_spaces = !self.config.editor.use_tabs;
1761
1762 self.next_lsp_request_id += 1;
1763 let request_id = self.next_lsp_request_id;
1764
1765 if let Some(lsp) = &mut self.lsp {
1766 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Format) {
1767 if let Err(e) =
1768 sh.handle
1769 .document_formatting(request_id, uri, tab_size, insert_spaces)
1770 {
1771 tracing::warn!("Failed to request formatting: {}", e);
1772 }
1773 } else {
1774 self.set_status_message("Formatting not supported by LSP server".to_string());
1775 }
1776 }
1777 }
1778
1779 pub(crate) fn handle_references_response(
1781 &mut self,
1782 request_id: u64,
1783 locations: Vec<lsp_types::Location>,
1784 ) -> AnyhowResult<()> {
1785 tracing::info!(
1786 "handle_references_response: received {} locations for request_id={}",
1787 locations.len(),
1788 request_id
1789 );
1790
1791 if self.pending_references_request != Some(request_id) {
1793 tracing::debug!("Ignoring stale references response: {}", request_id);
1794 return Ok(());
1795 }
1796
1797 self.pending_references_request = None;
1798 self.update_lsp_status_from_server_statuses();
1799
1800 if locations.is_empty() {
1801 self.set_status_message(t!("lsp.no_references").to_string());
1802 return Ok(());
1803 }
1804
1805 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
1807 .iter()
1808 .map(|loc| {
1809 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
1811 loc.uri.path().as_str().to_string()
1813 } else {
1814 loc.uri.as_str().to_string()
1815 };
1816
1817 crate::services::plugins::hooks::LspLocation {
1818 file,
1819 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
1822 })
1823 .collect();
1824
1825 let count = lsp_locations.len();
1826 let symbol = std::mem::take(&mut self.pending_references_symbol);
1827 self.set_status_message(
1828 t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
1829 );
1830
1831 self.plugin_manager.run_hook(
1833 "lsp_references",
1834 crate::services::plugins::hooks::HookArgs::LspReferences {
1835 symbol: symbol.clone(),
1836 locations: lsp_locations,
1837 },
1838 );
1839
1840 tracing::info!(
1841 "Fired lsp_references hook with {} locations for symbol '{}'",
1842 count,
1843 symbol
1844 );
1845
1846 Ok(())
1847 }
1848
1849 pub(crate) fn apply_lsp_text_edits(
1852 &mut self,
1853 buffer_id: BufferId,
1854 mut edits: Vec<lsp_types::TextEdit>,
1855 ) -> AnyhowResult<usize> {
1856 if edits.is_empty() {
1857 return Ok(0);
1858 }
1859
1860 edits.sort_by(|a, b| {
1862 b.range
1863 .start
1864 .line
1865 .cmp(&a.range.start.line)
1866 .then(b.range.start.character.cmp(&a.range.start.character))
1867 });
1868
1869 let mut batch_events = Vec::new();
1871 let mut changes = 0;
1872
1873 let cursor_id = {
1875 let split_id = self
1876 .split_manager
1877 .splits_for_buffer(buffer_id)
1878 .into_iter()
1879 .next()
1880 .unwrap_or_else(|| self.split_manager.active_split());
1881 self.split_view_states
1882 .get(&split_id)
1883 .map(|vs| vs.cursors.primary_id())
1884 .unwrap_or_else(|| self.active_cursors().primary_id())
1885 };
1886
1887 for edit in edits {
1889 let state = self
1890 .buffers
1891 .get_mut(&buffer_id)
1892 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
1893
1894 let start_line = edit.range.start.line as usize;
1896 let start_char = edit.range.start.character as usize;
1897 let end_line = edit.range.end.line as usize;
1898 let end_char = edit.range.end.character as usize;
1899
1900 let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
1901 let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
1902 let buffer_len = state.buffer.len();
1903
1904 let old_text = if start_pos < end_pos && end_pos <= buffer_len {
1906 state.get_text_range(start_pos, end_pos)
1907 } else {
1908 format!(
1909 "<invalid range: start={}, end={}, buffer_len={}>",
1910 start_pos, end_pos, buffer_len
1911 )
1912 };
1913 tracing::debug!(
1914 " Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
1915 start_line, start_char, end_line, end_char,
1916 start_pos, end_pos, old_text, edit.new_text
1917 );
1918
1919 if start_pos < end_pos {
1921 let deleted_text = state.get_text_range(start_pos, end_pos);
1922 let delete_event = Event::Delete {
1923 range: start_pos..end_pos,
1924 deleted_text,
1925 cursor_id,
1926 };
1927 batch_events.push(delete_event);
1928 }
1929
1930 if !edit.new_text.is_empty() {
1932 let insert_event = Event::Insert {
1933 position: start_pos,
1934 text: edit.new_text.clone(),
1935 cursor_id,
1936 };
1937 batch_events.push(insert_event);
1938 }
1939
1940 changes += 1;
1941 }
1942
1943 if !batch_events.is_empty() {
1945 self.apply_events_to_buffer_as_bulk_edit(
1946 buffer_id,
1947 batch_events,
1948 "LSP Rename".to_string(),
1949 )?;
1950 }
1951
1952 Ok(changes)
1953 }
1954
1955 fn apply_text_document_edit(
1961 &mut self,
1962 text_doc_edit: lsp_types::TextDocumentEdit,
1963 ) -> AnyhowResult<usize> {
1964 let uri = text_doc_edit.text_document.uri;
1965
1966 if let Some(expected_version) = text_doc_edit.text_document.version {
1969 if let Ok(path) = uri_to_path(&uri) {
1970 if let Some(lsp) = &self.lsp {
1971 let language = self
1972 .buffers
1973 .get(&self.active_buffer())
1974 .map(|s| s.language.clone())
1975 .unwrap_or_default();
1976 for sh in lsp.get_handles(&language) {
1977 if let Some(current_version) = sh.handle.document_version(&path) {
1978 if (expected_version as i64) != current_version {
1979 tracing::warn!(
1980 "Rejecting stale TextDocumentEdit for {:?}: \
1981 server version {} != our version {}",
1982 path,
1983 expected_version,
1984 current_version,
1985 );
1986 return Ok(0);
1987 }
1988 }
1989 }
1990 }
1991 }
1992 }
1993
1994 if let Ok(path) = uri_to_path(&uri) {
1995 let buffer_id = match self.open_file(&path) {
1996 Ok(id) => id,
1997 Err(e) => {
1998 if let Some(confirmation) =
1999 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2000 {
2001 self.start_large_file_encoding_confirmation(confirmation);
2002 } else {
2003 self.set_status_message(
2004 t!("file.error_opening", error = e.to_string()).to_string(),
2005 );
2006 }
2007 return Ok(0);
2008 }
2009 };
2010
2011 let edits: Vec<lsp_types::TextEdit> = text_doc_edit
2012 .edits
2013 .into_iter()
2014 .map(|one_of| match one_of {
2015 lsp_types::OneOf::Left(text_edit) => text_edit,
2016 lsp_types::OneOf::Right(annotated) => annotated.text_edit,
2017 })
2018 .collect();
2019
2020 tracing::info!("Applying {} edits for {:?}:", edits.len(), path);
2021 for (i, edit) in edits.iter().enumerate() {
2022 tracing::info!(
2023 " Edit {}: line {}:{}-{}:{} -> {:?}",
2024 i,
2025 edit.range.start.line,
2026 edit.range.start.character,
2027 edit.range.end.line,
2028 edit.range.end.character,
2029 edit.new_text
2030 );
2031 }
2032
2033 self.apply_lsp_text_edits(buffer_id, edits)
2034 } else {
2035 Ok(0)
2036 }
2037 }
2038
2039 fn apply_resource_operation(&mut self, op: lsp_types::ResourceOp) -> AnyhowResult<()> {
2041 match op {
2042 lsp_types::ResourceOp::Create(create) => {
2043 let path = std::path::PathBuf::from(create.uri.path().as_str());
2044 let overwrite = create
2045 .options
2046 .as_ref()
2047 .and_then(|o| o.overwrite)
2048 .unwrap_or(false);
2049 let ignore_if_exists = create
2050 .options
2051 .as_ref()
2052 .and_then(|o| o.ignore_if_exists)
2053 .unwrap_or(false);
2054
2055 if path.exists() {
2056 if ignore_if_exists {
2057 tracing::debug!("CreateFile: {:?} already exists, ignoring", path);
2058 return Ok(());
2059 }
2060 if !overwrite {
2061 tracing::warn!("CreateFile: {:?} already exists and overwrite=false", path);
2062 return Ok(());
2063 }
2064 }
2065
2066 if let Some(parent) = path.parent() {
2068 std::fs::create_dir_all(parent)?;
2069 }
2070 std::fs::write(&path, "")?;
2071 tracing::info!("CreateFile: created {:?}", path);
2072
2073 if let Err(e) = self.open_file(&path) {
2075 tracing::warn!("CreateFile: failed to open created file {:?}: {}", path, e);
2076 }
2077 }
2078 lsp_types::ResourceOp::Rename(rename) => {
2079 let old_path = std::path::PathBuf::from(rename.old_uri.path().as_str());
2080 let new_path = std::path::PathBuf::from(rename.new_uri.path().as_str());
2081 let overwrite = rename
2082 .options
2083 .as_ref()
2084 .and_then(|o| o.overwrite)
2085 .unwrap_or(false);
2086 let ignore_if_exists = rename
2087 .options
2088 .as_ref()
2089 .and_then(|o| o.ignore_if_exists)
2090 .unwrap_or(false);
2091
2092 if new_path.exists() {
2093 if ignore_if_exists {
2094 tracing::debug!("RenameFile: {:?} already exists, ignoring", new_path);
2095 return Ok(());
2096 }
2097 if !overwrite {
2098 tracing::warn!(
2099 "RenameFile: {:?} already exists and overwrite=false",
2100 new_path
2101 );
2102 return Ok(());
2103 }
2104 }
2105
2106 if let Some(parent) = new_path.parent() {
2108 std::fs::create_dir_all(parent)?;
2109 }
2110 std::fs::rename(&old_path, &new_path)?;
2111 tracing::info!("RenameFile: {:?} -> {:?}", old_path, new_path);
2112 }
2113 lsp_types::ResourceOp::Delete(delete) => {
2114 let path = std::path::PathBuf::from(delete.uri.path().as_str());
2115 let recursive = delete
2116 .options
2117 .as_ref()
2118 .and_then(|o| o.recursive)
2119 .unwrap_or(false);
2120 let ignore_if_not_exists = delete
2121 .options
2122 .as_ref()
2123 .and_then(|o| o.ignore_if_not_exists)
2124 .unwrap_or(false);
2125
2126 if !path.exists() {
2127 if ignore_if_not_exists {
2128 tracing::debug!("DeleteFile: {:?} does not exist, ignoring", path);
2129 return Ok(());
2130 }
2131 tracing::warn!("DeleteFile: {:?} does not exist", path);
2132 return Ok(());
2133 }
2134
2135 if path.is_dir() && recursive {
2136 std::fs::remove_dir_all(&path)?;
2137 } else if path.is_file() {
2138 std::fs::remove_file(&path)?;
2139 }
2140 tracing::info!("DeleteFile: deleted {:?}", path);
2141 }
2142 }
2143 Ok(())
2144 }
2145
2146 pub(crate) fn apply_workspace_edit(
2150 &mut self,
2151 workspace_edit: lsp_types::WorkspaceEdit,
2152 ) -> AnyhowResult<usize> {
2153 tracing::debug!(
2154 "Applying WorkspaceEdit: changes={:?}, document_changes={:?}",
2155 workspace_edit.changes.as_ref().map(|c| c.len()),
2156 workspace_edit.document_changes.as_ref().map(|dc| match dc {
2157 lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
2158 lsp_types::DocumentChanges::Operations(o) => format!("{} operations", o.len()),
2159 })
2160 );
2161
2162 let mut total_changes = 0;
2163
2164 if let Some(changes) = workspace_edit.changes {
2166 for (uri, edits) in changes {
2167 if let Ok(path) = uri_to_path(&uri) {
2168 let buffer_id = match self.open_file(&path) {
2169 Ok(id) => id,
2170 Err(e) => {
2171 if let Some(confirmation) = e.downcast_ref::<
2172 crate::model::buffer::LargeFileEncodingConfirmation,
2173 >() {
2174 self.start_large_file_encoding_confirmation(confirmation);
2175 } else {
2176 self.set_status_message(
2177 t!("file.error_opening", error = e.to_string())
2178 .to_string(),
2179 );
2180 }
2181 return Ok(0);
2182 }
2183 };
2184 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
2185 }
2186 }
2187 }
2188
2189 if let Some(document_changes) = workspace_edit.document_changes {
2191 use lsp_types::DocumentChanges;
2192
2193 match document_changes {
2194 DocumentChanges::Edits(edits) => {
2195 for text_doc_edit in edits {
2196 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2197 }
2198 }
2199 DocumentChanges::Operations(ops) => {
2200 for op in ops {
2203 match op {
2204 lsp_types::DocumentChangeOperation::Edit(text_doc_edit) => {
2205 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2206 }
2207 lsp_types::DocumentChangeOperation::Op(resource_op) => {
2208 self.apply_resource_operation(resource_op)?;
2209 total_changes += 1;
2210 }
2211 }
2212 }
2213 }
2214 }
2215 }
2216
2217 Ok(total_changes)
2218 }
2219
2220 pub fn handle_rename_response(
2222 &mut self,
2223 _request_id: u64,
2224 result: Result<lsp_types::WorkspaceEdit, String>,
2225 ) -> AnyhowResult<()> {
2226 self.update_lsp_status_from_server_statuses();
2227
2228 match result {
2229 Ok(workspace_edit) => {
2230 let total_changes = self.apply_workspace_edit(workspace_edit)?;
2231 self.status_message = Some(t!("lsp.renamed", count = total_changes).to_string());
2232 }
2233 Err(error) => {
2234 if error.contains("content modified") || error.contains("-32801") {
2236 tracing::debug!(
2237 "LSP rename: ContentModified error (expected, ignoring): {}",
2238 error
2239 );
2240 self.status_message = Some(t!("lsp.rename_cancelled").to_string());
2241 } else {
2242 self.status_message = Some(t!("lsp.rename_failed", error = &error).to_string());
2243 }
2244 }
2245 }
2246
2247 Ok(())
2248 }
2249
2250 pub(crate) fn apply_events_to_buffer_as_bulk_edit(
2255 &mut self,
2256 buffer_id: BufferId,
2257 events: Vec<Event>,
2258 description: String,
2259 ) -> AnyhowResult<()> {
2260 use crate::model::event::CursorId;
2261
2262 if events.is_empty() {
2263 return Ok(());
2264 }
2265
2266 let batch_for_lsp = Event::Batch {
2268 events: events.clone(),
2269 description: description.clone(),
2270 };
2271
2272 let original_active = self.active_buffer();
2275 self.split_manager.set_active_buffer_id(buffer_id);
2276 let lsp_changes = self.collect_lsp_changes(&batch_for_lsp);
2277 self.split_manager.set_active_buffer_id(original_active);
2278
2279 let split_id_for_cursors = self
2282 .split_manager
2283 .splits_for_buffer(buffer_id)
2284 .into_iter()
2285 .next()
2286 .unwrap_or_else(|| self.split_manager.active_split());
2287 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2288 .split_view_states
2289 .get(&split_id_for_cursors)
2290 .and_then(|vs| vs.keyed_states.get(&buffer_id))
2291 .map(|bvs| {
2292 bvs.cursors
2293 .iter()
2294 .map(|(id, c)| (id, c.position, c.anchor))
2295 .collect()
2296 })
2297 .unwrap_or_default();
2298
2299 let state = self
2300 .buffers
2301 .get_mut(&buffer_id)
2302 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2303
2304 let old_snapshot = state.buffer.snapshot_buffer_state();
2306
2307 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2309 for event in &events {
2310 match event {
2311 Event::Insert { position, text, .. } => {
2312 edits.push((*position, 0, text.clone()));
2313 }
2314 Event::Delete { range, .. } => {
2315 edits.push((range.start, range.len(), String::new()));
2316 }
2317 _ => {}
2318 }
2319 }
2320
2321 edits.sort_by(|a, b| b.0.cmp(&a.0));
2323
2324 let edit_refs: Vec<(usize, usize, &str)> = edits
2326 .iter()
2327 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2328 .collect();
2329
2330 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2332
2333 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2335
2336 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2338 for (pos, del_len, text) in &edits {
2339 let delta = text.len() as isize - *del_len as isize;
2340 position_deltas.push((*pos, delta));
2341 }
2342 position_deltas.sort_by_key(|(pos, _)| *pos);
2343
2344 let calc_shift = |original_pos: usize| -> isize {
2345 let mut shift: isize = 0;
2346 for (edit_pos, delta) in &position_deltas {
2347 if *edit_pos < original_pos {
2348 shift += delta;
2349 }
2350 }
2351 shift
2352 };
2353
2354 let buffer_len = state.buffer.len();
2356 let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
2357 .iter()
2358 .map(|(id, pos, anchor)| {
2359 let shift = calc_shift(*pos);
2360 let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
2361 let new_anchor = anchor.map(|a| {
2362 let anchor_shift = calc_shift(a);
2363 ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
2364 });
2365 (*id, new_pos, new_anchor)
2366 })
2367 .collect();
2368
2369 let new_snapshot = state.buffer.snapshot_buffer_state();
2371
2372 state.highlighter.invalidate_all();
2374
2375 if let Some(vs) = self.split_view_states.get_mut(&split_id_for_cursors) {
2377 if let Some(bvs) = vs.keyed_states.get_mut(&buffer_id) {
2378 for (cursor_id, new_pos, new_anchor) in &new_cursors {
2379 if let Some(cursor) = bvs.cursors.get_mut(*cursor_id) {
2380 cursor.position = *new_pos;
2381 cursor.anchor = *new_anchor;
2382 }
2383 }
2384 }
2385 }
2386
2387 let edit_lengths: Vec<(usize, usize, usize)> = {
2390 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2391 for (pos, del_len, text) in &edits {
2392 if let Some(last) = lengths.last_mut() {
2393 if last.0 == *pos {
2394 last.1 += del_len;
2395 last.2 += text.len();
2396 continue;
2397 }
2398 }
2399 lengths.push((*pos, *del_len, text.len()));
2400 }
2401 lengths
2402 };
2403
2404 for &(pos, del_len, ins_len) in &edit_lengths {
2406 if del_len > 0 && ins_len > 0 {
2407 if ins_len > del_len {
2408 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2409 state.margins.adjust_for_insert(pos, ins_len - del_len);
2410 } else if del_len > ins_len {
2411 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2412 state.margins.adjust_for_delete(pos, del_len - ins_len);
2413 }
2414 } else if del_len > 0 {
2415 state.marker_list.adjust_for_delete(pos, del_len);
2416 state.margins.adjust_for_delete(pos, del_len);
2417 } else if ins_len > 0 {
2418 state.marker_list.adjust_for_insert(pos, ins_len);
2419 state.margins.adjust_for_insert(pos, ins_len);
2420 }
2421 }
2422
2423 let bulk_edit = Event::BulkEdit {
2425 old_snapshot: Some(old_snapshot),
2426 new_snapshot: Some(new_snapshot),
2427 old_cursors,
2428 new_cursors,
2429 description,
2430 edits: edit_lengths,
2431 displaced_markers,
2432 };
2433
2434 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
2436 event_log.append(bulk_edit);
2437 }
2438
2439 self.send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2441
2442 Ok(())
2443 }
2444
2445 pub(crate) fn send_lsp_changes_for_buffer(
2447 &mut self,
2448 buffer_id: BufferId,
2449 changes: Vec<TextDocumentContentChangeEvent>,
2450 ) {
2451 if changes.is_empty() {
2452 return;
2453 }
2454
2455 let metadata = match self.buffer_metadata.get(&buffer_id) {
2457 Some(m) => m,
2458 None => {
2459 tracing::debug!(
2460 "send_lsp_changes_for_buffer: no metadata for buffer {:?}",
2461 buffer_id
2462 );
2463 return;
2464 }
2465 };
2466
2467 if !metadata.lsp_enabled {
2468 tracing::debug!("send_lsp_changes_for_buffer: LSP disabled for this buffer");
2469 return;
2470 }
2471
2472 let uri = match metadata.file_uri() {
2474 Some(u) => u.clone(),
2475 None => {
2476 tracing::debug!(
2477 "send_lsp_changes_for_buffer: no URI for buffer (not a file or URI creation failed)"
2478 );
2479 return;
2480 }
2481 };
2482 let file_path = metadata.file_path().cloned();
2483
2484 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
2486 Some(l) => l,
2487 None => {
2488 tracing::debug!(
2489 "send_lsp_changes_for_buffer: no buffer state for {:?}",
2490 buffer_id
2491 );
2492 return;
2493 }
2494 };
2495
2496 tracing::trace!(
2497 "send_lsp_changes_for_buffer: sending {} changes to {} in single didChange notification",
2498 changes.len(),
2499 uri.as_str()
2500 );
2501
2502 use crate::services::lsp::manager::LspSpawnResult;
2504 let Some(lsp) = self.lsp.as_mut() else {
2505 tracing::debug!("send_lsp_changes_for_buffer: no LSP manager available");
2506 return;
2507 };
2508
2509 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
2510 tracing::debug!(
2511 "send_lsp_changes_for_buffer: LSP not running for {} (auto_start disabled)",
2512 language
2513 );
2514 return;
2515 }
2516
2517 let handles_needing_open: Vec<_> = {
2519 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2520 return;
2521 };
2522 lsp.get_handles(&language)
2523 .into_iter()
2524 .filter(|sh| !metadata.lsp_opened_with.contains(&sh.handle.id()))
2525 .map(|sh| (sh.name.clone(), sh.handle.id()))
2526 .collect()
2527 };
2528
2529 if !handles_needing_open.is_empty() {
2530 let text = match self
2532 .buffers
2533 .get(&buffer_id)
2534 .and_then(|s| s.buffer.to_string())
2535 {
2536 Some(t) => t,
2537 None => {
2538 tracing::debug!(
2539 "send_lsp_changes_for_buffer: buffer text not available for didOpen"
2540 );
2541 return;
2542 }
2543 };
2544
2545 let Some(lsp) = self.lsp.as_mut() else { return };
2547 for sh in lsp.get_handles_mut(&language) {
2548 if handles_needing_open
2549 .iter()
2550 .any(|(_, id)| *id == sh.handle.id())
2551 {
2552 if let Err(e) = sh
2553 .handle
2554 .did_open(uri.clone(), text.clone(), language.clone())
2555 {
2556 tracing::warn!(
2557 "Failed to send didOpen to '{}' before didChange: {}",
2558 sh.name,
2559 e
2560 );
2561 } else {
2562 tracing::debug!(
2563 "Sent didOpen for {} to LSP handle '{}' before didChange",
2564 uri.as_str(),
2565 sh.name
2566 );
2567 }
2568 }
2569 }
2570
2571 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
2573 for (_, handle_id) in &handles_needing_open {
2574 metadata.lsp_opened_with.insert(*handle_id);
2575 }
2576 }
2577
2578 return;
2582 }
2583
2584 let Some(lsp) = self.lsp.as_mut() else { return };
2586 let mut any_sent = false;
2587 for sh in lsp.get_handles_mut(&language) {
2588 if let Err(e) = sh.handle.did_change(uri.clone(), changes.clone()) {
2589 tracing::warn!("Failed to send didChange to '{}': {}", sh.name, e);
2590 } else {
2591 any_sent = true;
2592 }
2593 }
2594 if any_sent {
2595 tracing::trace!("Successfully sent batched didChange to LSP");
2596
2597 if let Some(state) = self.buffers.get(&buffer_id) {
2600 if let Some(path) = state.buffer.file_path() {
2601 crate::services::lsp::diagnostics::invalidate_cache_for_file(
2602 &path.to_string_lossy(),
2603 );
2604 }
2605 }
2606
2607 self.scheduled_diagnostic_pull = Some((
2609 buffer_id,
2610 std::time::Instant::now() + std::time::Duration::from_millis(1000),
2611 ));
2612 }
2613 }
2614
2615 pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2617 if self.server_supports_prepare_rename() {
2619 self.send_prepare_rename();
2620 return Ok(());
2621 }
2622
2623 self.show_rename_prompt()
2624 }
2625
2626 pub(crate) fn handle_prepare_rename_response(
2628 &mut self,
2629 result: Result<serde_json::Value, String>,
2630 ) {
2631 match result {
2632 Ok(value) if !value.is_null() => {
2633 if let Err(e) = self.show_rename_prompt() {
2635 self.set_status_message(format!("Rename failed: {e}"));
2636 }
2637 }
2638 Ok(_) => {
2639 self.set_status_message("Cannot rename at this position".to_string());
2640 }
2641 Err(e) => {
2642 self.set_status_message(format!("Cannot rename: {e}"));
2643 }
2644 }
2645 }
2646
2647 fn server_supports_prepare_rename(&self) -> bool {
2649 let language = match self
2650 .buffers
2651 .get(&self.active_buffer())
2652 .map(|s| s.language.clone())
2653 {
2654 Some(l) => l,
2655 None => return false,
2656 };
2657
2658 if let Some(lsp) = &self.lsp {
2659 for sh in lsp.get_handles(&language) {
2660 if sh.capabilities.rename {
2661 return true;
2664 }
2665 }
2666 }
2667 false
2668 }
2669
2670 fn send_prepare_rename(&mut self) {
2672 let cursor_pos = self.active_cursors().primary().position;
2673 let (line, character) = self
2674 .active_state()
2675 .buffer
2676 .position_to_lsp_position(cursor_pos);
2677
2678 let buffer_id = self.active_buffer();
2679 let metadata = match self.buffer_metadata.get(&buffer_id) {
2680 Some(m) if m.lsp_enabled => m,
2681 _ => return,
2682 };
2683 let uri = match metadata.file_uri() {
2684 Some(u) => u.clone(),
2685 None => return,
2686 };
2687 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
2688 Some(l) => l,
2689 None => return,
2690 };
2691
2692 self.next_lsp_request_id += 1;
2693 let request_id = self.next_lsp_request_id;
2694
2695 if let Some(lsp) = &mut self.lsp {
2696 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Rename) {
2697 if let Err(e) =
2698 sh.handle
2699 .prepare_rename(request_id, uri, line as u32, character as u32)
2700 {
2701 tracing::warn!("Failed to send prepareRename: {}", e);
2702 }
2703 }
2704 }
2705 }
2706
2707 fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2709 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2710
2711 let cursor_pos = self.active_cursors().primary().position;
2713 let (word_start, word_end) = {
2714 let state = self.active_state();
2715
2716 let word_start = find_word_start(&state.buffer, cursor_pos);
2718 let word_end = find_word_end(&state.buffer, cursor_pos);
2719
2720 if word_start >= word_end {
2722 self.status_message = Some(t!("lsp.no_symbol_at_cursor").to_string());
2723 return Ok(());
2724 }
2725
2726 (word_start, word_end)
2727 };
2728
2729 let word_text = self.active_state_mut().get_text_range(word_start, word_end);
2731
2732 let overlay_handle = self.add_overlay(
2734 None,
2735 word_start..word_end,
2736 crate::model::event::OverlayFace::Background {
2737 color: (50, 100, 200), },
2739 100,
2740 Some(t!("lsp.popup_renaming").to_string()),
2741 );
2742
2743 let mut prompt = Prompt::new(
2746 "Rename to: ".to_string(),
2747 PromptType::LspRename {
2748 original_text: word_text.clone(),
2749 start_pos: word_start,
2750 end_pos: word_end,
2751 overlay_handle,
2752 },
2753 );
2754 prompt.set_input(word_text);
2756
2757 self.prompt = Some(prompt);
2758 Ok(())
2759 }
2760
2761 pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
2763 self.remove_overlay(handle.clone());
2764 }
2765
2766 pub(crate) fn perform_lsp_rename(
2768 &mut self,
2769 new_name: String,
2770 original_text: String,
2771 start_pos: usize,
2772 overlay_handle: crate::view::overlay::OverlayHandle,
2773 ) {
2774 self.cancel_rename_overlay(&overlay_handle);
2776
2777 if new_name == original_text {
2779 self.status_message = Some(t!("lsp.name_unchanged").to_string());
2780 return;
2781 }
2782
2783 let rename_pos = start_pos;
2786
2787 let state = self.active_state();
2790 let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
2791 let buffer_id = self.active_buffer();
2792 let request_id = self.next_lsp_request_id;
2793
2794 let sent = self
2796 .with_lsp_for_buffer(buffer_id, LspFeature::Rename, |handle, uri, _language| {
2797 let result = handle.rename(
2798 request_id,
2799 uri.clone(),
2800 line as u32,
2801 character as u32,
2802 new_name.clone(),
2803 );
2804 if result.is_ok() {
2805 tracing::info!(
2806 "Requested rename at {}:{}:{} to '{}'",
2807 uri.as_str(),
2808 line,
2809 character,
2810 new_name
2811 );
2812 }
2813 result.is_ok()
2814 })
2815 .unwrap_or(false);
2816
2817 if sent {
2818 self.next_lsp_request_id += 1;
2819 self.lsp_status = "LSP: rename...".to_string();
2820 } else if self
2821 .buffer_metadata
2822 .get(&buffer_id)
2823 .and_then(|m| m.file_path())
2824 .is_none()
2825 {
2826 self.status_message = Some(t!("lsp.cannot_rename_unsaved").to_string());
2827 }
2828 }
2829
2830 pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
2832 if !self.config.editor.enable_inlay_hints {
2833 return;
2834 }
2835
2836 let buffer_id = self.active_buffer();
2837
2838 let line_count = if let Some(state) = self.buffers.get(&buffer_id) {
2840 state.buffer.line_count().unwrap_or(1000)
2841 } else {
2842 return;
2843 };
2844 let last_line = line_count.saturating_sub(1) as u32;
2845 let request_id = self.next_lsp_request_id;
2846
2847 let sent = self
2849 .with_lsp_for_buffer(
2850 buffer_id,
2851 LspFeature::InlayHints,
2852 |handle, uri, _language| {
2853 let result =
2854 handle.inlay_hints(request_id, uri.clone(), 0, 0, last_line, 10000);
2855 if result.is_ok() {
2856 tracing::info!(
2857 "Requested inlay hints for {} (request_id={})",
2858 uri.as_str(),
2859 request_id
2860 );
2861 } else if let Err(e) = &result {
2862 tracing::debug!("Failed to request inlay hints: {}", e);
2863 }
2864 result.is_ok()
2865 },
2866 )
2867 .unwrap_or(false);
2868
2869 if sent {
2870 self.next_lsp_request_id += 1;
2871 self.pending_inlay_hints_request = Some(request_id);
2872 }
2873 }
2874
2875 pub(crate) fn schedule_folding_ranges_refresh(&mut self, buffer_id: BufferId) {
2877 let next_time = Instant::now() + Duration::from_millis(FOLDING_RANGES_DEBOUNCE_MS);
2878 self.folding_ranges_debounce.insert(buffer_id, next_time);
2879 }
2880
2881 pub(crate) fn maybe_request_folding_ranges_debounced(&mut self, buffer_id: BufferId) {
2883 let Some(ready_at) = self.folding_ranges_debounce.get(&buffer_id).copied() else {
2884 return;
2885 };
2886 if Instant::now() < ready_at {
2887 return;
2888 }
2889
2890 self.folding_ranges_debounce.remove(&buffer_id);
2891 self.request_folding_ranges_for_buffer(buffer_id);
2892 }
2893
2894 pub(crate) fn request_folding_ranges_for_buffer(&mut self, buffer_id: BufferId) {
2896 if self.folding_ranges_in_flight.contains_key(&buffer_id) {
2897 return;
2898 }
2899
2900 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2901 return;
2902 };
2903 if !metadata.lsp_enabled {
2904 return;
2905 }
2906 let Some(uri) = metadata.file_uri().cloned() else {
2907 return;
2908 };
2909 let file_path = metadata.file_path().cloned();
2910
2911 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2912 return;
2913 };
2914
2915 let Some(lsp) = self.lsp.as_mut() else {
2916 return;
2917 };
2918
2919 if !lsp.folding_ranges_supported(&language) {
2920 return;
2921 }
2922
2923 use crate::services::lsp::manager::LspSpawnResult;
2925 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
2926 return;
2927 }
2928
2929 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::FoldingRange) else {
2930 return;
2931 };
2932 let handle = &mut sh.handle;
2933
2934 let request_id = self.next_lsp_request_id;
2935 self.next_lsp_request_id += 1;
2936 let buffer_version = self
2937 .buffers
2938 .get(&buffer_id)
2939 .map(|s| s.buffer.version())
2940 .unwrap_or(0);
2941
2942 match handle.folding_ranges(request_id, uri) {
2943 Ok(()) => {
2944 self.pending_folding_range_requests.insert(
2945 request_id,
2946 super::FoldingRangeRequest {
2947 buffer_id,
2948 version: buffer_version,
2949 },
2950 );
2951 self.folding_ranges_in_flight
2952 .insert(buffer_id, (request_id, buffer_version));
2953 }
2954 Err(e) => {
2955 tracing::debug!("Failed to request folding ranges: {}", e);
2956 }
2957 }
2958 }
2959
2960 pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
2962 if !self.config.editor.enable_semantic_tokens_full {
2963 return;
2964 }
2965
2966 if self.semantic_tokens_in_flight.contains_key(&buffer_id) {
2968 return;
2969 }
2970
2971 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2972 return;
2973 };
2974 if !metadata.lsp_enabled {
2975 return;
2976 }
2977 let Some(uri) = metadata.file_uri().cloned() else {
2978 return;
2979 };
2980 let file_path_for_spawn = metadata.file_path().cloned();
2981 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2983 return;
2984 };
2985
2986 let Some(lsp) = self.lsp.as_mut() else {
2987 return;
2988 };
2989
2990 use crate::services::lsp::manager::LspSpawnResult;
2992 if lsp.try_spawn(&language, file_path_for_spawn.as_deref()) != LspSpawnResult::Spawned {
2993 return;
2994 }
2995
2996 if !lsp.semantic_tokens_full_supported(&language) {
2998 return;
2999 }
3000 if lsp.semantic_tokens_legend(&language).is_none() {
3001 return;
3002 }
3003
3004 let Some(state) = self.buffers.get(&buffer_id) else {
3005 return;
3006 };
3007 let buffer_version = state.buffer.version();
3008 if let Some(store) = state.semantic_tokens.as_ref() {
3009 if store.version == buffer_version {
3010 return; }
3012 }
3013
3014 let previous_result_id = state
3015 .semantic_tokens
3016 .as_ref()
3017 .and_then(|store| store.result_id.clone());
3018
3019 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3020 return;
3021 };
3022 let supports_delta = sh.capabilities.semantic_tokens_full_delta;
3024 let use_delta = previous_result_id.is_some() && supports_delta;
3025 let handle = &mut sh.handle;
3026
3027 let request_id = self.next_lsp_request_id;
3028 self.next_lsp_request_id += 1;
3029
3030 let request_kind = if use_delta {
3031 super::SemanticTokensFullRequestKind::FullDelta
3032 } else {
3033 super::SemanticTokensFullRequestKind::Full
3034 };
3035
3036 let request_result = if use_delta {
3037 handle.semantic_tokens_full_delta(request_id, uri, previous_result_id.unwrap())
3038 } else {
3039 handle.semantic_tokens_full(request_id, uri)
3040 };
3041
3042 match request_result {
3043 Ok(_) => {
3044 self.pending_semantic_token_requests.insert(
3045 request_id,
3046 super::SemanticTokenFullRequest {
3047 buffer_id,
3048 version: buffer_version,
3049 kind: request_kind,
3050 },
3051 );
3052 self.semantic_tokens_in_flight
3053 .insert(buffer_id, (request_id, buffer_version, request_kind));
3054 }
3055 Err(e) => {
3056 tracing::debug!("Failed to request semantic tokens: {}", e);
3057 }
3058 }
3059 }
3060
3061 pub(crate) fn schedule_semantic_tokens_full_refresh(&mut self, buffer_id: BufferId) {
3063 if !self.config.editor.enable_semantic_tokens_full {
3064 return;
3065 }
3066
3067 let next_time = Instant::now() + Duration::from_millis(SEMANTIC_TOKENS_FULL_DEBOUNCE_MS);
3068 self.semantic_tokens_full_debounce
3069 .insert(buffer_id, next_time);
3070 }
3071
3072 pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
3074 if !self.config.editor.enable_semantic_tokens_full {
3075 self.semantic_tokens_full_debounce.remove(&buffer_id);
3076 return;
3077 }
3078
3079 let Some(ready_at) = self.semantic_tokens_full_debounce.get(&buffer_id).copied() else {
3080 return;
3081 };
3082 if Instant::now() < ready_at {
3083 return;
3084 }
3085
3086 self.semantic_tokens_full_debounce.remove(&buffer_id);
3087 self.maybe_request_semantic_tokens(buffer_id);
3088 }
3089
3090 pub(crate) fn maybe_request_semantic_tokens_range(
3092 &mut self,
3093 buffer_id: BufferId,
3094 start_line: usize,
3095 end_line: usize,
3096 ) {
3097 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
3098 return;
3099 };
3100 if !metadata.lsp_enabled {
3101 return;
3102 }
3103 let Some(uri) = metadata.file_uri().cloned() else {
3104 return;
3105 };
3106 let file_path = metadata.file_path().cloned();
3107 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
3109 return;
3110 };
3111
3112 let Some(lsp) = self.lsp.as_mut() else {
3113 return;
3114 };
3115
3116 use crate::services::lsp::manager::LspSpawnResult;
3118 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3119 return;
3120 }
3121
3122 if !lsp.semantic_tokens_range_supported(&language) {
3123 self.maybe_request_semantic_tokens(buffer_id);
3125 return;
3126 }
3127 if lsp.semantic_tokens_legend(&language).is_none() {
3128 return;
3129 }
3130
3131 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3132 return;
3133 };
3134 if !sh.capabilities.semantic_tokens_range {
3137 return;
3138 }
3139 let handle = &mut sh.handle;
3140 let Some(state) = self.buffers.get(&buffer_id) else {
3141 return;
3142 };
3143
3144 let buffer_version = state.buffer.version();
3145 let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3146 let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3147
3148 if let Some(line_count) = state.buffer.line_count() {
3149 if line_count == 0 {
3150 return;
3151 }
3152 let max_line = line_count.saturating_sub(1);
3153 padded_start = padded_start.min(max_line);
3154 padded_end = padded_end.min(max_line);
3155 }
3156
3157 let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
3158 let end_char = state
3159 .buffer
3160 .get_line(padded_end)
3161 .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
3162 .unwrap_or(0);
3163 let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
3164 state.buffer.lsp_position_to_byte(padded_end, end_char)
3165 } else {
3166 state.buffer.len()
3167 };
3168
3169 if start_byte >= end_byte {
3170 return;
3171 }
3172
3173 let range = start_byte..end_byte;
3174 if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
3175 self.semantic_tokens_range_in_flight.get(&buffer_id)
3176 {
3177 if *in_flight_start == padded_start
3178 && *in_flight_end == padded_end
3179 && *in_flight_version == buffer_version
3180 {
3181 return;
3182 }
3183 if let Err(e) = handle.cancel_request(*in_flight_id) {
3184 tracing::debug!("Failed to cancel semantic token range request: {}", e);
3185 }
3186 self.pending_semantic_token_range_requests
3187 .remove(in_flight_id);
3188 self.semantic_tokens_range_in_flight.remove(&buffer_id);
3189 }
3190
3191 if let Some((applied_start, applied_end, applied_version)) =
3192 self.semantic_tokens_range_applied.get(&buffer_id)
3193 {
3194 if *applied_start == padded_start
3195 && *applied_end == padded_end
3196 && *applied_version == buffer_version
3197 {
3198 return;
3199 }
3200 }
3201
3202 let now = Instant::now();
3203 if let Some((last_start, last_end, last_version, last_time)) =
3204 self.semantic_tokens_range_last_request.get(&buffer_id)
3205 {
3206 if *last_start == padded_start
3207 && *last_end == padded_end
3208 && *last_version == buffer_version
3209 && now.duration_since(*last_time)
3210 < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
3211 {
3212 return;
3213 }
3214 }
3215
3216 let lsp_range = lsp_types::Range {
3217 start: lsp_types::Position {
3218 line: padded_start as u32,
3219 character: 0,
3220 },
3221 end: lsp_types::Position {
3222 line: padded_end as u32,
3223 character: end_char as u32,
3224 },
3225 };
3226
3227 let request_id = self.next_lsp_request_id;
3228 self.next_lsp_request_id += 1;
3229
3230 match handle.semantic_tokens_range(request_id, uri, lsp_range) {
3231 Ok(_) => {
3232 self.pending_semantic_token_range_requests.insert(
3233 request_id,
3234 SemanticTokenRangeRequest {
3235 buffer_id,
3236 version: buffer_version,
3237 range: range.clone(),
3238 start_line: padded_start,
3239 end_line: padded_end,
3240 },
3241 );
3242 self.semantic_tokens_range_in_flight.insert(
3243 buffer_id,
3244 (request_id, padded_start, padded_end, buffer_version),
3245 );
3246 self.semantic_tokens_range_last_request
3247 .insert(buffer_id, (padded_start, padded_end, buffer_version, now));
3248 }
3249 Err(e) => {
3250 tracing::debug!("Failed to request semantic token range: {}", e);
3251 }
3252 }
3253 }
3254}
3255
3256#[cfg(test)]
3257mod tests {
3258 use crate::model::filesystem::StdFileSystem;
3259 use std::sync::Arc;
3260
3261 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
3262 Arc::new(StdFileSystem)
3263 }
3264 use super::Editor;
3265 use crate::model::buffer::Buffer;
3266 use crate::state::EditorState;
3267 use crate::view::virtual_text::VirtualTextPosition;
3268 use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
3269
3270 fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
3271 InlayHint {
3272 position: Position { line, character },
3273 label: InlayHintLabel::String(label.to_string()),
3274 kind,
3275 text_edits: None,
3276 tooltip: None,
3277 padding_left: None,
3278 padding_right: None,
3279 data: None,
3280 }
3281 }
3282
3283 #[test]
3284 fn test_inlay_hint_inserts_before_character() {
3285 let mut state = EditorState::new(
3286 80,
3287 24,
3288 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3289 test_fs(),
3290 );
3291 state.buffer = Buffer::from_str_test("ab");
3292
3293 if !state.buffer.is_empty() {
3294 state.marker_list.adjust_for_insert(0, state.buffer.len());
3295 }
3296
3297 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3298 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3299
3300 let lookup = state
3301 .virtual_texts
3302 .build_lookup(&state.marker_list, 0, state.buffer.len());
3303 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3304 assert_eq!(vtexts.len(), 1);
3305 assert_eq!(vtexts[0].text, ": i32");
3306 assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
3307 }
3308
3309 #[test]
3310 fn test_inlay_hint_at_eof_renders_after_last_char() {
3311 let mut state = EditorState::new(
3312 80,
3313 24,
3314 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3315 test_fs(),
3316 );
3317 state.buffer = Buffer::from_str_test("ab");
3318
3319 if !state.buffer.is_empty() {
3320 state.marker_list.adjust_for_insert(0, state.buffer.len());
3321 }
3322
3323 let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
3324 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3325
3326 let lookup = state
3327 .virtual_texts
3328 .build_lookup(&state.marker_list, 0, state.buffer.len());
3329 let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
3330 assert_eq!(vtexts.len(), 1);
3331 assert_eq!(vtexts[0].text, ": i32");
3332 assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
3333 }
3334
3335 #[test]
3336 fn test_inlay_hint_empty_buffer_is_ignored() {
3337 let mut state = EditorState::new(
3338 80,
3339 24,
3340 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3341 test_fs(),
3342 );
3343 state.buffer = Buffer::from_str_test("");
3344
3345 let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
3346 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3347
3348 assert!(state.virtual_texts.is_empty());
3349 }
3350
3351 #[test]
3352 fn test_space_doc_paragraphs_inserts_blank_lines() {
3353 use super::space_doc_paragraphs;
3354
3355 let input = "sep\n description.\nend\n another.";
3357 let result = space_doc_paragraphs(input);
3358 assert_eq!(result, "sep\n\n description.\n\nend\n\n another.");
3359 }
3360
3361 #[test]
3362 fn test_space_doc_paragraphs_preserves_existing_blank_lines() {
3363 use super::space_doc_paragraphs;
3364
3365 let input = "First paragraph.\n\nSecond paragraph.";
3367 let result = space_doc_paragraphs(input);
3368 assert_eq!(result, "First paragraph.\n\nSecond paragraph.");
3369 }
3370
3371 #[test]
3372 fn test_space_doc_paragraphs_plain_text() {
3373 use super::space_doc_paragraphs;
3374
3375 let input = "Just a single line of docs.";
3376 let result = space_doc_paragraphs(input);
3377 assert_eq!(result, "Just a single line of docs.");
3378 }
3379}