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