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