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