Skip to main content

fresh/app/
popup_overlay_actions.rs

1//! Popup, overlay, and LSP-confirmation orchestrators on `Editor`.
2//!
3//! Three loosely-related clusters that all manipulate the active buffer's
4//! popup stack and overlay list via Event dispatch:
5//!
6//!   - Overlay management (add_overlay, remove_overlay,
7//!     remove_overlays_in_range, clear_overlays)
8//!   - Popup lifecycle (show_popup, hide_popup, dismiss_transient_popups,
9//!     scroll_popup, on_editor_focus_lost, clear_popups, popup nav)
10//!   - LSP confirmation popup (show_lsp_confirmation_popup,
11//!     handle_lsp_confirmation_response, notify_lsp_current_file_opened,
12//!     has_pending_lsp_confirmation)
13
14use std::ops::Range;
15
16use rust_i18n::t;
17
18use crate::model::event::Event;
19
20use super::Editor;
21
22impl Editor {
23    // === Overlay Management (Event-Driven) ===
24
25    /// Add an overlay for decorations (underlines, highlights, etc.)
26    pub fn add_overlay(
27        &mut self,
28        namespace: Option<crate::view::overlay::OverlayNamespace>,
29        range: Range<usize>,
30        face: crate::model::event::OverlayFace,
31        priority: i32,
32        message: Option<String>,
33    ) -> crate::view::overlay::OverlayHandle {
34        let event = Event::AddOverlay {
35            namespace,
36            range,
37            face,
38            priority,
39            message,
40            extend_to_line_end: false,
41            url: None,
42        };
43        self.apply_event_to_active_buffer(&event);
44        // Return the handle of the last added overlay
45        let state = self.active_state();
46        state
47            .overlays
48            .all()
49            .last()
50            .map(|o| o.handle.clone())
51            .unwrap_or_default()
52    }
53
54    /// Remove an overlay by handle
55    pub fn remove_overlay(&mut self, handle: crate::view::overlay::OverlayHandle) {
56        let event = Event::RemoveOverlay { handle };
57        self.apply_event_to_active_buffer(&event);
58    }
59
60    /// Remove all overlays in a range
61    pub fn remove_overlays_in_range(&mut self, range: Range<usize>) {
62        let event = Event::RemoveOverlaysInRange { range };
63        self.active_event_log_mut().append(event.clone());
64        self.apply_event_to_active_buffer(&event);
65    }
66
67    /// Clear all overlays
68    pub fn clear_overlays(&mut self) {
69        let event = Event::ClearOverlays;
70        self.active_event_log_mut().append(event.clone());
71        self.apply_event_to_active_buffer(&event);
72    }
73
74    // === Popup Management (Event-Driven) ===
75
76    /// Show a popup window
77    pub fn show_popup(&mut self, popup: crate::model::event::PopupData) {
78        let event = Event::ShowPopup { popup };
79        self.active_event_log_mut().append(event.clone());
80        self.apply_event_to_active_buffer(&event);
81    }
82
83    /// Hide the topmost popup
84    pub fn hide_popup(&mut self) {
85        let event = Event::HidePopup;
86        self.active_event_log_mut().append(event.clone());
87        self.apply_event_to_active_buffer(&event);
88
89        // Complete --wait tracking if this buffer had a popup-based wait
90        let active = self.active_buffer();
91        if let Some((wait_id, true)) = self.wait_tracking.remove(&active) {
92            self.completed_waits.push(wait_id);
93        }
94
95        // Clear hover symbol highlight if present
96        if let Some(handle) = self.hover.take_symbol_overlay() {
97            let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
98            self.apply_event_to_active_buffer(&remove_overlay_event);
99        }
100        self.hover.set_symbol_range(None);
101    }
102
103    /// Dismiss transient popups if present
104    /// These popups should be dismissed on scroll or other user actions
105    pub(super) fn dismiss_transient_popups(&mut self) {
106        let is_transient_popup = self
107            .active_state()
108            .popups
109            .top()
110            .is_some_and(|p| p.transient);
111
112        if is_transient_popup {
113            self.hide_popup();
114            tracing::trace!("Dismissed transient popup");
115        }
116    }
117
118    /// Scroll any popup content by delta lines
119    /// Positive delta scrolls down, negative scrolls up
120    pub(super) fn scroll_popup(&mut self, delta: i32) {
121        if let Some(popup) = self.active_state_mut().popups.top_mut() {
122            popup.scroll_by(delta);
123            tracing::debug!(
124                "Scrolled popup by {}, new offset: {}",
125                delta,
126                popup.scroll_offset
127            );
128        }
129    }
130
131    /// Called when the editor buffer loses focus (e.g., switching buffers,
132    /// opening prompts/menus, focusing file explorer, etc.)
133    ///
134    /// This is the central handler for focus loss that:
135    /// - Dismisses transient popups (Hover, Signature Help)
136    /// - Clears LSP hover state and pending requests
137    /// - Removes hover symbol highlighting
138    pub(super) fn on_editor_focus_lost(&mut self) {
139        // Dismiss transient popups via EditorState
140        self.active_state_mut().on_focus_lost();
141
142        // Clear hover state
143        self.mouse_state.lsp_hover_state = None;
144        self.mouse_state.lsp_hover_request_sent = false;
145        self.hover.clear_pending();
146
147        // Clear hover symbol highlight if present
148        if let Some(handle) = self.hover.take_symbol_overlay() {
149            let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
150            self.apply_event_to_active_buffer(&remove_overlay_event);
151        }
152        self.hover.set_symbol_range(None);
153    }
154
155    /// Clear all popups
156    pub fn clear_popups(&mut self) {
157        let event = Event::ClearPopups;
158        self.active_event_log_mut().append(event.clone());
159        self.apply_event_to_active_buffer(&event);
160    }
161
162    // === LSP Confirmation Popup ===
163
164    /// Show the LSP confirmation popup for a language server
165    ///
166    /// This displays a centered popup asking the user to confirm whether
167    /// they want to start the LSP server for the given language.
168    pub fn show_lsp_confirmation_popup(&mut self, language: &str) {
169        use crate::model::event::{
170            PopupContentData, PopupData, PopupKindHint, PopupListItemData, PopupPositionData,
171        };
172
173        // Store the pending confirmation
174        self.pending_lsp_confirmation = Some(language.to_string());
175
176        // Get the server command for display
177        let server_info = if let Some(lsp) = &self.lsp {
178            if let Some(config) = lsp.get_config(language) {
179                if !config.command.is_empty() {
180                    format!("{} ({})", language, config.command)
181                } else {
182                    language.to_string()
183                }
184            } else {
185                language.to_string()
186            }
187        } else {
188            language.to_string()
189        };
190
191        let popup = PopupData {
192            kind: PopupKindHint::List,
193            title: Some(format!("Start LSP Server: {}?", server_info)),
194            description: None,
195            transient: false,
196            content: PopupContentData::List {
197                items: vec![
198                    PopupListItemData {
199                        text: "Allow this time".to_string(),
200                        detail: Some("Start the LSP server for this session".to_string()),
201                        icon: None,
202                        data: Some("allow_once".to_string()),
203                    },
204                    PopupListItemData {
205                        text: "Always allow".to_string(),
206                        detail: Some("Always start this LSP server automatically".to_string()),
207                        icon: None,
208                        data: Some("allow_always".to_string()),
209                    },
210                    PopupListItemData {
211                        text: "Don't start".to_string(),
212                        detail: Some("Cancel LSP server startup".to_string()),
213                        icon: None,
214                        data: Some("deny".to_string()),
215                    },
216                ],
217                selected: 0,
218            },
219            position: PopupPositionData::Centered,
220            width: 50,
221            max_height: 8,
222            bordered: true,
223        };
224
225        self.show_popup(popup);
226    }
227
228    /// Handle the LSP confirmation popup response
229    ///
230    /// This is called when the user confirms their selection in the LSP
231    /// confirmation popup. It processes the response and starts the LSP
232    /// server if approved.
233    ///
234    /// Returns true if a response was handled, false if there was no pending confirmation.
235    pub fn handle_lsp_confirmation_response(&mut self, action: &str) -> bool {
236        let Some(language) = self.pending_lsp_confirmation.take() else {
237            return false;
238        };
239
240        // Get file path from active buffer for workspace root detection
241        let file_path = self
242            .buffer_metadata
243            .get(&self.active_buffer())
244            .and_then(|meta| meta.file_path().cloned());
245
246        match action {
247            "allow_once" => {
248                // Spawn the LSP server just this once (don't add to always-allowed)
249                if let Some(lsp) = &mut self.lsp {
250                    // Temporarily allow this language for spawning
251                    lsp.allow_language(&language);
252                    // Use force_spawn since user explicitly confirmed
253                    if lsp.force_spawn(&language, file_path.as_deref()).is_some() {
254                        tracing::info!("LSP server for {} started (allowed once)", language);
255                        self.set_status_message(
256                            t!("lsp.server_started", language = language).to_string(),
257                        );
258                    } else {
259                        self.set_status_message(
260                            t!("lsp.failed_to_start", language = language).to_string(),
261                        );
262                    }
263                }
264                // Notify LSP about the current file
265                self.notify_lsp_current_file_opened(&language);
266            }
267            "allow_always" => {
268                // Spawn the LSP server and remember the preference
269                if let Some(lsp) = &mut self.lsp {
270                    lsp.allow_language(&language);
271                    // Use force_spawn since user explicitly confirmed
272                    if lsp.force_spawn(&language, file_path.as_deref()).is_some() {
273                        tracing::info!("LSP server for {} started (always allowed)", language);
274                        self.set_status_message(
275                            t!("lsp.server_started_auto", language = language).to_string(),
276                        );
277                    } else {
278                        self.set_status_message(
279                            t!("lsp.failed_to_start", language = language).to_string(),
280                        );
281                    }
282                }
283                // Notify LSP about the current file
284                self.notify_lsp_current_file_opened(&language);
285            }
286            _ => {
287                // User declined - don't start the server
288                tracing::info!("LSP server for {} startup declined by user", language);
289                self.set_status_message(
290                    t!("lsp.startup_cancelled", language = language).to_string(),
291                );
292            }
293        }
294
295        true
296    }
297
298    /// Notify LSP about the currently open file
299    ///
300    /// This is called after an LSP server is started to notify it about
301    /// the current file so it can provide features like diagnostics.
302    fn notify_lsp_current_file_opened(&mut self, language: &str) {
303        // Get buffer metadata for the active buffer
304        let metadata = match self.buffer_metadata.get(&self.active_buffer()) {
305            Some(m) => m,
306            None => {
307                tracing::debug!(
308                    "notify_lsp_current_file_opened: no metadata for buffer {:?}",
309                    self.active_buffer()
310                );
311                return;
312            }
313        };
314
315        if !metadata.lsp_enabled {
316            tracing::debug!("notify_lsp_current_file_opened: LSP disabled for this buffer");
317            return;
318        }
319
320        // Get file path for LSP spawn
321        let file_path = metadata.file_path().cloned();
322
323        // Get the URI (computed once in with_file)
324        let uri = match metadata.file_uri() {
325            Some(u) => u.clone(),
326            None => {
327                tracing::debug!(
328                    "notify_lsp_current_file_opened: no URI for buffer (not a file or URI creation failed)"
329                );
330                return;
331            }
332        };
333
334        // Get the buffer text and line count before borrowing lsp
335        let active_buffer = self.active_buffer();
336
337        // Use buffer's stored language to verify it matches the LSP server
338        let file_language = match self.buffers.get(&active_buffer).map(|s| s.language.clone()) {
339            Some(l) => l,
340            None => {
341                tracing::debug!("notify_lsp_current_file_opened: no buffer state");
342                return;
343            }
344        };
345
346        // Only notify if the file's language matches the LSP server we just started
347        if file_language != language {
348            tracing::debug!(
349                "notify_lsp_current_file_opened: file language {} doesn't match server {}",
350                file_language,
351                language
352            );
353            return;
354        }
355        let (text, line_count, buffer_version) =
356            if let Some(state) = self.buffers.get(&active_buffer) {
357                let text = match state.buffer.to_string() {
358                    Some(t) => t,
359                    None => {
360                        tracing::debug!("notify_lsp_current_file_opened: buffer not fully loaded");
361                        return;
362                    }
363                };
364                let line_count = state.buffer.line_count().unwrap_or(1000);
365                (text, line_count, state.buffer.version())
366            } else {
367                tracing::debug!("notify_lsp_current_file_opened: no buffer state");
368                return;
369            };
370
371        // Send didOpen to all LSP handles (use force_spawn to ensure they're started)
372        if let Some(lsp) = &mut self.lsp {
373            // force_spawn starts all servers for this language
374            if lsp.force_spawn(language, file_path.as_deref()).is_some() {
375                tracing::info!("Sending didOpen to LSP servers for: {}", uri.as_str());
376                let mut any_opened = false;
377                for sh in lsp.get_handles_mut(language) {
378                    if let Err(e) =
379                        sh.handle
380                            .did_open(uri.clone(), text.clone(), file_language.clone())
381                    {
382                        tracing::warn!("Failed to send didOpen to '{}': {}", sh.name, e);
383                    } else {
384                        any_opened = true;
385                    }
386                }
387
388                if any_opened {
389                    tracing::info!("Successfully sent didOpen to LSP after confirmation");
390
391                    // Request pull diagnostics from primary handle
392                    if let Some(handle) = lsp.get_handle_mut(language) {
393                        let previous_result_id =
394                            self.diagnostic_result_ids.get(uri.as_str()).cloned();
395                        let request_id = self.next_lsp_request_id;
396                        self.next_lsp_request_id += 1;
397
398                        if let Err(e) =
399                            handle.document_diagnostic(request_id, uri.clone(), previous_result_id)
400                        {
401                            tracing::debug!(
402                                "Failed to request pull diagnostics (server may not support): {}",
403                                e
404                            );
405                        }
406
407                        // Request inlay hints if enabled
408                        if self.config.editor.enable_inlay_hints {
409                            let request_id = self.next_lsp_request_id;
410                            self.next_lsp_request_id += 1;
411
412                            let last_line = line_count.saturating_sub(1) as u32;
413                            let last_char = 10000u32;
414
415                            if let Err(e) = handle.inlay_hints(
416                                request_id,
417                                uri.clone(),
418                                0,
419                                0,
420                                last_line,
421                                last_char,
422                            ) {
423                                tracing::debug!(
424                                    "Failed to request inlay hints (server may not support): {}",
425                                    e
426                                );
427                            } else {
428                                self.pending_inlay_hints_requests.insert(
429                                    request_id,
430                                    super::InlayHintsRequest {
431                                        buffer_id: active_buffer,
432                                        version: buffer_version,
433                                    },
434                                );
435                            }
436                        }
437                    }
438                }
439            }
440        }
441    }
442
443    /// Check if there's a pending LSP confirmation
444    pub fn has_pending_lsp_confirmation(&self) -> bool {
445        self.pending_lsp_confirmation.is_some()
446    }
447
448    /// Navigate popup selection (next item)
449    pub fn popup_select_next(&mut self) {
450        let event = Event::PopupSelectNext;
451        self.active_event_log_mut().append(event.clone());
452        self.apply_event_to_active_buffer(&event);
453    }
454
455    /// Navigate popup selection (previous item)
456    pub fn popup_select_prev(&mut self) {
457        let event = Event::PopupSelectPrev;
458        self.active_event_log_mut().append(event.clone());
459        self.apply_event_to_active_buffer(&event);
460    }
461
462    /// Navigate popup (page down)
463    pub fn popup_page_down(&mut self) {
464        let event = Event::PopupPageDown;
465        self.active_event_log_mut().append(event.clone());
466        self.apply_event_to_active_buffer(&event);
467    }
468
469    /// Navigate popup (page up)
470    pub fn popup_page_up(&mut self) {
471        let event = Event::PopupPageUp;
472        self.active_event_log_mut().append(event.clone());
473        self.apply_event_to_active_buffer(&event);
474    }
475}