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