Skip to main content

fresh/app/
toggle_actions.rs

1//! Toggle actions and configuration operations for the Editor.
2//!
3//! This module contains toggle methods and configuration operations:
4//! - Toggle line numbers, debug highlights, menu bar
5//! - Toggle mouse capture, mouse hover, inlay hints
6//! - Reset buffer settings
7//! - Config dump, save, and reload
8
9use crate::types::LspServerConfig;
10use rust_i18n::t;
11
12use crate::config::Config;
13use crate::config_io::{ConfigLayer, ConfigResolver};
14use crate::input::keybindings::KeybindingResolver;
15
16use super::Editor;
17
18impl Editor {
19    /// Toggle line numbers in the gutter for the active split.
20    ///
21    /// Line number visibility is stored per-split in `BufferViewState` so that
22    /// different splits of the same buffer can independently show/hide line numbers
23    /// (e.g., source mode shows them, compose mode hides them).
24    pub fn toggle_line_numbers(&mut self) {
25        let active_split = self
26            .windows
27            .get(&self.active_window)
28            .and_then(|w| w.buffers.splits())
29            .map(|(mgr, _)| mgr)
30            .expect("active window must have a populated split layout")
31            .active_split();
32        if let Some(vs) = self
33            .windows
34            .get_mut(&self.active_window)
35            .and_then(|w| w.split_view_states_mut())
36            .expect("active window must have a populated split layout")
37            .get_mut(&active_split)
38        {
39            let currently_shown = vs.show_line_numbers;
40            vs.show_line_numbers = !currently_shown;
41            if currently_shown {
42                self.set_status_message(t!("toggle.line_numbers_hidden").to_string());
43            } else {
44                self.set_status_message(t!("toggle.line_numbers_shown").to_string());
45            }
46        }
47    }
48
49    /// Toggle menu bar visibility.
50    ///
51    /// `editor.show_menu_bar` is a global preference, so the toggle updates the
52    /// runtime config and persists the change to the user config layer (same
53    /// pattern as the file-explorer toggles). See issue #1156.
54    pub fn toggle_menu_bar(&mut self) {
55        let new_value = !self.active_window_mut().menu_bar_visible;
56        self.config_mut().editor.show_menu_bar = new_value;
57        self.active_window_mut().menu_bar_visible = new_value;
58        // When explicitly toggling, clear auto-show state
59        self.active_window_mut().menu_bar_auto_shown = false;
60        // Close any open menu when hiding the menu bar
61        if !self.active_window_mut().menu_bar_visible {
62            self.menu_state.close_menu();
63        }
64        self.persist_config_change("/editor/show_menu_bar", serde_json::Value::Bool(new_value));
65        let status = if self.active_window_mut().menu_bar_visible {
66            t!("toggle.menu_bar_shown")
67        } else {
68            t!("toggle.menu_bar_hidden")
69        };
70        self.set_status_message(status.to_string());
71    }
72
73    // `toggle_tab_bar` / `toggle_status_bar` / `toggle_prompt_line` and
74    // their `*_visible` getters live on `impl Window` — call them via
75    // `self.active_window_mut().toggle_tab_bar()` etc. (or read
76    // `active_window().tab_bar_visible` for the flag directly).
77
78    /// Toggle vertical scrollbar visibility
79    pub fn toggle_vertical_scrollbar(&mut self) {
80        let new_value = !self.config.editor.show_vertical_scrollbar;
81        self.config_mut().editor.show_vertical_scrollbar = new_value;
82        let status = if self.config.editor.show_vertical_scrollbar {
83            t!("toggle.vertical_scrollbar_shown")
84        } else {
85            t!("toggle.vertical_scrollbar_hidden")
86        };
87        self.set_status_message(status.to_string());
88    }
89
90    /// Toggle horizontal scrollbar visibility
91    pub fn toggle_horizontal_scrollbar(&mut self) {
92        let new_value = !self.config.editor.show_horizontal_scrollbar;
93        self.config_mut().editor.show_horizontal_scrollbar = new_value;
94        let status = if self.config.editor.show_horizontal_scrollbar {
95            t!("toggle.horizontal_scrollbar_shown")
96        } else {
97            t!("toggle.horizontal_scrollbar_hidden")
98        };
99        self.set_status_message(status.to_string());
100    }
101
102    /// Reset buffer settings (tab_size, use_tabs, auto_close, whitespace visibility) to config defaults
103    pub fn reset_buffer_settings(&mut self) {
104        use crate::config::WhitespaceVisibility;
105        let buffer_id = self.active_buffer();
106
107        // Determine settings from config using buffer's stored language
108        let mut whitespace = WhitespaceVisibility::from_editor_config(&self.config.editor);
109        let mut auto_close = self.config.editor.auto_close;
110        let mut word_characters = String::new();
111        let (tab_size, use_tabs) = if let Some(state) = self
112            .windows
113            .get(&self.active_window)
114            .map(|w| &w.buffers)
115            .expect("active window present")
116            .get(&buffer_id)
117        {
118            let language = &state.language;
119            if let Some(lang_config) = self.config.languages.get(language) {
120                whitespace =
121                    whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
122                // Auto close: language override (only if globally enabled)
123                if auto_close {
124                    if let Some(lang_auto_close) = lang_config.auto_close {
125                        auto_close = lang_auto_close;
126                    }
127                }
128                if let Some(ref wc) = lang_config.word_characters {
129                    word_characters = wc.clone();
130                }
131                (
132                    lang_config.tab_size.unwrap_or(self.config.editor.tab_size),
133                    lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs),
134                )
135            } else {
136                (self.config.editor.tab_size, self.config.editor.use_tabs)
137            }
138        } else {
139            (self.config.editor.tab_size, self.config.editor.use_tabs)
140        };
141
142        // Apply settings to buffer
143        if let Some(state) = self
144            .windows
145            .get_mut(&self.active_window)
146            .map(|w| &mut w.buffers)
147            .expect("active window present")
148            .get_mut(&buffer_id)
149        {
150            state.buffer_settings.tab_size = tab_size;
151            state.buffer_settings.use_tabs = use_tabs;
152            state.buffer_settings.auto_close = auto_close;
153            state.buffer_settings.whitespace = whitespace;
154            state.buffer_settings.word_characters = word_characters;
155        }
156
157        self.set_status_message(t!("toggle.buffer_settings_reset").to_string());
158    }
159
160    /// Toggle mouse capture on/off
161    pub fn toggle_mouse_capture(&mut self) {
162        use std::io::stdout;
163
164        self.active_window_mut().mouse_enabled = !self.active_window_mut().mouse_enabled;
165
166        if self.active_window_mut().mouse_enabled {
167            // Best-effort terminal mouse capture toggle.
168            #[allow(clippy::let_underscore_must_use)]
169            let _ = crossterm::execute!(stdout(), crossterm::event::EnableMouseCapture);
170            self.set_status_message(t!("toggle.mouse_capture_enabled").to_string());
171        } else {
172            // Best-effort terminal mouse capture toggle.
173            #[allow(clippy::let_underscore_must_use)]
174            let _ = crossterm::execute!(stdout(), crossterm::event::DisableMouseCapture);
175            self.set_status_message(t!("toggle.mouse_capture_disabled").to_string());
176        }
177    }
178
179    /// Check if mouse capture is enabled
180    pub fn is_mouse_enabled(&self) -> bool {
181        self.active_window().mouse_enabled
182    }
183
184    /// Toggle mouse hover for LSP on/off
185    ///
186    /// On Windows, this also switches the mouse tracking mode: mode 1003
187    /// (all motion) when enabled, mode 1002 (cell motion) when disabled.
188    pub fn toggle_mouse_hover(&mut self) {
189        let new_value = !self.config.editor.mouse_hover_enabled;
190        self.config_mut().editor.mouse_hover_enabled = new_value;
191
192        if self.config.editor.mouse_hover_enabled {
193            self.set_status_message(t!("toggle.mouse_hover_enabled").to_string());
194        } else {
195            // Clear any pending hover state
196            self.active_window_mut().mouse_state.lsp_hover_state = None;
197            self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
198            self.set_status_message(t!("toggle.mouse_hover_disabled").to_string());
199        }
200
201        // On Windows, switch mouse tracking mode to match
202        #[cfg(windows)]
203        {
204            let mode = if self.config.editor.mouse_hover_enabled {
205                fresh_winterm::MouseMode::AllMotion
206            } else {
207                fresh_winterm::MouseMode::CellMotion
208            };
209            if let Err(e) = fresh_winterm::set_mouse_mode(mode) {
210                tracing::error!("Failed to switch mouse mode: {}", e);
211            }
212        }
213    }
214
215    /// Check if mouse hover is enabled
216    pub fn is_mouse_hover_enabled(&self) -> bool {
217        self.config.editor.mouse_hover_enabled
218    }
219
220    /// Set GPM active flag (enables software mouse cursor rendering)
221    ///
222    /// When GPM is used for mouse input on Linux consoles, we need to draw
223    /// our own mouse cursor because GPM can't draw on the alternate screen
224    /// buffer used by TUI applications.
225    pub fn set_gpm_active(&mut self, active: bool) {
226        self.active_window_mut().gpm_active = active;
227    }
228
229    /// Toggle inlay hints visibility
230    pub fn toggle_inlay_hints(&mut self) {
231        let new_value = !self.config.editor.enable_inlay_hints;
232        self.config_mut().editor.enable_inlay_hints = new_value;
233        // `Window::send_lsp_changes_for_buffer` reads
234        // `resources.config.editor.enable_inlay_hints`; sync so the per-edit
235        // LSP refresh sees the new value without waiting for a reload.
236        self.sync_windows_config();
237
238        if self.config.editor.enable_inlay_hints {
239            // Re-request inlay hints for the active buffer
240            self.request_inlay_hints_for_active_buffer();
241            self.set_status_message(t!("toggle.inlay_hints_enabled").to_string());
242        } else {
243            // Clear inlay hints from all buffers
244            for (_, state) in self
245                .windows
246                .get_mut(&self.active_window)
247                .map(|w| &mut w.buffers)
248                .expect("active window present")
249            {
250                state.virtual_texts.clear(&mut state.marker_list);
251            }
252            self.set_status_message(t!("toggle.inlay_hints_disabled").to_string());
253        }
254    }
255
256    /// Dump the current configuration to the user's config file
257    pub fn dump_config(&mut self) {
258        // Create the config directory if it doesn't exist
259        if let Err(e) = self
260            .authority
261            .filesystem
262            .create_dir_all(&self.dir_context.config_dir)
263        {
264            self.set_status_message(
265                t!("error.config_dir_failed", error = e.to_string()).to_string(),
266            );
267            return;
268        }
269
270        let config_path = self.dir_context.config_path();
271        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
272
273        // Save the config to user layer
274        match resolver.save_to_layer(&self.config, ConfigLayer::User) {
275            Ok(()) => {
276                // Open the saved config file in a new buffer
277                match self.open_file(&config_path) {
278                    Ok(_buffer_id) => {
279                        self.set_status_message(
280                            t!("config.saved", path = config_path.display().to_string())
281                                .to_string(),
282                        );
283                    }
284                    Err(e) => {
285                        // Check if this is a large file encoding confirmation error
286                        if let Some(confirmation) =
287                            e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
288                        {
289                            self.start_large_file_encoding_confirmation(confirmation);
290                        } else {
291                            self.set_status_message(
292                                t!("config.saved_failed_open", error = e.to_string()).to_string(),
293                            );
294                        }
295                    }
296                }
297            }
298            Err(e) => {
299                self.set_status_message(
300                    t!("error.config_save_failed", error = e.to_string()).to_string(),
301                );
302            }
303        }
304    }
305
306    /// Save the current configuration to file (without opening it)
307    ///
308    /// Returns Ok(()) on success, or an error message on failure
309    pub fn save_config(&self) -> Result<(), String> {
310        // Create the config directory if it doesn't exist
311        self.authority
312            .filesystem
313            .create_dir_all(&self.dir_context.config_dir)
314            .map_err(|e| format!("Failed to create config directory: {}", e))?;
315
316        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
317        resolver
318            .save_to_layer(&self.config, ConfigLayer::User)
319            .map_err(|e| format!("Failed to save config: {}", e))
320    }
321
322    /// Reload configuration from the config file
323    ///
324    /// This reloads the config from disk, applies runtime changes (theme, keybindings),
325    /// and emits a config_changed event so plugins can update their state accordingly.
326    /// Uses the layered config system to properly merge with defaults.
327    pub fn reload_config(&mut self) {
328        let old_theme = self.config.theme.clone();
329        self.set_config(Config::load_with_layers(
330            &self.dir_context,
331            &self.working_dir,
332        ));
333
334        // Refresh cached raw user config for plugins
335        self.set_user_config_raw(Config::read_user_config_raw(&self.working_dir));
336
337        // Apply theme change if needed
338        if old_theme != self.config.theme {
339            if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
340                *self.theme.write().unwrap() = theme;
341                tracing::info!("Theme changed to '{}'", self.config.theme.0);
342            } else {
343                tracing::error!("Theme '{}' not found", self.config.theme.0);
344            }
345        }
346
347        // Always reload keybindings (complex types don't implement PartialEq)
348        *self.keybindings.write().unwrap() = KeybindingResolver::new(&self.config);
349
350        // Update clipboard configuration
351        self.clipboard.apply_config(&self.config.clipboard);
352
353        // Apply bar visibility changes immediately
354        self.active_window_mut().menu_bar_visible = self.config.editor.show_menu_bar;
355        self.active_window_mut().tab_bar_visible = self.config.editor.show_tab_bar;
356        self.active_window_mut().status_bar_visible = self.config.editor.show_status_bar;
357        self.active_window_mut().prompt_line_visible = self.config.editor.show_prompt_line;
358
359        // Update LSP configs
360        let __active_id = self.active_window;
361        if let Some(lsp) = self
362            .windows
363            .get_mut(&__active_id)
364            .and_then(|w| w.lsp.as_mut())
365        {
366            for (language, lsp_configs) in &self.config.lsp {
367                lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
368            }
369            // Configure universal (global) LSP servers
370            let universal_servers: Vec<LspServerConfig> = self
371                .config
372                .universal_lsp
373                .values()
374                .flat_map(|lc| lc.as_slice().to_vec())
375                .filter(|c| c.enabled)
376                .collect();
377            lsp.set_universal_configs(universal_servers);
378        }
379
380        // Emit event so plugins know config changed
381        let config_path = Config::find_config_path(&self.working_dir);
382        self.emit_event(
383            "config_changed",
384            serde_json::json!({
385                "path": config_path.map(|p| p.to_string_lossy().into_owned()),
386            }),
387        );
388    }
389
390    /// Reload the theme registry from disk.
391    ///
392    /// Call this after installing new theme packages or saving new themes.
393    /// This rescans all theme directories and updates the available themes list.
394    pub fn reload_themes(&mut self) {
395        use crate::view::theme::ThemeLoader;
396
397        let theme_loader = ThemeLoader::new(self.dir_context.themes_dir());
398        self.theme_registry = std::sync::Arc::new(theme_loader.load_all(&[]));
399        self.expanded_menus_cache.invalidate();
400
401        // Propagate the new registry to every window's resources so
402        // window-side reads see the updated catalogue. (Theme registry
403        // is `Arc<ThemeRegistry>` not `Arc<RwLock<>>`, so swapping the
404        // Editor's pointer leaves Window clones stale unless we sync.)
405        for w in self.windows.values_mut() {
406            w.resources.theme_registry = self.theme_registry.clone();
407        }
408
409        // Update shared theme cache for plugin access
410        *self.theme_cache.write().unwrap() = self.theme_registry.to_json_map();
411
412        // Re-apply current theme if it still exists, otherwise it might have been updated
413        if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
414            *self.theme.write().unwrap() = theme;
415        }
416
417        tracing::info!(
418            "Theme registry reloaded ({} themes)",
419            self.theme_registry.len()
420        );
421
422        // Emit event so plugins know themes changed
423        self.emit_event("themes_changed", serde_json::json!({}));
424    }
425
426    /// Persist a single config change to the user config file.
427    ///
428    /// Used when toggling settings via menu/command palette so that
429    /// the change is saved immediately (matching the settings UI behavior).
430    pub(super) fn persist_config_change(&self, json_pointer: &str, value: serde_json::Value) {
431        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
432        let changes = std::collections::HashMap::from([(json_pointer.to_string(), value)]);
433        let deletions = std::collections::HashSet::new();
434        if let Err(e) = resolver.save_changes_to_layer(&changes, &deletions, ConfigLayer::User) {
435            tracing::error!("Failed to persist config change {}: {}", json_pointer, e);
436        }
437    }
438}