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