fresh-editor 0.3.8

A lightweight, fast terminal-based text editor with LSP support and TypeScript plugins
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
//! Toggle actions and configuration operations for the Editor.
//!
//! This module contains toggle methods and configuration operations:
//! - Toggle line numbers, debug highlights, menu bar
//! - Toggle mouse capture, mouse hover, inlay hints
//! - Reset buffer settings
//! - Config dump, save, and reload

use crate::types::LspServerConfig;
use rust_i18n::t;

use crate::config::{Config, FileExplorerSide};
use crate::config_io::{ConfigLayer, ConfigResolver};
use crate::input::keybindings::KeybindingResolver;

use super::Editor;

impl Editor {
    /// Toggle line numbers in the gutter for the active split.
    ///
    /// Line number visibility is stored per-split in `BufferViewState` so that
    /// different splits of the same buffer can independently show/hide line numbers
    /// (e.g., source mode shows them, compose mode hides them).
    pub fn toggle_line_numbers(&mut self) {
        let active_split = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .active_split();
        if let Some(vs) = self
            .windows
            .get_mut(&self.active_window)
            .and_then(|w| w.split_view_states_mut())
            .expect("active window must have a populated split layout")
            .get_mut(&active_split)
        {
            let currently_shown = vs.show_line_numbers;
            vs.show_line_numbers = !currently_shown;
            if currently_shown {
                self.set_status_message(t!("toggle.line_numbers_hidden").to_string());
            } else {
                self.set_status_message(t!("toggle.line_numbers_shown").to_string());
            }
        }
    }

    /// Toggle menu bar visibility.
    ///
    /// `editor.show_menu_bar` is a global preference, so the toggle updates the
    /// runtime config and persists the change to the user config layer (same
    /// pattern as the file-explorer toggles). See issue #1156.
    pub fn toggle_menu_bar(&mut self) {
        let new_value = !self.active_window_mut().menu_bar_visible;
        self.config_mut().editor.show_menu_bar = new_value;
        self.active_window_mut().menu_bar_visible = new_value;
        // When explicitly toggling, clear auto-show state
        self.active_window_mut().menu_bar_auto_shown = false;
        // Close any open menu when hiding the menu bar
        if !self.active_window_mut().menu_bar_visible {
            self.menu_state.close_menu();
        }
        self.persist_config_change("/editor/show_menu_bar", serde_json::Value::Bool(new_value));
        let status = if self.active_window_mut().menu_bar_visible {
            t!("toggle.menu_bar_shown")
        } else {
            t!("toggle.menu_bar_hidden")
        };
        self.set_status_message(status.to_string());
    }

    // `toggle_tab_bar` / `toggle_status_bar` / `toggle_prompt_line` and
    // their `*_visible` getters live on `impl Window` — call them via
    // `self.active_window_mut().toggle_tab_bar()` etc. (or read
    // `active_window().tab_bar_visible` for the flag directly).

    /// Toggle the file explorer side between left and right.
    ///
    /// Updates the runtime config and the active window's runtime side
    /// shadow, then persists the change to the user config layer (same
    /// pattern as `toggle_menu_bar`).
    pub fn toggle_file_explorer_side(&mut self) {
        let new_side = match self.config.file_explorer.side {
            FileExplorerSide::Left => FileExplorerSide::Right,
            FileExplorerSide::Right => FileExplorerSide::Left,
        };
        self.config_mut().file_explorer.side = new_side;
        self.active_window_mut().file_explorer_side = new_side;
        self.persist_config_change(
            "/file_explorer/side",
            serde_json::json!(match new_side {
                FileExplorerSide::Left => "left",
                FileExplorerSide::Right => "right",
            }),
        );
        let status = match new_side {
            FileExplorerSide::Left => t!("toggle.file_explorer_side_left"),
            FileExplorerSide::Right => t!("toggle.file_explorer_side_right"),
        };
        self.set_status_message(status.to_string());
    }

    /// Toggle vertical scrollbar visibility
    pub fn toggle_vertical_scrollbar(&mut self) {
        let new_value = !self.config.editor.show_vertical_scrollbar;
        self.config_mut().editor.show_vertical_scrollbar = new_value;
        let status = if self.config.editor.show_vertical_scrollbar {
            t!("toggle.vertical_scrollbar_shown")
        } else {
            t!("toggle.vertical_scrollbar_hidden")
        };
        self.set_status_message(status.to_string());
    }

