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 crate::model::event::{BufferId, Event};
19use crate::primitives::word_navigation::{find_word_end, find_word_start};
20use crate::view::prompt::{Prompt, PromptType};
21
22use crate::services::lsp::async_handler::LspHandle;
23use crate::types::LspFeature;
24
25use super::{Editor, SemanticTokenRangeRequest};
26
27/// Ensure every line in a docstring is separated by a blank line.
28///
29/// LSP documentation (e.g. from pyright) often uses single newlines between
30/// lines, which markdown treats as soft breaks within one paragraph. This
31/// doubles all single newlines so each line becomes its own paragraph with
32/// spacing between them.
33fn space_doc_paragraphs(text: &str) -> String {
34    text.replace("\n\n", "\x00").replace(['\n', '\x00'], "\n\n")
35}
36
37/// Whether an LSP range (half-open end, like `[start, end)`) contains the given
38/// `(line, character)` LSP position. Zero-length ranges (start == end) are
39/// treated as containing their single anchor point so point-style diagnostics
40/// still match a hover that lands exactly on them.
41fn lsp_range_contains(range: &lsp_types::Range, line: u32, character: u32) -> bool {
42    let start = range.start;
43    let end = range.end;
44    // Before start?
45    if line < start.line || (line == start.line && character < start.character) {
46        return false;
47    }
48    // Zero-length range: accept exact anchor match.
49    if start.line == end.line && start.character == end.character {
50        return line == start.line && character == start.character;
51    }
52    // After end? (half-open)
53    if line > end.line || (line == end.line && character >= end.character) {
54        return false;
55    }
56    true
57}
58
59/// Whether an LSP range overlaps the half-open range
60/// `[(b_start_line, b_start_char), (b_end_line, b_end_char))`. Zero-length
61/// ranges on either side are treated as their single anchor point — so a
62/// point cursor matches any diagnostic whose range covers that point, and a
63/// point-style diagnostic matches a selection that covers its anchor.
64fn lsp_range_overlaps(
65    a: &lsp_types::Range,
66    b_start_line: u32,
67    b_start_char: u32,
68    b_end_line: u32,
69    b_end_char: u32,
70) -> bool {
71    let b_is_point = b_start_line == b_end_line && b_start_char == b_end_char;
72    if b_is_point {
73        return lsp_range_contains(a, b_start_line, b_start_char);
74    }
75    let a_is_point = a.start.line == a.end.line && a.start.character == a.end.character;
76    if a_is_point {
77        let (p_line, p_char) = (a.start.line, a.start.character);
78        if p_line < b_start_line || (p_line == b_start_line && p_char < b_start_char) {
79            return false;
80        }
81        if p_line > b_end_line || (p_line == b_end_line && p_char >= b_end_char) {
82            return false;
83        }
84        return true;
85    }
86    // Both have extent. A entirely before B?
87    if a.end.line < b_start_line || (a.end.line == b_start_line && a.end.character <= b_start_char)
88    {
89        return false;
90    }
91    // A entirely after B?
92    if a.start.line > b_end_line || (a.start.line == b_end_line && a.start.character >= b_end_char)
93    {
94        return false;
95    }
96    true
97}
98
99const SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS: u64 = 50;
100const SEMANTIC_TOKENS_RANGE_PADDING_LINES: usize = 10;
101
102impl Editor {
103    /// Handle LSP completion response.
104    /// Supports merging from multiple servers: first response creates the menu,
105    /// subsequent responses extend it.
106    pub(crate) fn handle_completion_response(
107        &mut self,
108        request_id: u64,
109        items: Vec<lsp_types::CompletionItem>,
110    ) -> AnyhowResult<()> {
111        // Check if this is one of the pending completion requests
112        if !self
113            .active_window_mut()
114            .pending_completion_requests
115            .remove(&request_id)
116        {
117            tracing::debug!(
118                "Ignoring completion response for outdated request {}",
119                request_id
120            );
121            return Ok(());
122        }
123
124        if items.is_empty() {
125            tracing::debug!("No completion items received");
126            if self.active_window().pending_completion_requests.is_empty()
127                && self.active_window().completion_items.is_none()
128            {
129                // All servers responded with nothing — fall back to buffer-word completions,
130                // matching the behaviour when no LSP servers are available at all.
131                self.show_buffer_word_completion_popup();
132            }
133            return Ok(());
134        }
135
136        // Get the partial word at cursor to filter completions
137        use crate::primitives::word_navigation::find_completion_word_start;
138        let cursor_pos = self.active_cursors().primary().position;
139        let (word_start, cursor_pos) = {
140            let state = self.active_state();
141            let word_start = find_completion_word_start(&state.buffer, cursor_pos);
142            (word_start, cursor_pos)
143        };
144        let prefix = if word_start < cursor_pos {
145            self.active_state_mut()
146                .get_text_range(word_start, cursor_pos)
147                .to_lowercase()
148        } else {
149            String::new()
150        };
151
152        let matches_prefix = |item: &lsp_types::CompletionItem| -> bool {
153            prefix.is_empty()
154                || item.label.to_lowercase().starts_with(&prefix)
155                || item
156                    .filter_text
157                    .as_ref()
158                    .map(|ft| ft.to_lowercase().starts_with(&prefix))
159                    .unwrap_or(false)
160        };
161
162        let filtered_items: Vec<&lsp_types::CompletionItem> =
163            items.iter().filter(|item| matches_prefix(item)).collect();
164
165        if filtered_items.is_empty() && self.active_window().completion_items.is_none() {
166            tracing::debug!("No completion items match prefix '{}'", prefix);
167            return Ok(());
168        }
169
170        // Store/extend original items for type-to-filter (merge from multiple servers)
171        match &mut self.active_window_mut().completion_items {
172            Some(existing) => {
173                existing.extend(items);
174                tracing::debug!("Extended completion items, now {} total", existing.len());
175            }
176            None => {
177                self.active_window_mut().completion_items = Some(items);
178            }
179        }
180
181        // Rebuild popup from ALL merged items (not just the new batch)
182        let all_items = self.active_window_mut().completion_items.as_ref().unwrap();
183        let all_filtered: Vec<&lsp_types::CompletionItem> = all_items
184            .iter()
185            .filter(|item| matches_prefix(item))
186            .collect();
187
188        if all_filtered.is_empty() {
189            tracing::debug!("No completion items match prefix '{}'", prefix);
190            return Ok(());
191        }
192
193        // Build LSP popup items, then append buffer-word items below.
194        let mut all_popup_items =
195            crate::app::popup_actions::lsp_items_to_popup_items(&all_filtered);
196        let buffer_word_items = self.get_buffer_completion_popup_items();
197        // Deduplicate: skip buffer-word items whose label already appears in LSP results.
198        let lsp_labels: std::collections::HashSet<String> = all_popup_items
199            .iter()
200            .map(|i| i.text.to_lowercase())
201            .collect();
202        all_popup_items.extend(
203            buffer_word_items
204                .into_iter()
205                .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
206        );
207
208        let popup_data =
209            crate::app::popup_actions::build_completion_popup_from_items(all_popup_items, 0);
210        let accept_hint = self.completion_accept_key_hint();
211        let focus_hint = self.popup_focus_key_hint();
212
213        {
214            let buffer_id = self.active_buffer();
215            let state = self
216                .windows
217                .get_mut(&self.active_window)
218                .map(|w| &mut w.buffers)
219                .expect("active window present")
220                .get_mut(&buffer_id)
221                .unwrap();
222            // Convert PopupData to Popup and use show_or_replace to avoid stacking
223            let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
224            popup_obj.accept_key_hint = accept_hint;
225            popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
226            popup_obj.focus_key_hint = focus_hint;
227            state.popups.show_or_replace(popup_obj);
228        }
229
230        tracing::info!(
231            "Showing completion popup with {} items",
232            self.active_window_mut()
233                .completion_items
234                .as_ref()
235                .map_or(0, |i| i.len())
236        );
237
238        Ok(())
239    }
240
241    /// Handle LSP go-to-definition response
242    pub(crate) fn handle_goto_definition_response(
243        &mut self,
244        request_id: u64,
245        locations: Vec<lsp_types::Location>,
246    ) -> AnyhowResult<()> {
247        // Check if this is the pending request
248        if self.active_window_mut().pending_goto_definition_request != Some(request_id) {
249            tracing::debug!(
250                "Ignoring go-to-definition response for outdated request {}",
251                request_id
252            );
253            return Ok(());
254        }
255
256        self.active_window_mut().pending_goto_definition_request = None;
257
258        if locations.is_empty() {
259            self.active_window_mut().status_message = Some(t!("lsp.no_definition").to_string());
260            return Ok(());
261        }
262
263        // For now, just jump to the first location
264        let location = &locations[0];
265
266        // Resolve the URI to a buffer. `open_lsp_uri_target` handles
267        // all three cases: host file under the workspace mount,
268        // container-only file fetched via `docker exec cat`, and
269        // unreachable (no file at the host path AND container fetch
270        // failed). The last case becomes a user-visible status
271        // message instead of a phantom empty buffer.
272        let wire = crate::app::types::LspUri::from_wire(location.uri.clone());
273        let buffer_id = match self.open_lsp_uri_target(&wire) {
274            Ok(id) => id,
275            Err(e) => {
276                if let Some(confirmation) =
277                    e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
278                {
279                    self.start_large_file_encoding_confirmation(confirmation);
280                } else {
281                    self.set_status_message(
282                        t!("file.error_opening", error = e.to_string()).to_string(),
283                    );
284                }
285                return Ok(());
286            }
287        };
288
289        // Move cursor to the definition position. The buffer's
290        // `file_path` is the *destination* path — the host path on a
291        // bind-mounted file, the container path on a fetched one —
292        // so we read it back for the status message rather than
293        // formatting the original wire URI.
294        let line = location.range.start.line as usize;
295        let character = location.range.start.character as usize;
296        let position = self
297            .buffers()
298            .get(&buffer_id)
299            .map(|state| state.buffer.line_col_to_position(line, character));
300
301        if let Some(position) = position {
302            let (cursor_id, old_position, old_anchor, old_sticky_column) = {
303                let cursors = self.active_cursors();
304                let primary = cursors.primary();
305                (
306                    cursors.primary_id(),
307                    primary.position,
308                    primary.anchor,
309                    primary.sticky_column,
310                )
311            };
312            let event = crate::model::event::Event::MoveCursor {
313                cursor_id,
314                old_position,
315                new_position: position,
316                old_anchor,
317                new_anchor: None,
318                old_sticky_column,
319                new_sticky_column: 0,
320            };
321
322            let split_id = self
323                .windows
324                .get(&self.active_window)
325                .and_then(|w| w.buffers.splits())
326                .map(|(mgr, _)| mgr)
327                .expect("active window must have a populated split layout")
328                .active_split();
329            self.active_window_mut()
330                .apply_event_to_buffer(buffer_id, split_id, &event);
331            // Without this the cursor lands at the definition but the
332            // viewport never scrolls when the target file is already
333            // open (#1689).
334            self.active_window_mut()
335                .ensure_active_cursor_visible_for_navigation(true);
336        }
337
338        let display_path = self
339            .buffers()
340            .get(&buffer_id)
341            .and_then(|s| s.buffer.file_path().map(|p| p.display().to_string()))
342            .unwrap_or_default();
343        self.active_window_mut().status_message = Some(
344            t!(
345                "lsp.jumped_to_definition",
346                path = display_path,
347                line = line + 1
348            )
349            .to_string(),
350        );
351
352        Ok(())
353    }
354
355    /// Dispatch an exclusive LSP feature request to the first handle that allows the feature.
356    ///
357    /// Ensures all handles receive didOpen first, then calls the closure with the first
358    /// handle matching the feature filter. For features like hover, definition, rename, etc.
359    pub(crate) fn with_lsp_for_buffer<F, R>(
360        &mut self,
361        buffer_id: BufferId,
362        feature: LspFeature,
363        f: F,
364    ) -> Option<R>
365    where
366        F: FnOnce(&LspHandle, &crate::app::types::LspUri, &str) -> R,
367    {
368        use crate::services::lsp::manager::LspSpawnResult;
369
370        let (uri, language, file_path) = {
371            let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
372            if !metadata.lsp_enabled {
373                return None;
374            }
375            let uri = metadata.file_uri()?.clone();
376            let file_path = metadata.file_path().cloned();
377            let language = self
378                .windows
379                .get(&self.active_window)
380                .map(|w| &w.buffers)
381                .expect("active window present")
382                .get(&buffer_id)?
383                .language
384                .clone();
385            (uri, language, file_path)
386        };
387
388        let lsp = self.lsp_mut()?;
389        if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
390            return None;
391        }
392
393        // Ensure didOpen is sent to all handles
394        self.ensure_did_open_all(buffer_id, &uri, &language)?;
395
396        // Dispatch to the first handle that allows this feature
397        let lsp = self.lsp_mut()?;
398        let sh = lsp.handle_for_feature_mut(&language, feature)?;
399        Some(f(&sh.handle, &uri, &language))
400    }
401
402    /// Dispatch a merged LSP feature request to all handles that allow the feature.
403    ///
404    /// Ensures all handles receive didOpen first, then calls the closure for each
405    /// handle matching the feature filter, collecting all results. For features like
406    /// completion, code actions, diagnostics, etc.
407    pub(crate) fn with_all_lsp_for_buffer_feature<F, R>(
408        &mut self,
409        buffer_id: BufferId,
410        feature: LspFeature,
411        f: F,
412    ) -> Vec<R>
413    where
414        F: Fn(&LspHandle, &crate::app::types::LspUri, &str) -> R,
415    {
416        use crate::services::lsp::manager::LspSpawnResult;
417
418        let (uri, language, file_path) = match (|| {
419            let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
420            if !metadata.lsp_enabled {
421                return None;
422            }
423            let uri = metadata.file_uri()?.clone();
424            let file_path = metadata.file_path().cloned();
425            let language = self
426                .windows
427                .get(&self.active_window)
428                .map(|w| &w.buffers)
429                .expect("active window present")
430                .get(&buffer_id)?
431                .language
432                .clone();
433            Some((uri, language, file_path))
434        })() {
435            Some(v) => v,
436            None => return Vec::new(),
437        };
438
439        let lsp = match self.lsp_mut() {
440            Some(l) => l,
441            None => return Vec::new(),
442        };
443        if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
444            return Vec::new();
445        }
446
447        // Ensure didOpen is sent to all handles
448        if self
449            .ensure_did_open_all(buffer_id, &uri, &language)
450            .is_none()
451        {
452            return Vec::new();
453        }
454
455        // Dispatch to all handles that allow this feature
456        let lsp = match self.lsp_mut() {
457            Some(l) => l,
458            None => return Vec::new(),
459        };
460        lsp.handles_for_feature_mut(&language, feature)
461            .into_iter()
462            .map(|sh| f(&sh.handle, &uri, &language))
463            .collect()
464    }
465
466    /// Like `with_all_lsp_for_buffer_feature`, but also passes the server name
467    /// to the closure for attribution purposes.
468    pub(crate) fn with_all_lsp_for_buffer_feature_named<F, R>(
469        &mut self,
470        buffer_id: BufferId,
471        feature: LspFeature,
472        f: F,
473    ) -> Vec<R>
474    where
475        F: Fn(&LspHandle, &crate::app::types::LspUri, &str, &str) -> R,
476    {
477        use crate::services::lsp::manager::LspSpawnResult;
478
479        let (uri, language, file_path) = match (|| {
480            let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
481            if !metadata.lsp_enabled {
482                return None;
483            }
484            let uri = metadata.file_uri()?.clone();
485            let file_path = metadata.file_path().cloned();
486            let language = self
487                .windows
488                .get(&self.active_window)
489                .map(|w| &w.buffers)
490                .expect("active window present")
491                .get(&buffer_id)?
492                .language
493                .clone();
494            Some((uri, language, file_path))
495        })() {
496            Some(v) => v,
497            None => return Vec::new(),
498        };
499
500        let lsp = match self.lsp_mut() {
501            Some(l) => l,
502            None => return Vec::new(),
503        };
504        if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
505            return Vec::new();
506        }
507
508        if self
509            .ensure_did_open_all(buffer_id, &uri, &language)
510            .is_none()
511        {
512            return Vec::new();
513        }
514
515        let lsp = match self.lsp_mut() {
516            Some(l) => l,
517            None => return Vec::new(),
518        };
519        lsp.handles_for_feature_mut(&language, feature)
520            .into_iter()
521            .map(|sh| f(&sh.handle, &uri, &language, &sh.name))
522            .collect()
523    }
524
525    /// Ensure didOpen has been sent to all handles for the given buffer's language.
526    /// Returns Some(()) on success, None if we can't access required state.
527    fn ensure_did_open_all(
528        &mut self,
529        buffer_id: BufferId,
530        uri: &crate::app::types::LspUri,
531        language: &str,
532    ) -> Option<()> {
533        let lsp = self.lsp_mut()?;
534        let handle_ids: Vec<u64> = lsp
535            .get_handles(language)
536            .iter()
537            .map(|sh| sh.handle.id())
538            .collect();
539
540        let needs_open: Vec<u64> = {
541            let metadata = self.active_window().buffer_metadata.get(&buffer_id)?;
542            handle_ids
543                .iter()
544                .filter(|id| !metadata.lsp_opened_with.contains(id))
545                .copied()
546                .collect()
547        };
548
549        if !needs_open.is_empty() {
550            let text = self
551                .windows
552                .get(&self.active_window)
553                .map(|w| &w.buffers)
554                .expect("active window present")
555                .get(&buffer_id)?
556                .buffer
557                .to_string()?;
558            let active_id = self.active_window;
559            let __win = self.windows.get_mut(&active_id)?;
560            let lsp = &mut __win.lsp;
561            for sh in lsp.get_handles_mut(language) {
562                if needs_open.contains(&sh.handle.id()) {
563                    if let Err(e) =
564                        sh.handle
565                            .did_open(uri.as_uri().clone(), text.clone(), language.to_string())
566                    {
567                        tracing::warn!("Failed to send didOpen to '{}': {}", sh.name, e);
568                        continue;
569                    }
570                    let metadata = __win.buffer_metadata.get_mut(&buffer_id)?;
571                    metadata.lsp_opened_with.insert(sh.handle.id());
572                    tracing::debug!(
573                        "Sent didOpen for {} to LSP handle '{}' (language: {})",
574                        uri.as_str(),
575                        sh.name,
576                        language
577                    );
578                }
579            }
580        }
581
582        Some(())
583    }
584
585    /// Request LSP completion at current cursor position.
586    /// Sends completion requests to all eligible servers for merged results.
587    pub(crate) fn request_completion(&mut self) {
588        // A new completion request starts a fresh batch. Cancel any
589        // previous in-flight completion requests so their late responses
590        // are ignored (handle_completion_response drops responses whose
591        // request_id isn't in pending_completion_requests), and drop any
592        // leftover items from a previous popup that was closed via the
593        // "pass-through" path (hide_popup() without handle_popup_cancel,
594        // e.g. Enter or a non-word character while the popup was open).
595        // Without this, the new response would be merged into the stale
596        // items by `handle_completion_response`'s extend branch, leading
597        // to duplicate / stale entries in the rendered popup — see the
598        // regression test in
599        // crates/fresh-editor/tests/e2e/lsp_completion_duplicate_entries_1514.rs
600        // and sinelaw/fresh#1514.
601        if !self.active_window().pending_completion_requests.is_empty() {
602            let ids: Vec<u64> = self
603                .active_window_mut()
604                .pending_completion_requests
605                .drain()
606                .collect();
607            for request_id in ids {
608                tracing::debug!(
609                    "Canceling previous pending LSP completion request {}",
610                    request_id
611                );
612                self.active_window_mut().send_lsp_cancel_request(request_id);
613            }
614        }
615        self.active_window_mut().completion_items = None;
616
617        // Get the current buffer and cursor position
618        let cursor_pos = self.active_cursors().primary().position;
619        let state = self.active_state();
620
621        // Convert byte position to LSP position (line, UTF-16 code units)
622        let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
623        let buffer_id = self.active_buffer();
624
625        // Pre-allocate request IDs for all eligible servers
626        let base_request_id = self.active_window_mut().next_lsp_request_id;
627        // Use an atomic counter in the closure
628        let counter = std::sync::atomic::AtomicU64::new(0);
629
630        let results = self.with_all_lsp_for_buffer_feature(
631            buffer_id,
632            LspFeature::Completion,
633            |handle, uri, _language| {
634                let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
635                let request_id = base_request_id + idx;
636                let result = handle.completion(
637                    request_id,
638                    uri.as_uri().clone(),
639                    line as u32,
640                    character as u32,
641                );
642                if result.is_ok() {
643                    tracing::info!(
644                        "Requested completion at {}:{}:{} (request_id={})",
645                        uri.as_str(),
646                        line,
647                        character,
648                        request_id
649                    );
650                }
651                (request_id, result.is_ok())
652            },
653        );
654
655        let mut sent_ids = Vec::new();
656        for (request_id, ok) in &results {
657            if *ok {
658                sent_ids.push(*request_id);
659            }
660        }
661        // Advance the ID counter past all allocated IDs
662        self.active_window_mut().next_lsp_request_id = base_request_id + results.len() as u64;
663
664        if !sent_ids.is_empty() {
665            self.active_window_mut()
666                .pending_completion_requests
667                .extend(sent_ids);
668        } else {
669            // No LSP servers available — show buffer-word completions as popup.
670            self.show_buffer_word_completion_popup();
671        }
672    }
673
674    /// Show a completion popup with buffer-word results only (no LSP).
675    ///
676    /// Called when no LSP servers are available for the current buffer.
677    fn show_buffer_word_completion_popup(&mut self) {
678        let items = self.get_buffer_completion_popup_items();
679        if items.is_empty() {
680            return;
681        }
682
683        let popup_data = crate::app::popup_actions::build_completion_popup_from_items(items, 0);
684        let accept_hint = self.completion_accept_key_hint();
685        let focus_hint = self.popup_focus_key_hint();
686
687        let buffer_id = self.active_buffer();
688        let state = self
689            .windows
690            .get_mut(&self.active_window)
691            .map(|w| &mut w.buffers)
692            .expect("active window present")
693            .get_mut(&buffer_id)
694            .unwrap();
695        let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
696        popup_obj.accept_key_hint = accept_hint;
697        popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
698        popup_obj.focus_key_hint = focus_hint;
699        state.popups.show_or_replace(popup_obj);
700    }
701
702    /// Check if the inserted character should trigger completion
703    /// and if so, request completion automatically (possibly after a delay).
704    ///
705    /// Only triggers when `completion_popup_auto_show` is enabled. Then:
706    /// 1. Trigger characters (like `.`, `::`, etc.): immediate if suggest_on_trigger_characters is enabled
707    /// 2. Word characters: delayed by quick_suggestions_delay_ms if quick_suggestions is enabled
708    ///
709    /// This provides VS Code-like behavior where suggestions appear while typing,
710    /// with debouncing to avoid spamming the LSP server.
711    pub(crate) fn maybe_trigger_completion(&mut self, c: char) {
712        // Auto-show must be enabled for any automatic triggering
713        if !self.config.editor.completion_popup_auto_show {
714            return;
715        }
716
717        // Get the active buffer's language
718        let language = self.active_state().language.clone();
719
720        // Check if this character is a trigger character for this language
721        let is_lsp_trigger = self
722            .lsp()
723            .as_ref()
724            .map(|lsp| lsp.is_completion_trigger_char(c, &language))
725            .unwrap_or(false);
726
727        // Check if quick suggestions is enabled and this is a word character
728        let quick_suggestions_enabled = self.config.editor.quick_suggestions;
729        let suggest_on_trigger_chars = self.config.editor.suggest_on_trigger_characters;
730        let is_word_char = c.is_alphanumeric() || c == '_';
731
732        // Case 1: Trigger character - immediate trigger (bypasses delay)
733        if is_lsp_trigger && suggest_on_trigger_chars {
734            tracing::debug!(
735                "Trigger character '{}' immediately triggers completion for language {}",
736                c,
737                language
738            );
739            // Cancel any pending scheduled trigger
740            self.active_window_mut().scheduled_completion_trigger = None;
741            self.request_completion();
742            return;
743        }
744
745        // Case 2: Word character with quick suggestions - schedule delayed trigger
746        if quick_suggestions_enabled && is_word_char {
747            let delay_ms = self.config.editor.quick_suggestions_delay_ms;
748            let trigger_time = Instant::now() + Duration::from_millis(delay_ms);
749
750            tracing::debug!(
751                "Scheduling completion trigger in {}ms for language {} (char '{}')",
752                delay_ms,
753                language,
754                c
755            );
756
757            // Schedule (or reschedule) the completion trigger
758            // This effectively debounces - each keystroke resets the timer
759            self.active_window_mut().scheduled_completion_trigger = Some(trigger_time);
760        } else {
761            // Non-word, non-trigger character (space, punctuation, etc.) —
762            // cancel any pending scheduled trigger so a stale timer from the
763            // previous word doesn't fire at the wrong cursor position.
764            self.active_window_mut().scheduled_completion_trigger = None;
765        }
766    }
767
768    /// Request LSP go-to-definition at current cursor position
769    pub(crate) fn request_goto_definition(&mut self) -> AnyhowResult<()> {
770        // Get the current buffer and cursor position
771        let cursor_pos = self.active_cursors().primary().position;
772        let state = self.active_state();
773
774        // Convert byte position to LSP position (line, UTF-16 code units)
775        let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
776        let buffer_id = self.active_buffer();
777        let request_id = self.active_window_mut().next_lsp_request_id;
778
779        // Use helper to ensure didOpen is sent before the request
780        let sent = self
781            .with_lsp_for_buffer(
782                buffer_id,
783                LspFeature::Definition,
784                |handle, uri, _language| {
785                    let result = handle.goto_definition(
786                        request_id,
787                        uri.as_uri().clone(),
788                        line as u32,
789                        character as u32,
790                    );
791                    if result.is_ok() {
792                        tracing::info!(
793                            "Requested go-to-definition at {}:{}:{}",
794                            uri.as_str(),
795                            line,
796                            character
797                        );
798                    }
799                    result.is_ok()
800                },
801            )
802            .unwrap_or(false);
803
804        if sent {
805            self.active_window_mut().next_lsp_request_id += 1;
806            self.active_window_mut().pending_goto_definition_request = Some(request_id);
807        }
808
809        Ok(())
810    }
811
812    /// Request LSP hover documentation at current cursor position
813    pub fn request_hover(&mut self) -> AnyhowResult<()> {
814        // Get the current buffer and cursor position
815        let cursor_pos = self.active_cursors().primary().position;
816        let state = self.active_state();
817
818        // Convert byte position to LSP position (line, UTF-16 code units)
819        let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
820
821        // Debug: Log the position conversion details
822        if let Some(pos) = state.buffer.offset_to_position(cursor_pos) {
823            tracing::debug!(
824                "Hover request: cursor_byte={}, line={}, byte_col={}, utf16_col={}",
825                cursor_pos,
826                pos.line,
827                pos.column,
828                character
829            );
830        }
831
832        let buffer_id = self.active_buffer();
833        let request_id = self.active_window_mut().next_lsp_request_id;
834
835        // Use helper to ensure didOpen is sent before the request
836        let sent = self
837            .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
838                let result = handle.hover(
839                    request_id,
840                    uri.as_uri().clone(),
841                    line as u32,
842                    character as u32,
843                );
844                if result.is_ok() {
845                    tracing::info!(
846                        "Requested hover at {}:{}:{} (byte_pos={})",
847                        uri.as_str(),
848                        line,
849                        character,
850                        cursor_pos
851                    );
852                }
853                result.is_ok()
854            })
855            .unwrap_or(false);
856
857        if sent {
858            self.active_window_mut().next_lsp_request_id += 1;
859            self.active_window_mut().hover.record_request(
860                request_id,
861                line as u32,
862                character as u32,
863            );
864        }
865
866        Ok(())
867    }
868
869    /// Request LSP hover documentation at a specific byte position
870    /// Used for mouse-triggered hover
871    /// Returns `Ok(true)` if the request was dispatched, `Ok(false)` if no
872    /// eligible server was available (e.g. not yet initialized).
873    pub(crate) fn request_hover_at_position(&mut self, byte_pos: usize) -> AnyhowResult<bool> {
874        // Get the current buffer
875        let state = self.active_state();
876
877        // Convert byte position to LSP position (line, UTF-16 code units)
878        let (line, character) = state.buffer.position_to_lsp_position(byte_pos);
879
880        // Debug: Log the position conversion details
881        if let Some(pos) = state.buffer.offset_to_position(byte_pos) {
882            tracing::trace!(
883                "Mouse hover request: byte_pos={}, line={}, byte_col={}, utf16_col={}",
884                byte_pos,
885                pos.line,
886                pos.column,
887                character
888            );
889        }
890
891        let buffer_id = self.active_buffer();
892        let request_id = self.active_window_mut().next_lsp_request_id;
893
894        // Use helper to ensure didOpen is sent before the request
895        let sent = self
896            .with_lsp_for_buffer(buffer_id, LspFeature::Hover, |handle, uri, _language| {
897                let result = handle.hover(
898                    request_id,
899                    uri.as_uri().clone(),
900                    line as u32,
901                    character as u32,
902                );
903                if result.is_ok() {
904                    tracing::trace!(
905                        "Mouse hover requested at {}:{}:{} (byte_pos={})",
906                        uri.as_str(),
907                        line,
908                        character,
909                        byte_pos
910                    );
911                }
912                result.is_ok()
913            })
914            .unwrap_or(false);
915
916        if sent {
917            self.active_window_mut().next_lsp_request_id += 1;
918            self.active_window_mut().hover.record_request(
919                request_id,
920                line as u32,
921                character as u32,
922            );
923        }
924
925        Ok(sent)
926    }
927
928    /// Handle hover response from LSP
929    pub(crate) fn handle_hover_response(
930        &mut self,
931        request_id: u64,
932        contents: String,
933        is_markdown: bool,
934        range: Option<((u32, u32), (u32, u32))>,
935    ) {
936        // Check if this response is for the current pending request.
937        // `claim_pending` also drains the stored LSP position, which we keep
938        // around for diagnostic correlation below.
939        let Some(position) = self.active_window_mut().hover.claim_pending(request_id) else {
940            tracing::debug!("Ignoring stale hover response: {}", request_id);
941            return;
942        };
943
944        // A hover request may have been in flight when the user opened the
945        // status-bar LSP status popup. Don't show the hover card on top of it.
946        if self.is_lsp_status_popup_open() {
947            tracing::debug!("Suppressing hover response: LSP status popup is open");
948            self.active_window_mut().hover.set_symbol_range(None);
949            return;
950        }
951        let hover_lsp_position = Some(position);
952
953        // Gather any diagnostics whose range overlaps the hover position so
954        // they can be fused into the top of the hover card. Without this the
955        // user has to leave hover and go chase the error elsewhere in the UI
956        // even though the cursor is already on the offending symbol.
957        let diagnostic_lines = hover_lsp_position
958            .map(|pos| self.compose_hover_diagnostic_lines(pos))
959            .unwrap_or_default();
960
961        if contents.is_empty() && diagnostic_lines.is_empty() {
962            self.set_status_message(t!("lsp.no_hover").to_string());
963            self.active_window_mut().hover.set_symbol_range(None);
964            return;
965        }
966
967        // Debug: log raw hover content to diagnose formatting issues
968        tracing::debug!(
969            "LSP hover content (markdown={}):\n{}",
970            is_markdown,
971            contents
972        );
973
974        // Convert LSP range to byte offsets for highlighting
975        if let Some(((start_line, start_char), (end_line, end_char))) = range {
976            let state = self.active_state();
977            let start_byte = state
978                .buffer
979                .lsp_position_to_byte(start_line as usize, start_char as usize);
980            let end_byte = state
981                .buffer
982                .lsp_position_to_byte(end_line as usize, end_char as usize);
983            self.active_window_mut()
984                .hover
985                .set_symbol_range(Some((start_byte, end_byte)));
986            tracing::debug!(
987                "Hover symbol range: {}..{} (LSP {}:{}..{}:{})",
988                start_byte,
989                end_byte,
990                start_line,
991                start_char,
992                end_line,
993                end_char
994            );
995
996            // Remove previous hover overlay if any
997            if let Some(old_handle) = self.active_window_mut().hover.take_symbol_overlay() {
998                let remove_event = crate::model::event::Event::RemoveOverlay { handle: old_handle };
999                self.apply_event_to_active_buffer(&remove_event);
1000            }
1001
1002            // Add overlay to highlight the hovered symbol
1003            let event = crate::model::event::Event::AddOverlay {
1004                namespace: None,
1005                range: start_byte..end_byte,
1006                face: crate::model::event::OverlayFace::Background {
1007                    color: (80, 80, 120), // Subtle highlight for hovered symbol
1008                },
1009                priority: 90, // Below rename (100) but above syntax (lower)
1010                message: None,
1011                extend_to_line_end: false,
1012                url: None,
1013            };
1014            self.apply_event_to_active_buffer(&event);
1015            // Store the handle for later removal
1016            if let Some(state) = self
1017                .windows
1018                .get(&self.active_window)
1019                .map(|w| &w.buffers)
1020                .expect("active window present")
1021                .get(&self.active_buffer())
1022            {
1023                if let Some(handle) = state.overlays.all().last().map(|o| o.handle.clone()) {
1024                    self.active_window_mut().hover.set_symbol_overlay(handle);
1025                }
1026            }
1027        } else {
1028            // No range provided by LSP - compute word boundaries at hover position
1029            // This prevents the popup from following the mouse within the same word
1030            let computed_range = if let Some((hover_byte_pos, _, _, _)) =
1031                self.active_window_mut().mouse_state.lsp_hover_state
1032            {
1033                let state = self.active_state();
1034                let start_byte = find_word_start(&state.buffer, hover_byte_pos);
1035                let end_byte = find_word_end(&state.buffer, hover_byte_pos);
1036                if start_byte < end_byte {
1037                    tracing::debug!(
1038                        "Hover symbol range (computed from word boundaries): {}..{}",
1039                        start_byte,
1040                        end_byte
1041                    );
1042                    Some((start_byte, end_byte))
1043                } else {
1044                    None
1045                }
1046            } else {
1047                None
1048            };
1049            self.active_window_mut()
1050                .hover
1051                .set_symbol_range(computed_range);
1052        }
1053
1054        // Create a popup with the hover contents.
1055        //
1056        // When a diagnostic overlaps the hover position, we pre-style its
1057        // lines (severity-colored header + plain message) and concatenate
1058        // with the parsed hover body into a single `PopupContent::Markdown`
1059        // vector. This avoids the previous approach of injecting a
1060        // `**bold**` heading and a `---` horizontal rule into the markdown
1061        // input — which rendered as uncolored bold text + a thick 40-cell
1062        // divider with blank-line padding, wasting vertical space and
1063        // losing the "this is an error" visual signal.
1064        use crate::view::markdown::{parse_markdown, StyledLine};
1065        use crate::view::popup::{Popup, PopupContent, PopupPosition};
1066        use ratatui::style::Style;
1067        use unicode_width::UnicodeWidthStr;
1068
1069        let hover_lines: Vec<StyledLine> = if contents.is_empty() {
1070            Vec::new()
1071        } else if is_markdown {
1072            parse_markdown(
1073                &contents,
1074                &*self.theme.read().unwrap(),
1075                Some(&self.grammar_registry),
1076            )
1077        } else {
1078            contents
1079                .lines()
1080                .map(|s| {
1081                    let mut sl = StyledLine::new();
1082                    sl.push(
1083                        s.to_string(),
1084                        Style::default().fg(self.theme.read().unwrap().popup_text_fg),
1085                    );
1086                    sl
1087                })
1088                .collect()
1089        };
1090
1091        let has_diagnostic = !diagnostic_lines.is_empty();
1092        let mut all_lines: Vec<StyledLine> = Vec::new();
1093        all_lines.extend(diagnostic_lines);
1094        if has_diagnostic && !hover_lines.is_empty() {
1095            // Compact single-line separator — no blank padding, no 40-cell
1096            // dash run. One row of dashes the width of the content, in the
1097            // popup border color so it reads as "same card, new section."
1098            let mut sep = StyledLine::new();
1099            sep.push(
1100                "─".repeat(12),
1101                Style::default().fg(self.theme.read().unwrap().popup_border_fg),
1102            );
1103            all_lines.push(sep);
1104        }
1105        all_lines.extend(hover_lines);
1106
1107        // Drop trailing empty lines that some markdown payloads carry.
1108        while all_lines
1109            .last()
1110            .map(|l| l.spans.iter().all(|s| s.text.trim().is_empty()))
1111            .unwrap_or(false)
1112        {
1113            all_lines.pop();
1114        }
1115
1116        // Fit width to content so short hovers stop rendering in an 80-col
1117        // card with half the width empty. Measured as the widest styled
1118        // line (display cells, not bytes), plus 4 for borders + padding,
1119        // clamped to [30, 80]. Height stays dynamic on terminal size.
1120        let content_width: usize = all_lines
1121            .iter()
1122            .map(|l| {
1123                l.spans
1124                    .iter()
1125                    .map(|s| UnicodeWidthStr::width(s.text.as_str()))
1126                    .sum::<usize>()
1127            })
1128            .max()
1129            .unwrap_or(0);
1130        let popup_width = (content_width as u16 + 4).clamp(30, 80);
1131        let dynamic_height = (self.terminal_height * 60 / 100).clamp(15, 40);
1132
1133        // Construct the popup with the fused content.
1134        let mut popup = Popup::text(Vec::new(), &*self.theme.read().unwrap());
1135        popup.content = PopupContent::Markdown(all_lines);
1136        popup.title = Some(t!("lsp.popup_hover").to_string());
1137        popup.transient = true;
1138        popup.position = if let Some((x, y)) = self.active_window_mut().hover.take_screen_position()
1139        {
1140            PopupPosition::Fixed { x, y: y + 1 }
1141        } else {
1142            PopupPosition::BelowCursor
1143        };
1144        popup.width = popup_width;
1145        popup.max_height = dynamic_height;
1146        popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1147        popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1148        popup.focus_key_hint = self.popup_focus_key_hint();
1149
1150        // Show the popup. Replace any existing transient (hover/signature)
1151        // popup so successive hovers don't pile up on the popup stack —
1152        // the user expects exactly one hover card on screen at a time.
1153        let __buffer_id = self.active_buffer();
1154        if let Some(state) = self
1155            .windows
1156            .get_mut(&self.active_window)
1157            .map(|w| &mut w.buffers)
1158            .expect("active window present")
1159            .get_mut(&__buffer_id)
1160        {
1161            while state.popups.top().is_some_and(|p| p.transient) {
1162                state.popups.hide();
1163            }
1164            state.popups.show(popup);
1165            tracing::info!("Showing hover popup (markdown={})", is_markdown);
1166        }
1167
1168        // Mark hover request as sent to prevent duplicate popups during race conditions
1169        // (e.g., when mouse moves while a hover response is pending)
1170        self.active_window_mut().mouse_state.lsp_hover_request_sent = true;
1171    }
1172
1173    /// Pre-style any diagnostics overlapping the hover position into lines
1174    /// ready to stack into the hover popup. Each diagnostic yields two or
1175    /// more styled lines:
1176    ///   1. severity marker + label in `diagnostic_*_fg`, followed by
1177    ///      `  (source)` dimmed — italic on theme-default foreground,
1178    ///   2. one styled line per message line, in `popup_text_fg`.
1179    ///
1180    /// Multiple overlapping diagnostics are separated by a blank line.
1181    /// Returns an empty vec when there are no overlapping diagnostics,
1182    /// or no buffer/URI resolves.
1183    fn compose_hover_diagnostic_lines(
1184        &self,
1185        lsp_pos: (u32, u32),
1186    ) -> Vec<crate::view::markdown::StyledLine> {
1187        use crate::view::markdown::StyledLine;
1188        use lsp_types::DiagnosticSeverity;
1189        use ratatui::style::{Modifier, Style};
1190
1191        let buffer_id = self.active_buffer();
1192        let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
1193            return Vec::new();
1194        };
1195        let Some(uri) = metadata.file_uri() else {
1196            return Vec::new();
1197        };
1198        let Some(diagnostics) = self.get_stored_diagnostics().get(uri.as_str()) else {
1199            return Vec::new();
1200        };
1201
1202        let (hover_line, hover_char) = lsp_pos;
1203        let overlapping: Vec<&lsp_types::Diagnostic> = diagnostics
1204            .iter()
1205            .filter(|d| lsp_range_contains(&d.range, hover_line, hover_char))
1206            .collect();
1207
1208        if overlapping.is_empty() {
1209            return Vec::new();
1210        }
1211
1212        let mut out: Vec<StyledLine> = Vec::new();
1213        for (idx, diag) in overlapping.iter().enumerate() {
1214            if idx > 0 {
1215                out.push(StyledLine::new());
1216            }
1217
1218            let (label, marker, severity_color) = match diag.severity {
1219                Some(DiagnosticSeverity::ERROR) => {
1220                    ("Error", "✖", self.theme.read().unwrap().diagnostic_error_fg)
1221                }
1222                Some(DiagnosticSeverity::WARNING) => (
1223                    "Warning",
1224                    "⚠",
1225                    self.theme.read().unwrap().diagnostic_warning_fg,
1226                ),
1227                Some(DiagnosticSeverity::INFORMATION) => {
1228                    ("Info", "ℹ", self.theme.read().unwrap().diagnostic_info_fg)
1229                }
1230                Some(DiagnosticSeverity::HINT) => {
1231                    ("Hint", "ℹ", self.theme.read().unwrap().diagnostic_hint_fg)
1232                }
1233                _ => ("Diagnostic", "•", self.theme.read().unwrap().popup_text_fg),
1234            };
1235
1236            let header_style = Style::default()
1237                .fg(severity_color)
1238                .add_modifier(Modifier::BOLD);
1239            let mut header = StyledLine::new();
1240            header.push(format!("{} {}", marker, label), header_style);
1241            if let Some(source) = diag.source.as_deref().filter(|s| !s.is_empty()) {
1242                // Dim italic source tag — reads as metadata, not as part
1243                // of the diagnostic text.
1244                header.push(
1245                    format!("  ({})", source),
1246                    Style::default()
1247                        .fg(self.theme.read().unwrap().tab_inactive_fg)
1248                        .add_modifier(Modifier::ITALIC),
1249                );
1250            }
1251            out.push(header);
1252
1253            // Message verbatim: one styled line per message line. Using
1254            // `popup_text_fg` lets themes override the body color; the
1255            // severity information is already conveyed by the header.
1256            for message_line in diag.message.lines() {
1257                let mut line = StyledLine::new();
1258                line.push(
1259                    message_line.to_string(),
1260                    Style::default().fg(self.theme.read().unwrap().popup_text_fg),
1261                );
1262                out.push(line);
1263            }
1264        }
1265        out
1266    }
1267
1268    /// Apply inlay hints to editor state as virtual text
1269    #[doc(hidden)]
1270    pub fn apply_inlay_hints_to_state(
1271        state: &mut crate::state::EditorState,
1272        hints: &[lsp_types::InlayHint],
1273    ) {
1274        use crate::view::virtual_text::VirtualTextPosition;
1275        use ratatui::style::{Color, Style};
1276
1277        // Clear existing inlay hints
1278        state.virtual_texts.clear(&mut state.marker_list);
1279
1280        if hints.is_empty() {
1281            return;
1282        }
1283
1284        // Fallback style for inlay hints - dimmed to not distract from actual
1285        // code. The actual on-screen color is resolved from the theme key
1286        // below (`editor.line_number_fg`) so the hints follow the active
1287        // theme. This fallback only applies when the theme doesn't define
1288        // the key.
1289        let hint_style = Style::default().fg(Color::Rgb(128, 128, 128));
1290        let hint_fg_theme_key = Some("editor.line_number_fg".to_string());
1291
1292        for hint in hints {
1293            // Convert LSP position to byte offset
1294            let byte_offset = state.buffer.lsp_position_to_byte(
1295                hint.position.line as usize,
1296                hint.position.character as usize,
1297            );
1298
1299            // Extract text from hint label
1300            let text = match &hint.label {
1301                lsp_types::InlayHintLabel::String(s) => s.clone(),
1302                lsp_types::InlayHintLabel::LabelParts(parts) => {
1303                    parts.iter().map(|p| p.value.as_str()).collect::<String>()
1304                }
1305            };
1306
1307            // LSP inlay hint positions are insertion points between characters.
1308            // For positions within the buffer, render hints before the character at the
1309            // byte offset so they appear at the correct location (e.g., before punctuation
1310            // or newline). Hints at or beyond EOF are anchored to the last character and
1311            // rendered after it.
1312            if state.buffer.is_empty() {
1313                continue;
1314            }
1315
1316            // Pick the anchor character for this hint. If the LSP-computed
1317            // byte lies on a line terminator (\n or the \r of a CRLF), the
1318            // "following character" is the first byte of the next line.
1319            // Anchoring to it would make the hint drift one line down on
1320            // any whitespace edit adjacent to the brace (issue #1572), so
1321            // instead anchor to the *preceding* non-newline character with
1322            // `AfterChar`. That keeps the hint stuck to the glyph the LSP
1323            // intended to annotate even as edits shift bytes around it.
1324            let buf_len = state.buffer.len();
1325            let byte_here = if byte_offset < buf_len {
1326                state
1327                    .buffer
1328                    .slice_bytes(byte_offset..byte_offset + 1)
1329                    .first()
1330                    .copied()
1331            } else {
1332                None
1333            };
1334            let at_line_break = matches!(byte_here, Some(b'\n' | b'\r'));
1335
1336            let (byte_offset, position) = if byte_offset >= buf_len {
1337                // Hint is at EOF: anchor to last character and render
1338                // after it.
1339                (buf_len.saturating_sub(1), VirtualTextPosition::AfterChar)
1340            } else if at_line_break && byte_offset > 0 {
1341                // Hint points past the last glyph on a line: anchor to
1342                // that glyph with AfterChar so the marker cannot drift
1343                // onto a subsequent line when whitespace is edited.
1344                (byte_offset - 1, VirtualTextPosition::AfterChar)
1345            } else {
1346                (byte_offset, VirtualTextPosition::BeforeChar)
1347            };
1348
1349            // Use the hint text as-is - spacing is handled during rendering
1350            let display_text = text;
1351
1352            state.virtual_texts.add_with_theme_keys(
1353                &mut state.marker_list,
1354                byte_offset,
1355                display_text,
1356                hint_style,
1357                hint_fg_theme_key.clone(),
1358                None,
1359                position,
1360                0, // Default priority
1361            );
1362        }
1363
1364        tracing::debug!("Applied {} inlay hints as virtual text", hints.len());
1365    }
1366
1367    /// Request LSP find references at current cursor position
1368    pub(crate) fn request_references(&mut self) -> AnyhowResult<()> {
1369        use crate::primitives::word_navigation::{find_word_end, find_word_start};
1370
1371        let cursor_pos = self.active_cursors().primary().position;
1372        let (line, character, symbol) = {
1373            let state = self.active_state();
1374            let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1375            let word_start = find_word_start(&state.buffer, cursor_pos);
1376            let word_end = find_word_end(&state.buffer, cursor_pos);
1377            let symbol = String::from_utf8_lossy(&state.buffer.slice_bytes(word_start..word_end))
1378                .into_owned();
1379            (line, character, symbol)
1380        };
1381
1382        let buffer_id = self.active_buffer();
1383        let request_id = self.active_window_mut().next_lsp_request_id;
1384
1385        // Use helper to ensure didOpen is sent before the request
1386        let sent = self
1387            .with_lsp_for_buffer(
1388                buffer_id,
1389                LspFeature::References,
1390                |handle, uri, _language| {
1391                    let result = handle.references(
1392                        request_id,
1393                        uri.as_uri().clone(),
1394                        line as u32,
1395                        character as u32,
1396                    );
1397                    if result.is_ok() {
1398                        tracing::info!(
1399                            "Requested find references at {}:{}:{} (byte_pos={})",
1400                            uri.as_str(),
1401                            line,
1402                            character,
1403                            cursor_pos
1404                        );
1405                    }
1406                    result.is_ok()
1407                },
1408            )
1409            .unwrap_or(false);
1410
1411        if sent {
1412            self.active_window_mut().next_lsp_request_id += 1;
1413            self.active_window_mut().pending_references_request = Some(request_id);
1414            self.active_window_mut().pending_references_symbol = symbol;
1415        }
1416
1417        Ok(())
1418    }
1419
1420    /// Request LSP signature help at current cursor position
1421    pub(crate) fn request_signature_help(&mut self) {
1422        // Get the current buffer and cursor position
1423        let cursor_pos = self.active_cursors().primary().position;
1424        let state = self.active_state();
1425
1426        // Convert byte position to LSP position (line, UTF-16 code units)
1427        let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1428        let buffer_id = self.active_buffer();
1429        let request_id = self.active_window_mut().next_lsp_request_id;
1430
1431        // Use helper to ensure didOpen is sent before the request
1432        let sent = self
1433            .with_lsp_for_buffer(
1434                buffer_id,
1435                LspFeature::SignatureHelp,
1436                |handle, uri, _language| {
1437                    let result = handle.signature_help(
1438                        request_id,
1439                        uri.as_uri().clone(),
1440                        line as u32,
1441                        character as u32,
1442                    );
1443                    if result.is_ok() {
1444                        tracing::info!(
1445                            "Requested signature help at {}:{}:{} (byte_pos={})",
1446                            uri.as_str(),
1447                            line,
1448                            character,
1449                            cursor_pos
1450                        );
1451                    }
1452                    result.is_ok()
1453                },
1454            )
1455            .unwrap_or(false);
1456
1457        if sent {
1458            self.active_window_mut().next_lsp_request_id += 1;
1459            self.active_window_mut().pending_signature_help_request = Some(request_id);
1460        }
1461    }
1462
1463    /// Handle signature help response from LSP
1464    pub(crate) fn handle_signature_help_response(
1465        &mut self,
1466        request_id: u64,
1467        signature_help: Option<lsp_types::SignatureHelp>,
1468    ) {
1469        // Check if this response is for the current pending request
1470        if self.active_window_mut().pending_signature_help_request != Some(request_id) {
1471            tracing::debug!("Ignoring stale signature help response: {}", request_id);
1472            return;
1473        }
1474
1475        self.active_window_mut().pending_signature_help_request = None;
1476        let signature_help = match signature_help {
1477            Some(help) if !help.signatures.is_empty() => help,
1478            _ => {
1479                tracing::debug!("No signature help available");
1480                return;
1481            }
1482        };
1483
1484        // Get the active signature
1485        let active_signature_idx = signature_help.active_signature.unwrap_or(0) as usize;
1486        let signature = match signature_help.signatures.get(active_signature_idx) {
1487            Some(sig) => sig,
1488            None => return,
1489        };
1490
1491        // Build the display content as markdown
1492        let mut content = String::new();
1493
1494        // Add the signature label (function signature)
1495        content.push_str(&signature.label);
1496        content.push('\n');
1497
1498        // Add parameter highlighting info
1499        let active_param = signature_help
1500            .active_parameter
1501            .or(signature.active_parameter)
1502            .unwrap_or(0) as usize;
1503
1504        // If there are parameters, highlight the active one
1505        if let Some(params) = &signature.parameters {
1506            if let Some(param) = params.get(active_param) {
1507                // Get parameter label
1508                let param_label = match &param.label {
1509                    lsp_types::ParameterLabel::Simple(s) => s.clone(),
1510                    lsp_types::ParameterLabel::LabelOffsets(offsets) => {
1511                        // Extract substring from signature label
1512                        let start = offsets[0] as usize;
1513                        let end = offsets[1] as usize;
1514                        if end <= signature.label.len() {
1515                            signature.label[start..end].to_string()
1516                        } else {
1517                            String::new()
1518                        }
1519                    }
1520                };
1521
1522                if !param_label.is_empty() {
1523                    content.push_str(&format!("\n> {}\n", param_label));
1524                }
1525
1526                // Add parameter documentation if available
1527                if let Some(doc) = &param.documentation {
1528                    let doc_text = match doc {
1529                        lsp_types::Documentation::String(s) => s.clone(),
1530                        lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1531                    };
1532                    if !doc_text.is_empty() {
1533                        content.push('\n');
1534                        content.push_str(&doc_text);
1535                        content.push('\n');
1536                    }
1537                }
1538            }
1539        }
1540
1541        // Add function documentation if available
1542        if let Some(doc) = &signature.documentation {
1543            let doc_text = match doc {
1544                lsp_types::Documentation::String(s) => s.clone(),
1545                lsp_types::Documentation::MarkupContent(m) => m.value.clone(),
1546            };
1547            if !doc_text.is_empty() {
1548                content.push_str("\n---\n\n");
1549                content.push_str(&space_doc_paragraphs(&doc_text));
1550            }
1551        }
1552
1553        // Create a popup with markdown rendering (like hover popup)
1554        use crate::view::popup::{Popup, PopupPosition};
1555        use ratatui::style::Style;
1556
1557        let mut popup = Popup::markdown(
1558            &content,
1559            &*self.theme.read().unwrap(),
1560            Some(&self.grammar_registry),
1561        );
1562        popup.title = Some(t!("lsp.popup_signature").to_string());
1563        popup.transient = true;
1564        popup.position = PopupPosition::BelowCursor;
1565        popup.width = 60;
1566        popup.max_height = 20;
1567        popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1568        popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1569        popup.focus_key_hint = self.popup_focus_key_hint();
1570
1571        // Show the popup
1572        let __buffer_id = self.active_buffer();
1573        if let Some(state) = self
1574            .windows
1575            .get_mut(&self.active_window)
1576            .map(|w| &mut w.buffers)
1577            .expect("active window present")
1578            .get_mut(&__buffer_id)
1579        {
1580            state.popups.show(popup);
1581            tracing::info!(
1582                "Showing signature help popup for {} signatures",
1583                signature_help.signatures.len()
1584            );
1585        }
1586    }
1587
1588    /// Request LSP code actions at current cursor position.
1589    /// Sends code action requests to all eligible servers for merged results.
1590    pub(crate) fn request_code_actions(&mut self) -> AnyhowResult<()> {
1591        // A new invocation starts a fresh batch. Cancel any previous
1592        // in-flight code-action requests so their late responses are
1593        // ignored (handle_code_actions_response drops responses whose
1594        // request_id isn't in pending_code_actions_requests). Without
1595        // this, actions from a prior cursor position would be merged
1596        // into the new popup — same bug class we already avoid for
1597        // completion (sinelaw/fresh#1514) and inlay hints (multi-buffer
1598        // quiescent).
1599        if !self
1600            .active_window()
1601            .pending_code_actions_requests
1602            .is_empty()
1603        {
1604            let ids: Vec<u64> = self
1605                .active_window_mut()
1606                .pending_code_actions_requests
1607                .drain()
1608                .collect();
1609            for request_id in ids {
1610                tracing::debug!(
1611                    "Canceling previous pending LSP code actions request {}",
1612                    request_id
1613                );
1614                self.active_window_mut().send_lsp_cancel_request(request_id);
1615            }
1616        }
1617        self.active_window_mut()
1618            .pending_code_actions_server_names
1619            .clear();
1620        self.active_window_mut().pending_code_actions = None;
1621
1622        // Get the current buffer and cursor position
1623        let cursor_pos = self.active_cursors().primary().position;
1624        let selection_range = self.active_cursors().primary().selection_range();
1625        let state = self.active_state();
1626
1627        // Convert byte position to LSP position (line, UTF-16 code units)
1628        let (line, character) = state.buffer.position_to_lsp_position(cursor_pos);
1629
1630        // Get selection range (if any) or use cursor position
1631        let (start_line, start_char, end_line, end_char) = if let Some(range) = selection_range {
1632            let (s_line, s_char) = state.buffer.position_to_lsp_position(range.start);
1633            let (e_line, e_char) = state.buffer.position_to_lsp_position(range.end);
1634            (s_line as u32, s_char as u32, e_line as u32, e_char as u32)
1635        } else {
1636            (line as u32, character as u32, line as u32, character as u32)
1637        };
1638
1639        let buffer_id = self.active_buffer();
1640
1641        // Populate `context.diagnostics` with stored diagnostics whose ranges
1642        // overlap the code-action request range. Many servers (clangd,
1643        // eslint, ...) gate quickfix actions on this — without it, every
1644        // diagnostic-driven "fix available" produces zero actions
1645        // (sinelaw/fresh#2212).
1646        let diagnostics: Vec<lsp_types::Diagnostic> = {
1647            let window = self.active_window();
1648            window
1649                .buffer_metadata
1650                .get(&buffer_id)
1651                .and_then(|m| m.file_uri())
1652                .and_then(|uri| window.stored_diagnostics.get(uri.as_str()))
1653                .map(|diags| {
1654                    diags
1655                        .iter()
1656                        .filter(|d| {
1657                            lsp_range_overlaps(&d.range, start_line, start_char, end_line, end_char)
1658                        })
1659                        .cloned()
1660                        .collect()
1661                })
1662                .unwrap_or_default()
1663        };
1664
1665        // Pre-allocate request IDs for all eligible servers
1666        let base_request_id = self.active_window_mut().next_lsp_request_id;
1667        let counter = std::sync::atomic::AtomicU64::new(0);
1668
1669        let results = self.with_all_lsp_for_buffer_feature_named(
1670            buffer_id,
1671            LspFeature::CodeAction,
1672            |handle, uri, _language, server_name| {
1673                let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1674                let request_id = base_request_id + idx;
1675                let result = handle.code_actions(
1676                    request_id,
1677                    uri.as_uri().clone(),
1678                    start_line,
1679                    start_char,
1680                    end_line,
1681                    end_char,
1682                    diagnostics.clone(),
1683                );
1684                if result.is_ok() {
1685                    tracing::info!(
1686                        "Requested code actions at {}:{}:{}-{}:{} (byte_pos={}, request_id={}, server={})",
1687                        uri.as_str(),
1688                        start_line,
1689                        start_char,
1690                        end_line,
1691                        end_char,
1692                        cursor_pos,
1693                        request_id,
1694                        server_name
1695                    );
1696                }
1697                (request_id, result.is_ok(), server_name.to_string())
1698            },
1699        );
1700
1701        let mut sent_ids = Vec::new();
1702        for (request_id, ok, server_name) in &results {
1703            if *ok {
1704                sent_ids.push(*request_id);
1705                self.active_window_mut()
1706                    .pending_code_actions_server_names
1707                    .insert(*request_id, server_name.clone());
1708            }
1709        }
1710        // Advance the ID counter past all allocated IDs
1711        self.active_window_mut().next_lsp_request_id = base_request_id + results.len() as u64;
1712
1713        if !sent_ids.is_empty() {
1714            // pending_code_actions was already cleared above alongside the
1715            // cancel-previous-requests logic.
1716            self.active_window_mut()
1717                .pending_code_actions_requests
1718                .extend(sent_ids);
1719        }
1720
1721        Ok(())
1722    }
1723
1724    /// Handle code actions response from LSP.
1725    /// Supports merging from multiple servers: each response extends the action
1726    /// list, and the popup is shown/updated with each arriving response.
1727    pub(crate) fn handle_code_actions_response(
1728        &mut self,
1729        request_id: u64,
1730        actions: Vec<lsp_types::CodeActionOrCommand>,
1731    ) {
1732        // Check if this response is for one of the pending requests
1733        if !self
1734            .active_window_mut()
1735            .pending_code_actions_requests
1736            .remove(&request_id)
1737        {
1738            tracing::debug!("Ignoring stale code actions response: {}", request_id);
1739            return;
1740        }
1741
1742        // Look up the server name for this request
1743        let server_name = self
1744            .active_window_mut()
1745            .pending_code_actions_server_names
1746            .remove(&request_id)
1747            .unwrap_or_default();
1748
1749        if actions.is_empty() {
1750            // Only show "no code actions" if all responses are in and we have nothing
1751            if self
1752                .active_window()
1753                .pending_code_actions_requests
1754                .is_empty()
1755                && self
1756                    .active_window_mut()
1757                    .pending_code_actions
1758                    .as_ref()
1759                    .is_none_or(|a| a.is_empty())
1760            {
1761                self.set_status_message(t!("lsp.no_code_actions").to_string());
1762            }
1763            return;
1764        }
1765
1766        // Tag each action with its server name and store/extend for merging
1767        let tagged_actions: Vec<(String, lsp_types::CodeActionOrCommand)> = actions
1768            .into_iter()
1769            .map(|a| (server_name.clone(), a))
1770            .collect();
1771
1772        match &mut self.active_window_mut().pending_code_actions {
1773            Some(existing) => {
1774                existing.extend(tagged_actions);
1775                tracing::debug!("Extended code actions, now {} total", existing.len());
1776            }
1777            None => {
1778                self.active_window_mut().pending_code_actions = Some(tagged_actions);
1779            }
1780        }
1781
1782        // Build list items from all accumulated code actions
1783        use crate::view::popup::{Popup, PopupListItem, PopupPosition};
1784        use ratatui::style::Style;
1785
1786        let items: Vec<PopupListItem> = {
1787            let all_actions = self.active_window().pending_code_actions.as_ref().unwrap();
1788            let multiple_servers = {
1789                let mut names = std::collections::HashSet::new();
1790                for (name, _) in all_actions {
1791                    names.insert(name.as_str());
1792                }
1793                names.len() > 1
1794            };
1795            all_actions
1796                .iter()
1797                .enumerate()
1798                .map(|(i, (srv_name, action))| {
1799                    let title = match action {
1800                        lsp_types::CodeActionOrCommand::Command(cmd) => &cmd.title,
1801                        lsp_types::CodeActionOrCommand::CodeAction(ca) => &ca.title,
1802                    };
1803                    let kind = match action {
1804                        lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1805                            ca.kind.as_ref().map(|k| k.as_str().to_string())
1806                        }
1807                        _ => None,
1808                    };
1809                    let detail = if multiple_servers && !srv_name.is_empty() {
1810                        match kind {
1811                            Some(k) => Some(format!("[{}] {}", srv_name, k)),
1812                            None => Some(format!("[{}]", srv_name)),
1813                        }
1814                    } else {
1815                        kind
1816                    };
1817                    PopupListItem {
1818                        text: format!("{}. {}", i + 1, title),
1819                        detail,
1820                        icon: None,
1821                        data: Some(i.to_string()),
1822                        disabled: false,
1823                    }
1824                })
1825                .collect()
1826        };
1827
1828        let mut popup = Popup::list(items, &*self.theme.read().unwrap());
1829        popup.kind = crate::view::popup::PopupKind::Action;
1830        popup.title = Some(t!("lsp.popup_code_actions").to_string());
1831        popup.position = PopupPosition::BelowCursor;
1832        popup.width = 60;
1833        popup.max_height = 15;
1834        popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
1835        popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
1836        // Confirm reads the selected row's `data` as an index into
1837        // `self.active_window_mut().pending_code_actions` — the heavy lsp_types payload
1838        // stays on the Editor to keep the view crate LSP-free.
1839        popup.resolver = crate::view::popup::PopupResolver::CodeAction;
1840        // Code actions are an explicit user invocation (`lsp_code_actions`
1841        // command); the user expects to choose immediately, so the popup
1842        // grabs focus on creation. Unfocused-by-default behavior applies
1843        // only to popups that *appear under the cursor* (completion,
1844        // hover, signature help, the LSP-server auto-prompt).
1845        popup.focused = true;
1846
1847        // Show the popup, replacing any existing action popup to avoid stacking
1848        let __buffer_id = self.active_buffer();
1849        let action_count = self
1850            .active_window()
1851            .pending_code_actions
1852            .as_ref()
1853            .map_or(0, |v| v.len());
1854        if let Some(state) = self
1855            .windows
1856            .get_mut(&self.active_window)
1857            .map(|w| &mut w.buffers)
1858            .expect("active window present")
1859            .get_mut(&__buffer_id)
1860        {
1861            state.popups.show_or_replace(popup);
1862            tracing::info!("Showing code actions popup with {} actions", action_count);
1863        }
1864    }
1865
1866    /// Execute a code action by index from the stored pending_code_actions.
1867    pub(crate) fn execute_code_action(&mut self, index: usize) {
1868        let action = match &self.active_window_mut().pending_code_actions {
1869            Some(actions) => actions.get(index).map(|(_, a)| a.clone()),
1870            None => None,
1871        };
1872
1873        let Some(action) = action else {
1874            tracing::warn!("Code action index {} out of range", index);
1875            return;
1876        };
1877
1878        match action {
1879            lsp_types::CodeActionOrCommand::CodeAction(ca) => {
1880                // If the action has no edit and no command, it may need resolve first.
1881                // Only resolve if the action has `data` and the server supports resolveProvider.
1882                if ca.edit.is_none()
1883                    && ca.command.is_none()
1884                    && ca.data.is_some()
1885                    && self.active_window().server_supports_code_action_resolve()
1886                {
1887                    tracing::info!(
1888                        "Code action '{}' needs resolve, sending codeAction/resolve",
1889                        ca.title
1890                    );
1891                    self.send_code_action_resolve(ca);
1892                    return;
1893                }
1894                self.execute_resolved_code_action(ca);
1895            }
1896            lsp_types::CodeActionOrCommand::Command(cmd) => {
1897                self.send_execute_command(cmd);
1898            }
1899        }
1900    }
1901
1902    /// Execute a code action that has been fully resolved (has edit and/or command).
1903    pub(crate) fn execute_resolved_code_action(&mut self, ca: lsp_types::CodeAction) {
1904        let title = ca.title.clone();
1905
1906        // Apply workspace edit if present
1907        if let Some(edit) = ca.edit {
1908            match self.apply_workspace_edit(edit) {
1909                Ok(n) => {
1910                    self.set_status_message(
1911                        t!("lsp.code_action_applied", title = &title, count = n).to_string(),
1912                    );
1913                }
1914                Err(e) => {
1915                    self.set_status_message(format!("Code action failed: {e}"));
1916                    return;
1917                }
1918            }
1919        }
1920
1921        // Execute command if present (may trigger workspace/applyEdit from server)
1922        if let Some(cmd) = ca.command {
1923            self.send_execute_command(cmd);
1924        }
1925    }
1926
1927    /// Send workspace/executeCommand to the LSP server
1928    fn send_execute_command(&mut self, cmd: lsp_types::Command) {
1929        tracing::info!("Executing LSP command: {} ({})", cmd.title, cmd.command);
1930        self.set_status_message(
1931            t!(
1932                "lsp.code_action_applied",
1933                title = &cmd.title,
1934                count = 0_usize
1935            )
1936            .to_string(),
1937        );
1938
1939        // Get the language for this buffer to find the right LSP handle
1940        let language = match self
1941            .buffers()
1942            .get(&self.active_buffer())
1943            .map(|s| s.language.clone())
1944        {
1945            Some(l) => l,
1946            None => return,
1947        };
1948
1949        let __active_id = self.active_window;
1950
1951        if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1952            for sh in lsp.get_handles_mut(&language) {
1953                if let Err(e) = sh
1954                    .handle
1955                    .execute_command(cmd.command.clone(), cmd.arguments.clone())
1956                {
1957                    tracing::warn!("Failed to send executeCommand to '{}': {}", sh.name, e);
1958                }
1959            }
1960        }
1961    }
1962
1963    /// Send codeAction/resolve to the LSP server
1964    fn send_code_action_resolve(&mut self, action: lsp_types::CodeAction) {
1965        let language = match self
1966            .buffers()
1967            .get(&self.active_buffer())
1968            .map(|s| s.language.clone())
1969        {
1970            Some(l) => l,
1971            None => return,
1972        };
1973
1974        self.active_window_mut().next_lsp_request_id += 1;
1975        let request_id = self.active_window_mut().next_lsp_request_id;
1976
1977        let __active_id = self.active_window;
1978
1979        if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1980            for sh in lsp.get_handles_mut(&language) {
1981                if let Err(e) = sh.handle.code_action_resolve(request_id, action.clone()) {
1982                    tracing::warn!("Failed to send codeAction/resolve to '{}': {}", sh.name, e);
1983                }
1984            }
1985        }
1986    }
1987
1988    /// Handle a resolved completion item — apply additional_text_edits (e.g. auto-imports).
1989    pub(crate) fn handle_completion_resolved(&mut self, item: lsp_types::CompletionItem) {
1990        if let Some(additional_edits) = item.additional_text_edits {
1991            if !additional_edits.is_empty() {
1992                tracing::info!(
1993                    "Applying {} additional text edits from completion resolve",
1994                    additional_edits.len()
1995                );
1996                let buffer_id = self.active_buffer();
1997                if let Err(e) = self.apply_lsp_text_edits(buffer_id, additional_edits) {
1998                    tracing::error!("Failed to apply completion additional_text_edits: {}", e);
1999                }
2000            }
2001        }
2002    }
2003
2004    /// Apply formatting edits from textDocument/formatting response.
2005    pub(crate) fn apply_formatting_edits(
2006        &mut self,
2007        uri: &str,
2008        edits: Vec<lsp_types::TextEdit>,
2009    ) -> AnyhowResult<usize> {
2010        // Find the buffer for this URI
2011        let buffer_id = self
2012            .active_window()
2013            .buffer_metadata
2014            .iter()
2015            .find(|(_, meta)| meta.file_uri().map(|u| u.as_str() == uri).unwrap_or(false))
2016            .map(|(id, _)| *id);
2017
2018        if let Some(buffer_id) = buffer_id {
2019            let count = self.apply_lsp_text_edits(buffer_id, edits)?;
2020            self.set_status_message(format!("Formatted ({} edits)", count));
2021            Ok(count)
2022        } else {
2023            tracing::warn!("Cannot apply formatting: no buffer for URI {}", uri);
2024            Ok(0)
2025        }
2026    }
2027
2028    /// Request document formatting from LSP.
2029    pub(crate) fn request_formatting(&mut self) {
2030        let buffer_id = self.active_buffer();
2031        let metadata = match self.active_window().buffer_metadata.get(&buffer_id) {
2032            Some(m) if m.lsp_enabled => m,
2033            _ => {
2034                self.set_status_message("LSP not available for this buffer".to_string());
2035                return;
2036            }
2037        };
2038
2039        let uri = match metadata.file_uri() {
2040            Some(u) => u.clone(),
2041            None => return,
2042        };
2043
2044        let language = match self
2045            .windows
2046            .get(&self.active_window)
2047            .map(|w| &w.buffers)
2048            .expect("active window present")
2049            .get(&buffer_id)
2050            .map(|s| s.language.clone())
2051        {
2052            Some(l) => l,
2053            None => return,
2054        };
2055
2056        let tab_size = self.config.editor.tab_size as u32;
2057        let insert_spaces = !self.config.editor.use_tabs;
2058
2059        self.active_window_mut().next_lsp_request_id += 1;
2060        let request_id = self.active_window_mut().next_lsp_request_id;
2061
2062        let __active_id = self.active_window;
2063
2064        if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
2065            if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Format) {
2066                if let Err(e) = sh.handle.document_formatting(
2067                    request_id,
2068                    uri.as_uri().clone(),
2069                    tab_size,
2070                    insert_spaces,
2071                ) {
2072                    tracing::warn!("Failed to request formatting: {}", e);
2073                }
2074            } else {
2075                self.set_status_message("Formatting not supported by LSP server".to_string());
2076            }
2077        }
2078    }
2079
2080    /// Handle find references response from LSP
2081    pub(crate) fn handle_references_response(
2082        &mut self,
2083        request_id: u64,
2084        locations: Vec<lsp_types::Location>,
2085    ) -> AnyhowResult<()> {
2086        tracing::info!(
2087            "handle_references_response: received {} locations for request_id={}",
2088            locations.len(),
2089            request_id
2090        );
2091
2092        // Check if this response is for the current pending request
2093        if self.active_window_mut().pending_references_request != Some(request_id) {
2094            tracing::debug!("Ignoring stale references response: {}", request_id);
2095            return Ok(());
2096        }
2097
2098        self.active_window_mut().pending_references_request = None;
2099        if locations.is_empty() {
2100            self.set_status_message(t!("lsp.no_references").to_string());
2101            return Ok(());
2102        }
2103
2104        // Convert locations to hook args format. Each `loc.uri` is a
2105        // wire-side URI from the LSP, so wrap it in [`LspUri`] and run
2106        // it through the active authority's translation before
2107        // handing a host-path string to the references hook —
2108        // otherwise plugins (notably `find_references`) try to open
2109        // an in-container path on the host and fail.
2110        let translation = self.authority().path_translation.clone();
2111        let lsp_locations: Vec<crate::services::plugins::hooks::LspLocation> = locations
2112            .iter()
2113            .map(|loc| {
2114                let wire = crate::app::types::LspUri::from_wire(loc.uri.clone());
2115                // Prefer the host-side path (after translation) so
2116                // plugin-side file ops resolve. Fall back to the raw
2117                // string for non-`file://` URIs so callers can still
2118                // see *something*.
2119                let file = if loc.uri.scheme().map(|s| s.as_str()) == Some("file") {
2120                    wire.to_host_path(translation.as_ref())
2121                        .map(|p| p.to_string_lossy().into_owned())
2122                        .unwrap_or_else(|| loc.uri.path().as_str().to_string())
2123                } else {
2124                    loc.uri.as_str().to_string()
2125                };
2126
2127                crate::services::plugins::hooks::LspLocation {
2128                    file,
2129                    line: loc.range.start.line + 1, // LSP is 0-based, convert to 1-based
2130                    column: loc.range.start.character + 1, // LSP is 0-based
2131                }
2132            })
2133            .collect();
2134
2135        let count = lsp_locations.len();
2136        let symbol = std::mem::take(&mut self.active_window_mut().pending_references_symbol);
2137        self.set_status_message(
2138            t!("lsp.found_references", count = count, symbol = &symbol).to_string(),
2139        );
2140
2141        // Fire the lsp_references hook so plugins can display the results
2142        self.plugin_manager.read().unwrap().run_hook(
2143            "lsp_references",
2144            crate::services::plugins::hooks::HookArgs::LspReferences {
2145                symbol: symbol.clone(),
2146                locations: lsp_locations,
2147            },
2148        );
2149
2150        tracing::info!(
2151            "Fired lsp_references hook with {} locations for symbol '{}'",
2152            count,
2153            symbol
2154        );
2155
2156        Ok(())
2157    }
2158
2159    /// Apply LSP text edits to a buffer and return the number of changes made.
2160    /// Edits are sorted in reverse order and applied as a batch.
2161    pub(crate) fn apply_lsp_text_edits(
2162        &mut self,
2163        buffer_id: BufferId,
2164        mut edits: Vec<lsp_types::TextEdit>,
2165    ) -> AnyhowResult<usize> {
2166        if edits.is_empty() {
2167            return Ok(0);
2168        }
2169
2170        // Sort edits by position (reverse order to avoid offset issues)
2171        edits.sort_by(|a, b| {
2172            b.range
2173                .start
2174                .line
2175                .cmp(&a.range.start.line)
2176                .then(b.range.start.character.cmp(&a.range.start.character))
2177        });
2178
2179        // Collect all events for this buffer into a batch
2180        let mut batch_events = Vec::new();
2181        let mut changes = 0;
2182
2183        // Get cursor_id for this buffer from split view state
2184        let cursor_id = {
2185            let split_id = self
2186                .split_manager_mut()
2187                .splits_for_buffer(buffer_id)
2188                .into_iter()
2189                .next()
2190                .unwrap_or_else(|| {
2191                    self.windows
2192                        .get(&self.active_window)
2193                        .and_then(|w| w.buffers.splits())
2194                        .map(|(mgr, _)| mgr)
2195                        .expect("active window must have a populated split layout")
2196                        .active_split()
2197                });
2198            self.windows
2199                .get(&self.active_window)
2200                .and_then(|w| w.buffers.splits())
2201                .map(|(_, vs)| vs)
2202                .expect("active window must have a populated split layout")
2203                .get(&split_id)
2204                .map(|vs| vs.cursors.primary_id())
2205                .unwrap_or_else(|| self.active_cursors().primary_id())
2206        };
2207
2208        // Create events for all edits
2209        for edit in edits {
2210            let state = self
2211                .buffers_mut()
2212                .get_mut(&buffer_id)
2213                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))?;
2214
2215            // Convert LSP range to byte positions
2216            let start_line = edit.range.start.line as usize;
2217            let start_char = edit.range.start.character as usize;
2218            let end_line = edit.range.end.line as usize;
2219            let end_char = edit.range.end.character as usize;
2220
2221            let start_pos = state.buffer.lsp_position_to_byte(start_line, start_char);
2222            let end_pos = state.buffer.lsp_position_to_byte(end_line, end_char);
2223            let buffer_len = state.buffer.len();
2224
2225            // Log the conversion for debugging
2226            let old_text = if start_pos < end_pos && end_pos <= buffer_len {
2227                state.get_text_range(start_pos, end_pos)
2228            } else {
2229                format!(
2230                    "<invalid range: start={}, end={}, buffer_len={}>",
2231                    start_pos, end_pos, buffer_len
2232                )
2233            };
2234            tracing::debug!(
2235                "  Converting LSP range line {}:{}-{}:{} to bytes {}..{} (replacing {:?} with {:?})",
2236                start_line, start_char, end_line, end_char,
2237                start_pos, end_pos, old_text, edit.new_text
2238            );
2239
2240            // Delete old text
2241            if start_pos < end_pos {
2242                let deleted_text = state.get_text_range(start_pos, end_pos);
2243                let delete_event = Event::Delete {
2244                    range: start_pos..end_pos,
2245                    deleted_text,
2246                    cursor_id,
2247                };
2248                batch_events.push(delete_event);
2249            }
2250
2251            // Insert new text
2252            if !edit.new_text.is_empty() {
2253                let insert_event = Event::Insert {
2254                    position: start_pos,
2255                    text: edit.new_text.clone(),
2256                    cursor_id,
2257                };
2258                batch_events.push(insert_event);
2259            }
2260
2261            changes += 1;
2262        }
2263
2264        // Apply all rename changes using bulk edit for O(n) performance
2265        if !batch_events.is_empty() {
2266            self.apply_events_to_buffer_as_bulk_edit(
2267                buffer_id,
2268                batch_events,
2269                "LSP Rename".to_string(),
2270            )?;
2271        }
2272
2273        Ok(changes)
2274    }
2275
2276    /// Apply a single TextDocumentEdit from a workspace edit.
2277    ///
2278    /// Per LSP spec: if `text_document.version` is non-null, it must match the
2279    /// version we last sent via didOpen/didChange. On mismatch the edit is stale
2280    /// and we skip it to avoid corrupting the buffer.
2281    fn apply_text_document_edit(
2282        &mut self,
2283        text_doc_edit: lsp_types::TextDocumentEdit,
2284    ) -> AnyhowResult<usize> {
2285        // Wrap the incoming wire URI once; both the version-check
2286        // lookup and the file-open below need the host-path form.
2287        let uri = crate::app::types::LspUri::from_wire(text_doc_edit.text_document.uri);
2288
2289        // Version check: if the server specifies a version, verify it matches
2290        // what we sent. A mismatch means the edit was computed against stale content.
2291        if let Some(expected_version) = text_doc_edit.text_document.version {
2292            if let Ok(path) =
2293                super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2294            {
2295                if let Some(lsp) = self.lsp() {
2296                    let language = self
2297                        .buffers()
2298                        .get(&self.active_buffer())
2299                        .map(|s| s.language.clone())
2300                        .unwrap_or_default();
2301                    for sh in lsp.get_handles(&language) {
2302                        if let Some(current_version) = sh.handle.document_version(&path) {
2303                            if (expected_version as i64) != current_version {
2304                                tracing::warn!(
2305                                    "Rejecting stale TextDocumentEdit for {:?}: \
2306                                     server version {} != our version {}",
2307                                    path,
2308                                    expected_version,
2309                                    current_version,
2310                                );
2311                                return Ok(0);
2312                            }
2313                        }
2314                    }
2315                }
2316            }
2317        }
2318
2319        if let Ok(path) =
2320            super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2321        {
2322            let buffer_id = match self.open_file(&path) {
2323                Ok(id) => id,
2324                Err(e) => {
2325                    if let Some(confirmation) =
2326                        e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2327                    {
2328                        self.start_large_file_encoding_confirmation(confirmation);
2329                    } else {
2330                        self.set_status_message(
2331                            t!("file.error_opening", error = e.to_string()).to_string(),
2332                        );
2333                    }
2334                    return Ok(0);
2335                }
2336            };
2337
2338            let edits: Vec<lsp_types::TextEdit> = text_doc_edit
2339                .edits
2340                .into_iter()
2341                .map(|one_of| match one_of {
2342                    lsp_types::OneOf::Left(text_edit) => text_edit,
2343                    lsp_types::OneOf::Right(annotated) => annotated.text_edit,
2344                })
2345                .collect();
2346
2347            tracing::info!("Applying {} edits for {:?}:", edits.len(), path);
2348            for (i, edit) in edits.iter().enumerate() {
2349                tracing::info!(
2350                    "  Edit {}: line {}:{}-{}:{} -> {:?}",
2351                    i,
2352                    edit.range.start.line,
2353                    edit.range.start.character,
2354                    edit.range.end.line,
2355                    edit.range.end.character,
2356                    edit.new_text
2357                );
2358            }
2359
2360            self.apply_lsp_text_edits(buffer_id, edits)
2361        } else {
2362            Ok(0)
2363        }
2364    }
2365
2366    /// Apply a resource operation (CreateFile, RenameFile, DeleteFile) from a workspace edit.
2367    fn apply_resource_operation(&mut self, op: lsp_types::ResourceOp) -> AnyhowResult<()> {
2368        // Each URI in a resource operation is wire-side and must be
2369        // translated back to the host before we touch the host
2370        // filesystem. Wrapping in [`LspUri`] and calling
2371        // `to_host_path` is the type-checked path.
2372        let translation = self.authority().path_translation.clone();
2373        let to_host = |uri: &lsp_types::Uri| -> std::path::PathBuf {
2374            crate::app::types::LspUri::from_wire(uri.clone())
2375                .to_host_path(translation.as_ref())
2376                .unwrap_or_else(|| std::path::PathBuf::from(uri.path().as_str()))
2377        };
2378        match op {
2379            lsp_types::ResourceOp::Create(create) => {
2380                let path = to_host(&create.uri);
2381                let overwrite = create
2382                    .options
2383                    .as_ref()
2384                    .and_then(|o| o.overwrite)
2385                    .unwrap_or(false);
2386                let ignore_if_exists = create
2387                    .options
2388                    .as_ref()
2389                    .and_then(|o| o.ignore_if_exists)
2390                    .unwrap_or(false);
2391
2392                if path.exists() {
2393                    if ignore_if_exists {
2394                        tracing::debug!("CreateFile: {:?} already exists, ignoring", path);
2395                        return Ok(());
2396                    }
2397                    if !overwrite {
2398                        tracing::warn!("CreateFile: {:?} already exists and overwrite=false", path);
2399                        return Ok(());
2400                    }
2401                }
2402
2403                // Create parent directories if needed
2404                if let Some(parent) = path.parent() {
2405                    std::fs::create_dir_all(parent)?;
2406                }
2407                std::fs::write(&path, "")?;
2408                tracing::info!("CreateFile: created {:?}", path);
2409
2410                // Open the new file as a buffer
2411                if let Err(e) = self.open_file(&path) {
2412                    tracing::warn!("CreateFile: failed to open created file {:?}: {}", path, e);
2413                }
2414            }
2415            lsp_types::ResourceOp::Rename(rename) => {
2416                let old_path = to_host(&rename.old_uri);
2417                let new_path = to_host(&rename.new_uri);
2418                let overwrite = rename
2419                    .options
2420                    .as_ref()
2421                    .and_then(|o| o.overwrite)
2422                    .unwrap_or(false);
2423                let ignore_if_exists = rename
2424                    .options
2425                    .as_ref()
2426                    .and_then(|o| o.ignore_if_exists)
2427                    .unwrap_or(false);
2428
2429                if new_path.exists() {
2430                    if ignore_if_exists {
2431                        tracing::debug!("RenameFile: {:?} already exists, ignoring", new_path);
2432                        return Ok(());
2433                    }
2434                    if !overwrite {
2435                        tracing::warn!(
2436                            "RenameFile: {:?} already exists and overwrite=false",
2437                            new_path
2438                        );
2439                        return Ok(());
2440                    }
2441                }
2442
2443                // Create parent directories if needed
2444                if let Some(parent) = new_path.parent() {
2445                    std::fs::create_dir_all(parent)?;
2446                }
2447                std::fs::rename(&old_path, &new_path)?;
2448                tracing::info!("RenameFile: {:?} -> {:?}", old_path, new_path);
2449            }
2450            lsp_types::ResourceOp::Delete(delete) => {
2451                let path = to_host(&delete.uri);
2452                let recursive = delete
2453                    .options
2454                    .as_ref()
2455                    .and_then(|o| o.recursive)
2456                    .unwrap_or(false);
2457                let ignore_if_not_exists = delete
2458                    .options
2459                    .as_ref()
2460                    .and_then(|o| o.ignore_if_not_exists)
2461                    .unwrap_or(false);
2462
2463                if !path.exists() {
2464                    if ignore_if_not_exists {
2465                        tracing::debug!("DeleteFile: {:?} does not exist, ignoring", path);
2466                        return Ok(());
2467                    }
2468                    tracing::warn!("DeleteFile: {:?} does not exist", path);
2469                    return Ok(());
2470                }
2471
2472                if path.is_dir() && recursive {
2473                    std::fs::remove_dir_all(&path)?;
2474                } else if path.is_file() {
2475                    std::fs::remove_file(&path)?;
2476                }
2477                tracing::info!("DeleteFile: deleted {:?}", path);
2478            }
2479        }
2480        Ok(())
2481    }
2482
2483    /// Apply an LSP WorkspaceEdit (used by rename, code actions, etc.).
2484    ///
2485    /// Returns the total number of text changes applied.
2486    pub(crate) fn apply_workspace_edit(
2487        &mut self,
2488        workspace_edit: lsp_types::WorkspaceEdit,
2489    ) -> AnyhowResult<usize> {
2490        tracing::debug!(
2491            "Applying WorkspaceEdit: changes={:?}, document_changes={:?}",
2492            workspace_edit.changes.as_ref().map(|c| c.len()),
2493            workspace_edit.document_changes.as_ref().map(|dc| match dc {
2494                lsp_types::DocumentChanges::Edits(e) => format!("{} edits", e.len()),
2495                lsp_types::DocumentChanges::Operations(o) => format!("{} operations", o.len()),
2496            })
2497        );
2498
2499        let mut total_changes = 0;
2500
2501        // Handle changes (map of URI -> Vec<TextEdit>)
2502        if let Some(changes) = workspace_edit.changes {
2503            for (uri, edits) in changes {
2504                let uri = crate::app::types::LspUri::from_wire(uri);
2505                if let Ok(path) =
2506                    super::lsp_uri_to_host_path(&uri, self.authority().path_translation.as_ref())
2507                {
2508                    let buffer_id = match self.open_file(&path) {
2509                        Ok(id) => id,
2510                        Err(e) => {
2511                            if let Some(confirmation) = e.downcast_ref::<
2512                                crate::model::buffer::LargeFileEncodingConfirmation,
2513                            >() {
2514                                self.start_large_file_encoding_confirmation(confirmation);
2515                            } else {
2516                                self.set_status_message(
2517                                    t!("file.error_opening", error = e.to_string())
2518                                        .to_string(),
2519                                );
2520                            }
2521                            return Ok(0);
2522                        }
2523                    };
2524                    total_changes += self.apply_lsp_text_edits(buffer_id, edits)?;
2525                }
2526            }
2527        }
2528
2529        // Handle document_changes (TextDocumentEdit[] or DocumentChangeOperation[])
2530        if let Some(document_changes) = workspace_edit.document_changes {
2531            use lsp_types::DocumentChanges;
2532
2533            match document_changes {
2534                DocumentChanges::Edits(edits) => {
2535                    for text_doc_edit in edits {
2536                        total_changes += self.apply_text_document_edit(text_doc_edit)?;
2537                    }
2538                }
2539                DocumentChanges::Operations(ops) => {
2540                    // Process operations in order — resource ops (create/rename/delete)
2541                    // must be applied before text edits on the created/renamed files.
2542                    for op in ops {
2543                        match op {
2544                            lsp_types::DocumentChangeOperation::Edit(text_doc_edit) => {
2545                                total_changes += self.apply_text_document_edit(text_doc_edit)?;
2546                            }
2547                            lsp_types::DocumentChangeOperation::Op(resource_op) => {
2548                                self.apply_resource_operation(resource_op)?;
2549                                total_changes += 1;
2550                            }
2551                        }
2552                    }
2553                }
2554            }
2555        }
2556
2557        Ok(total_changes)
2558    }
2559
2560    /// Handle rename response from LSP
2561    pub fn handle_rename_response(
2562        &mut self,
2563        _request_id: u64,
2564        result: Result<lsp_types::WorkspaceEdit, String>,
2565    ) -> AnyhowResult<()> {
2566        match result {
2567            Ok(workspace_edit) => {
2568                let total_changes = self.apply_workspace_edit(workspace_edit)?;
2569                self.active_window_mut().status_message =
2570                    Some(t!("lsp.renamed", count = total_changes).to_string());
2571            }
2572            Err(error) => {
2573                // Per LSP spec: ContentModified errors (-32801) should NOT be shown to user
2574                if error.contains("content modified") || error.contains("-32801") {
2575                    tracing::debug!(
2576                        "LSP rename: ContentModified error (expected, ignoring): {}",
2577                        error
2578                    );
2579                    self.active_window_mut().status_message =
2580                        Some(t!("lsp.rename_cancelled").to_string());
2581                } else {
2582                    self.active_window_mut().status_message =
2583                        Some(t!("lsp.rename_failed", error = &error).to_string());
2584                }
2585            }
2586        }
2587
2588        Ok(())
2589    }
2590
2591    /// Apply events to a specific buffer using bulk edit optimization (O(n) vs O(n²))
2592    ///
2593    /// This is similar to `apply_events_as_bulk_edit` but works on a specific buffer
2594    /// (which may not be the active buffer) and handles LSP notifications correctly.
2595    pub(crate) fn apply_events_to_buffer_as_bulk_edit(
2596        &mut self,
2597        buffer_id: BufferId,
2598        events: Vec<Event>,
2599        description: String,
2600    ) -> AnyhowResult<()> {
2601        use crate::model::event::CursorId;
2602
2603        if events.is_empty() {
2604            return Ok(());
2605        }
2606
2607        // Create a temporary batch for collecting LSP changes (before applying)
2608        let batch_for_lsp = Event::Batch {
2609            events: events.clone(),
2610            description: description.clone(),
2611        };
2612
2613        // IMPORTANT: Calculate LSP changes BEFORE applying to buffer!
2614        // The byte positions in the events are relative to the ORIGINAL buffer.
2615        //
2616        // The tree-only swap below violates the pane-buffer invariant
2617        // transiently (see active_focus.rs for the invariant's contract)
2618        // but `collect_lsp_changes` does not route any input, call
2619        // `apply_event_to_active_buffer`, or otherwise read
2620        // `active_buffer()` while the invariant is broken, so the drift
2621        // is contained within this synchronous section. If that changes,
2622        // switch to a read-only accessor that takes `buffer_id` directly
2623        // rather than mutating tree state.
2624        let original_active = self.active_buffer();
2625        self.windows
2626            .get_mut(&self.active_window)
2627            .and_then(|w| w.split_manager_mut())
2628            .expect("active window must have a populated split layout")
2629            .set_active_buffer_id(buffer_id);
2630        let lsp_changes = self.active_window().collect_lsp_changes(&batch_for_lsp);
2631        self.windows
2632            .get_mut(&self.active_window)
2633            .and_then(|w| w.split_manager_mut())
2634            .expect("active window must have a populated split layout")
2635            .set_active_buffer_id(original_active);
2636
2637        // Capture old cursor states from split view state
2638        // Find a split that has this buffer in its keyed_states
2639        let split_id_for_cursors = self
2640            .split_manager_mut()
2641            .splits_for_buffer(buffer_id)
2642            .into_iter()
2643            .next()
2644            .unwrap_or_else(|| {
2645                self.windows
2646                    .get(&self.active_window)
2647                    .and_then(|w| w.buffers.splits())
2648                    .map(|(mgr, _)| mgr)
2649                    .expect("active window must have a populated split layout")
2650                    .active_split()
2651            });
2652        let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2653            .windows
2654            .get(&self.active_window)
2655            .and_then(|w| w.buffers.splits())
2656            .map(|(_, vs)| vs)
2657            .expect("active window must have a populated split layout")
2658            .get(&split_id_for_cursors)
2659            .and_then(|vs| vs.keyed_states.get(&buffer_id))
2660            .map(|bvs| {
2661                bvs.cursors
2662                    .iter()
2663                    .map(|(id, c)| (id, c.position, c.anchor))
2664                    .collect()
2665            })
2666            .unwrap_or_default();
2667
2668        // TODO: move this whole bulk-edit method to impl Window — the
2669        // body is window-scoped except for `send_lsp_changes_for_buffer`
2670        // at the tail which is LSP coordination on Editor. The block
2671        // below uses a single-window split borrow because the bulk edit
2672        // needs `&mut state` (buffer) and `&mut __vs_map` (split view
2673        // states) live together for the cursor-positioning loop.
2674        let __win = self
2675            .windows
2676            .get_mut(&self.active_window)
2677            .expect("active window must exist");
2678        let bulk_edit = __win
2679            .buffers
2680            .with_buffer_and_view_states(buffer_id, |state, vs_map| -> AnyhowResult<Event> {
2681                // Snapshot buffer state for undo (piece tree + buffers)
2682                let old_snapshot = state.buffer.snapshot_buffer_state();
2683
2684                // Convert events to edit tuples: (position, delete_len, insert_text)
2685                let mut edits: Vec<(usize, usize, String)> = Vec::new();
2686                for event in &events {
2687                    match event {
2688                        Event::Insert { position, text, .. } => {
2689                            edits.push((*position, 0, text.clone()));
2690                        }
2691                        Event::Delete { range, .. } => {
2692                            edits.push((range.start, range.len(), String::new()));
2693                        }
2694                        _ => {}
2695                    }
2696                }
2697
2698                // Sort edits by position descending (required by apply_bulk_edits)
2699                edits.sort_by(|a, b| b.0.cmp(&a.0));
2700
2701                // Convert to references for apply_bulk_edits
2702                let edit_refs: Vec<(usize, usize, &str)> = edits
2703                    .iter()
2704                    .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2705                    .collect();
2706
2707                // Snapshot displaced markers before edits so undo can restore them exactly.
2708                let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2709
2710                // Apply bulk edits - O(n) instead of O(n²)
2711                let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2712
2713                // Calculate new cursor positions based on edits
2714                let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2715                for (pos, del_len, text) in &edits {
2716                    let delta = text.len() as isize - *del_len as isize;
2717                    position_deltas.push((*pos, delta));
2718                }
2719                position_deltas.sort_by_key(|(pos, _)| *pos);
2720
2721                let calc_shift = |original_pos: usize| -> isize {
2722                    let mut shift: isize = 0;
2723                    for (edit_pos, delta) in &position_deltas {
2724                        if *edit_pos < original_pos {
2725                            shift += delta;
2726                        }
2727                    }
2728                    shift
2729                };
2730
2731                // Calculate new cursor positions
2732                let buffer_len = state.buffer.len();
2733                let new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors
2734                    .iter()
2735                    .map(|(id, pos, anchor)| {
2736                        let shift = calc_shift(*pos);
2737                        let new_pos = ((*pos as isize + shift).max(0) as usize).min(buffer_len);
2738                        let new_anchor = anchor.map(|a| {
2739                            let anchor_shift = calc_shift(a);
2740                            ((a as isize + anchor_shift).max(0) as usize).min(buffer_len)
2741                        });
2742                        (*id, new_pos, new_anchor)
2743                    })
2744                    .collect();
2745
2746                // Snapshot buffer state after edits (for redo)
2747                let new_snapshot = state.buffer.snapshot_buffer_state();
2748
2749                // Invalidate syntax highlighting
2750                state.highlighter.invalidate_all();
2751
2752                // Apply new cursor positions to split view state
2753                if let Some(vs) = vs_map.get_mut(&split_id_for_cursors) {
2754                    if let Some(bvs) = vs.keyed_states.get_mut(&buffer_id) {
2755                        for (cursor_id, new_pos, new_anchor) in &new_cursors {
2756                            if let Some(cursor) = bvs.cursors.get_mut(*cursor_id) {
2757                                cursor.position = *new_pos;
2758                                cursor.anchor = *new_anchor;
2759                            }
2760                        }
2761                    }
2762                }
2763
2764                // Convert edit list to lengths-only for undo/redo marker replay.
2765                // Merge edits at the same position into a single replacement.
2766                let edit_lengths: Vec<(usize, usize, usize)> = {
2767                    let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2768                    for (pos, del_len, text) in &edits {
2769                        if let Some(last) = lengths.last_mut() {
2770                            if last.0 == *pos {
2771                                last.1 += del_len;
2772                                last.2 += text.len();
2773                                continue;
2774                            }
2775                        }
2776                        lengths.push((*pos, *del_len, text.len()));
2777                    }
2778                    lengths
2779                };
2780
2781                // Adjust markers using merged net-delta
2782                for &(pos, del_len, ins_len) in &edit_lengths {
2783                    if del_len > 0 && ins_len > 0 {
2784                        if ins_len > del_len {
2785                            state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2786                            state.margins.adjust_for_insert(pos, ins_len - del_len);
2787                        } else if del_len > ins_len {
2788                            state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2789                            state.margins.adjust_for_delete(pos, del_len - ins_len);
2790                        }
2791                    } else if del_len > 0 {
2792                        state.marker_list.adjust_for_delete(pos, del_len);
2793                        state.margins.adjust_for_delete(pos, del_len);
2794                    } else if ins_len > 0 {
2795                        state.marker_list.adjust_for_insert(pos, ins_len);
2796                        state.margins.adjust_for_insert(pos, ins_len);
2797                    }
2798                }
2799
2800                Ok(Event::BulkEdit {
2801                    old_snapshot: Some(old_snapshot),
2802                    new_snapshot: Some(new_snapshot),
2803                    old_cursors,
2804                    new_cursors,
2805                    description,
2806                    edits: edit_lengths,
2807                    displaced_markers,
2808                })
2809            })
2810            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Buffer not found"))??;
2811
2812        // Add to event log
2813        if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
2814            event_log.append(bulk_edit);
2815        }
2816
2817        // Notify LSP about the changes using pre-calculated positions
2818        self.active_window_mut()
2819            .send_lsp_changes_for_buffer(buffer_id, lsp_changes);
2820
2821        Ok(())
2822    }
2823
2824    /// Start rename mode - select the symbol at cursor and allow inline editing
2825    pub(crate) fn start_rename(&mut self) -> AnyhowResult<()> {
2826        // If server supports prepareRename, validate first
2827        if self.active_window().server_supports_prepare_rename() {
2828            self.active_window_mut().send_prepare_rename();
2829            return Ok(());
2830        }
2831
2832        self.show_rename_prompt()
2833    }
2834
2835    /// Handle prepareRename response — if valid, show rename prompt; if error, show message.
2836    pub(crate) fn handle_prepare_rename_response(
2837        &mut self,
2838        result: Result<serde_json::Value, String>,
2839    ) {
2840        match result {
2841            Ok(value) if !value.is_null() => {
2842                // prepareRename succeeded — show the rename prompt
2843                if let Err(e) = self.show_rename_prompt() {
2844                    self.set_status_message(format!("Rename failed: {e}"));
2845                }
2846            }
2847            Ok(_) => {
2848                self.set_status_message("Cannot rename at this position".to_string());
2849            }
2850            Err(e) => {
2851                self.set_status_message(format!("Cannot rename: {e}"));
2852            }
2853        }
2854    }
2855
2856    /// Send textDocument/prepareRename to the LSP server
2857    /// Show the rename prompt (called directly or after prepareRename succeeds).
2858    fn show_rename_prompt(&mut self) -> AnyhowResult<()> {
2859        use crate::primitives::word_navigation::{find_word_end, find_word_start};
2860
2861        // Get the current buffer and cursor position
2862        let cursor_pos = self.active_cursors().primary().position;
2863        let (word_start, word_end) = {
2864            let state = self.active_state();
2865
2866            // Find the word boundaries
2867            let word_start = find_word_start(&state.buffer, cursor_pos);
2868            let word_end = find_word_end(&state.buffer, cursor_pos);
2869
2870            // Check if we're on a word
2871            if word_start >= word_end {
2872                self.active_window_mut().status_message =
2873                    Some(t!("lsp.no_symbol_at_cursor").to_string());
2874                return Ok(());
2875            }
2876
2877            (word_start, word_end)
2878        };
2879
2880        // Get the word text
2881        let word_text = self.active_state_mut().get_text_range(word_start, word_end);
2882
2883        // Create an overlay to highlight the symbol being renamed
2884        let overlay_handle = self.add_overlay(
2885            None,
2886            word_start..word_end,
2887            crate::model::event::OverlayFace::Background {
2888                color: (50, 100, 200), // Blue background for rename
2889            },
2890            100,
2891            Some(t!("lsp.popup_renaming").to_string()),
2892        );
2893
2894        // Enter rename mode using the Prompt system
2895        // Store the rename metadata in the PromptType and pre-fill the input with the current name
2896        let mut prompt = Prompt::new(
2897            "Rename to: ".to_string(),
2898            PromptType::LspRename {
2899                original_text: word_text.clone(),
2900                start_pos: word_start,
2901                end_pos: word_end,
2902                overlay_handle,
2903            },
2904        );
2905        // Pre-fill the input with the current name and position cursor at the end
2906        prompt.set_input(word_text);
2907
2908        self.active_window_mut().prompt = Some(prompt);
2909        Ok(())
2910    }
2911
2912    /// Cancel rename mode - removes overlay if the prompt was for LSP rename
2913    pub(crate) fn cancel_rename_overlay(&mut self, handle: &crate::view::overlay::OverlayHandle) {
2914        self.remove_overlay(handle.clone());
2915    }
2916
2917    /// Perform the actual LSP rename request
2918    pub(crate) fn perform_lsp_rename(
2919        &mut self,
2920        new_name: String,
2921        original_text: String,
2922        start_pos: usize,
2923        overlay_handle: crate::view::overlay::OverlayHandle,
2924    ) {
2925        // Remove the overlay first
2926        self.cancel_rename_overlay(&overlay_handle);
2927
2928        // Check if the name actually changed
2929        if new_name == original_text {
2930            self.active_window_mut().status_message = Some(t!("lsp.name_unchanged").to_string());
2931            return;
2932        }
2933
2934        // Use the position from when we entered rename mode, NOT the current cursor position
2935        // This ensures we send the rename request for the correct symbol even if cursor moved
2936        let rename_pos = start_pos;
2937
2938        // Convert byte position to LSP position (line, UTF-16 code units)
2939        // LSP uses UTF-16 code units for character offsets, not byte offsets
2940        let state = self.active_state();
2941        let (line, character) = state.buffer.position_to_lsp_position(rename_pos);
2942        let buffer_id = self.active_buffer();
2943        let request_id = self.active_window_mut().next_lsp_request_id;
2944
2945        // Use helper to ensure didOpen is sent before the request
2946        let sent = self
2947            .with_lsp_for_buffer(buffer_id, LspFeature::Rename, |handle, uri, _language| {
2948                let result = handle.rename(
2949                    request_id,
2950                    uri.as_uri().clone(),
2951                    line as u32,
2952                    character as u32,
2953                    new_name.clone(),
2954                );
2955                if result.is_ok() {
2956                    tracing::info!(
2957                        "Requested rename at {}:{}:{} to '{}'",
2958                        uri.as_str(),
2959                        line,
2960                        character,
2961                        new_name
2962                    );
2963                }
2964                result.is_ok()
2965            })
2966            .unwrap_or(false);
2967
2968        if sent {
2969            self.active_window_mut().next_lsp_request_id += 1;
2970        } else if self
2971            .active_window()
2972            .buffer_metadata
2973            .get(&buffer_id)
2974            .and_then(|m| m.file_path())
2975            .is_none()
2976        {
2977            self.active_window_mut().status_message =
2978                Some(t!("lsp.cannot_rename_unsaved").to_string());
2979        }
2980    }
2981
2982    /// Request inlay hints for the active buffer (if enabled and LSP available)
2983    pub(crate) fn request_inlay_hints_for_active_buffer(&mut self) {
2984        let buffer_id = self.active_buffer();
2985        self.request_inlay_hints_for_buffer(buffer_id);
2986    }
2987
2988    /// Request inlay hints for a specific buffer (if enabled and LSP available)
2989    pub(crate) fn request_inlay_hints_for_buffer(&mut self, buffer_id: BufferId) {
2990        if !self.config.editor.enable_inlay_hints {
2991            return;
2992        }
2993
2994        // Get line count and version from buffer state — both are needed so
2995        // the response handler can drop stale data if the buffer has moved
2996        // on by the time hints arrive.
2997        let (line_count, version) = if let Some(state) = self
2998            .windows
2999            .get(&self.active_window)
3000            .map(|w| &w.buffers)
3001            .expect("active window present")
3002            .get(&buffer_id)
3003        {
3004            (
3005                state.buffer.line_count().unwrap_or(1000),
3006                state.buffer.version(),
3007            )
3008        } else {
3009            return;
3010        };
3011        let last_line = line_count.saturating_sub(1) as u32;
3012        let request_id = self.active_window_mut().next_lsp_request_id;
3013
3014        // Use helper to ensure didOpen is sent before the request
3015        let sent = self
3016            .with_lsp_for_buffer(
3017                buffer_id,
3018                LspFeature::InlayHints,
3019                |handle, uri, _language| {
3020                    let result = handle.inlay_hints(
3021                        request_id,
3022                        uri.as_uri().clone(),
3023                        0,
3024                        0,
3025                        last_line,
3026                        10000,
3027                    );
3028                    if result.is_ok() {
3029                        tracing::info!(
3030                            "Requested inlay hints for {} (request_id={})",
3031                            uri.as_str(),
3032                            request_id
3033                        );
3034                    } else if let Err(e) = &result {
3035                        tracing::debug!("Failed to request inlay hints: {}", e);
3036                    }
3037                    result.is_ok()
3038                },
3039            )
3040            .unwrap_or(false);
3041
3042        if sent {
3043            self.active_window_mut().next_lsp_request_id += 1;
3044            self.active_window_mut()
3045                .pending_inlay_hints_requests
3046                .insert(request_id, super::InlayHintsRequest { buffer_id, version });
3047        }
3048    }
3049
3050    /// Issue a debounced folding range request if the timer has elapsed.
3051    pub(crate) fn maybe_request_folding_ranges_debounced(&mut self, buffer_id: BufferId) {
3052        let Some(ready_at) = self
3053            .active_window()
3054            .folding_ranges_debounce
3055            .get(&buffer_id)
3056            .copied()
3057        else {
3058            return;
3059        };
3060        if Instant::now() < ready_at {
3061            return;
3062        }
3063
3064        self.active_window_mut()
3065            .folding_ranges_debounce
3066            .remove(&buffer_id);
3067        self.request_folding_ranges_for_buffer(buffer_id);
3068    }
3069
3070    /// Request folding ranges for a buffer if supported and needed.
3071    pub(crate) fn request_folding_ranges_for_buffer(&mut self, buffer_id: BufferId) {
3072        if self
3073            .active_window_mut()
3074            .folding_ranges_in_flight
3075            .contains_key(&buffer_id)
3076        {
3077            return;
3078        }
3079
3080        let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3081            return;
3082        };
3083        if !metadata.lsp_enabled {
3084            return;
3085        }
3086        let Some(uri) = metadata.file_uri().cloned() else {
3087            return;
3088        };
3089        let file_path = metadata.file_path().cloned();
3090
3091        let Some(language) = self
3092            .windows
3093            .get(&self.active_window)
3094            .map(|w| &w.buffers)
3095            .expect("active window present")
3096            .get(&buffer_id)
3097            .map(|s| s.language.clone())
3098        else {
3099            return;
3100        };
3101
3102        let __active_id = self.active_window;
3103        // Pre-collect buffer version so we don't re-read self.buffers
3104        // while the &mut lsp borrow is alive.
3105        let __buffer_version_for_request = self
3106            .windows
3107            .get(&__active_id)
3108            .and_then(|w| w.buffers.get(&buffer_id))
3109            .map(|s| s.buffer.version())
3110            .unwrap_or(0);
3111
3112        let Some(__win) = self.windows.get_mut(&__active_id) else {
3113            return;
3114        };
3115        let __next_id = &mut __win.next_lsp_request_id;
3116        let __pending_folding = &mut __win.pending_folding_range_requests;
3117        let __folding_in_flight = &mut __win.folding_ranges_in_flight;
3118        let lsp = &mut __win.lsp;
3119
3120        if !lsp.folding_ranges_supported(&language) {
3121            return;
3122        }
3123
3124        // Ensure there is a running server
3125        use crate::services::lsp::manager::LspSpawnResult;
3126        if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3127            return;
3128        }
3129
3130        let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::FoldingRange) else {
3131            return;
3132        };
3133        let handle = &mut sh.handle;
3134
3135        let request_id = {
3136            let id = *__next_id;
3137            *__next_id += 1;
3138            id
3139        };
3140        let buffer_version = __buffer_version_for_request;
3141        let _ = __folding_in_flight;
3142
3143        match handle.folding_ranges(request_id, uri.as_uri().clone()) {
3144            Ok(()) => {
3145                __pending_folding.insert(
3146                    request_id,
3147                    super::FoldingRangeRequest {
3148                        buffer_id,
3149                        version: buffer_version,
3150                    },
3151                );
3152                __folding_in_flight.insert(buffer_id, (request_id, buffer_version));
3153            }
3154            Err(e) => {
3155                tracing::debug!("Failed to request folding ranges: {}", e);
3156            }
3157        }
3158    }
3159
3160    /// Request semantic tokens for a specific buffer if supported and needed.
3161    pub(crate) fn maybe_request_semantic_tokens(&mut self, buffer_id: BufferId) {
3162        if !self.config.editor.enable_semantic_tokens_full {
3163            return;
3164        }
3165
3166        // Avoid duplicate in-flight requests per buffer
3167        if self
3168            .active_window_mut()
3169            .semantic_tokens_in_flight
3170            .contains_key(&buffer_id)
3171        {
3172            return;
3173        }
3174
3175        let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3176            return;
3177        };
3178        if !metadata.lsp_enabled {
3179            return;
3180        }
3181        let Some(uri) = metadata.file_uri().cloned() else {
3182            return;
3183        };
3184        let file_path_for_spawn = metadata.file_path().cloned();
3185        // Get language from buffer state
3186        let Some(language) = self
3187            .windows
3188            .get(&self.active_window)
3189            .map(|w| &w.buffers)
3190            .expect("active window present")
3191            .get(&buffer_id)
3192            .map(|s| s.language.clone())
3193        else {
3194            return;
3195        };
3196
3197        let __active_id = self.active_window;
3198        // Pre-extract buffer state info so we don't re-borrow self
3199        // while the &mut lsp borrow is alive.
3200        let Some((buffer_version, existing_version, previous_result_id)) = self
3201            .windows
3202            .get(&__active_id)
3203            .and_then(|w| w.buffers.get(&buffer_id))
3204            .map(|state| {
3205                (
3206                    state.buffer.version(),
3207                    state.semantic_tokens.as_ref().map(|s| s.version),
3208                    state
3209                        .semantic_tokens
3210                        .as_ref()
3211                        .and_then(|s| s.result_id.clone()),
3212                )
3213            })
3214        else {
3215            return;
3216        };
3217        if Some(buffer_version) == existing_version {
3218            return; // Already up to date
3219        }
3220
3221        let Some(__win) = self.windows.get_mut(&__active_id) else {
3222            return;
3223        };
3224        let __next_id = &mut __win.next_lsp_request_id;
3225        let __pending_st = &mut __win.pending_semantic_token_requests;
3226        let __st_in_flight = &mut __win.semantic_tokens_in_flight;
3227        let lsp = &mut __win.lsp;
3228
3229        // Ensure there is a running server
3230        use crate::services::lsp::manager::LspSpawnResult;
3231        if lsp.try_spawn(&language, file_path_for_spawn.as_deref()) != LspSpawnResult::Spawned {
3232            return;
3233        }
3234
3235        // Check that a server actually supports full semantic tokens
3236        if !lsp.semantic_tokens_full_supported(&language) {
3237            return;
3238        }
3239        if lsp.semantic_tokens_legend(&language).is_none() {
3240            return;
3241        }
3242
3243        let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3244            return;
3245        };
3246        // Check capabilities on the specific server we'll send to
3247        let supports_delta = sh.capabilities.semantic_tokens_full_delta;
3248        let use_delta = previous_result_id.is_some() && supports_delta;
3249        let handle = &mut sh.handle;
3250
3251        let request_id = {
3252            let id = *__next_id;
3253            *__next_id += 1;
3254            id
3255        };
3256
3257        let request_kind = if use_delta {
3258            super::SemanticTokensFullRequestKind::FullDelta
3259        } else {
3260            super::SemanticTokensFullRequestKind::Full
3261        };
3262
3263        let request_result = if use_delta {
3264            handle.semantic_tokens_full_delta(
3265                request_id,
3266                uri.as_uri().clone(),
3267                previous_result_id.unwrap(),
3268            )
3269        } else {
3270            handle.semantic_tokens_full(request_id, uri.as_uri().clone())
3271        };
3272
3273        match request_result {
3274            Ok(_) => {
3275                __pending_st.insert(
3276                    request_id,
3277                    super::SemanticTokenFullRequest {
3278                        buffer_id,
3279                        version: buffer_version,
3280                        kind: request_kind,
3281                    },
3282                );
3283                __st_in_flight.insert(buffer_id, (request_id, buffer_version, request_kind));
3284            }
3285            Err(e) => {
3286                tracing::debug!("Failed to request semantic tokens: {}", e);
3287            }
3288        }
3289    }
3290
3291    /// Issue a debounced full semantic token request if the timer has elapsed.
3292    pub(crate) fn maybe_request_semantic_tokens_full_debounced(&mut self, buffer_id: BufferId) {
3293        if !self.config.editor.enable_semantic_tokens_full {
3294            self.active_window_mut()
3295                .semantic_tokens_full_debounce
3296                .remove(&buffer_id);
3297            return;
3298        }
3299
3300        let Some(ready_at) = self
3301            .active_window()
3302            .semantic_tokens_full_debounce
3303            .get(&buffer_id)
3304            .copied()
3305        else {
3306            return;
3307        };
3308        if Instant::now() < ready_at {
3309            return;
3310        }
3311
3312        self.active_window_mut()
3313            .semantic_tokens_full_debounce
3314            .remove(&buffer_id);
3315        self.maybe_request_semantic_tokens(buffer_id);
3316    }
3317
3318    /// Request semantic tokens for a viewport range (with padding).
3319    pub(crate) fn maybe_request_semantic_tokens_range(
3320        &mut self,
3321        buffer_id: BufferId,
3322        start_line: usize,
3323        end_line: usize,
3324    ) {
3325        let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
3326            return;
3327        };
3328        if !metadata.lsp_enabled {
3329            return;
3330        }
3331        let Some(uri) = metadata.file_uri().cloned() else {
3332            return;
3333        };
3334        let file_path = metadata.file_path().cloned();
3335        // Get language from buffer state
3336        let Some(language) = self
3337            .windows
3338            .get(&self.active_window)
3339            .map(|w| &w.buffers)
3340            .expect("active window present")
3341            .get(&buffer_id)
3342            .map(|s| s.language.clone())
3343        else {
3344            return;
3345        };
3346
3347        let __active_id = self.active_window;
3348        // Single &mut on the active window — split-borrow into &mut lsp
3349        // and &buffers so we can use both concurrently.
3350        let __win = self
3351            .windows
3352            .get_mut(&__active_id)
3353            .expect("active window must exist");
3354        let __next_id = &mut __win.next_lsp_request_id;
3355        let __pending_st_range = &mut __win.pending_semantic_token_range_requests;
3356        let __st_range_in_flight = &mut __win.semantic_tokens_range_in_flight;
3357        let __st_range_last = &mut __win.semantic_tokens_range_last_request;
3358        let __st_range_applied = &__win.semantic_tokens_range_applied;
3359        let lsp = &mut __win.lsp;
3360        let __buffers_ref: &crate::app::window::WindowBuffers = &__win.buffers;
3361
3362        // Ensure there is a running server
3363        use crate::services::lsp::manager::LspSpawnResult;
3364        if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3365            return;
3366        }
3367
3368        if !lsp.semantic_tokens_range_supported(&language) {
3369            // Fall back to full document tokens if no server supports range.
3370            self.maybe_request_semantic_tokens(buffer_id);
3371            return;
3372        }
3373        if lsp.semantic_tokens_legend(&language).is_none() {
3374            return;
3375        }
3376
3377        let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::SemanticTokens) else {
3378            return;
3379        };
3380        // The handle_for_feature_mut check ensures has_capability(SemanticTokens) which is
3381        // full || range. Double-check this specific server supports range.
3382        if !sh.capabilities.semantic_tokens_range {
3383            return;
3384        }
3385        let handle = &mut sh.handle;
3386        let Some(state) = __buffers_ref.get(&buffer_id) else {
3387            return;
3388        };
3389
3390        let buffer_version = state.buffer.version();
3391        let mut padded_start = start_line.saturating_sub(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3392        let mut padded_end = end_line.saturating_add(SEMANTIC_TOKENS_RANGE_PADDING_LINES);
3393
3394        if let Some(line_count) = state.buffer.line_count() {
3395            if line_count == 0 {
3396                return;
3397            }
3398            let max_line = line_count.saturating_sub(1);
3399            padded_start = padded_start.min(max_line);
3400            padded_end = padded_end.min(max_line);
3401        }
3402
3403        let start_byte = state.buffer.line_start_offset(padded_start).unwrap_or(0);
3404        let end_char = state
3405            .buffer
3406            .get_line(padded_end)
3407            .map(|line| String::from_utf8_lossy(&line).encode_utf16().count())
3408            .unwrap_or(0);
3409        let end_byte = if state.buffer.line_start_offset(padded_end).is_some() {
3410            state.buffer.lsp_position_to_byte(padded_end, end_char)
3411        } else {
3412            state.buffer.len()
3413        };
3414
3415        if start_byte >= end_byte {
3416            return;
3417        }
3418
3419        let range = start_byte..end_byte;
3420        if let Some((in_flight_id, in_flight_start, in_flight_end, in_flight_version)) =
3421            __st_range_in_flight.get(&buffer_id).copied()
3422        {
3423            if in_flight_start == padded_start
3424                && in_flight_end == padded_end
3425                && in_flight_version == buffer_version
3426            {
3427                return;
3428            }
3429            if let Err(e) = handle.cancel_request(in_flight_id) {
3430                tracing::debug!("Failed to cancel semantic token range request: {}", e);
3431            }
3432            __pending_st_range.remove(&in_flight_id);
3433            __st_range_in_flight.remove(&buffer_id);
3434        }
3435
3436        if let Some((applied_start, applied_end, applied_version)) =
3437            __st_range_applied.get(&buffer_id).copied()
3438        {
3439            if applied_start == padded_start
3440                && applied_end == padded_end
3441                && applied_version == buffer_version
3442            {
3443                return;
3444            }
3445        }
3446
3447        let now = Instant::now();
3448        if let Some((last_start, last_end, last_version, last_time)) =
3449            __st_range_last.get(&buffer_id).copied()
3450        {
3451            if last_start == padded_start
3452                && last_end == padded_end
3453                && last_version == buffer_version
3454                && now.duration_since(last_time)
3455                    < Duration::from_millis(SEMANTIC_TOKENS_RANGE_DEBOUNCE_MS)
3456            {
3457                return;
3458            }
3459        }
3460
3461        let lsp_range = lsp_types::Range {
3462            start: lsp_types::Position {
3463                line: padded_start as u32,
3464                character: 0,
3465            },
3466            end: lsp_types::Position {
3467                line: padded_end as u32,
3468                character: end_char as u32,
3469            },
3470        };
3471
3472        let request_id = {
3473            let id = *__next_id;
3474            *__next_id += 1;
3475            id
3476        };
3477        let _ = __st_range_applied;
3478
3479        match handle.semantic_tokens_range(request_id, uri.as_uri().clone(), lsp_range) {
3480            Ok(_) => {
3481                __pending_st_range.insert(
3482                    request_id,
3483                    SemanticTokenRangeRequest {
3484                        buffer_id,
3485                        version: buffer_version,
3486                        range: range.clone(),
3487                        start_line: padded_start,
3488                        end_line: padded_end,
3489                    },
3490                );
3491                __st_range_in_flight.insert(
3492                    buffer_id,
3493                    (request_id, padded_start, padded_end, buffer_version),
3494                );
3495                __st_range_last.insert(buffer_id, (padded_start, padded_end, buffer_version, now));
3496            }
3497            Err(e) => {
3498                tracing::debug!("Failed to request semantic token range: {}", e);
3499            }
3500        }
3501    }
3502}
3503
3504#[cfg(test)]
3505mod tests {
3506    use crate::model::filesystem::StdFileSystem;
3507    use std::sync::Arc;
3508
3509    fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
3510        Arc::new(StdFileSystem)
3511    }
3512    use super::{lsp_range_contains, lsp_range_overlaps, Editor};
3513
3514    fn range(sl: u32, sc: u32, el: u32, ec: u32) -> lsp_types::Range {
3515        lsp_types::Range {
3516            start: lsp_types::Position {
3517                line: sl,
3518                character: sc,
3519            },
3520            end: lsp_types::Position {
3521                line: el,
3522                character: ec,
3523            },
3524        }
3525    }
3526
3527    #[test]
3528    fn test_lsp_range_contains_inclusive_start_exclusive_end() {
3529        let r = range(3, 10, 3, 20);
3530        // Before start
3531        assert!(!lsp_range_contains(&r, 3, 9));
3532        assert!(!lsp_range_contains(&r, 2, 50));
3533        // At start (inclusive)
3534        assert!(lsp_range_contains(&r, 3, 10));
3535        // Inside
3536        assert!(lsp_range_contains(&r, 3, 15));
3537        // Just before end (inclusive)
3538        assert!(lsp_range_contains(&r, 3, 19));
3539        // At end (exclusive)
3540        assert!(!lsp_range_contains(&r, 3, 20));
3541        // After end
3542        assert!(!lsp_range_contains(&r, 3, 21));
3543        assert!(!lsp_range_contains(&r, 4, 0));
3544    }
3545
3546    #[test]
3547    fn test_lsp_range_contains_multiline() {
3548        let r = range(2, 5, 4, 3);
3549        // Line before start
3550        assert!(!lsp_range_contains(&r, 1, 100));
3551        // On start line, before start character
3552        assert!(!lsp_range_contains(&r, 2, 4));
3553        // On start line, at start character (inclusive)
3554        assert!(lsp_range_contains(&r, 2, 5));
3555        // Interior line — any character is inside.
3556        assert!(lsp_range_contains(&r, 3, 0));
3557        assert!(lsp_range_contains(&r, 3, 9999));
3558        // End line, before end character (inclusive)
3559        assert!(lsp_range_contains(&r, 4, 2));
3560        // End line, at end character (exclusive)
3561        assert!(!lsp_range_contains(&r, 4, 3));
3562        // Line after end
3563        assert!(!lsp_range_contains(&r, 5, 0));
3564    }
3565
3566    #[test]
3567    fn test_lsp_range_contains_zero_length_matches_anchor_only() {
3568        // Point diagnostic: start == end.
3569        let r = range(7, 4, 7, 4);
3570        assert!(lsp_range_contains(&r, 7, 4));
3571        assert!(!lsp_range_contains(&r, 7, 3));
3572        assert!(!lsp_range_contains(&r, 7, 5));
3573        assert!(!lsp_range_contains(&r, 6, 4));
3574        assert!(!lsp_range_contains(&r, 8, 4));
3575    }
3576
3577    #[test]
3578    fn test_lsp_range_overlaps_point_cursor_in_diagnostic_range() {
3579        // Diagnostic at line 3, cols 10..20. A zero-width cursor anywhere
3580        // inside (including at start) overlaps; just outside does not.
3581        let diag = range(3, 10, 3, 20);
3582        // Cursor on the start anchor.
3583        assert!(lsp_range_overlaps(&diag, 3, 10, 3, 10));
3584        // Cursor inside.
3585        assert!(lsp_range_overlaps(&diag, 3, 15, 3, 15));
3586        // Cursor at exclusive end — not contained.
3587        assert!(!lsp_range_overlaps(&diag, 3, 20, 3, 20));
3588        // Cursor before start.
3589        assert!(!lsp_range_overlaps(&diag, 3, 9, 3, 9));
3590        // Cursor on a different line.
3591        assert!(!lsp_range_overlaps(&diag, 4, 15, 4, 15));
3592    }
3593
3594    #[test]
3595    fn test_lsp_range_overlaps_selection_intersects_diagnostic() {
3596        // Diagnostic at line 3, cols 10..20.
3597        let diag = range(3, 10, 3, 20);
3598        // Selection entirely inside diag.
3599        assert!(lsp_range_overlaps(&diag, 3, 12, 3, 18));
3600        // Selection straddling the start of diag.
3601        assert!(lsp_range_overlaps(&diag, 3, 5, 3, 15));
3602        // Selection straddling the end of diag.
3603        assert!(lsp_range_overlaps(&diag, 3, 15, 3, 25));
3604        // Selection entirely covering diag.
3605        assert!(lsp_range_overlaps(&diag, 3, 0, 3, 30));
3606        // Selection adjacent before (touches at start, half-open end).
3607        assert!(!lsp_range_overlaps(&diag, 3, 0, 3, 10));
3608        // Selection adjacent after (starts at exclusive end of diag).
3609        assert!(!lsp_range_overlaps(&diag, 3, 20, 3, 30));
3610        // Selection on the wrong line.
3611        assert!(!lsp_range_overlaps(&diag, 4, 0, 4, 100));
3612    }
3613
3614    #[test]
3615    fn test_lsp_range_overlaps_point_diagnostic_within_selection() {
3616        // Point-style diagnostic at line 3, col 10.
3617        let diag = range(3, 10, 3, 10);
3618        // Selection covering the point.
3619        assert!(lsp_range_overlaps(&diag, 3, 5, 3, 15));
3620        // Selection starting at the point (half-open includes start).
3621        assert!(lsp_range_overlaps(&diag, 3, 10, 3, 15));
3622        // Selection ending at the point (half-open excludes end).
3623        assert!(!lsp_range_overlaps(&diag, 3, 0, 3, 10));
3624        // Zero-width cursor on the exact anchor.
3625        assert!(lsp_range_overlaps(&diag, 3, 10, 3, 10));
3626        // Zero-width cursor not on the anchor.
3627        assert!(!lsp_range_overlaps(&diag, 3, 9, 3, 9));
3628    }
3629
3630    #[test]
3631    fn test_lsp_range_overlaps_multiline() {
3632        // Diagnostic spans line 2 col 5 through line 4 col 3.
3633        let diag = range(2, 5, 4, 3);
3634        // Cursor on an interior line, anywhere.
3635        assert!(lsp_range_overlaps(&diag, 3, 0, 3, 0));
3636        // Selection crossing line boundary into the diagnostic.
3637        assert!(lsp_range_overlaps(&diag, 1, 0, 2, 6));
3638        // Selection that starts at the exclusive end of the diagnostic — no overlap.
3639        assert!(!lsp_range_overlaps(&diag, 4, 3, 4, 10));
3640        // Selection entirely after the diagnostic.
3641        assert!(!lsp_range_overlaps(&diag, 5, 0, 5, 10));
3642    }
3643
3644    use crate::model::buffer::Buffer;
3645    use crate::state::EditorState;
3646    use crate::view::virtual_text::VirtualTextPosition;
3647    use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position};
3648
3649    fn make_hint(line: u32, character: u32, label: &str, kind: Option<InlayHintKind>) -> InlayHint {
3650        InlayHint {
3651            position: Position { line, character },
3652            label: InlayHintLabel::String(label.to_string()),
3653            kind,
3654            text_edits: None,
3655            tooltip: None,
3656            padding_left: None,
3657            padding_right: None,
3658            data: None,
3659        }
3660    }
3661
3662    #[test]
3663    fn test_inlay_hint_inserts_before_character() {
3664        let mut state = EditorState::new(
3665            80,
3666            24,
3667            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3668            test_fs(),
3669        );
3670        state.buffer = Buffer::from_str_test("ab");
3671
3672        if !state.buffer.is_empty() {
3673            state.marker_list.adjust_for_insert(0, state.buffer.len());
3674        }
3675
3676        let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3677        Editor::apply_inlay_hints_to_state(&mut state, &hints);
3678
3679        let lookup = state
3680            .virtual_texts
3681            .build_lookup(&state.marker_list, 0, state.buffer.len());
3682        let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3683        assert_eq!(vtexts.len(), 1);
3684        assert_eq!(vtexts[0].text, ": i32");
3685        assert_eq!(vtexts[0].position, VirtualTextPosition::BeforeChar);
3686    }
3687
3688    #[test]
3689    fn test_inlay_hint_at_eof_renders_after_last_char() {
3690        let mut state = EditorState::new(
3691            80,
3692            24,
3693            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3694            test_fs(),
3695        );
3696        state.buffer = Buffer::from_str_test("ab");
3697
3698        if !state.buffer.is_empty() {
3699            state.marker_list.adjust_for_insert(0, state.buffer.len());
3700        }
3701
3702        let hints = vec![make_hint(0, 2, ": i32", Some(InlayHintKind::TYPE))];
3703        Editor::apply_inlay_hints_to_state(&mut state, &hints);
3704
3705        let lookup = state
3706            .virtual_texts
3707            .build_lookup(&state.marker_list, 0, state.buffer.len());
3708        let vtexts = lookup.get(&1).expect("expected hint anchored to last byte");
3709        assert_eq!(vtexts.len(), 1);
3710        assert_eq!(vtexts[0].text, ": i32");
3711        assert_eq!(vtexts[0].position, VirtualTextPosition::AfterChar);
3712    }
3713
3714    #[test]
3715    fn test_inlay_hint_empty_buffer_is_ignored() {
3716        let mut state = EditorState::new(
3717            80,
3718            24,
3719            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3720            test_fs(),
3721        );
3722        state.buffer = Buffer::from_str_test("");
3723
3724        let hints = vec![make_hint(0, 0, ": i32", Some(InlayHintKind::TYPE))];
3725        Editor::apply_inlay_hints_to_state(&mut state, &hints);
3726
3727        assert!(state.virtual_texts.is_empty());
3728    }
3729
3730    #[test]
3731    fn test_inlay_hint_uses_theme_key_for_foreground() {
3732        // Verify that apply_inlay_hints_to_state stores the theme key so
3733        // hints follow the active theme rather than a hardcoded color.
3734        let mut state = EditorState::new(
3735            80,
3736            24,
3737            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3738            test_fs(),
3739        );
3740        state.buffer = Buffer::from_str_test("ab");
3741
3742        if !state.buffer.is_empty() {
3743            state.marker_list.adjust_for_insert(0, state.buffer.len());
3744        }
3745
3746        let hints = vec![make_hint(0, 1, ": i32", Some(InlayHintKind::TYPE))];
3747        Editor::apply_inlay_hints_to_state(&mut state, &hints);
3748
3749        let lookup = state
3750            .virtual_texts
3751            .build_lookup(&state.marker_list, 0, state.buffer.len());
3752        let vtexts = lookup.get(&1).expect("expected hint at byte offset 1");
3753        assert_eq!(
3754            vtexts[0].fg_theme_key.as_deref(),
3755            Some("editor.line_number_fg")
3756        );
3757        assert_eq!(vtexts[0].bg_theme_key, None);
3758    }
3759
3760    #[test]
3761    fn test_inlay_hint_removed_when_its_range_is_deleted() {
3762        // Regression: deleting a range that covers the anchor byte of an
3763        // inlay hint used to leave the hint visible (the marker snapped to
3764        // the deletion start). apply_delete now calls
3765        // virtual_texts.remove_in_range before adjusting markers, so the
3766        // hint vanishes immediately. A future LSP refresh can repopulate
3767        // hints elsewhere.
3768        let mut state = EditorState::new(
3769            80,
3770            24,
3771            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3772            test_fs(),
3773        );
3774        state.buffer = Buffer::from_str_test("let x = 42;");
3775        state.marker_list.adjust_for_insert(0, state.buffer.len());
3776
3777        // Hint anchored at byte 5 (after "let x" -> rendered before '=').
3778        let hints = vec![make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE))];
3779        Editor::apply_inlay_hints_to_state(&mut state, &hints);
3780        assert_eq!(state.virtual_texts.len(), 1);
3781
3782        // Simulate user deleting "x = 42" (bytes 4..10, half-open) — the
3783        // hint anchor at byte 5 is inside this range.
3784        let removed = state
3785            .virtual_texts
3786            .remove_in_range(&mut state.marker_list, 4, 10);
3787        assert_eq!(removed, 1, "hint inside deleted range must be removed");
3788        assert!(state.virtual_texts.is_empty());
3789    }
3790
3791    #[test]
3792    fn test_marker_delete_after_repeat_clear_recreate() {
3793        // Regression: simulates what apply_inlay_hints_to_state does on
3794        // every LSP refresh — clear every virtual_text's marker then
3795        // recreate markers at fresh positions. After a few rounds,
3796        // delete one marker and adjust for a deletion and check the
3797        // remaining markers' positions.
3798        use crate::model::marker::MarkerList;
3799        use crate::view::virtual_text::{VirtualTextManager, VirtualTextPosition};
3800        use ratatui::style::Style;
3801
3802        let mut markers = MarkerList::new();
3803        let mut vtexts = VirtualTextManager::new();
3804
3805        // Initial marker layout at six positions (same as the e2e test).
3806        let positions = [200usize, 401, 602, 803, 1205, 1406];
3807        for &p in &positions {
3808            vtexts.add(
3809                &mut markers,
3810                p,
3811                format!("hint-at-{p}"),
3812                Style::default(),
3813                VirtualTextPosition::BeforeChar,
3814                0,
3815            );
3816        }
3817
3818        // Simulate a couple of clear/recreate cycles (each LSP refresh
3819        // goes through this exact path via apply_inlay_hints_to_state).
3820        for _ in 0..3 {
3821            vtexts.clear(&mut markers);
3822            for &p in &positions {
3823                vtexts.add(
3824                    &mut markers,
3825                    p,
3826                    format!("hint-at-{p}"),
3827                    Style::default(),
3828                    VirtualTextPosition::BeforeChar,
3829                    0,
3830                );
3831            }
3832        }
3833
3834        // remove_in_range + adjust_for_delete equivalent to apply_delete.
3835        let removed = vtexts.remove_in_range(&mut markers, 1005, 1206);
3836        assert_eq!(
3837            removed, 1,
3838            "exactly one marker inside [1005, 1206) should be removed"
3839        );
3840        markers.adjust_for_delete(1005, 201);
3841
3842        let lookup = vtexts.build_lookup(&markers, 0, 10_000);
3843        let mut positions: Vec<usize> = lookup.keys().copied().collect();
3844        positions.sort();
3845        assert_eq!(
3846            positions,
3847            vec![200, 401, 602, 803, 1205],
3848            "after delete+adjust, expected marker byte positions {:?}, got {:?}",
3849            vec![200, 401, 602, 803, 1205],
3850            positions
3851        );
3852    }
3853
3854    #[test]
3855    fn test_marker_delete_then_adjust_preserves_last_marker_position() {
3856        // Regression for the user-observed flip of an end-of-line inlay
3857        // hint to the start of its line after a nearby line is deleted.
3858        //
3859        // Scenario (real numbers from the failing e2e test): six markers
3860        // at byte offsets that correspond to the `\n` of each line,
3861        // then delete-one-marker (simulating remove_in_range on the
3862        // line being deleted) followed by adjust_for_delete on the
3863        // remaining markers.
3864        //
3865        // The last marker (at byte 1406) should end up at byte 1205
3866        // after subtracting the 201-byte deleted range. Observed bug:
3867        // it ends up at byte 1005 (the deletion start) — exactly as
3868        // though the delta were applied twice.
3869        use crate::model::marker::MarkerList;
3870
3871        let mut markers = MarkerList::new();
3872        let m0 = markers.create(200, false);
3873        let m1 = markers.create(401, false);
3874        let m2 = markers.create(602, false);
3875        let m3 = markers.create(803, false);
3876        let m5 = markers.create(1205, false);
3877        let m6 = markers.create(1406, false);
3878
3879        // Simulate remove_in_range removing marker m5 inside [1005, 1206).
3880        markers.delete(m5);
3881
3882        // Now simulate adjust_for_delete over that range.
3883        markers.adjust_for_delete(1005, 201);
3884
3885        assert_eq!(markers.get_position(m0), Some(200), "m0 unchanged");
3886        assert_eq!(markers.get_position(m1), Some(401), "m1 unchanged");
3887        assert_eq!(markers.get_position(m2), Some(602), "m2 unchanged");
3888        assert_eq!(markers.get_position(m3), Some(803), "m3 unchanged");
3889        assert_eq!(
3890            markers.get_position(m6),
3891            Some(1205),
3892            "m6 must shift from 1406 to 1205 (1406 - 201), not be clamped to delete-start 1005"
3893        );
3894    }
3895
3896    #[test]
3897    fn test_inlay_hint_outside_deletion_survives() {
3898        // Anchors outside the deleted range must not be collateral damage.
3899        let mut state = EditorState::new(
3900            80,
3901            24,
3902            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
3903            test_fs(),
3904        );
3905        state.buffer = Buffer::from_str_test("let x = 42; let y = 0;");
3906        state.marker_list.adjust_for_insert(0, state.buffer.len());
3907
3908        let hints = vec![
3909            make_hint(0, 5, ": i32", Some(InlayHintKind::TYPE)), // byte 5 - inside deletion
3910            make_hint(0, 17, ": i32", Some(InlayHintKind::TYPE)), // byte 17 - outside
3911        ];
3912        Editor::apply_inlay_hints_to_state(&mut state, &hints);
3913        assert_eq!(state.virtual_texts.len(), 2);
3914
3915        let removed = state
3916            .virtual_texts
3917            .remove_in_range(&mut state.marker_list, 4, 10);
3918        assert_eq!(removed, 1);
3919        assert_eq!(state.virtual_texts.len(), 1);
3920    }
3921
3922    #[test]
3923    fn test_space_doc_paragraphs_inserts_blank_lines() {
3924        use super::space_doc_paragraphs;
3925
3926        // Single newlines become double newlines
3927        let input = "sep\n  description.\nend\n  another.";
3928        let result = space_doc_paragraphs(input);
3929        assert_eq!(result, "sep\n\n  description.\n\nend\n\n  another.");
3930    }
3931
3932    #[test]
3933    fn test_space_doc_paragraphs_preserves_existing_blank_lines() {
3934        use super::space_doc_paragraphs;
3935
3936        // Already-double newlines stay double (not quadrupled)
3937        let input = "First paragraph.\n\nSecond paragraph.";
3938        let result = space_doc_paragraphs(input);
3939        assert_eq!(result, "First paragraph.\n\nSecond paragraph.");
3940    }
3941
3942    #[test]
3943    fn test_space_doc_paragraphs_plain_text() {
3944        use super::space_doc_paragraphs;
3945
3946        let input = "Just a single line of docs.";
3947        let result = space_doc_paragraphs(input);
3948        assert_eq!(result, "Just a single line of docs.");
3949    }
3950}