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 rust_i18n::t;
10
11use crate::config::Config;
12use crate::config_io::{ConfigLayer, ConfigResolver};
13use crate::input::keybindings::KeybindingResolver;
14use crate::services::lsp::manager::detect_language;
15
16use super::Editor;
17
18impl Editor {
19    /// Toggle line numbers in the gutter for the active buffer
20    pub fn toggle_line_numbers(&mut self) {
21        if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
22            let currently_shown = state.margins.show_line_numbers;
23            state.margins.set_line_numbers(!currently_shown);
24            if currently_shown {
25                self.set_status_message(t!("toggle.line_numbers_hidden").to_string());
26            } else {
27                // Restore proper width based on buffer size
28                let total_lines = state.buffer.line_count().unwrap_or(1);
29                state.margins.update_width_for_buffer(total_lines);
30                self.set_status_message(t!("toggle.line_numbers_shown").to_string());
31            }
32        }
33    }
34
35    /// Toggle debug highlight mode for the active buffer
36    /// When enabled, shows byte positions and highlight span info for debugging
37    pub fn toggle_debug_highlights(&mut self) {
38        if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
39            state.debug_highlight_mode = !state.debug_highlight_mode;
40            if state.debug_highlight_mode {
41                self.set_status_message(t!("toggle.debug_mode_on").to_string());
42            } else {
43                self.set_status_message(t!("toggle.debug_mode_off").to_string());
44            }
45        }
46    }
47
48    /// Toggle menu bar visibility
49    pub fn toggle_menu_bar(&mut self) {
50        self.menu_bar_visible = !self.menu_bar_visible;
51        // When explicitly toggling, clear auto-show state
52        self.menu_bar_auto_shown = false;
53        // Close any open menu when hiding the menu bar
54        if !self.menu_bar_visible {
55            self.menu_state.close_menu();
56        }
57        let status = if self.menu_bar_visible {
58            t!("toggle.menu_bar_shown")
59        } else {
60            t!("toggle.menu_bar_hidden")
61        };
62        self.set_status_message(status.to_string());
63    }
64
65    /// Toggle tab bar visibility
66    pub fn toggle_tab_bar(&mut self) {
67        self.tab_bar_visible = !self.tab_bar_visible;
68        let status = if self.tab_bar_visible {
69            t!("toggle.tab_bar_shown")
70        } else {
71            t!("toggle.tab_bar_hidden")
72        };
73        self.set_status_message(status.to_string());
74    }
75
76    /// Get tab bar visibility
77    pub fn tab_bar_visible(&self) -> bool {
78        self.tab_bar_visible
79    }
80
81    /// Reset buffer settings (tab_size, use_tabs, show_whitespace_tabs) to config defaults
82    pub fn reset_buffer_settings(&mut self) {
83        let buffer_id = self.active_buffer();
84
85        // Get the file path to determine language-specific settings
86        let file_path = self
87            .buffer_metadata
88            .get(&buffer_id)
89            .and_then(|m| m.file_path().cloned());
90
91        // Determine settings from config (with language fallback)
92        let (tab_size, use_tabs, show_whitespace_tabs) = if let Some(path) = &file_path {
93            if let Some(language) = detect_language(path, &self.config.languages) {
94                if let Some(lang_config) = self.config.languages.get(&language) {
95                    (
96                        lang_config.tab_size.unwrap_or(self.config.editor.tab_size),
97                        lang_config.use_tabs,
98                        lang_config.show_whitespace_tabs,
99                    )
100                } else {
101                    (self.config.editor.tab_size, false, true)
102                }
103            } else {
104                (self.config.editor.tab_size, false, true)
105            }
106        } else {
107            (self.config.editor.tab_size, false, true)
108        };
109
110        // Apply settings to buffer
111        if let Some(state) = self.buffers.get_mut(&buffer_id) {
112            state.tab_size = tab_size;
113            state.use_tabs = use_tabs;
114            state.show_whitespace_tabs = show_whitespace_tabs;
115        }
116
117        self.set_status_message(t!("toggle.buffer_settings_reset").to_string());
118    }
119
120    /// Toggle mouse capture on/off
121    pub fn toggle_mouse_capture(&mut self) {
122        use std::io::stdout;
123
124        self.mouse_enabled = !self.mouse_enabled;
125
126        if self.mouse_enabled {
127            let _ = crossterm::execute!(stdout(), crossterm::event::EnableMouseCapture);
128            self.set_status_message(t!("toggle.mouse_capture_enabled").to_string());
129        } else {
130            let _ = crossterm::execute!(stdout(), crossterm::event::DisableMouseCapture);
131            self.set_status_message(t!("toggle.mouse_capture_disabled").to_string());
132        }
133    }
134
135    /// Check if mouse capture is enabled
136    pub fn is_mouse_enabled(&self) -> bool {
137        self.mouse_enabled
138    }
139
140    /// Toggle mouse hover for LSP on/off
141    pub fn toggle_mouse_hover(&mut self) {
142        self.config.editor.mouse_hover_enabled = !self.config.editor.mouse_hover_enabled;
143
144        if self.config.editor.mouse_hover_enabled {
145            self.set_status_message(t!("toggle.mouse_hover_enabled").to_string());
146        } else {
147            // Clear any pending hover state
148            self.mouse_state.lsp_hover_state = None;
149            self.mouse_state.lsp_hover_request_sent = false;
150            self.set_status_message(t!("toggle.mouse_hover_disabled").to_string());
151        }
152    }
153
154    /// Check if mouse hover is enabled
155    pub fn is_mouse_hover_enabled(&self) -> bool {
156        self.config.editor.mouse_hover_enabled
157    }
158
159    /// Set GPM active flag (enables software mouse cursor rendering)
160    ///
161    /// When GPM is used for mouse input on Linux consoles, we need to draw
162    /// our own mouse cursor because GPM can't draw on the alternate screen
163    /// buffer used by TUI applications.
164    pub fn set_gpm_active(&mut self, active: bool) {
165        self.gpm_active = active;
166    }
167
168    /// Toggle inlay hints visibility
169    pub fn toggle_inlay_hints(&mut self) {
170        self.config.editor.enable_inlay_hints = !self.config.editor.enable_inlay_hints;
171
172        if self.config.editor.enable_inlay_hints {
173            // Re-request inlay hints for the active buffer
174            self.request_inlay_hints_for_active_buffer();
175            self.set_status_message(t!("toggle.inlay_hints_enabled").to_string());
176        } else {
177            // Clear inlay hints from all buffers
178            for state in self.buffers.values_mut() {
179                state.virtual_texts.clear(&mut state.marker_list);
180            }
181            self.set_status_message(t!("toggle.inlay_hints_disabled").to_string());
182        }
183    }
184
185    /// Dump the current configuration to the user's config file
186    pub fn dump_config(&mut self) {
187        // Create the config directory if it doesn't exist
188        if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
189            self.set_status_message(
190                t!("error.config_dir_failed", error = e.to_string()).to_string(),
191            );
192            return;
193        }
194
195        let config_path = self.dir_context.config_path();
196        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
197
198        // Save the config to user layer
199        match resolver.save_to_layer(&self.config, ConfigLayer::User) {
200            Ok(()) => {
201                // Open the saved config file in a new buffer
202                match self.open_file(&config_path) {
203                    Ok(_buffer_id) => {
204                        self.set_status_message(
205                            t!("config.saved", path = config_path.display().to_string())
206                                .to_string(),
207                        );
208                    }
209                    Err(e) => {
210                        self.set_status_message(
211                            t!("config.saved_failed_open", error = e.to_string()).to_string(),
212                        );
213                    }
214                }
215            }
216            Err(e) => {
217                self.set_status_message(
218                    t!("error.config_save_failed", error = e.to_string()).to_string(),
219                );
220            }
221        }
222    }
223
224    /// Save the current configuration to file (without opening it)
225    ///
226    /// Returns Ok(()) on success, or an error message on failure
227    pub fn save_config(&self) -> Result<(), String> {
228        // Create the config directory if it doesn't exist
229        self.filesystem
230            .create_dir_all(&self.dir_context.config_dir)
231            .map_err(|e| format!("Failed to create config directory: {}", e))?;
232
233        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
234        resolver
235            .save_to_layer(&self.config, ConfigLayer::User)
236            .map_err(|e| format!("Failed to save config: {}", e))
237    }
238
239    /// Reload configuration from the config file
240    ///
241    /// This reloads the config from disk, applies runtime changes (theme, keybindings),
242    /// and emits a config_changed event so plugins can update their state accordingly.
243    /// Uses the layered config system to properly merge with defaults.
244    pub fn reload_config(&mut self) {
245        let old_theme = self.config.theme.clone();
246        self.config = Config::load_with_layers(&self.dir_context, &self.working_dir);
247
248        // Apply theme change if needed
249        if old_theme != self.config.theme {
250            if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
251                self.theme = theme;
252                tracing::info!("Theme changed to '{}'", self.config.theme.0);
253            } else {
254                tracing::error!("Theme '{}' not found", self.config.theme.0);
255            }
256        }
257
258        // Always reload keybindings (complex types don't implement PartialEq)
259        self.keybindings = KeybindingResolver::new(&self.config);
260
261        // Update LSP configs
262        if let Some(ref mut lsp) = self.lsp {
263            for (language, lsp_config) in &self.config.lsp {
264                lsp.set_language_config(language.clone(), lsp_config.clone());
265            }
266        }
267
268        // Emit event so plugins know config changed
269        let config_path = Config::find_config_path(&self.working_dir);
270        self.emit_event(
271            "config_changed",
272            serde_json::json!({
273                "path": config_path.map(|p| p.to_string_lossy().into_owned()),
274            }),
275        );
276    }
277
278    /// Reload the theme registry from disk.
279    ///
280    /// Call this after installing new theme packages or saving new themes.
281    /// This rescans all theme directories and updates the available themes list.
282    pub fn reload_themes(&mut self) {
283        use crate::view::theme::ThemeLoader;
284
285        let theme_loader = ThemeLoader::new();
286        self.theme_registry = theme_loader.load_all();
287
288        // Re-apply current theme if it still exists, otherwise it might have been updated
289        if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
290            self.theme = theme;
291        }
292
293        tracing::info!(
294            "Theme registry reloaded ({} themes)",
295            self.theme_registry.len()
296        );
297
298        // Emit event so plugins know themes changed
299        self.emit_event("themes_changed", serde_json::json!({}));
300    }
301}