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