Skip to main content

fresh/app/
lsp_requests.rs

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