    /// Toggle horizontal scrollbar visibility
    pub fn toggle_horizontal_scrollbar(&mut self) {
        let new_value = !self.config.editor.show_horizontal_scrollbar;
        self.config_mut().editor.show_horizontal_scrollbar = new_value;
        let status = if self.config.editor.show_horizontal_scrollbar {
            t!("toggle.horizontal_scrollbar_shown")
        } else {
            t!("toggle.horizontal_scrollbar_hidden")
        };
        self.set_status_message(status.to_string());
    }

    /// Reset buffer settings (tab_size, use_tabs, auto_close, whitespace visibility) to config defaults
    pub fn reset_buffer_settings(&mut self) {
        use crate::config::WhitespaceVisibility;
        let buffer_id = self.active_buffer();

        // Determine settings from config using buffer's stored language
        let mut whitespace = WhitespaceVisibility::from_editor_config(&self.config.editor);
        let mut auto_close = self.config.editor.auto_close;
        let mut word_characters = String::new();
        let (tab_size, use_tabs) = if let Some(state) = self
            .windows
            .get(&self.active_window)
            .map(|w| &w.buffers)
            .expect("active window present")
            .get(&buffer_id)
        {
            let language = &state.language;
            if let Some(lang_config) = self.config.languages.get(language) {
                whitespace =
                    whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
                // Auto close: language override (only if globally enabled)
                if auto_close {
                    if let Some(lang_auto_close) = lang_config.auto_close {
                        auto_close = lang_auto_close;
                    }
                }
                if let Some(ref wc) = lang_config.word_characters {
                    word_characters = wc.clone();
                }
                (
                    lang_config.tab_size.unwrap_or(self.config.editor.tab_size),
                    lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs),
                )
            } else {
                (self.config.editor.tab_size, self.config.editor.use_tabs)
            }
        } else {
            (self.config.editor.tab_size, self.config.editor.use_tabs)
        };

        // Apply settings to buffer
        if let Some(state) = self
            .windows
            .get_mut(&self.active_window)
            .map(|w| &mut w.buffers)
            .expect("active window present")
            .get_mut(&buffer_id)
        {
            state.buffer_settings.tab_size = tab_size;
            state.buffer_settings.use_tabs = use_tabs;
            state.buffer_settings.auto_close = auto_close;
            state.buffer_settings.whitespace = whitespace;
            state.buffer_settings.word_characters = word_characters;
        }

