Skip to main content

fresh/app/
menu_context.rs

1//! Menu context computation.
2//!
3//! This module provides methods to compute menu context values that determine
4//! when menu items and commands should be enabled or disabled. Each context
5//! value has a dedicated method that encapsulates the logic for checking
6//! whether that feature is available.
7
8use super::Editor;
9use crate::view::ui::context_keys;
10
11impl Editor {
12    /// Return a clone of the current menu context (boolean state flags).
13    ///
14    /// This is used by the GUI layer to sync native menu item states
15    /// (enabled/disabled, checkmarks) without knowing about the editor's
16    /// internal state.
17    pub fn menu_context(&self) -> crate::view::ui::MenuContext {
18        self.menu_state.context.clone()
19    }
20
21    /// Return the fully-expanded menu definitions (with `DynamicSubmenu`
22    /// items resolved to `Submenu`).  Used by the GUI layer to build
23    /// platform-native menus.
24    pub fn expanded_menu_definitions(&self) -> Vec<fresh_core::menu::Menu> {
25        use crate::config::{MenuConfig, MenuExt};
26
27        let mut menus = MenuConfig::translated_menus();
28        let themes_dir = self.menu_state.themes_dir.clone();
29        for menu in &mut menus {
30            menu.expand_dynamic_items(&themes_dir);
31        }
32        menus
33    }
34
35    /// The exact menu list the TUI `MenuRenderer` renders: the configured menus
36    /// (`self.menus`, dynamic submenus expanded — reusing the renderer's
37    /// `expanded_menus_cache` when populated) followed by the plugin-contributed
38    /// menus (also expanded). This is the single expansion path shared by the
39    /// TUI renderer and the web `menu_view()` projection, so the two frontends
40    /// never diverge on which menus/items exist.
41    pub fn all_menus_expanded(&self) -> Vec<fresh_core::menu::Menu> {
42        use crate::config::MenuExt;
43
44        let themes_dir = self.menu_state.themes_dir.clone();
45        // Config menus: reuse the renderer's cached expansion (avoids rescanning
46        // theme JSON every frame); fall back to expanding `self.menus` directly.
47        let mut all: Vec<fresh_core::menu::Menu> = match self.expanded_menus_cache.get() {
48            Some(cached) => cached.menus.clone(),
49            None => {
50                let mut m = self.menus.clone();
51                for menu in &mut m.menus {
52                    menu.expand_dynamic_items(&themes_dir);
53                }
54                m.menus
55            }
56        };
57        for plugin_menu in &self.menu_state.plugin_menus {
58            let mut menu = plugin_menu.clone();
59            menu.expand_dynamic_items(&themes_dir);
60            all.push(menu);
61        }
62        all
63    }
64
65    /// Update all menu context values based on current editor state.
66    /// This should be called before rendering the menu bar.
67    pub fn update_menu_context(&mut self) {
68        // Window-scoped lookups delegated to `Window`.
69        let line_numbers = self.active_window().is_line_numbers_visible();
70        let line_wrap = self.active_window().is_line_wrap_enabled();
71        let page_view = self.active_window().is_page_view();
72        let file_explorer_visible = self.file_explorer_visible();
73        let file_explorer_focused = self.active_window().is_file_explorer_focused();
74        let mouse_capture = self.active_window_mut().mouse_enabled;
75        let mouse_hover = self.config.editor.mouse_hover_enabled;
76        let inlay_hints = self.config.editor.enable_inlay_hints;
77        // True for any real buffer; false when the active buffer is the
78        // synthesized placeholder kept alive after a last-buffer close with
79        // `auto_create_empty_buffer_on_last_buffer_close` disabled.
80        let has_buffer = !self
81            .active_window()
82            .buffer_metadata
83            .get(&self.active_buffer())
84            .map(|m| m.synthetic_placeholder)
85            .unwrap_or(false);
86        let has_selection = has_buffer && self.has_active_selection();
87        let can_copy = has_selection
88            || file_explorer_focused
89            || self
90                .file_explorer()
91                .as_ref()
92                .map(|fe| fe.get_selected().is_some())
93                .unwrap_or(false);
94        // Paste is available in the explorer only when a file is in the clipboard,
95        // or in the editor only when no file is in the clipboard. There's no
96        // buffer to paste into in placeholder mode, so suppress it there.
97        let can_paste = if file_explorer_focused {
98            self.active_window().file_explorer_clipboard.is_some()
99        } else {
100            has_buffer && self.active_window().file_explorer_clipboard.is_none()
101        };
102        let menu_bar = self.active_window_mut().menu_bar_visible;
103        let vertical_scrollbar = self.config.editor.show_vertical_scrollbar;
104        let horizontal_scrollbar = self.config.editor.show_horizontal_scrollbar;
105
106        // File explorer state
107        let show_hidden = self.active_window().is_file_explorer_showing_hidden();
108        let show_gitignored = self.active_window().is_file_explorer_showing_gitignored();
109
110        // Language-dependent context values
111        let lsp_available = self.active_window().is_lsp_available();
112        let formatter_available = self.active_window().is_formatter_available();
113
114        // Session mode (for detach command availability)
115        let session_mode = self.session_mode;
116
117        // Scroll sync state
118        let scroll_sync = self.active_window().same_buffer_scroll_sync;
119        let has_same_buffer_splits = self.active_window().has_same_buffer_splits();
120
121        // Keybinding map state
122        let active_keymap: &str = &self.config.active_keybinding_map;
123
124        // Apply all context values
125        self.menu_state
126            .context
127            .set(context_keys::HAS_BUFFER, has_buffer)
128            .set(context_keys::KEYMAP_DEFAULT, active_keymap == "default")
129            .set(context_keys::KEYMAP_EMACS, active_keymap == "emacs")
130            .set(context_keys::KEYMAP_VSCODE, active_keymap == "vscode")
131            .set(context_keys::KEYMAP_MACOS_GUI, active_keymap == "macos-gui")
132            .set(context_keys::LINE_NUMBERS, line_numbers)
133            .set(context_keys::LINE_WRAP, line_wrap)
134            .set(context_keys::PAGE_VIEW, page_view)
135            // Keep backward-compatible key for existing keybindings/menus
136            .set(context_keys::COMPOSE_MODE, page_view)
137            .set(context_keys::FILE_EXPLORER, file_explorer_visible)
138            .set(context_keys::FILE_EXPLORER_FOCUSED, file_explorer_focused)
139            .set(context_keys::MOUSE_CAPTURE, mouse_capture)
140            .set(context_keys::MOUSE_HOVER, mouse_hover)
141            .set(context_keys::INLAY_HINTS, inlay_hints)
142            .set(context_keys::LSP_AVAILABLE, lsp_available)
143            .set(context_keys::FILE_EXPLORER_SHOW_HIDDEN, show_hidden)
144            .set(context_keys::FILE_EXPLORER_SHOW_GITIGNORED, show_gitignored)
145            .set(context_keys::HAS_SELECTION, has_selection)
146            .set(context_keys::CAN_COPY, can_copy)
147            .set(context_keys::CAN_PASTE, can_paste)
148            .set(context_keys::MENU_BAR, menu_bar)
149            .set(context_keys::FORMATTER_AVAILABLE, formatter_available)
150            .set(context_keys::SESSION_MODE, session_mode)
151            .set(context_keys::VERTICAL_SCROLLBAR, vertical_scrollbar)
152            .set(context_keys::HORIZONTAL_SCROLLBAR, horizontal_scrollbar)
153            .set(context_keys::SCROLL_SYNC, scroll_sync)
154            .set(context_keys::HAS_SAME_BUFFER_SPLITS, has_same_buffer_splits);
155    }
156}
157
158impl crate::app::window::Window {
159    /// Check if line numbers are visible in the active split.
160    pub(crate) fn is_line_numbers_visible(&self) -> bool {
161        let (mgr, vs) = self
162            .buffers
163            .splits()
164            .expect("active window must have a populated split layout");
165        vs.get(&mgr.active_split())
166            .map(|vs| vs.show_line_numbers)
167            .unwrap_or(true)
168    }
169
170    /// Check if line wrap is enabled in the active split.
171    pub(crate) fn is_line_wrap_enabled(&self) -> bool {
172        let (mgr, vs) = self
173            .buffers
174            .splits()
175            .expect("active window must have a populated split layout");
176        vs.get(&mgr.active_split())
177            .map(|vs| vs.viewport.line_wrap_enabled)
178            .unwrap_or(false)
179    }
180
181    /// Check if compose mode is active in the current buffer.
182    pub(crate) fn is_page_view(&self) -> bool {
183        let (mgr, vs) = self
184            .buffers
185            .splits()
186            .expect("active window must have a populated split layout");
187        vs.get(&mgr.active_split())
188            .map(|vs| vs.view_mode == crate::state::ViewMode::PageView)
189            .unwrap_or(false)
190    }
191
192    /// Check if the file explorer is currently focused.
193    pub(crate) fn is_file_explorer_focused(&self) -> bool {
194        self.key_context == crate::input::keybindings::KeyContext::FileExplorer
195    }
196
197    /// Check if the file explorer is showing hidden files.
198    pub(crate) fn is_file_explorer_showing_hidden(&self) -> bool {
199        self.file_explorer
200            .as_ref()
201            .map(|fe| fe.ignore_patterns().show_hidden())
202            .unwrap_or(false)
203    }
204
205    /// Check if the file explorer is showing gitignored files.
206    pub(crate) fn is_file_explorer_showing_gitignored(&self) -> bool {
207        self.file_explorer
208            .as_ref()
209            .map(|fe| fe.ignore_patterns().show_gitignored())
210            .unwrap_or(false)
211    }
212
213    /// Check if an LSP server is available and ready for the current buffer's language.
214    pub(crate) fn is_lsp_available(&self) -> bool {
215        let buffer_id = self.active_buffer();
216
217        // Check if LSP is enabled for this buffer
218        if let Some(metadata) = self.buffer_metadata.get(&buffer_id) {
219            if !metadata.lsp_enabled {
220                return false;
221            }
222        } else {
223            return false;
224        }
225
226        // Use buffer's stored language
227        self.buffers
228            .get(&buffer_id)
229            .map(|state| self.lsp.is_server_ready(&state.language))
230            .unwrap_or(false)
231    }
232
233    /// Check if the active buffer is shown in more than one visible split.
234    pub(crate) fn has_same_buffer_splits(&self) -> bool {
235        let (mgr, vs) = self
236            .buffers
237            .splits()
238            .expect("active window must have a populated split layout");
239        let active_split = mgr.active_split();
240        let active_buf_id = mgr.buffer_for_split(active_split);
241        if let Some(buf_id) = active_buf_id {
242            vs.keys()
243                .any(|&s| s != active_split && mgr.buffer_for_split(s) == Some(buf_id))
244        } else {
245            false
246        }
247    }
248
249    /// Check if a formatter is configured for the current buffer's language.
250    pub(crate) fn is_formatter_available(&self) -> bool {
251        let buffer_id = self.active_buffer();
252        self.buffers
253            .get(&buffer_id)
254            .and_then(|state| {
255                self.config()
256                    .languages
257                    .get(&state.language)
258                    .and_then(|lc| lc.formatter.as_ref())
259                    .map(|_| true)
260            })
261            .unwrap_or(false)
262    }
263}