Skip to main content

fresh/app/
settings_actions.rs

1//! Settings modal UI operations for the Editor.
2//!
3//! This module contains all methods related to the settings modal:
4//! - Opening/closing the settings modal
5//! - Saving settings to config
6//! - Navigation (up/down)
7//! - Activating/toggling settings
8//! - Incrementing/decrementing numeric values
9
10use crate::config::Config;
11use crate::config_io::{ConfigLayer, ConfigResolver};
12use crate::input::keybindings::KeybindingResolver;
13use crate::types::LspServerConfig;
14use anyhow::Result as AnyhowResult;
15use rust_i18n::t;
16
17use super::Editor;
18
19impl Editor {
20    /// Open the settings modal
21    pub fn open_settings(&mut self) {
22        // Include schema at compile time
23        const SCHEMA_JSON: &str = include_str!("../../plugins/config-schema.json");
24
25        // Create settings state if not exists, or show existing
26        if self.settings_state.is_none() {
27            match crate::view::settings::SettingsState::new(SCHEMA_JSON, &self.config) {
28                Ok(mut state) => {
29                    // Load layer sources to show where each setting value comes from
30                    let resolver =
31                        ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
32                    if let Ok(sources) = resolver.get_layer_sources() {
33                        state.set_layer_sources(sources);
34                    }
35                    state.show();
36                    self.settings_state = Some(state);
37                }
38                Err(e) => {
39                    self.set_status_message(
40                        t!("settings.failed_to_open", error = e.to_string()).to_string(),
41                    );
42                }
43            }
44        } else if let Some(ref mut state) = self.settings_state {
45            state.show();
46        }
47    }
48
49    /// Close the settings modal
50    ///
51    /// If `save` is true and there are changes, they will be applied first.
52    pub fn close_settings(&mut self, save: bool) {
53        if save {
54            self.save_settings();
55        }
56        if let Some(ref mut state) = self.settings_state {
57            if !save && state.has_changes() {
58                // Discard changes
59                state.discard_changes();
60            }
61            state.hide();
62        }
63    }
64
65    /// Save the settings from the modal to config
66    pub fn save_settings(&mut self) {
67        let old_theme = self.config.theme.clone();
68        let old_locale = self.config.locale.clone();
69        let old_plugins = self.config.plugins.clone();
70        #[cfg(windows)]
71        let old_mouse_hover = self.config.editor.mouse_hover_enabled;
72
73        // Get target layer, new config, and the actual changes made
74        let (target_layer, new_config, pending_changes, pending_deletions) = {
75            if let Some(ref state) = self.settings_state {
76                if !state.has_changes() {
77                    return;
78                }
79                match state.apply_changes(&self.config) {
80                    Ok(config) => (
81                        state.target_layer,
82                        config,
83                        state.pending_changes.clone(),
84                        state.pending_deletions.clone(),
85                    ),
86                    Err(e) => {
87                        self.set_status_message(
88                            t!("settings.failed_to_apply", error = e.to_string()).to_string(),
89                        );
90                        return;
91                    }
92                }
93            } else {
94                return;
95            }
96        };
97
98        // Apply the new config
99        self.set_config(new_config.clone());
100
101        // Refresh cached raw user config for plugins
102        self.set_user_config_raw(Config::read_user_config_raw(&self.working_dir));
103
104        // Apply runtime changes
105        if old_theme != self.config.theme {
106            if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
107                self.theme = theme;
108                tracing::info!("Theme changed to '{}'", self.config.theme.0);
109            } else {
110                tracing::error!("Theme '{}' not found", self.config.theme.0);
111                self.set_status_message(format!("Theme '{}' not found", self.config.theme.0));
112            }
113        }
114
115        // Apply locale change at runtime
116        if old_locale != self.config.locale {
117            let locale_owned = self.config.locale.as_option().map(|s| s.to_string());
118            if let Some(locale) = locale_owned {
119                crate::i18n::set_locale(&locale);
120                // Regenerate menus with the new locale
121                self.set_menus(crate::config::MenuConfig::translated());
122                tracing::info!("Locale changed to '{}'", locale);
123            } else {
124                // Auto-detect from environment
125                crate::i18n::init();
126                self.set_menus(crate::config::MenuConfig::translated());
127                tracing::info!("Locale reset to auto-detect");
128            }
129            // Refresh command palette commands with new locale
130            if let Ok(mut registry) = self.command_registry.write() {
131                registry.refresh_builtin_commands();
132            }
133        }
134
135        // Handle plugin enable/disable changes
136        self.apply_plugin_config_changes(&old_plugins);
137
138        // Update keybindings
139        *self.keybindings.write().unwrap() = KeybindingResolver::new(&self.config);
140
141        // Update LSP configs
142        if let Some(ref mut lsp) = self.lsp {
143            for (language, lsp_configs) in &self.config.lsp {
144                lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
145            }
146            // Configure universal (global) LSP servers
147            let universal_servers: Vec<LspServerConfig> = self
148                .config
149                .universal_lsp
150                .values()
151                .flat_map(|lc| lc.as_slice().to_vec())
152                .filter(|c| c.enabled)
153                .collect();
154            lsp.set_universal_configs(universal_servers);
155        }
156
157        // Propagate editor config to all split and buffer view states
158        for view_state in self.split_view_states.values_mut() {
159            view_state.show_line_numbers = self.config.editor.line_numbers;
160            for buf_state in view_state.keyed_states.values_mut() {
161                buf_state.rulers = self.config.editor.rulers.clone();
162            }
163        }
164
165        // Apply bar visibility changes immediately
166        self.menu_bar_visible = self.config.editor.show_menu_bar;
167        self.tab_bar_visible = self.config.editor.show_tab_bar;
168        self.status_bar_visible = self.config.editor.show_status_bar;
169        self.prompt_line_visible = self.config.editor.show_prompt_line;
170
171        // Propagate file-explorer settings to live runtime state (IgnorePatterns
172        // and width are shadows of config, not read live on each render).
173        self.file_explorer_width = self.config.file_explorer.width;
174        self.file_explorer_side = self.config.file_explorer.side;
175        if let Some(ref mut explorer) = self.file_explorer {
176            let patterns = explorer.ignore_patterns_mut();
177            patterns.set_show_hidden(self.config.file_explorer.show_hidden);
178            patterns.set_show_gitignored(self.config.file_explorer.show_gitignored);
179        }
180
181        // On Windows, switch mouse tracking mode when mouse_hover_enabled changes.
182        // Mode 1003 (all motion) is used for hover; mode 1002 (cell motion) otherwise.
183        #[cfg(windows)]
184        if old_mouse_hover != self.config.editor.mouse_hover_enabled {
185            let mode = if self.config.editor.mouse_hover_enabled {
186                fresh_winterm::MouseMode::AllMotion
187            } else {
188                // Clear any pending hover state when disabling
189                self.mouse_state.lsp_hover_state = None;
190                self.mouse_state.lsp_hover_request_sent = false;
191                fresh_winterm::MouseMode::CellMotion
192            };
193            if let Err(e) = fresh_winterm::set_mouse_mode(mode) {
194                tracing::error!("Failed to switch mouse mode: {}", e);
195            }
196        }
197
198        // Propagate tab_size/use_tabs/auto_close/whitespace visibility to all open buffers
199        // Each buffer resolves its settings from its language + the new global config
200        for state in self.buffers.values_mut() {
201            let mut whitespace =
202                crate::config::WhitespaceVisibility::from_editor_config(&self.config.editor);
203            state.buffer_settings.auto_close = self.config.editor.auto_close;
204            if let Some(lang_config) = self.config.languages.get(&state.language) {
205                state.buffer_settings.tab_size =
206                    lang_config.tab_size.unwrap_or(self.config.editor.tab_size);
207                state.buffer_settings.use_tabs =
208                    lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs);
209                whitespace =
210                    whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
211                // Auto close: language override (only if globally enabled)
212                if state.buffer_settings.auto_close {
213                    if let Some(lang_auto_close) = lang_config.auto_close {
214                        state.buffer_settings.auto_close = lang_auto_close;
215                    }
216                }
217                // Word characters: from language config
218                if let Some(ref wc) = lang_config.word_characters {
219                    state.buffer_settings.word_characters = wc.clone();
220                } else {
221                    state.buffer_settings.word_characters.clear();
222                }
223            } else {
224                state.buffer_settings.tab_size = self.config.editor.tab_size;
225                state.buffer_settings.use_tabs = self.config.editor.use_tabs;
226            }
227            state.buffer_settings.whitespace = whitespace;
228        }
229
230        // Save ONLY the changes to disk (preserves external edits to the config file)
231        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
232
233        let layer_name = match target_layer {
234            ConfigLayer::User => "User",
235            ConfigLayer::Project => "Project",
236            ConfigLayer::Session => "Session",
237            ConfigLayer::System => "System", // Should never happen
238        };
239
240        match resolver.save_changes_to_layer(&pending_changes, &pending_deletions, target_layer) {
241            Ok(()) => {
242                self.set_status_message(
243                    t!("settings.saved_to_layer", layer = layer_name).to_string(),
244                );
245                // Clear settings state entirely so next open creates fresh state
246                // from the updated config. This fixes issue #474 where reopening
247                // settings after save would show stale values.
248                self.settings_state = None;
249            }
250            Err(e) => {
251                self.set_status_message(
252                    t!("settings.failed_to_save", error = e.to_string()).to_string(),
253                );
254            }
255        }
256    }
257
258    /// Open the config file for the specified layer in the editor.
259    /// Creates the file with default template if it doesn't exist.
260    /// If there are pending changes in the Settings UI, warns the user and doesn't proceed.
261    pub fn open_config_file(&mut self, layer: ConfigLayer) -> AnyhowResult<()> {
262        // Check for pending changes before opening config file
263        if let Some(ref state) = self.settings_state {
264            if state.has_changes() {
265                self.set_status_message(t!("settings.pending_changes").to_string());
266                return Ok(());
267            }
268        }
269
270        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
271
272        let path = match layer {
273            ConfigLayer::User => resolver.user_config_path(),
274            ConfigLayer::Project => resolver.project_config_write_path(),
275            ConfigLayer::Session => resolver.session_config_path(),
276            ConfigLayer::System => {
277                self.set_status_message(t!("settings.cannot_edit_system").to_string());
278                return Ok(());
279            }
280        };
281
282        // Create parent directory if needed
283        if let Some(parent) = path.parent() {
284            self.authority.filesystem.create_dir_all(parent)?;
285        }
286
287        // Create file with template if it doesn't exist
288        if !self.authority.filesystem.exists(&path) {
289            let template = match layer {
290                ConfigLayer::User => {
291                    r#"{
292  "version": 1,
293  "theme": "default",
294  "editor": {
295    "tab_size": 4,
296    "line_numbers": true
297  }
298}
299"#
300                }
301                ConfigLayer::Project => {
302                    r#"{
303  "version": 1,
304  "editor": {
305    "tab_size": 4
306  },
307  "languages": {}
308}
309"#
310                }
311                ConfigLayer::Session => {
312                    r#"{
313  "version": 1
314}
315"#
316                }
317                ConfigLayer::System => unreachable!(),
318            };
319            self.authority
320                .filesystem
321                .write_file(&path, template.as_bytes())?;
322        }
323
324        // Close settings and open the config file
325        self.settings_state = None;
326        match self.open_file(&path) {
327            Ok(_) => {
328                let layer_name = match layer {
329                    ConfigLayer::User => "User",
330                    ConfigLayer::Project => "Project",
331                    ConfigLayer::Session => "Session",
332                    ConfigLayer::System => "System",
333                };
334                self.set_status_message(
335                    t!(
336                        "settings.editing_config",
337                        layer = layer_name,
338                        path = path.display().to_string()
339                    )
340                    .to_string(),
341                );
342            }
343            Err(e) => {
344                // Check if this is a large file encoding confirmation error
345                if let Some(confirmation) =
346                    e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
347                {
348                    self.start_large_file_encoding_confirmation(confirmation);
349                } else {
350                    self.set_status_message(
351                        t!("file.error_opening", error = e.to_string()).to_string(),
352                    );
353                }
354            }
355        }
356
357        Ok(())
358    }
359
360    /// Navigate settings up
361    pub fn settings_navigate_up(&mut self) {
362        if let Some(ref mut state) = self.settings_state {
363            state.select_prev();
364        }
365    }
366
367    /// Navigate settings down
368    pub fn settings_navigate_down(&mut self) {
369        if let Some(ref mut state) = self.settings_state {
370            state.select_next();
371        }
372    }
373
374    /// Activate/toggle the currently selected setting
375    pub fn settings_activate_current(&mut self) {
376        use crate::view::settings::items::SettingControl;
377        use crate::view::settings::FocusPanel;
378
379        // Check if we're in the Footer panel - handle button activation
380        let focus_panel = self
381            .settings_state
382            .as_ref()
383            .map(|s| s.focus_panel())
384            .unwrap_or(FocusPanel::Categories);
385
386        if focus_panel == FocusPanel::Footer {
387            let button_index = self
388                .settings_state
389                .as_ref()
390                .map(|s| s.footer_button_index)
391                .unwrap_or(2);
392            match button_index {
393                0 => {
394                    // Layer button - cycle target layer
395                    if let Some(ref mut state) = self.settings_state {
396                        state.cycle_target_layer();
397                    }
398                }
399                1 => {
400                    // Reset/Inherit button — for nullable items, set to null (inherit);
401                    // for non-nullable items, reset to default
402                    if let Some(ref mut state) = self.settings_state {
403                        let is_nullable_set = state
404                            .current_item()
405                            .map(|item| item.nullable && !item.is_null)
406                            .unwrap_or(false);
407                        if is_nullable_set {
408                            state.set_current_to_null();
409                        } else {
410                            state.reset_current_to_default();
411                        }
412                    }
413                }
414                2 => {
415                    // Save button - save and close
416                    self.close_settings(true);
417                }
418                3 => {
419                    // Cancel button
420                    self.close_settings(false);
421                }
422                _ => {}
423            }
424            return;
425        }
426
427        // When Categories panel is focused, Enter does nothing to settings controls
428        // (keys should not leak to the right panel)
429        if focus_panel == FocusPanel::Categories {
430            return;
431        }
432
433        // Get the current item's control type to determine action
434        let control_type = {
435            if let Some(ref state) = self.settings_state {
436                state.current_item().map(|item| match &item.control {
437                    SettingControl::Toggle(_) => "toggle",
438                    SettingControl::Number(_) => "number",
439                    SettingControl::Dropdown(_) => "dropdown",
440                    SettingControl::Text(_) => "text",
441                    SettingControl::TextList(_) => "textlist",
442                    SettingControl::DualList(_) => "duallist",
443                    SettingControl::Map(_) => "map",
444                    SettingControl::ObjectArray(_) => "objectarray",
445                    SettingControl::Json(_) => "json",
446                    SettingControl::Complex { .. } => "complex",
447                })
448            } else {
449                None
450            }
451        };
452
453        // Perform the action based on control type
454        match control_type {
455            Some("toggle") => {
456                if let Some(ref mut state) = self.settings_state {
457                    if let Some(item) = state.current_item_mut() {
458                        if let SettingControl::Toggle(ref mut toggle_state) = item.control {
459                            toggle_state.checked = !toggle_state.checked;
460                        }
461                    }
462                    state.on_value_changed();
463                }
464            }
465            Some("dropdown") => {
466                // Toggle dropdown open/closed, or confirm selection if open
467                if let Some(ref mut state) = self.settings_state {
468                    if state.is_dropdown_open() {
469                        state.dropdown_confirm();
470                    } else {
471                        state.dropdown_toggle();
472                    }
473                }
474            }
475            Some("textlist") => {
476                // Enter text editing mode for TextList controls
477                if let Some(ref mut state) = self.settings_state {
478                    state.start_editing();
479                }
480            }
481            Some("map") => {
482                // For Map controls: check if map has a value schema (supports entry dialogs)
483                if let Some(ref mut state) = self.settings_state {
484                    if let Some(item) = state.current_item_mut() {
485                        if let SettingControl::Map(ref mut map_state) = item.control {
486                            if map_state.focused_entry.is_none() {
487                                // On add-new row: open dialog with empty key
488                                if map_state.value_schema.is_some() {
489                                    state.open_add_entry_dialog();
490                                }
491                            } else if map_state.value_schema.is_some() {
492                                // Map has schema: open entry dialog
493                                state.open_entry_dialog();
494                            } else {
495                                // For other maps: toggle expanded
496                                if let Some(idx) = map_state.focused_entry {
497                                    if map_state.expanded.contains(&idx) {
498                                        map_state.expanded.retain(|&i| i != idx);
499                                    } else {
500                                        map_state.expanded.push(idx);
501                                    }
502                                }
503                            }
504                        }
505                    }
506                    state.on_value_changed();
507                }
508            }
509            Some("text") => {
510                // For Text controls: enter text editing mode
511                if let Some(ref mut state) = self.settings_state {
512                    state.start_editing();
513                }
514            }
515            Some("number") => {
516                // For Number controls: enter number editing mode
517                if let Some(ref mut state) = self.settings_state {
518                    state.start_number_editing();
519                }
520            }
521            _ => {}
522        }
523    }
524
525    /// Increment the current setting value (for Number and Dropdown controls)
526    pub fn settings_increment_current(&mut self) {
527        use crate::view::settings::items::SettingControl;
528        use crate::view::settings::FocusPanel;
529
530        // Check if we're in the Footer panel - navigate buttons instead
531        let focus_panel = self
532            .settings_state
533            .as_ref()
534            .map(|s| s.focus_panel())
535            .unwrap_or(FocusPanel::Categories);
536
537        if focus_panel == FocusPanel::Footer {
538            if let Some(ref mut state) = self.settings_state {
539                // Navigate to next footer button (wrapping around)
540                state.footer_button_index = (state.footer_button_index + 1) % 4;
541            }
542            return;
543        }
544
545        // When Categories panel is focused, Left/Right don't affect settings controls
546        if focus_panel == FocusPanel::Categories {
547            return;
548        }
549
550        let control_type = {
551            if let Some(ref state) = self.settings_state {
552                state.current_item().map(|item| match &item.control {
553                    SettingControl::Number(_) => "number",
554                    SettingControl::Dropdown(_) => "dropdown",
555                    _ => "other",
556                })
557            } else {
558                None
559            }
560        };
561
562        match control_type {
563            Some("number") => {
564                if let Some(ref mut state) = self.settings_state {
565                    if let Some(item) = state.current_item_mut() {
566                        if let SettingControl::Number(ref mut num_state) = item.control {
567                            num_state.increment();
568                        }
569                    }
570                    state.on_value_changed();
571                }
572            }
573            Some("dropdown") => {
574                if let Some(ref mut state) = self.settings_state {
575                    if let Some(item) = state.current_item_mut() {
576                        if let SettingControl::Dropdown(ref mut dropdown_state) = item.control {
577                            dropdown_state.select_next();
578                        }
579                    }
580                    state.on_value_changed();
581                }
582            }
583            _ => {}
584        }
585    }
586
587    /// Decrement the current setting value (for Number and Dropdown controls)
588    pub fn settings_decrement_current(&mut self) {
589        use crate::view::settings::items::SettingControl;
590        use crate::view::settings::FocusPanel;
591
592        // Check if we're in the Footer panel - navigate buttons instead
593        let focus_panel = self
594            .settings_state
595            .as_ref()
596            .map(|s| s.focus_panel())
597            .unwrap_or(FocusPanel::Categories);
598
599        if focus_panel == FocusPanel::Footer {
600            if let Some(ref mut state) = self.settings_state {
601                // Navigate to previous footer button (wrapping around)
602                state.footer_button_index = if state.footer_button_index == 0 {
603                    3
604                } else {
605                    state.footer_button_index - 1
606                };
607            }
608            return;
609        }
610
611        // When Categories panel is focused, Left/Right don't affect settings controls
612        if focus_panel == FocusPanel::Categories {
613            return;
614        }
615
616        let control_type = {
617            if let Some(ref state) = self.settings_state {
618                state.current_item().map(|item| match &item.control {
619                    SettingControl::Number(_) => "number",
620                    SettingControl::Dropdown(_) => "dropdown",
621                    _ => "other",
622                })
623            } else {
624                None
625            }
626        };
627
628        match control_type {
629            Some("number") => {
630                if let Some(ref mut state) = self.settings_state {
631                    if let Some(item) = state.current_item_mut() {
632                        if let SettingControl::Number(ref mut num_state) = item.control {
633                            num_state.decrement();
634                        }
635                    }
636                    state.on_value_changed();
637                }
638            }
639            Some("dropdown") => {
640                if let Some(ref mut state) = self.settings_state {
641                    if let Some(item) = state.current_item_mut() {
642                        if let SettingControl::Dropdown(ref mut dropdown_state) = item.control {
643                            dropdown_state.select_prev();
644                        }
645                    }
646                    state.on_value_changed();
647                }
648            }
649            _ => {}
650        }
651    }
652
653    /// Apply plugin configuration changes by loading/unloading plugins as needed
654    fn apply_plugin_config_changes(
655        &mut self,
656        old_plugins: &std::collections::HashMap<String, crate::config::PluginConfig>,
657    ) {
658        // Collect changes first to avoid borrow issues
659        let changes: Vec<_> = self
660            .config
661            .plugins
662            .iter()
663            .filter_map(|(name, new_config)| {
664                let was_enabled = old_plugins.get(name).map(|c| c.enabled).unwrap_or(true);
665                if new_config.enabled != was_enabled {
666                    Some((name.clone(), new_config.enabled, new_config.path.clone()))
667                } else {
668                    None
669                }
670            })
671            .collect();
672
673        // Apply changes
674        for (name, now_enabled, path) in changes {
675            if now_enabled {
676                // Plugin was disabled, now enabled - load it
677                if let Some(ref path) = path {
678                    tracing::info!("Loading newly enabled plugin: {}", name);
679                    if let Err(e) = self.plugin_manager.load_plugin(path) {
680                        tracing::error!("Failed to load plugin '{}': {}", name, e);
681                        self.set_status_message(format!("Failed to load plugin '{}': {}", name, e));
682                    }
683                }
684            } else {
685                // Plugin was enabled, now disabled - unload it
686                tracing::info!("Unloading disabled plugin: {}", name);
687                if let Err(e) = self.plugin_manager.unload_plugin(&name) {
688                    tracing::error!("Failed to unload plugin '{}': {}", name, e);
689                    self.set_status_message(format!("Failed to unload plugin '{}': {}", name, e));
690                }
691            }
692        }
693    }
694}