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