Skip to main content

fresh/app/
lsp_requests.rs

1//! LSP (Language Server Protocol) request handling for the Editor.
2//!
3//! This module contains all methods related to LSP operations including:
4//! - Completion requests and response handling
5//! - Go-to-definition
6//! - Hover documentation
7//! - Find references
8//! - Signature help
9//! - Code actions
10//! - Rename operations
11//! - Inlay hints
12
13use 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
29/// Ensure every line in a docstring is separated by a blank line.
30///
31/// LSP documentation (e.g. from pyright) often uses single newlines between
32/// lines, which markdown treats as soft breaks within one paragraph. This
33/// doubles all single newlines so each line becomes its own paragraph with
34/// spacing between them.
35fn 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    /// Handle LSP completion response.
46    /// Supports merging from multiple servers: first response creates the menu,
47    /// subsequent responses extend it.
48    pub(crate) fn handle_completion_response(
49        &mut self,
50        request_id: u64,
51        items: Vec<lsp_types::CompletionItem>,
52    ) -> AnyhowResult<()> {
53        // Check if this is one of the pending completion requests
54        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        // Update status when all completion responses have arrived
63        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        // Get the partial word at cursor to filter completions
73        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        // Filter completions to match the typed prefix
89        let filtered_items: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
90            // No prefix - show all completions
91            items.iter().collect()
92        } else {
93            // Filter to items that start with the prefix (case-insensitive)
94            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        // Store/extend original items for type-to-filter (merge from multiple servers)
113        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        // Rebuild popup from ALL merged items (not just the new batch)
124        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        // Build LSP popup items, then append buffer-word items below.
147        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        // Deduplicate: skip buffer-word items whose label already appears in LSP results.
151        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            // Convert PopupData to Popup and use show_or_replace to avoid stacking
169            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    /// Handle LSP go-to-definition response
183    pub(crate) fn handle_goto_definition_response(
184        &mut self,
185        request_id: u64,
186        locations: Vec<lsp_types::Location>,
187    ) -> AnyhowResult<()> {
188        // Check if this is the pending request
189        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        // For now, just jump to the first location
205        let location = &locations[0];
206
207        // Convert URI to file path
208        if let Ok(path) = uri_to_path(&location.uri) {
209            // Open the file
210            let buffer_id = match self.open_file(&path) {
211                Ok(id) => id,
212                Err(e) => {
213                    // Check if this is a large file encoding confirmation error
214                    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            // Move cursor to the definition position
228            let line = location.range.start.line as usize;
229            let character = location.range.start.character as usize;
230
231            // Calculate byte position from line and character
232            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                // Move cursor - read cursor info from split view state
239                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, // Reset sticky column for goto definition
257                };
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    /// Check if there are any pending LSP requests
282    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    /// Cancel any pending LSP requests
288    /// This should be called when the user performs an action that would make
289    /// the pending request's results stale (e.g., cursor movement, text editing)
290    pub(crate) fn cancel_pending_lsp_requests(&mut self) {
291        // Cancel scheduled (not yet sent) completion trigger
292        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            // Send cancellation to the LSP server
307            self.send_lsp_cancel_request(request_id);
308            self.update_lsp_status_from_server_statuses();
309        }
310    }
311
312    /// Send a cancel request to the LSP server for a specific request ID
313    fn send_lsp_cancel_request(&mut self, request_id: u64) {
314        // Get language from buffer state
315        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            // Only send cancel if LSP is already running (no need to spawn just to cancel)
322            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    /// Dispatch an exclusive LSP feature request to the first handle that allows the feature.
333    ///
334    /// Ensures all handles receive didOpen first, then calls the closure with the first
335    /// handle matching the feature filter. For features like hover, definition, rename, etc.
336    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        // Ensure didOpen is sent to all handles
364        self.ensure_did_open_all(buffer_id, &uri, &language)?;
365
366        // Dispatch to the first handle that allows this feature
367        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    /// Dispatch a merged LSP feature request to all handles that allow the feature.
373    ///
374    /// Ensures all handles receive didOpen first, then calls the closure for each
375    /// handle matching the feature filter, collecting all results. For features like
376    /// completion, code actions, diagnostics, etc.
377    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        // Ensure didOpen is sent to all handles
411        if self
412            .ensure_did_open_all(buffer_id, &uri, &language)
413            .is_none()
414        {
415            return Vec::new();
416        }
417
418        // Dispatch to all handles that allow this feature
419        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    /// Like `with_all_lsp_for_buffer_feature`, but also passes the server name
430    /// to the closure for attribution purposes.
431    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    /// Ensure didOpen has been sent to all handles for the given buffer's language.
482    /// Returns Some(()) on success, None if we can't access required state.
483    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    /// Request LSP completion at current cursor position.
533    /// Sends completion requests to all eligible servers for merged results.
534    pub(crate) fn request_completion(&mut self) {
535        // A new completion request starts a fresh batch. Cancel any
536        // previous in-flight completion requests so their late responses
537        // are ignored (handle_completion_response drops responses whose
538        // request_id isn't in pending_completion_requests), and drop any
539        // leftover items from a previous popup that was closed via the
540        // "pass-through" path (hide_popup() without handle_popup_cancel,
541        // e.g. Enter or a non-word character while the popup was open).
542        // Without this, the new response would be merged into the stale
543        // items by `handle_completion_response`'s extend branch, leading
544        // to duplicate / stale entries in the rendered popup — see the
545        // regression test in
546        // crates/fresh-editor/tests/e2e/lsp_completion_duplicate_entries_1514.rs
547        // and sinelaw/fresh#1514.
548        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        // Get the current buffer and cursor position
561        let cursor_pos = self.active_cursors().primary().position;
562        let state = self.active_state();
563
564        // Convert byte position to LSP position (line, UTF-16 code units)
565        let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
566        let buffer_id = self.active_buffer();
567
568        // Pre-allocate request IDs for all eligible servers
569        let base_request_id = self.next_lsp_request_id;
570        // Use an atomic counter in the closure
571        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        // Advance the ID counter past all allocated IDs
601        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            // No LSP servers available — show buffer-word completions as popup.
608            self.show_buffer_word_completion_popup();
609        }
610    }
611
612    /// Show a completion popup with buffer-word results only (no LSP).
613    ///
614    /// Called when no LSP servers are available for the current buffer.
615    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    /// Check if the inserted character should trigger completion
632    /// and if so, request completion automatically (possibly after a delay).
633    ///
634    /// Only triggers when `completion_popup_auto_show` is enabled. Then:
635    /// 1. Trigger characters (like `.`, `::`, etc.): immediate if suggest_on_trigger_characters is enabled
636    /// 2. Word characters: delayed by quick_suggestions_delay_ms if quick_suggestions is enabled
637    ///
638    /// This provides VS Code-like behavior where suggestions appear while typing,
639    /// with debouncing to avoid spamming the LSP server.
640    pub(crate) fn maybe_trigger_completion(&mut self, c: char) {
641        // Auto-show must be enabled for any automatic triggering
642        if !self.config.editor.completion_popup_auto_show {
643            return;
644        }
645
646        // Get the active buffer's language
647        let language = self.active_state().language.clone();
648
649        // Check if this character is a trigger character for this language
650        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        // Check if quick suggestions is enabled and this is a word character
657        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        // Case 1: Trigger character - immediate trigger (bypasses delay)
662        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            // Cancel any pending scheduled trigger
669            self.scheduled_completion_trigger = None;
670            self.request_completion();
671            return;
672        }
673
674        // Case 2: Word character with quick suggestions - schedule delayed trigger
675        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            // Schedule (or reschedule) the completion trigger
687            // This effectively debounces - each keystroke resets the timer
688            self.scheduled_completion_trigger = Some(trigger_time);
689        } else {
690            // Non-word, non-trigger character (space, punctuation, etc.) —
691            // cancel any pending scheduled trigger so a stale timer from the
692            // previous word doesn't fire at the wrong cursor position.
693            self.scheduled_completion_trigger = None;
694        }
695    }
696
697    /// Request LSP go-to-definition at current cursor position
698    pub(crate) fn request_goto_definition(&mut self) -> AnyhowResult<()> {
699        // Get the current buffer and cursor position
700        let cursor_pos = self.active_cursors().primary().position;
701        let state = self.active_state();
702
703        // Convert byte position to LSP position (line, UTF-16 code units)
704        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        // Use helper to ensure didOpen is sent before the request
709        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    /// Request LSP hover documentation at current cursor position
742    pub(crate) fn request_hover(&mut self) -> AnyhowResult<()> {
743        // Get the current buffer and cursor position
744        let cursor_pos = self.active_cursors().primary().position;
745        let state = self.active_state();
746
747        // Convert byte position to LSP position (line, UTF-16 code units)
748        let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
749
750        // Debug: Log the position conversion details
751        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        // Use helper to ensure didOpen is sent before the request
765        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    /// Request LSP hover documentation at a specific byte position
791    /// Used for mouse-triggered hover
792    /// Returns `Ok(true)` if the request was dispatched, `Ok(false)` if no
793    /// eligible server was available (e.g. not yet initialized).
794    pub(crate) fn request_hover_at_position(&mut self, byte_pos: usize) -> AnyhowResult<bool> {
795        // Get the current buffer
796        let state = self.active_state();
797
798        // Convert byte position to LSP position (line, UTF-16 code units)
799        let (line, character) = state.buffer.position_to_lsp_position(byte_pos);
800
801        // Debug: Log the position conversion details
802        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        // Use helper to ensure didOpen is sent before the request
816        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    /// Handle hover response from LSP
842    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        // Check if this response is for the current pending request
850        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        // Debug: log raw hover content to diagnose formatting issues
865        tracing::debug!(
866            "LSP hover content (markdown={}):\n{}",
867            is_markdown,
868            contents
869        );
870
871        // Convert LSP range to byte offsets for highlighting
872        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            // Remove previous hover overlay if any
892            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            // Add overlay to highlight the hovered symbol
898            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), // Subtle highlight for hovered symbol
903                },
904                priority: 90, // Below rename (100) but above syntax (lower)
905                message: None,
906                extend_to_line_end: false,
907                url: None,
908            };
909            self.apply_event_to_active_buffer(&event);
910            // Store the handle for later removal
911            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            // No range provided by LSP - compute word boundaries at hover position
916            // This prevents the popup from following the mouse within the same word
917            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        // Create a popup with the hover contents
937        use crate::view::popup::{Popup, PopupPosition};
938        use ratatui::style::Style;
939
940        // Use markdown rendering if the content is markdown
941        let mut popup = if is_markdown {
942            Popup::markdown(&contents, &self.theme, Some(&self.grammar_registry))
943        } else {
944            // Plain text - split by lines
945            let lines: Vec<String> = contents.lines().map(|s| s.to_string()).collect();
946            Popup::text(lines, &self.theme)
947        };
948
949        // Configure popup properties
950        popup.title = Some(t!("lsp.popup_hover").to_string());
951        popup.transient = true;
952        // Use mouse position if this was a mouse-triggered hover, otherwise use cursor position
953        popup.position = if let Some((x, y)) = self.mouse_hover_screen_position.take() {
954            // Position below the mouse, offset by 1 row
955            PopupPosition::Fixed { x, y: y + 1 }
956        } else {
957            PopupPosition::BelowCursor
958        };
959        popup.width = 80;
960        // Use dynamic max_height based on terminal size (60% of height, min 15, max 40)
961        // This allows hover popups to show more documentation on larger terminals
962        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        // Show the popup
968        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        // Mark hover request as sent to prevent duplicate popups during race conditions
974        // (e.g., when mouse moves while a hover response is pending)
975        self.mouse_state.lsp_hover_request_sent = true;
976    }
977
978    /// Apply inlay hints to editor state as virtual text
979    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        // Clear existing inlay hints
987        state.virtual_texts.clear(&mut state.marker_list);
988
989        if hints.is_empty() {
990            return;
991        }
992
993        // Style for inlay hints - dimmed to not distract from actual code
994        let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
995
996        for hint in hints {
997            // Convert LSP position to byte offset
998            let byte_offset = state.buffer.lsp_position_to_byte(
999                hint.position.line as usize,
1000                hint.position.character as usize,
1001            );
1002
1003            // Extract text from hint label
1004            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            // LSP inlay hint positions are insertion points between characters.
1012            // For positions within the buffer, render hints before the character at the
1013            // byte offset so they appear at the correct location (e.g., before punctuation
1014            // or newline). Hints at or beyond EOF are anchored to the last character and
1015            // rendered after it.
1016            if state.buffer.is_empty() {
1017                continue;
1018            }
1019
1020            let (byte_offset, position) = if byte_offset >= state.buffer.len() {
1021                // If hint is at EOF, anchor to last character and render after it.
1022                (
1023                    state.buffer.len().saturating_sub(1),
1024                    VirtualTextPosition::AfterChar,
1025                )
1026            } else {
1027                (byte_offset, VirtualTextPosition::BeforeChar)
1028            };
1029
1030            // Use the hint text as-is - spacing is handled during rendering
1031            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, // Default priority
1040            );
1041        }
1042
1043        tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
1044    }
1045
1046    /// Request LSP find references at current cursor position
1047    pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
1048        // Get the current buffer and cursor position
1049        let cursor_pos = self.active_cursors().primary().position;
1050        let state = self.active_state();
1051
1052        // Extract the word under cursor for display
1053        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                // Find word boundaries
1066                let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
1067
1068                // Find start of word
1069                let mut start = cursor_pos;
1070                while start > 0 {
1071                    // Move to previous byte
1072                    start -= 1;
1073                    // Skip continuation bytes (UTF-8)
1074                    while start > 0 && (bytes[start] & 0xC0) == 0x80 {
1075                        start -= 1;
1076                    }
1077                    // Get the character at this position
1078                    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                // Find end of word
1089                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        // Convert byte position to LSP position (line, UTF-16 code units)
1113        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        // Use helper to ensure didOpen is sent before the request
1118        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    /// Request LSP signature help at current cursor position
1150    pub(crate) fn request_signature_help(&mut self) {
1151        // Get the current buffer and cursor position
1152        let cursor_pos = self.active_cursors().primary().position;
1153        let state = self.active_state();
1154
1155        // Convert byte position to LSP position (line, UTF-16 code units)
1156        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        // Use helper to ensure didOpen is sent before the request
1161        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    /// Handle signature help response from LSP
1194    pub(crate) fn handle_signature_help_response(
1195        &mut self,
1196        request_id: u64,
1197        signature_help: Option<lsp_types::SignatureHelp>,
1198    ) {
1199        // Check if this response is for the current pending request
1200        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        // Get the active signature
1217        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        // Build the display content as markdown
1224        let mut content = String::new();
1225
1226        // Add the signature label (function signature)
1227        content.push_str(&signature.label);
1228        content.push('\n');
1229
1230        // Add parameter highlighting info
1231        let active_param = signature_help
1232            .active_parameter
1233            .or(signature.active_parameter)
1234            .unwrap_or(0) as usize;
1235
1236        // If there are parameters, highlight the active one
1237        if let Some(params) = &signature.parameters {
1238            if let Some(param) = params.get(active_param) {
1239                // Get parameter label
1240                let param_label = match &param.label {
1241                    lsp_types::ParameterLabel::Simple(s) => s.clone(),
1242                    lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1243                        // Extract substring from signature label
1244                        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                // Add parameter documentation if available
1259                if let Some(doc) = &param.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        // Add function documentation if available
1274        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        // Create a popup with markdown rendering (like hover popup)
1286        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        // Show the popup
1299        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    /// Request LSP code actions at current cursor position.
1309    /// Sends code action requests to all eligible servers for merged results.
1310    pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1311        // Get the current buffer and cursor position
1312        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        // Convert byte position to LSP position (line, UTF-16 code units)
1317        let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1318
1319        // Get selection range (if any) or use cursor position
1320        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        // Get diagnostics at cursor position for context
1329        // TODO: Implement diagnostic retrieval when needed
1330        let diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
1331        let buffer_id = self.active_buffer();
1332
1333        // Pre-allocate request IDs for all eligible servers
1334        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        // Advance the ID counter past all allocated IDs
1378        self.next_lsp_request_id = base_request_id + results.len() as u64;
1379
1380        if !sent_ids.is_empty() {
1381            // Clear any previously accumulated actions for a fresh merge
1382            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    /// Handle code actions response from LSP.
1391    /// Supports merging from multiple servers: each response extends the action
1392    /// list, and the popup is shown/updated with each arriving response.
1393    pub(crate) fn handle_code_actions_response(
1394        &mut self,
1395        request_id: u64,
1396        actions: Vec<lsp_types::CodeActionOrCommand>,
1397    ) {
1398        // Check if this response is for one of the pending requests
1399        if !self.pending_code_actions_requests.remove(&request_id) {
1400            tracing::debug!("Ignoring stale code actions response: {}", request_id);
1401            return;
1402        }
1403
1404        // Update status when all code action responses have arrived
1405        if self.pending_code_actions_requests.is_empty() {
1406            self.update_lsp_status_from_server_statuses();
1407        }
1408
1409        // Look up the server name for this request
1410        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            // Only show "no code actions" if all responses are in and we have nothing
1417            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        // Tag each action with its server name and store/extend for merging
1429        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        // Build list items from all accumulated code actions
1445        use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1446        use ratatui::style::Style;
1447
1448        // Check if actions come from multiple servers
1449        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                // Show server name in detail when multiple servers contribute
1473                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        // Show the popup, replacing any existing action popup to avoid stacking
1500        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    /// Execute a code action by index from the stored pending_code_actions.
1510    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 the action has no edit and no command, it may need resolve first.
1524                // Only resolve if the action has `data` and the server supports resolveProvider.
1525                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    /// Execute a code action that has been fully resolved (has edit and/or command).
1546    pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1547        let title = ca.title.clone();
1548
1549        // Apply workspace edit if present
1550        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        // Execute command if present (may trigger workspace/applyEdit from server)
1565        if let Some(cmd) = ca.command {
1566            self.send_execute_command(cmd);
1567        }
1568    }
1569
1570    /// Send workspace/executeCommand to the LSP server
1571    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        // Get the language for this buffer to find the right LSP handle
1583        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    /// Send codeAction/resolve to the LSP server
1605    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    /// Check if any LSP server for the current buffer supports codeAction/resolve
1628    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    /// Check if any LSP server for the current buffer supports completionItem/resolve
1649    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    /// Send completionItem/resolve to the LSP server
1670    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    /// Handle a resolved completion item — apply additional_text_edits (e.g. auto-imports).
1700    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    /// Apply formatting edits from textDocument/formatting response.
1716    pub(crate) fn apply_formatting_edits(
1717        &mut self,
1718        uri: &str,
1719        edits: Vec<lsp_types::TextEdit>,
1720    ) -> AnyhowResult<usize> {
1721        // Find the buffer for this URI
1722        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    /// Request document formatting from LSP.
1739    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    /// Handle find references response from LSP
1780    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        // Check if this response is for the current pending request
1792        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        // Convert locations to hook args format
1806        let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
1807            .iter()
1808            .map(|loc| {
1809                // Convert URI to file path
1810                let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
1811                    // Extract path from file:// URI
1812                    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, // LSP is 0-based, convert to 1-based
1820                    column: loc.range.start.character + 1, // LSP is 0-based
1821                }
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        // Fire the lsp_references hook so plugins can display the results
1832        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    /// Apply LSP text edits to a buffer and return the number of changes made.
1850    /// Edits are sorted in reverse order and applied as a batch.
1851    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        // Sort edits by position (reverse order to avoid offset issues)
1861        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        // Collect all events for this buffer into a batch
1870        let mut batch_events = Vec::new();
1871        let mut changes = 0;
1872
1873        // Get cursor_id for this buffer from split view state
1874        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        // Create events for all edits
1888        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            // Convert LSP range to byte positions
1895            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            // Log the conversion for debugging
1905            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            // Delete old text
1920            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            // Insert new text
1931            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        // Apply all rename changes using bulk edit for O(n) performance
1944        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    /// Apply a single TextDocumentEdit from a workspace edit.
1956    ///
1957    /// Per LSP spec: if `text_document.version` is non-null, it must match the
1958    /// version we last sent via didOpen/didChange. On mismatch the edit is stale
1959    /// and we skip it to avoid corrupting the buffer.
1960    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        // Version check: if the server specifies a version, verify it matches
1967        // what we sent. A mismatch means the edit was computed against stale content.
1968        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    /// Apply a resource operation (CreateFile, RenameFile, DeleteFile) from a workspace edit.
2040    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                // Create parent directories if needed
2067                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                // Open the new file as a buffer
2074                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                // Create parent directories if needed
2107                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    /// Apply an LSP WorkspaceEdit (used by rename, code actions, etc.).
2147    ///
2148    /// Returns the total number of text changes applied.
2149    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        // Handle changes (map of URI -> Vec<TextEdit>)
2165        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        // Handle document_changes (TextDocumentEdit[] or DocumentChangeOperation[])
2190        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                    // Process operations in order — resource ops (create/rename/delete)
2201                    // must be applied before text edits on the created/renamed files.
2202                    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    /// Handle rename response from LSP
2221    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                // Per LSP spec: ContentModified errors (-32801) should NOT be shown to user
2235                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    /// Apply events to a specific buffer using bulk edit optimization (O(n) vs O(n²))
2251    ///
2252    /// This is similar to `apply_events_as_bulk_edit` but works on a specific buffer
2253    /// (which may not be the active buffer) and handles LSP notifications correctly.
2254    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        // Create a temporary batch for collecting LSP changes (before applying)
2267        let batch_for_lsp = Event::Batch {
2268            events: events.clone(),
2269            description: description.clone(),
2270        };
2271
2272        // IMPORTANT: Calculate LSP changes BEFORE applying to buffer!
2273        // The byte positions in the events are relative to the ORIGINAL buffer.
2274        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        // Capture old cursor states from split view state
2280        // Find a split that has this buffer in its keyed_states
2281        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        // Snapshot buffer state for undo (piece tree + buffers)
2305        let old_snapshot = state.buffer.snapshot_buffer_state();
2306
2307        // Convert events to edit tuples: (position, delete_len, insert_text)
2308        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        // Sort edits by position descending (required by apply_bulk_edits)
2322        edits.sort_by(|a, b| b.0.cmp(&a.0));
2323
2324        // Convert to references for apply_bulk_edits
2325        let edit_refs: Vec<(usize, usize, &str)> = edits
2326            .iter()
2327            .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2328            .collect();
2329
2330        // Snapshot displaced markers before edits so undo can restore them exactly.
2331        let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2332
2333        // Apply bulk edits - O(n) instead of O(n²)
2334        let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2335
2336        // Calculate new cursor positions based on edits
2337        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        // Calculate new cursor positions
2355        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        // Snapshot buffer state after edits (for redo)
2370        let new_snapshot = state.buffer.snapshot_buffer_state();
2371
2372        // Invalidate syntax highlighting
2373        state.highlighter.invalidate_all();
2374
2375        // Apply new cursor positions to split view state
2376        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        // Convert edit list to lengths-only for undo/redo marker replay.
2388        // Merge edits at the same position into a single replacement.
2389        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        // Adjust markers using merged net-delta (same logic as apply_events_as_bulk_edit)
2405        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        // Create BulkEdit event for undo log
2424        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        // Add to event log
2435        if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
2436            event_log.append(bulk_edit);
2437        }
2438
2439        // Notify LSP about the changes using pre-calculated positions
2440        self.send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2441
2442        Ok(())
2443    }
2444
2445    /// Send pre-calculated LSP changes for a specific buffer
2446    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        // Check if LSP is enabled for this buffer
2456        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        // Get the URI
2473        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        // Get language from buffer state
2485        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        // Check if we can use LSP (respects auto_start setting)
2503        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        // Check which handles need didOpen first
2518        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            // Get text for didOpen
2531            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            // Send didOpen to all handles that haven't been opened yet
2546            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            // Mark all as opened
2572            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            // didOpen already contains the full current buffer content, so we must
2579            // NOT also send didChange (which carries pre-edit incremental changes).
2580            // Sending both would corrupt the server's view of the document.
2581            return;
2582        }
2583
2584        // Now send didChange to all handles for this language
2585        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            // Invalidate diagnostic cache so the next diagnostic apply recomputes
2598            // overlay positions from fresh byte offsets (the buffer content changed)
2599            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            // Schedule debounced diagnostic re-pull (1000ms after last edit)
2608            self.scheduled_diagnostic_pull = Some((
2609                buffer_id,
2610                std::time::Instant::now() + std::time::Duration::from_millis(1000),
2611            ));
2612        }
2613    }
2614
2615    /// Start rename mode - select the symbol at cursor and allow inline editing
2616    pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2617        // If server supports prepareRename, validate first
2618        if self.server_supports_prepare_rename() {
2619            self.send_prepare_rename();
2620            return Ok(());
2621        }
2622
2623        self.show_rename_prompt()
2624    }
2625
2626    /// Handle prepareRename response — if valid, show rename prompt; if error, show message.
2627    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                // prepareRename succeeded — show the rename prompt
2634                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    /// Check if any LSP server for the current buffer supports prepareRename
2648    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                    // prepareRename is advertised via prepare_support in client caps
2662                    // and supported if server has rename capability
2663                    return true;
2664                }
2665            }
2666        }
2667        false
2668    }
2669
2670    /// Send textDocument/prepareRename to the LSP server
2671    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    /// Show the rename prompt (called directly or after prepareRename succeeds).
2708    fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2709        use crate::primitives::word_navigation::{find_word_end, find_word_start};
2710
2711        // Get the current buffer and cursor position
2712        let cursor_pos = self.active_cursors().primary().position;
2713        let (word_start, word_end) = {
2714            let state = self.active_state();
2715
2716            // Find the word boundaries
2717            let word_start = find_word_start(&state.buffer, cursor_pos);
2718            let word_end = find_word_end(&state.buffer, cursor_pos);
2719
2720            // Check if we're on a word
2721            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        // Get the word text
2730        let word_text = self.active_state_mut().get_text_range(word_start, word_end);
2731
2732        // Create an overlay to highlight the symbol being renamed
2733        let overlay_handle = self.add_overlay(
2734            None,
2735            word_start..word_end,
2736            crate::model::event::OverlayFace::Background {
2737                color: (50, 100, 200), // Blue background for rename
2738            },
2739            100,
2740            Some(t!("lsp.popup_renaming").to_string()),
2741        );
2742
2743        // Enter rename mode using the Prompt system
2744        // Store the rename metadata in the PromptType and pre-fill the input with the current name
2745        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        // Pre-fill the input with the current name and position cursor at the end
2755        prompt.set_input(word_text);
2756
2757        self.prompt = Some(prompt);
2758        Ok(())
2759    }
2760
2761    /// Cancel rename mode - removes overlay if the prompt was for LSP rename
2762    pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
2763        self.remove_overlay(handle.clone());
2764    }
2765
2766    /// Perform the actual LSP rename request
2767    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        // Remove the overlay first
2775        self.cancel_rename_overlay(&overlay_handle);
2776
2777        // Check if the name actually changed
2778        if new_name == original_text {
2779            self.status_message = Some(t!("lsp.name_unchanged").to_string());
2780            return;
2781        }
2782
2783        // Use the position from when we entered rename mode, NOT the current cursor position
2784        // This ensures we send the rename request for the correct symbol even if cursor moved
2785        let rename_pos = start_pos;
2786
2787        // Convert byte position to LSP position (line, UTF-16 code units)
2788        // LSP uses UTF-16 code units for character offsets, not byte offsets
2789        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        // Use helper to ensure didOpen is sent before the request
2795        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    /// Request inlay hints for the active buffer (if enabled and LSP available)
2831    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        // Get line count from buffer state
2839        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        // Use helper to ensure didOpen is sent before the request
2848        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    /// Schedule a folding range refresh for a buffer (debounced).
2876    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    /// Issue a debounced folding range request if the timer has elapsed.
2882    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    /// Request folding ranges for a buffer if supported and needed.
2895    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        // Ensure there is a running server
2924        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    /// Request semantic tokens for a specific buffer if supported and needed.
2961    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        // Avoid duplicate in-flight requests per buffer
2967        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        // Get language from buffer state
2982        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        // Ensure there is a running server
2991        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        // Check that a server actually supports full semantic tokens
2997        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; // Already up to date
3011            }
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        // Check capabilities on the specific server we'll send to
3023        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    /// Schedule a full semantic token refresh for a buffer (debounced).
3062    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    /// Issue a debounced full semantic token request if the timer has elapsed.
3073    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    /// Request semantic tokens for a viewport range (with padding).
3091    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        // Get language from buffer state
3108        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        // Ensure there is a running server
3117        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            // Fall back to full document tokens if no server supports range.
3124            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        // The handle_for_feature_mut check ensures has_capability(SemanticTokens) which is
3135        // full || range. Double-check this specific server supports range.
3136        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        // Single newlines become double newlines
3356        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        // Already-double newlines stay double (not quadrupled)
3366        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}