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 let cursor_pos = self.active_cursors().primary().position;
537 let state = self.active_state();
538
539 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
541 let buffer_id = self.active_buffer();
542
543 let base_request_id = self.next_lsp_request_id;
545 let counter = std::sync::atomic::AtomicU64::new(0);
547
548 let results = self.with_all_lsp_for_buffer_feature(
549 buffer_id,
550 LspFeature::Completion,
551 |handle, uri, _language| {
552 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
553 let request_id = base_request_id + idx;
554 let result =
555 handle.completion(request_id, uri.clone(), line as u32, character as u32);
556 if result.is_ok() {
557 tracing::info!(
558 "Requested completion at {}:{}:{} (request_id={})",
559 uri.as_str(),
560 line,
561 character,
562 request_id
563 );
564 }
565 (request_id, result.is_ok())
566 },
567 );
568
569 let mut sent_ids = Vec::new();
570 for (request_id, ok) in &results {
571 if *ok {
572 sent_ids.push(*request_id);
573 }
574 }
575 self.next_lsp_request_id = base_request_id + results.len() as u64;
577
578 if !sent_ids.is_empty() {
579 self.pending_completion_requests.extend(sent_ids);
580 self.lsp_status = "LSP: completion...".to_string();
581 } else {
582 self.show_buffer_word_completion_popup();
584 }
585 }
586
587 fn show_buffer_word_completion_popup(&mut self) {
591 let items = self.get_buffer_completion_popup_items();
592 if items.is_empty() {
593 return;
594 }
595
596 let popup_data = crate::app::popup_actions::build_completion_popup_from_items(items, 0);
597 let accept_hint = self.completion_accept_key_hint();
598
599 let buffer_id = self.active_buffer();
600 let state = self.buffers.get_mut(&buffer_id).unwrap();
601 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
602 popup_obj.accept_key_hint = accept_hint;
603 state.popups.show_or_replace(popup_obj);
604 }
605
606 pub(crate) fn maybe_trigger_completion(&mut self, c: char) {
616 if !self.config.editor.completion_popup_auto_show {
618 return;
619 }
620
621 let language = self.active_state().language.clone();
623
624 let is_lsp_trigger = self
626 .lsp
627 .as_ref()
628 .map(|lsp| lsp.is_completion_trigger_char(c, &language))
629 .unwrap_or(false);
630
631 let quick_suggestions_enabled = self.config.editor.quick_suggestions;
633 let suggest_on_trigger_chars = self.config.editor.suggest_on_trigger_characters;
634 let is_word_char = c.is_alphanumeric() || c == '_';
635
636 if is_lsp_trigger && suggest_on_trigger_chars {
638 tracing::debug!(
639 "Trigger character '{}' immediately triggers completion for language {}",
640 c,
641 language
642 );
643 self.scheduled_completion_trigger = None;
645 self.request_completion();
646 return;
647 }
648
649 if quick_suggestions_enabled && is_word_char {
651 let delay_ms = self.config.editor.quick_suggestions_delay_ms;
652 let trigger_time = Instant::now() + Duration::from_millis(delay_ms);
653
654 tracing::debug!(
655 "Scheduling completion trigger in {}ms for language {} (char '{}')",
656 delay_ms,
657 language,
658 c
659 );
660
661 self.scheduled_completion_trigger = Some(trigger_time);
664 } else {
665 self.scheduled_completion_trigger = None;
669 }
670 }
671
672 pub(crate) fn request_goto_definition(&mut self) -> AnyhowResult<()> {
674 let cursor_pos = self.active_cursors().primary().position;
676 let state = self.active_state();
677
678 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
680 let buffer_id = self.active_buffer();
681 let request_id = self.next_lsp_request_id;
682
683 let sent = self
685 .with_lsp_for_buffer(
686 buffer_id,
687 LspFeature::Definition,
688 |handle, uri, _language| {
689 let result = handle.goto_definition(
690 request_id,
691 uri.clone(),
692 line as u32,
693 character as u32,
694 );
695 if result.is_ok() {
696 tracing::info!(
697 "Requested go-to-definition at {}:{}:{}",
698 uri.as_str(),
699 line,
700 character
701 );
702 }
703 result.is_ok()
704 },
705 )
706 .unwrap_or(false);
707
708 if sent {
709 self.next_lsp_request_id += 1;
710 self.pending_goto_definition_request = Some(request_id);
711 }
712
713 Ok(())
714 }
715
716 pub(crate) fn request_hover(&mut self) -> AnyhowResult<()> {
718 let cursor_pos = self.active_cursors().primary().position;
720 let state = self.active_state();
721
722 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
724
725 if let Some(pos) = state.buffer.offset_to_position(cursor_pos) {
727 tracing::debug!(
728 "Hover request: cursor_byte={}, line={}, byte_col={}, utf16_col={}",
729 cursor_pos,
730 pos.line,
731 pos.column,
732 character
733 );
734 }
735
736 let buffer_id = self.active_buffer();
737 let request_id = self.next_lsp_request_id;
738
739 let sent = self
741 .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
742 let result = handle.hover(request_id, uri.clone(), line as u32, character as u32);
743 if result.is_ok() {
744 tracing::info!(
745 "Requested hover at {}:{}:{} (byte_pos={})",
746 uri.as_str(),
747 line,
748 character,
749 cursor_pos
750 );
751 }
752 result.is_ok()
753 })
754 .unwrap_or(false);
755
756 if sent {
757 self.next_lsp_request_id += 1;
758 self.pending_hover_request = Some(request_id);
759 self.lsp_status = "LSP: hover...".to_string();
760 }
761
762 Ok(())
763 }
764
765 pub(crate) fn request_hover_at_position(&mut self, byte_pos: usize) -> AnyhowResult<bool> {
770 let state = self.active_state();
772
773 let (line, character) = state.buffer.position_to_lsp_position(byte_pos);
775
776 if let Some(pos) = state.buffer.offset_to_position(byte_pos) {
778 tracing::trace!(
779 "Mouse hover request: byte_pos={}, line={}, byte_col={}, utf16_col={}",
780 byte_pos,
781 pos.line,
782 pos.column,
783 character
784 );
785 }
786
787 let buffer_id = self.active_buffer();
788 let request_id = self.next_lsp_request_id;
789
790 let sent = self
792 .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
793 let result = handle.hover(request_id, uri.clone(), line as u32, character as u32);
794 if result.is_ok() {
795 tracing::trace!(
796 "Mouse hover requested at {}:{}:{} (byte_pos={})",
797 uri.as_str(),
798 line,
799 character,
800 byte_pos
801 );
802 }
803 result.is_ok()
804 })
805 .unwrap_or(false);
806
807 if sent {
808 self.next_lsp_request_id += 1;
809 self.pending_hover_request = Some(request_id);
810 self.lsp_status = "LSP: hover...".to_string();
811 }
812
813 Ok(sent)
814 }
815
816 pub(crate) fn handle_hover_response(
818 &mut self,
819 request_id: u64,
820 contents: String,
821 is_markdown: bool,
822 range: Option<((u32, u32), (u32, u32))>,
823 ) {
824 if self.pending_hover_request != Some(request_id) {
826 tracing::debug!("Ignoring stale hover response: {}", request_id);
827 return;
828 }
829
830 self.pending_hover_request = None;
831 self.update_lsp_status_from_server_statuses();
832
833 if contents.is_empty() {
834 self.set_status_message(t!("lsp.no_hover").to_string());
835 self.hover_symbol_range = None;
836 return;
837 }
838
839 tracing::debug!(
841 "LSP hover content (markdown={}):\n{}",
842 is_markdown,
843 contents
844 );
845
846 if let Some(((start_line, start_char), (end_line, end_char))) = range {
848 let state = self.active_state();
849 let start_byte = state
850 .buffer
851 .lsp_position_to_byte(start_line as usize, start_char as usize);
852 let end_byte = state
853 .buffer
854 .lsp_position_to_byte(end_line as usize, end_char as usize);
855 self.hover_symbol_range = Some((start_byte, end_byte));
856 tracing::debug!(
857 "Hover symbol range: {}..{} (LSP {}:{}..{}:{})",
858 start_byte,
859 end_byte,
860 start_line,
861 start_char,
862 end_line,
863 end_char
864 );
865
866 if let Some(old_handle) = self.hover_symbol_overlay.take() {
868 let remove_event = crate::model::event::Event::RemoveOverlay { handle: old_handle };
869 self.apply_event_to_active_buffer(&remove_event);
870 }
871
872 let event = crate::model::event::Event::AddOverlay {
874 namespace: None,
875 range: start_byte..end_byte,
876 face: crate::model::event::OverlayFace::Background {
877 color: (80, 80, 120), },
879 priority: 90, message: None,
881 extend_to_line_end: false,
882 url: None,
883 };
884 self.apply_event_to_active_buffer(&event);
885 if let Some(state) = self.buffers.get(&self.active_buffer()) {
887 self.hover_symbol_overlay = state.overlays.all().last().map(|o| o.handle.clone());
888 }
889 } else {
890 if let Some((hover_byte_pos, _, _, _)) = self.mouse_state.lsp_hover_state {
893 let state = self.active_state();
894 let start_byte = find_word_start(&state.buffer, hover_byte_pos);
895 let end_byte = find_word_end(&state.buffer, hover_byte_pos);
896 if start_byte < end_byte {
897 self.hover_symbol_range = Some((start_byte, end_byte));
898 tracing::debug!(
899 "Hover symbol range (computed from word boundaries): {}..{}",
900 start_byte,
901 end_byte
902 );
903 } else {
904 self.hover_symbol_range = None;
905 }
906 } else {
907 self.hover_symbol_range = None;
908 }
909 }
910
911 use crate::view::popup::{Popup, PopupPosition};
913 use ratatui::style::Style;
914
915 let mut popup = if is_markdown {
917 Popup::markdown(&contents, &self.theme, Some(&self.grammar_registry))
918 } else {
919 let lines: Vec<String> = contents.lines().map(|s| s.to_string()).collect();
921 Popup::text(lines, &self.theme)
922 };
923
924 popup.title = Some(t!("lsp.popup_hover").to_string());
926 popup.transient = true;
927 popup.position = if let Some((x, y)) = self.mouse_hover_screen_position.take() {
929 PopupPosition::Fixed { x, y: y + 1 }
931 } else {
932 PopupPosition::BelowCursor
933 };
934 popup.width = 80;
935 let dynamic_height = (self.terminal_height * 60 / 100).clamp(15, 40);
938 popup.max_height = dynamic_height;
939 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
940 popup.background_style = Style::default().bg(self.theme.popup_bg);
941
942 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
944 state.popups.show(popup);
945 tracing::info!("Showing hover popup (markdown={})", is_markdown);
946 }
947
948 self.mouse_state.lsp_hover_request_sent = true;
951 }
952
953 pub(crate) fn apply_inlay_hints_to_state(
955 state: &mut crate::state::EditorState,
956 hints: &[lsp_types::InlayHint],
957 ) {
958 use crate::view::virtual_text::VirtualTextPosition;
959 use ratatui::style::{Color, Style};
960
961 state.virtual_texts.clear(&mut state.marker_list);
963
964 if hints.is_empty() {
965 return;
966 }
967
968 let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
970
971 for hint in hints {
972 let byte_offset = state.buffer.lsp_position_to_byte(
974 hint.position.line as usize,
975 hint.position.character as usize,
976 );
977
978 let text = match &hint.label {
980 lsp_types::InlayHintLabel::String(s) => s.clone(),
981 lsp_types::InlayHintLabel::LabelParts(parts) => {
982 parts.iter().map(|p| p.value.as_str()).collect::<String>()
983 }
984 };
985
986 if state.buffer.is_empty() {
992 continue;
993 }
994
995 let (byte_offset, position) = if byte_offset >= state.buffer.len() {
996 (
998 state.buffer.len().saturating_sub(1),
999 VirtualTextPosition::AfterChar,
1000 )
1001 } else {
1002 (byte_offset, VirtualTextPosition::BeforeChar)
1003 };
1004
1005 let display_text = text;
1007
1008 state.virtual_texts.add(
1009 &mut state.marker_list,
1010 byte_offset,
1011 display_text,
1012 hint_style,
1013 position,
1014 0, );
1016 }
1017
1018 tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
1019 }
1020
1021 pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
1023 let cursor_pos = self.active_cursors().primary().position;
1025 let state = self.active_state();
1026
1027 let symbol = {
1029 let text = match state.buffer.to_string() {
1030 Some(t) => t,
1031 None => {
1032 self.set_status_message(t!("error.buffer_not_loaded").to_string());
1033 return Ok(());
1034 }
1035 };
1036 let bytes = text.as_bytes();
1037 let buf_len = bytes.len();
1038
1039 if cursor_pos <= buf_len {
1040 let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
1042
1043 let mut start = cursor_pos;
1045 while start > 0 {
1046 start -= 1;
1048 while start > 0 && (bytes[start] & 0xC0) == 0x80 {
1050 start -= 1;
1051 }
1052 if let Some(ch) = text[start..].chars().next() {
1054 if !is_word_char(ch) {
1055 start += ch.len_utf8();
1056 break;
1057 }
1058 } else {
1059 break;
1060 }
1061 }
1062
1063 let mut end = cursor_pos;
1065 while end < buf_len {
1066 if let Some(ch) = text[end..].chars().next() {
1067 if is_word_char(ch) {
1068 end += ch.len_utf8();
1069 } else {
1070 break;
1071 }
1072 } else {
1073 break;
1074 }
1075 }
1076
1077 if start < end {
1078 text[start..end].to_string()
1079 } else {
1080 String::new()
1081 }
1082 } else {
1083 String::new()
1084 }
1085 };
1086
1087 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1089 let buffer_id = self.active_buffer();
1090 let request_id = self.next_lsp_request_id;
1091
1092 let sent = self
1094 .with_lsp_for_buffer(
1095 buffer_id,
1096 LspFeature::References,
1097 |handle, uri, _language| {
1098 let result =
1099 handle.references(request_id, uri.clone(), line as u32, character as u32);
1100 if result.is_ok() {
1101 tracing::info!(
1102 "Requested find references at {}:{}:{} (byte_pos={})",
1103 uri.as_str(),
1104 line,
1105 character,
1106 cursor_pos
1107 );
1108 }
1109 result.is_ok()
1110 },
1111 )
1112 .unwrap_or(false);
1113
1114 if sent {
1115 self.next_lsp_request_id += 1;
1116 self.pending_references_request = Some(request_id);
1117 self.pending_references_symbol = symbol;
1118 self.lsp_status = "LSP: finding references...".to_string();
1119 }
1120
1121 Ok(())
1122 }
1123
1124 pub(crate) fn request_signature_help(&mut self) {
1126 let cursor_pos = self.active_cursors().primary().position;
1128 let state = self.active_state();
1129
1130 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1132 let buffer_id = self.active_buffer();
1133 let request_id = self.next_lsp_request_id;
1134
1135 let sent = self
1137 .with_lsp_for_buffer(
1138 buffer_id,
1139 LspFeature::SignatureHelp,
1140 |handle, uri, _language| {
1141 let result = handle.signature_help(
1142 request_id,
1143 uri.clone(),
1144 line as u32,
1145 character as u32,
1146 );
1147 if result.is_ok() {
1148 tracing::info!(
1149 "Requested signature help at {}:{}:{} (byte_pos={})",
1150 uri.as_str(),
1151 line,
1152 character,
1153 cursor_pos
1154 );
1155 }
1156 result.is_ok()
1157 },
1158 )
1159 .unwrap_or(false);
1160
1161 if sent {
1162 self.next_lsp_request_id += 1;
1163 self.pending_signature_help_request = Some(request_id);
1164 self.lsp_status = "LSP: signature help...".to_string();
1165 }
1166 }
1167
1168 pub(crate) fn handle_signature_help_response(
1170 &mut self,
1171 request_id: u64,
1172 signature_help: Option<lsp_types::SignatureHelp>,
1173 ) {
1174 if self.pending_signature_help_request != Some(request_id) {
1176 tracing::debug!("Ignoring stale signature help response: {}", request_id);
1177 return;
1178 }
1179
1180 self.pending_signature_help_request = None;
1181 self.update_lsp_status_from_server_statuses();
1182
1183 let signature_help = match signature_help {
1184 Some(help) if !help.signatures.is_empty() => help,
1185 _ => {
1186 tracing::debug!("No signature help available");
1187 return;
1188 }
1189 };
1190
1191 let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
1193 let signature = match signature_help.signatures.get(active_signature_idx) {
1194 Some(sig) => sig,
1195 None => return,
1196 };
1197
1198 let mut content = String::new();
1200
1201 content.push_str(&signature.label);
1203 content.push('\n');
1204
1205 let active_param = signature_help
1207 .active_parameter
1208 .or(signature.active_parameter)
1209 .unwrap_or(0) as usize;
1210
1211 if let Some(params) = &signature.parameters {
1213 if let Some(param) = params.get(active_param) {
1214 let param_label = match ¶m.label {
1216 lsp_types::ParameterLabel::Simple(s) => s.clone(),
1217 lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1218 let start = offsets[0] as usize;
1220 let end = offsets[1] as usize;
1221 if end <= signature.label.len() {
1222 signature.label[start..end].to_string()
1223 } else {
1224 String::new()
1225 }
1226 }
1227 };
1228
1229 if !param_label.is_empty() {
1230 content.push_str(&format!("\n> {}\n", param_label));
1231 }
1232
1233 if let Some(doc) = ¶m.documentation {
1235 let doc_text = match doc {
1236 lsp_types::Documentation::String(s) => s.clone(),
1237 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1238 };
1239 if !doc_text.is_empty() {
1240 content.push('\n');
1241 content.push_str(&doc_text);
1242 content.push('\n');
1243 }
1244 }
1245 }
1246 }
1247
1248 if let Some(doc) = &signature.documentation {
1250 let doc_text = match doc {
1251 lsp_types::Documentation::String(s) => s.clone(),
1252 lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1253 };
1254 if !doc_text.is_empty() {
1255 content.push_str("\n---\n\n");
1256 content.push_str(&space_doc_paragraphs(&doc_text));
1257 }
1258 }
1259
1260 use crate::view::popup::{Popup, PopupPosition};
1262 use ratatui::style::Style;
1263
1264 let mut popup = Popup::markdown(&content, &self.theme, Some(&self.grammar_registry));
1265 popup.title = Some(t!("lsp.popup_signature").to_string());
1266 popup.transient = true;
1267 popup.position = PopupPosition::BelowCursor;
1268 popup.width = 60;
1269 popup.max_height = 20;
1270 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1271 popup.background_style = Style::default().bg(self.theme.popup_bg);
1272
1273 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1275 state.popups.show(popup);
1276 tracing::info!(
1277 "Showing signature help popup for {} signatures",
1278 signature_help.signatures.len()
1279 );
1280 }
1281 }
1282
1283 pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1286 let cursor_pos = self.active_cursors().primary().position;
1288 let selection_range = self.active_cursors().primary().selection_range();
1289 let state = self.active_state();
1290
1291 let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1293
1294 let (start_line, start_char, end_line, end_char) = if let Some(range) = selection_range {
1296 let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1297 let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1298 (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1299 } else {
1300 (line as u32, character as u32, line as u32, character as u32)
1301 };
1302
1303 let diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
1306 let buffer_id = self.active_buffer();
1307
1308 let base_request_id = self.next_lsp_request_id;
1310 let counter = std::sync::atomic::AtomicU64::new(0);
1311
1312 let results = self.with_all_lsp_for_buffer_feature_named(
1313 buffer_id,
1314 LspFeature::CodeAction,
1315 |handle, uri, _language, server_name| {
1316 let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1317 let request_id = base_request_id + idx;
1318 let result = handle.code_actions(
1319 request_id,
1320 uri.clone(),
1321 start_line,
1322 start_char,
1323 end_line,
1324 end_char,
1325 diagnostics.clone(),
1326 );
1327 if result.is_ok() {
1328 tracing::info!(
1329 "Requested code actions at {}:{}:{}-{}:{} (byte_pos={}, request_id={}, server={})",
1330 uri.as_str(),
1331 start_line,
1332 start_char,
1333 end_line,
1334 end_char,
1335 cursor_pos,
1336 request_id,
1337 server_name
1338 );
1339 }
1340 (request_id, result.is_ok(), server_name.to_string())
1341 },
1342 );
1343
1344 let mut sent_ids = Vec::new();
1345 for (request_id, ok, server_name) in &results {
1346 if *ok {
1347 sent_ids.push(*request_id);
1348 self.pending_code_actions_server_names
1349 .insert(*request_id, server_name.clone());
1350 }
1351 }
1352 self.next_lsp_request_id = base_request_id + results.len() as u64;
1354
1355 if !sent_ids.is_empty() {
1356 self.pending_code_actions = None;
1358 self.pending_code_actions_requests.extend(sent_ids);
1359 self.lsp_status = "LSP: code actions...".to_string();
1360 }
1361
1362 Ok(())
1363 }
1364
1365 pub(crate) fn handle_code_actions_response(
1369 &mut self,
1370 request_id: u64,
1371 actions: Vec<lsp_types::CodeActionOrCommand>,
1372 ) {
1373 if !self.pending_code_actions_requests.remove(&request_id) {
1375 tracing::debug!("Ignoring stale code actions response: {}", request_id);
1376 return;
1377 }
1378
1379 if self.pending_code_actions_requests.is_empty() {
1381 self.update_lsp_status_from_server_statuses();
1382 }
1383
1384 let server_name = self
1386 .pending_code_actions_server_names
1387 .remove(&request_id)
1388 .unwrap_or_default();
1389
1390 if actions.is_empty() {
1391 if self.pending_code_actions_requests.is_empty()
1393 && self
1394 .pending_code_actions
1395 .as_ref()
1396 .is_none_or(|a| a.is_empty())
1397 {
1398 self.set_status_message(t!("lsp.no_code_actions").to_string());
1399 }
1400 return;
1401 }
1402
1403 let tagged_actions: Vec<(String, lsp_types::CodeActionOrCommand)> = actions
1405 .into_iter()
1406 .map(|a| (server_name.clone(), a))
1407 .collect();
1408
1409 match &mut self.pending_code_actions {
1410 Some(existing) => {
1411 existing.extend(tagged_actions);
1412 tracing::debug!("Extended code actions, now {} total", existing.len());
1413 }
1414 None => {
1415 self.pending_code_actions = Some(tagged_actions);
1416 }
1417 }
1418
1419 use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1421 use ratatui::style::Style;
1422
1423 let all_actions = self.pending_code_actions.as_ref().unwrap();
1425 let multiple_servers = {
1426 let mut names = std::collections::HashSet::new();
1427 for (name, _) in all_actions {
1428 names.insert(name.as_str());
1429 }
1430 names.len() > 1
1431 };
1432
1433 let items: Vec<PopupListItem> = all_actions
1434 .iter()
1435 .enumerate()
1436 .map(|(i, (srv_name, action))| {
1437 let title = match action {
1438 lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1439 lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1440 };
1441 let kind = match action {
1442 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1443 ca.kind.as_ref().map(|k| k.as_str().to_string())
1444 }
1445 _ => None,
1446 };
1447 let detail = if multiple_servers && !srv_name.is_empty() {
1449 match kind {
1450 Some(k) => Some(format!("[{}] {}", srv_name, k)),
1451 None => Some(format!("[{}]", srv_name)),
1452 }
1453 } else {
1454 kind
1455 };
1456 PopupListItem {
1457 text: format!("{}. {}", i + 1, title),
1458 detail,
1459 icon: None,
1460 data: Some(i.to_string()),
1461 }
1462 })
1463 .collect();
1464
1465 let mut popup = Popup::list(items, &self.theme);
1466 popup.kind = crate::view::popup::PopupKind::Action;
1467 popup.title = Some(t!("lsp.popup_code_actions").to_string());
1468 popup.position = PopupPosition::BelowCursor;
1469 popup.width = 60;
1470 popup.max_height = 15;
1471 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1472 popup.background_style = Style::default().bg(self.theme.popup_bg);
1473
1474 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1476 state.popups.show_or_replace(popup);
1477 tracing::info!(
1478 "Showing code actions popup with {} actions",
1479 all_actions.len()
1480 );
1481 }
1482 }
1483
1484 pub(crate) fn execute_code_action(&mut self, index: usize) {
1486 let action = match &self.pending_code_actions {
1487 Some(actions) => actions.get(index).map(|(_, a)| a.clone()),
1488 None => None,
1489 };
1490
1491 let Some(action) = action else {
1492 tracing::warn!("Code action index {} out of range", index);
1493 return;
1494 };
1495
1496 match action {
1497 lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1498 if ca.edit.is_none()
1501 && ca.command.is_none()
1502 && ca.data.is_some()
1503 && self.server_supports_code_action_resolve()
1504 {
1505 tracing::info!(
1506 "Code action '{}' needs resolve, sending codeAction/resolve",
1507 ca.title
1508 );
1509 self.send_code_action_resolve(ca);
1510 return;
1511 }
1512 self.execute_resolved_code_action(ca);
1513 }
1514 lsp_types::CodeActionOrCommand::Command(cmd) => {
1515 self.send_execute_command(cmd);
1516 }
1517 }
1518 }
1519
1520 pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1522 let title = ca.title.clone();
1523
1524 if let Some(edit) = ca.edit {
1526 match self.apply_workspace_edit(edit) {
1527 Ok(n) => {
1528 self.set_status_message(
1529 t!("lsp.code_action_applied", title = &title, count = n).to_string(),
1530 );
1531 }
1532 Err(e) => {
1533 self.set_status_message(format!("Code action failed: {e}"));
1534 return;
1535 }
1536 }
1537 }
1538
1539 if let Some(cmd) = ca.command {
1541 self.send_execute_command(cmd);
1542 }
1543 }
1544
1545 fn send_execute_command(&mut self, cmd: lsp_types::Command) {
1547 tracing::info!("Executing LSP command: {} ({})", cmd.title, cmd.command);
1548 self.set_status_message(
1549 t!(
1550 "lsp.code_action_applied",
1551 title = &cmd.title,
1552 count = 0_usize
1553 )
1554 .to_string(),
1555 );
1556
1557 let language = match self
1559 .buffers
1560 .get(&self.active_buffer())
1561 .map(|s| s.language.clone())
1562 {
1563 Some(l) => l,
1564 None => return,
1565 };
1566
1567 if let Some(lsp) = &mut self.lsp {
1568 for sh in lsp.get_handles_mut(&language) {
1569 if let Err(e) = sh
1570 .handle
1571 .execute_command(cmd.command.clone(), cmd.arguments.clone())
1572 {
1573 tracing::warn!("Failed to send executeCommand to '{}': {}", sh.name, e);
1574 }
1575 }
1576 }
1577 }
1578
1579 fn send_code_action_resolve(&mut self, action: lsp_types::CodeAction) {
1581 let language = match self
1582 .buffers
1583 .get(&self.active_buffer())
1584 .map(|s| s.language.clone())
1585 {
1586 Some(l) => l,
1587 None => return,
1588 };
1589
1590 self.next_lsp_request_id += 1;
1591 let request_id = self.next_lsp_request_id;
1592
1593 if let Some(lsp) = &mut self.lsp {
1594 for sh in lsp.get_handles_mut(&language) {
1595 if let Err(e) = sh.handle.code_action_resolve(request_id, action.clone()) {
1596 tracing::warn!("Failed to send codeAction/resolve to '{}': {}", sh.name, e);
1597 }
1598 }
1599 }
1600 }
1601
1602 fn server_supports_code_action_resolve(&self) -> bool {
1604 let language = match self
1605 .buffers
1606 .get(&self.active_buffer())
1607 .map(|s| s.language.clone())
1608 {
1609 Some(l) => l,
1610 None => return false,
1611 };
1612
1613 if let Some(lsp) = &self.lsp {
1614 for sh in lsp.get_handles(&language) {
1615 if sh.capabilities.code_action_resolve {
1616 return true;
1617 }
1618 }
1619 }
1620 false
1621 }
1622
1623 pub(crate) fn server_supports_completion_resolve(&self) -> bool {
1625 let language = match self
1626 .buffers
1627 .get(&self.active_buffer())
1628 .map(|s| s.language.clone())
1629 {
1630 Some(l) => l,
1631 None => return false,
1632 };
1633
1634 if let Some(lsp) = &self.lsp {
1635 for sh in lsp.get_handles(&language) {
1636 if sh.capabilities.completion_resolve {
1637 return true;
1638 }
1639 }
1640 }
1641 false
1642 }
1643
1644 pub(crate) fn send_completion_resolve(&mut self, item: lsp_types::CompletionItem) {
1646 let language = match self
1647 .buffers
1648 .get(&self.active_buffer())
1649 .map(|s| s.language.clone())
1650 {
1651 Some(l) => l,
1652 None => return,
1653 };
1654
1655 self.next_lsp_request_id += 1;
1656 let request_id = self.next_lsp_request_id;
1657
1658 if let Some(lsp) = &mut self.lsp {
1659 for sh in lsp.get_handles_mut(&language) {
1660 if sh.capabilities.completion_resolve {
1661 if let Err(e) = sh.handle.completion_resolve(request_id, item.clone()) {
1662 tracing::warn!(
1663 "Failed to send completionItem/resolve to '{}': {}",
1664 sh.name,
1665 e
1666 );
1667 }
1668 return;
1669 }
1670 }
1671 }
1672 }
1673
1674 pub(crate) fn handle_completion_resolved(&mut self, item: lsp_types::CompletionItem) {
1676 if let Some(additional_edits) = item.additional_text_edits {
1677 if !additional_edits.is_empty() {
1678 tracing::info!(
1679 "Applying {} additional text edits from completion resolve",
1680 additional_edits.len()
1681 );
1682 let buffer_id = self.active_buffer();
1683 if let Err(e) = self.apply_lsp_text_edits(buffer_id, additional_edits) {
1684 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
1685 }
1686 }
1687 }
1688 }
1689
1690 pub(crate) fn apply_formatting_edits(
1692 &mut self,
1693 uri: &str,
1694 edits: Vec<lsp_types::TextEdit>,
1695 ) -> AnyhowResult<usize> {
1696 let buffer_id = self
1698 .buffer_metadata
1699 .iter()
1700 .find(|(_, meta)| meta.file_uri().map(|u| u.as_str() == uri).unwrap_or(false))
1701 .map(|(id, _)| *id);
1702
1703 if let Some(buffer_id) = buffer_id {
1704 let count = self.apply_lsp_text_edits(buffer_id, edits)?;
1705 self.set_status_message(format!("Formatted ({} edits)", count));
1706 Ok(count)
1707 } else {
1708 tracing::warn!("Cannot apply formatting: no buffer for URI {}", uri);
1709 Ok(0)
1710 }
1711 }
1712
1713 pub(crate) fn request_formatting(&mut self) {
1715 let buffer_id = self.active_buffer();
1716 let metadata = match self.buffer_metadata.get(&buffer_id) {
1717 Some(m) if m.lsp_enabled => m,
1718 _ => {
1719 self.set_status_message("LSP not available for this buffer".to_string());
1720 return;
1721 }
1722 };
1723
1724 let uri = match metadata.file_uri() {
1725 Some(u) => u.clone(),
1726 None => return,
1727 };
1728
1729 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
1730 Some(l) => l,
1731 None => return,
1732 };
1733
1734 let tab_size = self.config.editor.tab_size as u32;
1735 let insert_spaces = !self.config.editor.use_tabs;
1736
1737 self.next_lsp_request_id += 1;
1738 let request_id = self.next_lsp_request_id;
1739
1740 if let Some(lsp) = &mut self.lsp {
1741 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Format) {
1742 if let Err(e) =
1743 sh.handle
1744 .document_formatting(request_id, uri, tab_size, insert_spaces)
1745 {
1746 tracing::warn!("Failed to request formatting: {}", e);
1747 }
1748 } else {
1749 self.set_status_message("Formatting not supported by LSP server".to_string());
1750 }
1751 }
1752 }
1753
1754 pub(crate) fn handle_references_response(
1756 &mut self,
1757 request_id: u64,
1758 locations: Vec<lsp_types::Location>,
1759 ) -> AnyhowResult<()> {
1760 tracing::info!(
1761 "handle_references_response: received {} locations for request_id={}",
1762 locations.len(),
1763 request_id
1764 );
1765
1766 if self.pending_references_request != Some(request_id) {
1768 tracing::debug!("Ignoring stale references response: {}", request_id);
1769 return Ok(());
1770 }
1771
1772 self.pending_references_request = None;
1773 self.update_lsp_status_from_server_statuses();
1774
1775 if locations.is_empty() {
1776 self.set_status_message(t!("lsp.no_references").to_string());
1777 return Ok(());
1778 }
1779
1780 let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
1782 .iter()
1783 .map(|loc| {
1784 let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
1786 loc.uri.path().as_str().to_string()
1788 } else {
1789 loc.uri.as_str().to_string()
1790 };
1791
1792 crate::services::plugins::hooks::LspLocation {
1793 file,
1794 line: loc.range.start.line + 1, column: loc.range.start.character + 1, }
1797 })
1798 .collect();
1799
1800 let count = lsp_locations.len();
1801 let symbol = std::mem::take(&mut self.pending_references_symbol);
1802 self.set_status_message(
1803 t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
1804 );
1805
1806 self.plugin_manager.run_hook(
1808 "lsp_references",
1809 crate::services::plugins::hooks::HookArgs::LspReferences {
1810 symbol: symbol.clone(),
1811 locations: lsp_locations,
1812 },
1813 );
1814
1815 tracing::info!(
1816 "Fired lsp_references hook with {} locations for symbol '{}'",
1817 count,
1818 symbol
1819 );
1820
1821 Ok(())
1822 }
1823
1824 pub(crate) fn apply_lsp_text_edits(
1827 &mut self,
1828 buffer_id: BufferId,
1829 mut edits: Vec<lsp_types::TextEdit>,
1830 ) -> AnyhowResult<usize> {
1831 if edits.is_empty() {
1832 return Ok(0);
1833 }
1834
1835 edits.sort_by(|a, b| {
1837 b.range
1838 .start
1839 .line
1840 .cmp(&a.range.start.line)
1841 .then(b.range.start.character.cmp(&a.range.start.character))
1842 });
1843
1844 let mut batch_events = Vec::new();
1846 let mut changes = 0;
1847
1848 let cursor_id = {
1850 let split_id = self
1851 .split_manager
1852 .splits_for_buffer(buffer_id)
1853 .into_iter()
1854 .next()
1855 .unwrap_or_else(|| self.split_manager.active_split());
1856 self.split_view_states
1857 .get(&split_id)
1858 .map(|vs| vs.cursors.primary_id())
1859 .unwrap_or_else(|| self.active_cursors().primary_id())
1860 };
1861
1862 for edit in edits {
1864 let state = self
1865 .buffers
1866 .get_mut(&buffer_id)
1867 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
1868
1869 let start_line = edit.range.start.line as usize;
1871 let start_char = edit.range.start.character as usize;
1872 let end_line = edit.range.end.line as usize;
1873 let end_char = edit.range.end.character as usize;
1874
1875 let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
1876 let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
1877 let buffer_len = state.buffer.len();
1878
1879 let old_text = if start_pos < end_pos && end_pos <= buffer_len {
1881 state.get_text_range(start_pos, end_pos)
1882 } else {
1883 format!(
1884 "<invalid range: start={}, end={}, buffer_len={}>",
1885 start_pos, end_pos, buffer_len
1886 )
1887 };
1888 tracing::debug!(
1889 " Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
1890 start_line, start_char, end_line, end_char,
1891 start_pos, end_pos, old_text, edit.new_text
1892 );
1893
1894 if start_pos < end_pos {
1896 let deleted_text = state.get_text_range(start_pos, end_pos);
1897 let delete_event = Event::Delete {
1898 range: start_pos..end_pos,
1899 deleted_text,
1900 cursor_id,
1901 };
1902 batch_events.push(delete_event);
1903 }
1904
1905 if !edit.new_text.is_empty() {
1907 let insert_event = Event::Insert {
1908 position: start_pos,
1909 text: edit.new_text.clone(),
1910 cursor_id,
1911 };
1912 batch_events.push(insert_event);
1913 }
1914
1915 changes += 1;
1916 }
1917
1918 if !batch_events.is_empty() {
1920 self.apply_events_to_buffer_as_bulk_edit(
1921 buffer_id,
1922 batch_events,
1923 "LSP Rename".to_string(),
1924 )?;
1925 }
1926
1927 Ok(changes)
1928 }
1929
1930 fn apply_text_document_edit(
1936 &mut self,
1937 text_doc_edit: lsp_types::TextDocumentEdit,
1938 ) -> AnyhowResult<usize> {
1939 let uri = text_doc_edit.text_document.uri;
1940
1941 if let Some(expected_version) = text_doc_edit.text_document.version {
1944 if let Ok(path) = uri_to_path(&uri) {
1945 if let Some(lsp) = &self.lsp {
1946 let language = self
1947 .buffers
1948 .get(&self.active_buffer())
1949 .map(|s| s.language.clone())
1950 .unwrap_or_default();
1951 for sh in lsp.get_handles(&language) {
1952 if let Some(current_version) = sh.handle.document_version(&path) {
1953 if (expected_version as i64) != current_version {
1954 tracing::warn!(
1955 "Rejecting stale TextDocumentEdit for {:?}: \
1956 server version {} != our version {}",
1957 path,
1958 expected_version,
1959 current_version,
1960 );
1961 return Ok(0);
1962 }
1963 }
1964 }
1965 }
1966 }
1967 }
1968
1969 if let Ok(path) = uri_to_path(&uri) {
1970 let buffer_id = match self.open_file(&path) {
1971 Ok(id) => id,
1972 Err(e) => {
1973 if let Some(confirmation) =
1974 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1975 {
1976 self.start_large_file_encoding_confirmation(confirmation);
1977 } else {
1978 self.set_status_message(
1979 t!("file.error_opening", error = e.to_string()).to_string(),
1980 );
1981 }
1982 return Ok(0);
1983 }
1984 };
1985
1986 let edits: Vec<lsp_types::TextEdit> = text_doc_edit
1987 .edits
1988 .into_iter()
1989 .map(|one_of| match one_of {
1990 lsp_types::OneOf::Left(text_edit) => text_edit,
1991 lsp_types::OneOf::Right(annotated) => annotated.text_edit,
1992 })
1993 .collect();
1994
1995 tracing::info!("Applying {} edits for {:?}:", edits.len(), path);
1996 for (i, edit) in edits.iter().enumerate() {
1997 tracing::info!(
1998 " Edit {}: line {}:{}-{}:{} -> {:?}",
1999 i,
2000 edit.range.start.line,
2001 edit.range.start.character,
2002 edit.range.end.line,
2003 edit.range.end.character,
2004 edit.new_text
2005 );
2006 }
2007
2008 self.apply_lsp_text_edits(buffer_id, edits)
2009 } else {
2010 Ok(0)
2011 }
2012 }
2013
2014 fn apply_resource_operation(&mut self, op: lsp_types::ResourceOp) -> AnyhowResult<()> {
2016 match op {
2017 lsp_types::ResourceOp::Create(create) => {
2018 let path = std::path::PathBuf::from(create.uri.path().as_str());
2019 let overwrite = create
2020 .options
2021 .as_ref()
2022 .and_then(|o| o.overwrite)
2023 .unwrap_or(false);
2024 let ignore_if_exists = create
2025 .options
2026 .as_ref()
2027 .and_then(|o| o.ignore_if_exists)
2028 .unwrap_or(false);
2029
2030 if path.exists() {
2031 if ignore_if_exists {
2032 tracing::debug!("CreateFile: {:?} already exists, ignoring", path);
2033 return Ok(());
2034 }
2035 if !overwrite {
2036 tracing::warn!("CreateFile: {:?} already exists and overwrite=false", path);
2037 return Ok(());
2038 }
2039 }
2040
2041 if let Some(parent) = path.parent() {
2043 std::fs::create_dir_all(parent)?;
2044 }
2045 std::fs::write(&path, "")?;
2046 tracing::info!("CreateFile: created {:?}", path);
2047
2048 if let Err(e) = self.open_file(&path) {
2050 tracing::warn!("CreateFile: failed to open created file {:?}: {}", path, e);
2051 }
2052 }
2053 lsp_types::ResourceOp::Rename(rename) => {
2054 let old_path = std::path::PathBuf::from(rename.old_uri.path().as_str());
2055 let new_path = std::path::PathBuf::from(rename.new_uri.path().as_str());
2056 let overwrite = rename
2057 .options
2058 .as_ref()
2059 .and_then(|o| o.overwrite)
2060 .unwrap_or(false);
2061 let ignore_if_exists = rename
2062 .options
2063 .as_ref()
2064 .and_then(|o| o.ignore_if_exists)
2065 .unwrap_or(false);
2066
2067 if new_path.exists() {
2068 if ignore_if_exists {
2069 tracing::debug!("RenameFile: {:?} already exists, ignoring", new_path);
2070 return Ok(());
2071 }
2072 if !overwrite {
2073 tracing::warn!(
2074 "RenameFile: {:?} already exists and overwrite=false",
2075 new_path
2076 );
2077 return Ok(());
2078 }
2079 }
2080
2081 if let Some(parent) = new_path.parent() {
2083 std::fs::create_dir_all(parent)?;
2084 }
2085 std::fs::rename(&old_path, &new_path)?;
2086 tracing::info!("RenameFile: {:?} -> {:?}", old_path, new_path);
2087 }
2088 lsp_types::ResourceOp::Delete(delete) => {
2089 let path = std::path::PathBuf::from(delete.uri.path().as_str());
2090 let recursive = delete
2091 .options
2092 .as_ref()
2093 .and_then(|o| o.recursive)
2094 .unwrap_or(false);
2095 let ignore_if_not_exists = delete
2096 .options
2097 .as_ref()
2098 .and_then(|o| o.ignore_if_not_exists)
2099 .unwrap_or(false);
2100
2101 if !path.exists() {
2102 if ignore_if_not_exists {
2103 tracing::debug!("DeleteFile: {:?} does not exist, ignoring", path);
2104 return Ok(());
2105 }
2106 tracing::warn!("DeleteFile: {:?} does not exist", path);
2107 return Ok(());
2108 }
2109
2110 if path.is_dir() && recursive {
2111 std::fs::remove_dir_all(&path)?;
2112 } else if path.is_file() {
2113 std::fs::remove_file(&path)?;
2114 }
2115 tracing::info!("DeleteFile: deleted {:?}", path);
2116 }
2117 }
2118 Ok(())
2119 }
2120
2121 pub(crate) fn apply_workspace_edit(
2125 &mut self,
2126 workspace_edit: lsp_types::WorkspaceEdit,
2127 ) -> AnyhowResult<usize> {
2128 tracing::debug!(
2129 "Applying WorkspaceEdit: changes={:?}, document_changes={:?}",
2130 workspace_edit.changes.as_ref().map(|c| c.len()),
2131 workspace_edit.document_changes.as_ref().map(|dc| match dc {
2132 lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
2133 lsp_types::DocumentChanges::Operations(o) => format!("{} operations", o.len()),
2134 })
2135 );
2136
2137 let mut total_changes = 0;
2138
2139 if let Some(changes) = workspace_edit.changes {
2141 for (uri, edits) in changes {
2142 if let Ok(path) = uri_to_path(&uri) {
2143 let buffer_id = match self.open_file(&path) {
2144 Ok(id) => id,
2145 Err(e) => {
2146 if let Some(confirmation) = e.downcast_ref::<
2147 crate::model::buffer::LargeFileEncodingConfirmation,
2148 >() {
2149 self.start_large_file_encoding_confirmation(confirmation);
2150 } else {
2151 self.set_status_message(
2152 t!("file.error_opening", error = e.to_string())
2153 .to_string(),
2154 );
2155 }
2156 return Ok(0);
2157 }
2158 };
2159 total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
2160 }
2161 }
2162 }
2163
2164 if let Some(document_changes) = workspace_edit.document_changes {
2166 use lsp_types::DocumentChanges;
2167
2168 match document_changes {
2169 DocumentChanges::Edits(edits) => {
2170 for text_doc_edit in edits {
2171 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2172 }
2173 }
2174 DocumentChanges::Operations(ops) => {
2175 for op in ops {
2178 match op {
2179 lsp_types::DocumentChangeOperation::Edit(text_doc_edit) => {
2180 total_changes += self.apply_text_document_edit(text_doc_edit)?;
2181 }
2182 lsp_types::DocumentChangeOperation::Op(resource_op) => {
2183 self.apply_resource_operation(resource_op)?;
2184 total_changes += 1;
2185 }
2186 }
2187 }
2188 }
2189 }
2190 }
2191
2192 Ok(total_changes)
2193 }
2194
2195 pub fn handle_rename_response(
2197 &mut self,
2198 _request_id: u64,
2199 result: Result<lsp_types::WorkspaceEdit, String>,
2200 ) -> AnyhowResult<()> {
2201 self.update_lsp_status_from_server_statuses();
2202
2203 match result {
2204 Ok(workspace_edit) => {
2205 let total_changes = self.apply_workspace_edit(workspace_edit)?;
2206 self.status_message = Some(t!("lsp.renamed", count = total_changes).to_string());
2207 }
2208 Err(error) => {
2209 if error.contains("content modified") || error.contains("-32801") {
2211 tracing::debug!(
2212 "LSP rename: ContentModified error (expected, ignoring): {}",
2213 error
2214 );
2215 self.status_message = Some(t!("lsp.rename_cancelled").to_string());
2216 } else {
2217 self.status_message = Some(t!("lsp.rename_failed", error = &error).to_string());
2218 }
2219 }
2220 }
2221
2222 Ok(())
2223 }
2224
2225 pub(crate) fn apply_events_to_buffer_as_bulk_edit(
2230 &mut self,
2231 buffer_id: BufferId,
2232 events: Vec<Event>,
2233 description: String,
2234 ) -> AnyhowResult<()> {
2235 use crate::model::event::CursorId;
2236
2237 if events.is_empty() {
2238 return Ok(());
2239 }
2240
2241 let batch_for_lsp = Event::Batch {
2243 events: events.clone(),
2244 description: description.clone(),
2245 };
2246
2247 let original_active = self.active_buffer();
2250 self.split_manager.set_active_buffer_id(buffer_id);
2251 let lsp_changes = self.collect_lsp_changes(&batch_for_lsp);
2252 self.split_manager.set_active_buffer_id(original_active);
2253
2254 let split_id_for_cursors = self
2257 .split_manager
2258 .splits_for_buffer(buffer_id)
2259 .into_iter()
2260 .next()
2261 .unwrap_or_else(|| self.split_manager.active_split());
2262 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2263 .split_view_states
2264 .get(&split_id_for_cursors)
2265 .and_then(|vs| vs.keyed_states.get(&buffer_id))
2266 .map(|bvs| {
2267 bvs.cursors
2268 .iter()
2269 .map(|(id, c)| (id, c.position, c.anchor))
2270 .collect()
2271 })
2272 .unwrap_or_default();
2273
2274 let state = self
2275 .buffers
2276 .get_mut(&buffer_id)
2277 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2278
2279 let old_snapshot = state.buffer.snapshot_buffer_state();
2281
2282 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2284 for event in &events {
2285 match event {
2286 Event::Insert { position, text, .. } => {
2287 edits.push((*position, 0, text.clone()));
2288 }
2289 Event::Delete { range, .. } => {
2290 edits.push((range.start, range.len(), String::new()));
2291 }
2292 _ => {}
2293 }
2294 }
2295
2296 edits.sort_by(|a, b| b.0.cmp(&a.0));
2298
2299 let edit_refs: Vec<(usize, usize, &str)> = edits
2301 .iter()
2302 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2303 .collect();
2304
2305 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2307
2308 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2310
2311 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2313 for (pos, del_len, text) in &edits {
2314 let delta = text.len() as isize - *del_len as isize;
2315 position_deltas.push((*pos, delta));
2316 }
2317 position_deltas.sort_by_key(|(pos, _)| *pos);
2318
2319 let calc_shift = |original_pos: usize| -> isize {
2320 let mut shift: isize = 0;
2321 for (edit_pos, delta) in &position_deltas {
2322 if *edit_pos < original_pos {
2323 shift += delta;
2324 }
2325 }
2326 shift
2327 };
2328
2329 let buffer_len = state.buffer.len();
2331 let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
2332 .iter()
2333 .map(|(id, pos, anchor)| {
2334 let shift = calc_shift(*pos);
2335 let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
2336 let new_anchor = anchor.map(|a| {
2337 let anchor_shift = calc_shift(a);
2338 ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
2339 });
2340 (*id, new_pos, new_anchor)
2341 })
2342 .collect();
2343
2344 let new_snapshot = state.buffer.snapshot_buffer_state();
2346
2347 state.highlighter.invalidate_all();
2349
2350 if let Some(vs) = self.split_view_states.get_mut(&split_id_for_cursors) {
2352 if let Some(bvs) = vs.keyed_states.get_mut(&buffer_id) {
2353 for (cursor_id, new_pos, new_anchor) in &new_cursors {
2354 if let Some(cursor) = bvs.cursors.get_mut(*cursor_id) {
2355 cursor.position = *new_pos;
2356 cursor.anchor = *new_anchor;
2357 }
2358 }
2359 }
2360 }
2361
2362 let edit_lengths: Vec<(usize, usize, usize)> = {
2365 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2366 for (pos, del_len, text) in &edits {
2367 if let Some(last) = lengths.last_mut() {
2368 if last.0 == *pos {
2369 last.1 += del_len;
2370 last.2 += text.len();
2371 continue;
2372 }
2373 }
2374 lengths.push((*pos, *del_len, text.len()));
2375 }
2376 lengths
2377 };
2378
2379 for &(pos, del_len, ins_len) in &edit_lengths {
2381 if del_len > 0 && ins_len > 0 {
2382 if ins_len > del_len {
2383 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2384 state.margins.adjust_for_insert(pos, ins_len - del_len);
2385 } else if del_len > ins_len {
2386 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2387 state.margins.adjust_for_delete(pos, del_len - ins_len);
2388 }
2389 } else if del_len > 0 {
2390 state.marker_list.adjust_for_delete(pos, del_len);
2391 state.margins.adjust_for_delete(pos, del_len);
2392 } else if ins_len > 0 {
2393 state.marker_list.adjust_for_insert(pos, ins_len);
2394 state.margins.adjust_for_insert(pos, ins_len);
2395 }
2396 }
2397
2398 let bulk_edit = Event::BulkEdit {
2400 old_snapshot: Some(old_snapshot),
2401 new_snapshot: Some(new_snapshot),
2402 old_cursors,
2403 new_cursors,
2404 description,
2405 edits: edit_lengths,
2406 displaced_markers,
2407 };
2408
2409 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
2411 event_log.append(bulk_edit);
2412 }
2413
2414 self.send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2416
2417 Ok(())
2418 }
2419
2420 pub(crate) fn send_lsp_changes_for_buffer(
2422 &mut self,
2423 buffer_id: BufferId,
2424 changes: Vec<TextDocumentContentChangeEvent>,
2425 ) {
2426 if changes.is_empty() {
2427 return;
2428 }
2429
2430 let metadata = match self.buffer_metadata.get(&buffer_id) {
2432 Some(m) => m,
2433 None => {
2434 tracing::debug!(
2435 "send_lsp_changes_for_buffer: no metadata for buffer {:?}",
2436 buffer_id
2437 );
2438 return;
2439 }
2440 };
2441
2442 if !metadata.lsp_enabled {
2443 tracing::debug!("send_lsp_changes_for_buffer: LSP disabled for this buffer");
2444 return;
2445 }
2446
2447 let uri = match metadata.file_uri() {
2449 Some(u) => u.clone(),
2450 None => {
2451 tracing::debug!(
2452 "send_lsp_changes_for_buffer: no URI for buffer (not a file or URI creation failed)"
2453 );
2454 return;
2455 }
2456 };
2457 let file_path = metadata.file_path().cloned();
2458
2459 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
2461 Some(l) => l,
2462 None => {
2463 tracing::debug!(
2464 "send_lsp_changes_for_buffer: no buffer state for {:?}",
2465 buffer_id
2466 );
2467 return;
2468 }
2469 };
2470
2471 tracing::trace!(
2472 "send_lsp_changes_for_buffer: sending {} changes to {} in single didChange notification",
2473 changes.len(),
2474 uri.as_str()
2475 );
2476
2477 use crate::services::lsp::manager::LspSpawnResult;
2479 let Some(lsp) = self.lsp.as_mut() else {
2480 tracing::debug!("send_lsp_changes_for_buffer: no LSP manager available");
2481 return;
2482 };
2483
2484 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
2485 tracing::debug!(
2486 "send_lsp_changes_for_buffer: LSP not running for {} (auto_start disabled)",
2487 language
2488 );
2489 return;
2490 }
2491
2492 let handles_needing_open: Vec<_> = {
2494 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2495 return;
2496 };
2497 lsp.get_handles(&language)
2498 .iter()
2499 .filter(|sh| !metadata.lsp_opened_with.contains(&sh.handle.id()))
2500 .map(|sh| (sh.name.clone(), sh.handle.id()))
2501 .collect()
2502 };
2503
2504 if !handles_needing_open.is_empty() {
2505 let text = match self
2507 .buffers
2508 .get(&buffer_id)
2509 .and_then(|s| s.buffer.to_string())
2510 {
2511 Some(t) => t,
2512 None => {
2513 tracing::debug!(
2514 "send_lsp_changes_for_buffer: buffer text not available for didOpen"
2515 );
2516 return;
2517 }
2518 };
2519
2520 let Some(lsp) = self.lsp.as_mut() else { return };
2522 for sh in lsp.get_handles_mut(&language) {
2523 if handles_needing_open
2524 .iter()
2525 .any(|(_, id)| *id == sh.handle.id())
2526 {
2527 if let Err(e) = sh
2528 .handle
2529 .did_open(uri.clone(), text.clone(), language.clone())
2530 {
2531 tracing::warn!(
2532 "Failed to send didOpen to '{}' before didChange: {}",
2533 sh.name,
2534 e
2535 );
2536 } else {
2537 tracing::debug!(
2538 "Sent didOpen for {} to LSP handle '{}' before didChange",
2539 uri.as_str(),
2540 sh.name
2541 );
2542 }
2543 }
2544 }
2545
2546 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
2548 for (_, handle_id) in &handles_needing_open {
2549 metadata.lsp_opened_with.insert(*handle_id);
2550 }
2551 }
2552
2553 return;
2557 }
2558
2559 let Some(lsp) = self.lsp.as_mut() else { return };
2561 let mut any_sent = false;
2562 for sh in lsp.get_handles_mut(&language) {
2563 if let Err(e) = sh.handle.did_change(uri.clone(), changes.clone()) {
2564 tracing::warn!("Failed to send didChange to '{}': {}", sh.name, e);
2565 } else {
2566 any_sent = true;
2567 }
2568 }
2569 if any_sent {
2570 tracing::trace!("Successfully sent batched didChange to LSP");
2571
2572 if let Some(state) = self.buffers.get(&buffer_id) {
2575 if let Some(path) = state.buffer.file_path() {
2576 crate::services::lsp::diagnostics::invalidate_cache_for_file(
2577 &path.to_string_lossy(),
2578 );
2579 }
2580 }
2581
2582 self.scheduled_diagnostic_pull = Some((
2584 buffer_id,
2585 std::time::Instant::now() + std::time::Duration::from_millis(1000),
2586 ));
2587 }
2588 }
2589
2590 pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2592 if self.server_supports_prepare_rename() {
2594 self.send_prepare_rename();
2595 return Ok(());
2596 }
2597
2598 self.show_rename_prompt()
2599 }
2600
2601 pub(crate) fn handle_prepare_rename_response(
2603 &mut self,
2604 result: Result<serde_json::Value, String>,
2605 ) {
2606 match result {
2607 Ok(value) if !value.is_null() => {
2608 if let Err(e) = self.show_rename_prompt() {
2610 self.set_status_message(format!("Rename failed: {e}"));
2611 }
2612 }
2613 Ok(_) => {
2614 self.set_status_message("Cannot rename at this position".to_string());
2615 }
2616 Err(e) => {
2617 self.set_status_message(format!("Cannot rename: {e}"));
2618 }
2619 }
2620 }
2621
2622 fn server_supports_prepare_rename(&self) -> bool {
2624 let language = match self
2625 .buffers
2626 .get(&self.active_buffer())
2627 .map(|s| s.language.clone())
2628 {
2629 Some(l) => l,
2630 None => return false,
2631 };
2632
2633 if let Some(lsp) = &self.lsp {
2634 for sh in lsp.get_handles(&language) {
2635 if sh.capabilities.rename {
2636 return true;
2639 }
2640 }
2641 }
2642 false
2643 }
2644
2645 fn send_prepare_rename(&mut self) {
2647 let cursor_pos = self.active_cursors().primary().position;
2648 let (line, character) = self
2649 .active_state()
2650 .buffer
2651 .position_to_lsp_position(cursor_pos);
2652
2653 let buffer_id = self.active_buffer();
2654 let metadata = match self.buffer_metadata.get(&buffer_id) {
2655 Some(m) if m.lsp_enabled => m,
2656 _ => return,
2657 };
2658 let uri = match metadata.file_uri() {
2659 Some(u) => u.clone(),
2660 None => return,
2661 };
2662 let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
2663 Some(l) => l,
2664 None => return,
2665 };
2666
2667 self.next_lsp_request_id += 1;
2668 let request_id = self.next_lsp_request_id;
2669
2670 if let Some(lsp) = &mut self.lsp {
2671 if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Rename) {
2672 if let Err(e) =
2673 sh.handle
2674 .prepare_rename(request_id, uri, line as u32, character as u32)
2675 {
2676 tracing::warn!("Failed to send prepareRename: {}", e);
2677 }
2678 }
2679 }
2680 }
2681
2682 fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2684 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2685
2686 let cursor_pos = self.active_cursors().primary().position;
2688 let (word_start, word_end) = {
2689 let state = self.active_state();
2690
2691 let word_start = find_word_start(&state.buffer, cursor_pos);
2693 let word_end = find_word_end(&state.buffer, cursor_pos);
2694
2695 if word_start >= word_end {
2697 self.status_message = Some(t!("lsp.no_symbol_at_cursor").to_string());
2698 return Ok(());
2699 }
2700
2701 (word_start, word_end)
2702 };
2703
2704 let word_text = self.active_state_mut().get_text_range(word_start, word_end);
2706
2707 let overlay_handle = self.add_overlay(
2709 None,
2710 word_start..word_end,
2711 crate::model::event::OverlayFace::Background {
2712 color: (50, 100, 200), },
2714 100,
2715 Some(t!("lsp.popup_renaming").to_string()),
2716 );
2717
2718 let mut prompt = Prompt::new(
2721 "Rename to: ".to_string(),
2722 PromptType::LspRename {
2723 original_text: word_text.clone(),
2724 start_pos: word_start,
2725 end_pos: word_end,
2726 overlay_handle,
2727 },
2728 );
2729 prompt.set_input(word_text);
2731
2732 self.prompt = Some(prompt);
2733 Ok(())
2734 }
2735
2736 pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
2738 self.remove_overlay(handle.clone());
2739 }
2740
2741 pub(crate) fn perform_lsp_rename(
2743 &mut self,
2744 new_name: String,
2745 original_text: String,
2746 start_pos: usize,
2747 overlay_handle: crate::view::overlay::OverlayHandle,
2748 ) {
2749 self.cancel_rename_overlay(&overlay_handle);
2751
2752 if new_name == original_text {
2754 self.status_message = Some(t!("lsp.name_unchanged").to_string());
2755 return;
2756 }
2757
2758 let rename_pos = start_pos;
2761
2762 let state = self.active_state();
2765 let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
2766 let buffer_id = self.active_buffer();
2767 let request_id = self.next_lsp_request_id;
2768
2769 let sent = self
2771 .with_lsp_for_buffer(buffer_id, LspFeature::Rename, |handle, uri, _language| {
2772 let result = handle.rename(
2773 request_id,
2774 uri.clone(),
2775 line as u32,
2776 character as u32,
2777 new_name.clone(),
2778 );
2779 if result.is_ok() {
2780 tracing::info!(
2781 "Requested rename at {}:{}:{} to '{}'",
2782 uri.as_str(),
2783 line,
2784 character,
2785 new_name
2786 );
2787 }
2788 result.is_ok()
2789 })
2790 .unwrap_or(false);
2791
2792 if sent {
2793 self.next_lsp_request_id += 1;
2794 self.lsp_status = "LSP: rename...".to_string();
2795 } else if self
2796 .buffer_metadata
2797 .get(&buffer_id)
2798 .and_then(|m| m.file_path())
2799 .is_none()
2800 {
2801 self.status_message = Some(t!("lsp.cannot_rename_unsaved").to_string());
2802 }
2803 }
2804
2805 pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
2807 if !self.config.editor.enable_inlay_hints {
2808 return;
2809 }
2810
2811 let buffer_id = self.active_buffer();
2812
2813 let line_count = if let Some(state) = self.buffers.get(&buffer_id) {
2815 state.buffer.line_count().unwrap_or(1000)
2816 } else {
2817 return;
2818 };
2819 let last_line = line_count.saturating_sub(1) as u32;
2820 let request_id = self.next_lsp_request_id;
2821
2822 let sent = self
2824 .with_lsp_for_buffer(
2825 buffer_id,
2826 LspFeature::InlayHints,
2827 |handle, uri, _language| {
2828 let result =
2829 handle.inlay_hints(request_id, uri.clone(), 0, 0, last_line, 10000);
2830 if result.is_ok() {
2831 tracing::info!(
2832 "Requested inlay hints for {} (request_id={})",
2833 uri.as_str(),
2834 request_id
2835 );
2836 } else if let Err(e) = &result {
2837 tracing::debug!("Failed to request inlay hints: {}", e);
2838 }
2839 result.is_ok()
2840 },
2841 )
2842 .unwrap_or(false);
2843
2844 if sent {
2845 self.next_lsp_request_id += 1;
2846 self.pending_inlay_hints_request = Some(request_id);
2847 }
2848 }
2849
2850 pub(crate) fn schedule_folding_ranges_refresh(&mut self, buffer_id: BufferId) {
2852 let next_time = Instant::now() + Duration::from_millis(FOLDING_RANGES_DEBOUNCE_MS);
2853 self.folding_ranges_debounce.insert(buffer_id, next_time);
2854 }
2855
2856 pub(crate) fn maybe_request_folding_ranges_debounced(&mut self, buffer_id: BufferId) {
2858 let Some(ready_at) = self.folding_ranges_debounce.get(&buffer_id).copied() else {
2859 return;
2860 };
2861 if Instant::now() < ready_at {
2862 return;
2863 }
2864
2865 self.folding_ranges_debounce.remove(&buffer_id);
2866 self.request_folding_ranges_for_buffer(buffer_id);
2867 }
2868
2869 pub(crate) fn request_folding_ranges_for_buffer(&mut self, buffer_id: BufferId) {
2871 if self.folding_ranges_in_flight.contains_key(&buffer_id) {
2872 return;
2873 }
2874
2875 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2876 return;
2877 };
2878 if !metadata.lsp_enabled {
2879 return;
2880 }
2881 let Some(uri) = metadata.file_uri().cloned() else {
2882 return;
2883 };
2884 let file_path = metadata.file_path().cloned();
2885
2886 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2887 return;
2888 };
2889
2890 let Some(lsp) = self.lsp.as_mut() else {
2891 return;
2892 };
2893
2894 if !lsp.folding_ranges_supported(&language) {
2895 return;
2896 }
2897
2898 use crate::services::lsp::manager::LspSpawnResult;
2900 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
2901 return;
2902 }
2903
2904 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::FoldingRange) else {
2905 return;
2906 };
2907 let handle = &mut sh.handle;
2908
2909 let request_id = self.next_lsp_request_id;
2910 self.next_lsp_request_id += 1;
2911 let buffer_version = self
2912 .buffers
2913 .get(&buffer_id)
2914 .map(|s| s.buffer.version())
2915 .unwrap_or(0);
2916
2917 match handle.folding_ranges(request_id, uri) {
2918 Ok(()) => {
2919 self.pending_folding_range_requests.insert(
2920 request_id,
2921 super::FoldingRangeRequest {
2922 buffer_id,
2923 version: buffer_version,
2924 },
2925 );
2926 self.folding_ranges_in_flight
2927 .insert(buffer_id, (request_id, buffer_version));
2928 }
2929 Err(e) => {
2930 tracing::debug!("Failed to request folding ranges: {}", e);
2931 }
2932 }
2933 }
2934
2935 pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
2937 if !self.config.editor.enable_semantic_tokens_full {
2938 return;
2939 }
2940
2941 if self.semantic_tokens_in_flight.contains_key(&buffer_id) {
2943 return;
2944 }
2945
2946 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2947 return;
2948 };
2949 if !metadata.lsp_enabled {
2950 return;
2951 }
2952 let Some(uri) = metadata.file_uri().cloned() else {
2953 return;
2954 };
2955 let file_path_for_spawn = metadata.file_path().cloned();
2956 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2958 return;
2959 };
2960
2961 let Some(lsp) = self.lsp.as_mut() else {
2962 return;
2963 };
2964
2965 use crate::services::lsp::manager::LspSpawnResult;
2967 if lsp.try_spawn(&language, file_path_for_spawn.as_deref()) != LspSpawnResult::Spawned {
2968 return;
2969 }
2970
2971 if !lsp.semantic_tokens_full_supported(&language) {
2973 return;
2974 }
2975 if lsp.semantic_tokens_legend(&language).is_none() {
2976 return;
2977 }
2978
2979 let Some(state) = self.buffers.get(&buffer_id) else {
2980 return;
2981 };
2982 let buffer_version = state.buffer.version();
2983 if let Some(store) = state.semantic_tokens.as_ref() {
2984 if store.version == buffer_version {
2985 return; }
2987 }
2988
2989 let previous_result_id = state
2990 .semantic_tokens
2991 .as_ref()
2992 .and_then(|store| store.result_id.clone());
2993
2994 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
2995 return;
2996 };
2997 let supports_delta = sh.capabilities.semantic_tokens_full_delta;
2999 let use_delta = previous_result_id.is_some() && supports_delta;
3000 let handle = &mut sh.handle;
3001
3002 let request_id = self.next_lsp_request_id;
3003 self.next_lsp_request_id += 1;
3004
3005 let request_kind = if use_delta {
3006 super::SemanticTokensFullRequestKind::FullDelta
3007 } else {
3008 super::SemanticTokensFullRequestKind::Full
3009 };
3010
3011 let request_result = if use_delta {
3012 handle.semantic_tokens_full_delta(request_id, uri, previous_result_id.unwrap())
3013 } else {
3014 handle.semantic_tokens_full(request_id, uri)
3015 };
3016
3017 match request_result {
3018 Ok(_) => {
3019 self.pending_semantic_token_requests.insert(
3020 request_id,
3021 super::SemanticTokenFullRequest {
3022 buffer_id,
3023 version: buffer_version,
3024 kind: request_kind,
3025 },
3026 );
3027 self.semantic_tokens_in_flight
3028 .insert(buffer_id, (request_id, buffer_version, request_kind));
3029 }
3030 Err(e) => {
3031 tracing::debug!("Failed to request semantic tokens: {}", e);
3032 }
3033 }
3034 }
3035
3036 pub(crate) fn schedule_semantic_tokens_full_refresh(&mut self, buffer_id: BufferId) {
3038 if !self.config.editor.enable_semantic_tokens_full {
3039 return;
3040 }
3041
3042 let next_time = Instant::now() + Duration::from_millis(SEMANTIC_TOKENS_FULL_DEBOUNCE_MS);
3043 self.semantic_tokens_full_debounce
3044 .insert(buffer_id, next_time);
3045 }
3046
3047 pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
3049 if !self.config.editor.enable_semantic_tokens_full {
3050 self.semantic_tokens_full_debounce.remove(&buffer_id);
3051 return;
3052 }
3053
3054 let Some(ready_at) = self.semantic_tokens_full_debounce.get(&buffer_id).copied() else {
3055 return;
3056 };
3057 if Instant::now() < ready_at {
3058 return;
3059 }
3060
3061 self.semantic_tokens_full_debounce.remove(&buffer_id);
3062 self.maybe_request_semantic_tokens(buffer_id);
3063 }
3064
3065 pub(crate) fn maybe_request_semantic_tokens_range(
3067 &mut self,
3068 buffer_id: BufferId,
3069 start_line: usize,
3070 end_line: usize,
3071 ) {
3072 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
3073 return;
3074 };
3075 if !metadata.lsp_enabled {
3076 return;
3077 }
3078 let Some(uri) = metadata.file_uri().cloned() else {
3079 return;
3080 };
3081 let file_path = metadata.file_path().cloned();
3082 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
3084 return;
3085 };
3086
3087 let Some(lsp) = self.lsp.as_mut() else {
3088 return;
3089 };
3090
3091 use crate::services::lsp::manager::LspSpawnResult;
3093 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3094 return;
3095 }
3096
3097 if !lsp.semantic_tokens_range_supported(&language) {
3098 self.maybe_request_semantic_tokens(buffer_id);
3100 return;
3101 }
3102 if lsp.semantic_tokens_legend(&language).is_none() {
3103 return;
3104 }
3105
3106 let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3107 return;
3108 };
3109 if !sh.capabilities.semantic_tokens_range {
3112 return;
3113 }
3114 let handle = &mut sh.handle;
3115 let Some(state) = self.buffers.get(&buffer_id) else {
3116 return;
3117 };
3118
3119 let buffer_version = state.buffer.version();
3120 let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3121 let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3122
3123 if let Some(line_count) = state.buffer.line_count() {
3124 if line_count == 0 {
3125 return;
3126 }
3127 let max_line = line_count.saturating_sub(1);
3128 padded_start = padded_start.min(max_line);
3129 padded_end = padded_end.min(max_line);
3130 }
3131
3132 let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
3133 let end_char = state
3134 .buffer
3135 .get_line(padded_end)
3136 .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
3137 .unwrap_or(0);
3138 let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
3139 state.buffer.lsp_position_to_byte(padded_end, end_char)
3140 } else {
3141 state.buffer.len()
3142 };
3143
3144 if start_byte >= end_byte {
3145 return;
3146 }
3147
3148 let range = start_byte..end_byte;
3149 if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
3150 self.semantic_tokens_range_in_flight.get(&buffer_id)
3151 {
3152 if *in_flight_start == padded_start
3153 && *in_flight_end == padded_end
3154 && *in_flight_version == buffer_version
3155 {
3156 return;
3157 }
3158 if let Err(e) = handle.cancel_request(*in_flight_id) {
3159 tracing::debug!("Failed to cancel semantic token range request: {}", e);
3160 }
3161 self.pending_semantic_token_range_requests
3162 .remove(in_flight_id);
3163 self.semantic_tokens_range_in_flight.remove(&buffer_id);
3164 }
3165
3166 if let Some((applied_start, applied_end, applied_version)) =
3167 self.semantic_tokens_range_applied.get(&buffer_id)
3168 {
3169 if *applied_start == padded_start
3170 && *applied_end == padded_end
3171 && *applied_version == buffer_version
3172 {
3173 return;
3174 }
3175 }
3176
3177 let now = Instant::now();
3178 if let Some((last_start, last_end, last_version, last_time)) =
3179 self.semantic_tokens_range_last_request.get(&buffer_id)
3180 {
3181 if *last_start == padded_start
3182 && *last_end == padded_end
3183 && *last_version == buffer_version
3184 && now.duration_since(*last_time)
3185 < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
3186 {
3187 return;
3188 }
3189 }
3190
3191 let lsp_range = lsp_types::Range {
3192 start: lsp_types::Position {
3193 line: padded_start as u32,
3194 character: 0,
3195 },
3196 end: lsp_types::Position {
3197 line: padded_end as u32,
3198 character: end_char as u32,
3199 },
3200 };
3201
3202 let request_id = self.next_lsp_request_id;
3203 self.next_lsp_request_id += 1;
3204
3205 match handle.semantic_tokens_range(request_id, uri, lsp_range) {
3206 Ok(_) => {
3207 self.pending_semantic_token_range_requests.insert(
3208 request_id,
3209 SemanticTokenRangeRequest {
3210 buffer_id,
3211 version: buffer_version,
3212 range: range.clone(),
3213 start_line: padded_start,
3214 end_line: padded_end,
3215 },
3216 );
3217 self.semantic_tokens_range_in_flight.insert(
3218 buffer_id,
3219 (request_id, padded_start, padded_end, buffer_version),
3220 );
3221 self.semantic_tokens_range_last_request
3222 .insert(buffer_id, (padded_start, padded_end, buffer_version, now));
3223 }
3224 Err(e) => {
3225 tracing::debug!("Failed to request semantic token range: {}", e);
3226 }
3227 }
3228 }
3229}
3230
3231#[cfg(test)]
3232mod tests {
3233 use crate::model::filesystem::StdFileSystem;
3234 use std::sync::Arc;
3235
3236 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
3237 Arc::new(StdFileSystem)
3238 }
3239 use super::Editor;
3240 use crate::model::buffer::Buffer;
3241 use crate::state::EditorState;
3242 use crate::view::virtual_text::VirtualTextPosition;
3243 use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
3244
3245 fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
3246 InlayHint {
3247 position: Position { line, character },
3248 label: InlayHintLabel::String(label.to_string()),
3249 kind,
3250 text_edits: None,
3251 tooltip: None,
3252 padding_left: None,
3253 padding_right: None,
3254 data: None,
3255 }
3256 }
3257
3258 #[test]
3259 fn test_inlay_hint_inserts_before_character() {
3260 let mut state = EditorState::new(
3261 80,
3262 24,
3263 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3264 test_fs(),
3265 );
3266 state.buffer = Buffer::from_str_test("ab");
3267
3268 if !state.buffer.is_empty() {
3269 state.marker_list.adjust_for_insert(0, state.buffer.len());
3270 }
3271
3272 let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3273 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3274
3275 let lookup = state
3276 .virtual_texts
3277 .build_lookup(&state.marker_list, 0, state.buffer.len());
3278 let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3279 assert_eq!(vtexts.len(), 1);
3280 assert_eq!(vtexts[0].text, ": i32");
3281 assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
3282 }
3283
3284 #[test]
3285 fn test_inlay_hint_at_eof_renders_after_last_char() {
3286 let mut state = EditorState::new(
3287 80,
3288 24,
3289 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3290 test_fs(),
3291 );
3292 state.buffer = Buffer::from_str_test("ab");
3293
3294 if !state.buffer.is_empty() {
3295 state.marker_list.adjust_for_insert(0, state.buffer.len());
3296 }
3297
3298 let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
3299 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3300
3301 let lookup = state
3302 .virtual_texts
3303 .build_lookup(&state.marker_list, 0, state.buffer.len());
3304 let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
3305 assert_eq!(vtexts.len(), 1);
3306 assert_eq!(vtexts[0].text, ": i32");
3307 assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
3308 }
3309
3310 #[test]
3311 fn test_inlay_hint_empty_buffer_is_ignored() {
3312 let mut state = EditorState::new(
3313 80,
3314 24,
3315 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3316 test_fs(),
3317 );
3318 state.buffer = Buffer::from_str_test("");
3319
3320 let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
3321 Editor::apply_inlay_hints_to_state(&mut state, &hints);
3322
3323 assert!(state.virtual_texts.is_empty());
3324 }
3325
3326 #[test]
3327 fn test_space_doc_paragraphs_inserts_blank_lines() {
3328 use super::space_doc_paragraphs;
3329
3330 let input = "sep\n description.\nend\n another.";
3332 let result = space_doc_paragraphs(input);
3333 assert_eq!(result, "sep\n\n description.\n\nend\n\n another.");
3334 }
3335
3336 #[test]
3337 fn test_space_doc_paragraphs_preserves_existing_blank_lines() {
3338 use super::space_doc_paragraphs;
3339
3340 let input = "First paragraph.\n\nSecond paragraph.";
3342 let result = space_doc_paragraphs(input);
3343 assert_eq!(result, "First paragraph.\n\nSecond paragraph.");
3344 }
3345
3346 #[test]
3347 fn test_space_doc_paragraphs_plain_text() {
3348 use super::space_doc_paragraphs;
3349
3350 let input = "Just a single line of docs.";
3351 let result = space_doc_paragraphs(input);
3352 assert_eq!(result, "Just a single line of docs.");
3353 }
3354}