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