        self.set_status_message(t!("toggle.buffer_settings_reset").to_string());
    }

    /// Toggle mouse capture on/off
    pub fn toggle_mouse_capture(&mut self) {
        use std::io::stdout;

        self.active_window_mut().mouse_enabled = !self.active_window_mut().mouse_enabled;

        if self.active_window_mut().mouse_enabled {
            // Best-effort terminal mouse capture toggle.
            #[allow(clippy::let_underscore_must_use)]
            let _ = crossterm::execute!(stdout(), crossterm::event::EnableMouseCapture);
            self.set_status_message(t!("toggle.mouse_capture_enabled").to_string());
        } else {
            // Best-effort terminal mouse capture toggle.
            #[allow(clippy::let_underscore_must_use)]
            let _ = crossterm::execute!(stdout(), crossterm::event::DisableMouseCapture);
            self.set_status_message(t!("toggle.mouse_capture_disabled").to_string());
        }
    }

    /// Check if mouse capture is enabled
    pub fn is_mouse_enabled(&self) -> bool {
        self.active_window().mouse_enabled
    }

    /// Toggle mouse hover for LSP on/off
    ///
    /// On Windows, this also switches the mouse tracking mode: mode 1003
    /// (all motion) when enabled, mode 1002 (cell motion) when disabled.
    pub fn toggle_mouse_hover(&mut self) {
        let new_value = !self.config.editor.mouse_hover_enabled;
        self.config_mut().editor.mouse_hover_enabled = new_value;

        if self.config.editor.mouse_hover_enabled {
            self.set_status_message(t!("toggle.mouse_hover_enabled").to_string());
        } else {
            // Clear any pending hover state
            self.active_window_mut().mouse_state.lsp_hover_state = None;
            self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
            self.set_status_message(t!("toggle.mouse_hover_disabled").to_string());
        }

        // On Windows, switch mouse tracking mode to match
        #[cfg(windows)]
        {
            let mode = if self.config.editor.mouse_hover_enabled {
                fresh_winterm::MouseMode::AllMotion
            } else {
                fresh_winterm::MouseMode::CellMotion
            };
            if let Err(e) = fresh_winterm::set_mouse_mode(mode) {
                tracing::error!("Failed to switch mouse mode: {}", e);
            }
        }
    }

    /// Check if mouse hover is enabled
    pub fn is_mouse_hover_enabled(&self) -> bool {
        self.config.editor.mouse_hover_enabled
    }

    /// Set GPM active flag (enables software mouse cursor rendering)
    ///
    /// When GPM is used for mouse input on Linux consoles, we need to draw
    /// our own mouse cursor because GPM can't draw on the alternate screen
    /// buffer used by TUI applications.
    pub fn set_gpm_active(&mut self, active: bool) {
        self.active_window_mut().gpm_active = active;
    }

    /// Toggle inlay hints visibility
    pub fn toggle_inlay_hints(&mut self) {
        let new_value = !self.config.editor.enable_inlay_hints;
        self.config_mut().editor.enable_inlay_hints = new_value;
        // `Window::send_lsp_changes_for_buffer` reads
        // `resources.config.editor.enable_inlay_hints`; sync so the per-edit
        // LSP refresh sees the new value without waiting for a reload.
        self.sync_windows_config();

        if self.config.editor.enable_inlay_hints {
            // Re-request inlay hints for the active buffer
            self.request_inlay_hints_for_active_buffer();
            self.set_status_message(t!("toggle.inlay_hints_enabled").to_string());
        } else {
            // Clear inlay hints from all buffers
            for (_, state) in self
                .windows
                .get_mut(&self.active_window)
                .map(|w| &mut w.buffers)
                .expect("active window present")
            {
                state.virtual_texts.clear(&mut state.marker_list);
            }
            self.set_status_message(t!("toggle.inlay_hints_disabled").to_string());
        }
    }

    /// Dump the current configuration to the user's config file
    pub fn dump_config(&mut self) {
        // Create the config directory if it doesn't exist
        if let Err(e) = self
            .authority
            .filesystem
            .create_dir_all(&self.dir_context.config_dir)
        {
            self.set_status_message(
                t!("error.config_dir_failed", error = e.to_string()).to_string(),
            );
            return;
        }

        let config_path = self.dir_context.config_path();
        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());

        // Save the config to user layer
        match resolver.save_to_layer(&self.config, ConfigLayer::User) {
            Ok(()) => {
                // Open the saved config file in a new buffer
                match self.open_file(&config_path) {
                    Ok(_buffer_id) => {
                        self.set_status_message(
                            t!("config.saved", path = config_path.display().to_string())
                                .to_string(),
                        );
                    }
                    Err(e) => {
                        // Check if this is a large file encoding confirmation error
                        if let Some(confirmation) =
                            e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
                        {
                            self.start_large_file_encoding_confirmation(confirmation);
                        } else {
                            self.set_status_message(
                                t!("config.saved_failed_open", error = e.to_string()).to_string(),
                            );
                        }
                    }
                }
            }
            Err(e) => {
                self.set_status_message(
                    t!("error.config_save_failed", error = e.to_string()).to_string(),
                );
            }
        }
    }

    /// Save the current configuration to file (without opening it)
    ///
    /// Returns Ok(()) on success, or an error message on failure
    pub fn save_config(&self) -> Result<(), String> {
        // Create the config directory if it doesn't exist
        self.authority
            .filesystem
            .create_dir_all(&self.dir_context.config_dir)
            .map_err(|e| format!("Failed to create config directory: {}", e))?;

        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
        resolver
            .save_to_layer(&self.config, ConfigLayer::User)
            .map_err(|e| format!("Failed to save config: {}", e))
    }

    /// Reload configuration from the config file
    ///
    /// This reloads the config from disk, applies runtime changes (theme, keybindings),
    /// and emits a config_changed event so plugins can update their state accordingly.
    /// Uses the layered config system to properly merge with defaults.
    pub fn reload_config(&mut self) {
        let old_theme = self.config.theme.clone();
        self.set_config(Config::load_with_layers(
            &self.dir_context,
            &self.working_dir,
        ));

        // Refresh cached raw user config for plugins
        self.set_user_config_raw(Config::read_user_config_raw(&self.working_dir));

        // Apply theme change if needed
        if old_theme != self.config.theme {
            if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
                *self.theme.write().unwrap() = theme;
                tracing::info!("Theme changed to '{}'", self.config.theme.0);
            } else {
                tracing::error!("Theme '{}' not found", self.config.theme.0);
            }
        }

        // Always reload keybindings (complex types don't implement PartialEq)
        *self.keybindings.write().unwrap() = KeybindingResolver::new(&self.config);

        // Update clipboard configuration
        self.clipboard.apply_config(&self.config.clipboard);

        // Apply bar visibility changes immediately
        self.active_window_mut().menu_bar_visible = self.config.editor.show_menu_bar;
        self.active_window_mut().tab_bar_visible = self.config.editor.show_tab_bar;
        self.active_window_mut().status_bar_visible = self.config.editor.show_status_bar;
        self.active_window_mut().prompt_line_visible = self.config.editor.show_prompt_line;

        // Update LSP configs
        let __active_id = self.active_window;
        if let Some(lsp) = self
            .windows
            .get_mut(&__active_id)
            .and_then(|w| w.lsp.as_mut())
        {
            for (language, lsp_configs) in &self.config.lsp {
                lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
            }
            // Configure universal (global) LSP servers
            let universal_servers: Vec<LspServerConfig> = self
                .config
                .universal_lsp
                .values()
                .flat_map(|lc| lc.as_slice().to_vec())
                .filter(|c| c.enabled)
                .collect();
            lsp.set_universal_configs(universal_servers);
        }

        // Emit event so plugins know config changed
        let config_path = Config::find_config_path(&self.working_dir);
        self.emit_event(
            "config_changed",
            serde_json::json!({
                "path": config_path.map(|p| p.to_string_lossy().into_owned()),
            }),
        );
    }

    /// Reload the theme registry from disk.
    ///
    /// Call this after installing new theme packages or saving new themes.
    /// This rescans all theme directories and updates the available themes list.
    pub fn reload_themes(&mut self) {
        use crate::view::theme::ThemeLoader;

        let theme_loader = ThemeLoader::new(self.dir_context.themes_dir());
        self.theme_registry = std::sync::Arc::new(theme_loader.load_all(&[]));
        self.expanded_menus_cache.invalidate();

        // Propagate the new registry to every window's resources so
        // window-side reads see the updated catalogue. (Theme registry
        // is `Arc<ThemeRegistry>` not `Arc<RwLock<>>`, so swapping the
        // Editor's pointer leaves Window clones stale unless we sync.)
        for w in self.windows.values_mut() {
            w.resources.theme_registry = self.theme_registry.clone();
        }

        // Update shared theme cache for plugin access
        *self.theme_cache.write().unwrap() = self.theme_registry.to_json_map();

        // Re-apply current theme if it still exists, otherwise it might have been updated
        if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
            *self.theme.write().unwrap() = theme;
        }

        tracing::info!(
            "Theme registry reloaded ({} themes)",
            self.theme_registry.len()
        );

        // Emit event so plugins know themes changed
        self.emit_event("themes_changed", serde_json::json!({}));
    }

    /// Persist a single config change to the user config file.
    ///
    /// Used when toggling settings via menu/command palette so that
    /// the change is saved immediately (matching the settings UI behavior).
    pub(super) fn persist_config_change(&self, json_pointer: &str, value: serde_json::Value) {
        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
        let changes = std::collections::HashMap::from([(json_pointer.to_string(), value)]);
        let deletions = std::collections::HashSet::new();
        if let Err(e) = resolver.save_changes_to_layer(&changes, &deletions, ConfigLayer::User) {
            tracing::error!("Failed to persist config change {}: {}", json_pointer, e);
        }
    }
}