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    /// Update all menu context values based on current editor state.
36    /// This should be called before rendering the menu bar.
37    pub fn update_menu_context(&mut self) {
38        // Simple state lookups
39        let line_numbers = self.is_line_numbers_visible();
40        let line_wrap = self.is_line_wrap_enabled();
41        let page_view = self.is_page_view();
42        let file_explorer_visible = self.file_explorer_visible;
43        let file_explorer_focused = self.is_file_explorer_focused();
44        let mouse_capture = self.mouse_enabled;
45        let mouse_hover = self.config.editor.mouse_hover_enabled;
46        let inlay_hints = self.config.editor.enable_inlay_hints;
47        // True for any real buffer; false when the active buffer is the
48        // synthesized placeholder kept alive after a last-buffer close with
49        // `auto_create_empty_buffer_on_last_buffer_close` disabled.
50        let has_buffer = !self
51            .buffer_metadata
52            .get(&self.active_buffer())
53            .map(|m| m.synthetic_placeholder)
54            .unwrap_or(false);
55        let has_selection = has_buffer && self.has_active_selection();
56        let can_copy = has_selection
57            || file_explorer_focused
58            || self
59                .file_explorer
60                .as_ref()
61                .map(|fe| fe.get_selected().is_some())
62                .unwrap_or(false);
63        // Paste is available in the explorer only when a file is in the clipboard,
64        // or in the editor only when no file is in the clipboard. There's no
65        // buffer to paste into in placeholder mode, so suppress it there.
66        let can_paste = if file_explorer_focused {
67            self.file_explorer_clipboard.is_some()
68        } else {
69            has_buffer && self.file_explorer_clipboard.is_none()
70        };
71        let menu_bar = self.menu_bar_visible;
72        let vertical_scrollbar = self.config.editor.show_vertical_scrollbar;
73        let horizontal_scrollbar = self.config.editor.show_horizontal_scrollbar;
74
75        // File explorer state
76        let show_hidden = self.is_file_explorer_showing_hidden();
77        let show_gitignored = self.is_file_explorer_showing_gitignored();
78
79        // Language-dependent context values
80        let lsp_available = self.is_lsp_available();
81        let formatter_available = self.is_formatter_available();
82
83        // Session mode (for detach command availability)
84        let session_mode = self.session_mode;
85
86        // Scroll sync state
87        let scroll_sync = self.same_buffer_scroll_sync;
88        let has_same_buffer_splits = self.has_same_buffer_splits();
89
90        // Keybinding map state
91        let active_keymap: &str = &self.config.active_keybinding_map;
92
93        // Apply all context values
94        self.menu_state
95            .context
96            .set(context_keys::HAS_BUFFER, has_buffer)
97            .set(context_keys::KEYMAP_DEFAULT, active_keymap == "default")
98            .set(context_keys::KEYMAP_EMACS, active_keymap == "emacs")
99            .set(context_keys::KEYMAP_VSCODE, active_keymap == "vscode")
100            .set(context_keys::KEYMAP_MACOS_GUI, active_keymap == "macos-gui")
101            .set(context_keys::LINE_NUMBERS, line_numbers)
102            .set(context_keys::LINE_WRAP, line_wrap)
103            .set(context_keys::PAGE_VIEW, page_view)
104            // Keep backward-compatible key for existing keybindings/menus
105            .set(context_keys::COMPOSE_MODE, page_view)
106            .set(context_keys::FILE_EXPLORER, file_explorer_visible)
107            .set(context_keys::FILE_EXPLORER_FOCUSED, file_explorer_focused)
108            .set(context_keys::MOUSE_CAPTURE, mouse_capture)
109            .set(context_keys::MOUSE_HOVER, mouse_hover)
110            .set(context_keys::INLAY_HINTS, inlay_hints)
111            .set(context_keys::LSP_AVAILABLE, lsp_available)
112            .set(context_keys::FILE_EXPLORER_SHOW_HIDDEN, show_hidden)
113            .set(context_keys::FILE_EXPLORER_SHOW_GITIGNORED, show_gitignored)
114            .set(context_keys::HAS_SELECTION, has_selection)
115            .set(context_keys::CAN_COPY, can_copy)
116            .set(context_keys::CAN_PASTE, can_paste)
117            .set(context_keys::MENU_BAR, menu_bar)
118            .set(context_keys::FORMATTER_AVAILABLE, formatter_available)
119            .set(context_keys::SESSION_MODE, session_mode)
120            .set(context_keys::VERTICAL_SCROLLBAR, vertical_scrollbar)
121            .set(context_keys::HORIZONTAL_SCROLLBAR, horizontal_scrollbar)
122            .set(context_keys::SCROLL_SYNC, scroll_sync)
123            .set(context_keys::HAS_SAME_BUFFER_SPLITS, has_same_buffer_splits);
124    }
125
126    /// Check if line numbers are visible in the active split.
127    fn is_line_numbers_visible(&self) -> bool {
128        let active_split = self.split_manager.active_split();
129        self.split_view_states
130            .get(&active_split)
131            .map(|vs| vs.show_line_numbers)
132            .unwrap_or(true)
133    }
134
135    /// Check if line wrap is enabled in the active split.
136    fn is_line_wrap_enabled(&self) -> bool {
137        let active_split = self.split_manager.active_split();
138        self.split_view_states
139            .get(&active_split)
140            .map(|vs| vs.viewport.line_wrap_enabled)
141            .unwrap_or(false)
142    }
143
144    /// Check if compose mode is active in the current buffer.
145    fn is_page_view(&self) -> bool {
146        let active_split = self.split_manager.active_split();
147        self.split_view_states
148            .get(&active_split)
149            .map(|vs| vs.view_mode == crate::state::ViewMode::PageView)
150            .unwrap_or(false)
151    }
152
153    /// Check if the file explorer is currently focused.
154    fn is_file_explorer_focused(&self) -> bool {
155        self.key_context == crate::input::keybindings::KeyContext::FileExplorer
156    }
157
158    /// Check if the file explorer is showing hidden files.
159    fn is_file_explorer_showing_hidden(&self) -> bool {
160        self.file_explorer
161            .as_ref()
162            .map(|fe| fe.ignore_patterns().show_hidden())
163            .unwrap_or(false)
164    }
165
166    /// Check if the file explorer is showing gitignored files.
167    fn is_file_explorer_showing_gitignored(&self) -> bool {
168        self.file_explorer
169            .as_ref()
170            .map(|fe| fe.ignore_patterns().show_gitignored())
171            .unwrap_or(false)
172    }
173
174    /// Check if an LSP server is available and ready for the current buffer's language.
175    fn is_lsp_available(&self) -> bool {
176        let buffer_id = self.active_buffer();
177
178        // Check if LSP is enabled for this buffer
179        if let Some(metadata) = self.buffer_metadata.get(&buffer_id) {
180            if !metadata.lsp_enabled {
181                return false;
182            }
183        } else {
184            return false;
185        }
186
187        // Use buffer's stored language
188        self.buffers
189            .get(&buffer_id)
190            .and_then(|state| {
191                self.lsp
192                    .as_ref()
193                    .map(|lsp| lsp.is_server_ready(&state.language))
194            })
195            .unwrap_or(false)
196    }
197
198    /// Check if the active buffer is shown in more than one visible split.
199    fn has_same_buffer_splits(&self) -> bool {
200        let active_split = self.split_manager.active_split();
201        let active_buf_id = self.split_manager.buffer_for_split(active_split);
202        if let Some(buf_id) = active_buf_id {
203            self.split_view_states.keys().any(|&s| {
204                s != active_split && self.split_manager.buffer_for_split(s) == Some(buf_id)
205            })
206        } else {
207            false
208        }
209    }
210
211    /// Check if a formatter is configured for the current buffer's language.
212    fn is_formatter_available(&self) -> bool {
213        let buffer_id = self.active_buffer();
214
215        // Use buffer's stored language
216        self.buffers
217            .get(&buffer_id)
218            .and_then(|state| {
219                self.config
220                    .languages
221                    .get(&state.language)
222                    .and_then(|lc| lc.formatter.as_ref())
223                    .map(|_| true)
224            })
225            .unwrap_or(false)
226    }
227}