Skip to main content

fresh/
config.rs

1use crate::types::{context_keys, LspServerConfig, ProcessLimits};
2
3use rust_i18n::t;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use std::borrow::Cow;
7use std::collections::HashMap;
8use std::ops::Deref;
9use std::path::Path;
10
11/// Newtype for theme name that generates proper JSON Schema with enum options
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct ThemeName(pub String);
15
16impl ThemeName {
17    /// Built-in theme options shown in the settings dropdown
18    pub const BUILTIN_OPTIONS: &'static [&'static str] =
19        &["dark", "light", "high-contrast", "nostalgia"];
20}
21
22impl Deref for ThemeName {
23    type Target = str;
24    fn deref(&self) -> &Self::Target {
25        &self.0
26    }
27}
28
29impl From<String> for ThemeName {
30    fn from(s: String) -> Self {
31        Self(s)
32    }
33}
34
35impl From<&str> for ThemeName {
36    fn from(s: &str) -> Self {
37        Self(s.to_string())
38    }
39}
40
41impl PartialEq<str> for ThemeName {
42    fn eq(&self, other: &str) -> bool {
43        self.0 == other
44    }
45}
46
47impl PartialEq<ThemeName> for str {
48    fn eq(&self, other: &ThemeName) -> bool {
49        self == other.0
50    }
51}
52
53impl JsonSchema for ThemeName {
54    fn schema_name() -> Cow<'static, str> {
55        Cow::Borrowed("ThemeOptions")
56    }
57
58    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
59        schemars::json_schema!({
60            "description": "Available color themes",
61            "type": "string",
62            "enum": Self::BUILTIN_OPTIONS
63        })
64    }
65}
66
67/// Newtype for locale name that generates proper JSON Schema with enum options
68/// Wraps Option<String> to allow null for auto-detection from environment
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
70#[serde(transparent)]
71pub struct LocaleName(pub Option<String>);
72
73// Include the generated locale options from build.rs
74include!(concat!(env!("OUT_DIR"), "/locale_options.rs"));
75
76impl LocaleName {
77    /// Available locale options shown in the settings dropdown
78    /// null means auto-detect from environment
79    /// This is auto-generated from the locales/*.json files by build.rs
80    pub const LOCALE_OPTIONS: &'static [Option<&'static str>] = GENERATED_LOCALE_OPTIONS;
81
82    /// Get the inner value as Option<&str>
83    pub fn as_option(&self) -> Option<&str> {
84        self.0.as_deref()
85    }
86}
87
88impl From<Option<String>> for LocaleName {
89    fn from(s: Option<String>) -> Self {
90        Self(s)
91    }
92}
93
94impl From<Option<&str>> for LocaleName {
95    fn from(s: Option<&str>) -> Self {
96        Self(s.map(|s| s.to_string()))
97    }
98}
99
100impl JsonSchema for LocaleName {
101    fn schema_name() -> Cow<'static, str> {
102        Cow::Borrowed("LocaleOptions")
103    }
104
105    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
106        schemars::json_schema!({
107            "description": "UI locale (language). Use null for auto-detection from environment.",
108            "enum": Self::LOCALE_OPTIONS
109        })
110    }
111}
112
113/// Cursor style options for the terminal cursor
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
115#[serde(rename_all = "snake_case")]
116pub enum CursorStyle {
117    /// Use the terminal's default cursor style
118    #[default]
119    Default,
120    /// Blinking block cursor (█)
121    BlinkingBlock,
122    /// Solid block cursor (█)
123    SteadyBlock,
124    /// Blinking vertical bar cursor (│)
125    BlinkingBar,
126    /// Solid vertical bar cursor (│)
127    SteadyBar,
128    /// Blinking underline cursor (_)
129    BlinkingUnderline,
130    /// Solid underline cursor (_)
131    SteadyUnderline,
132}
133
134impl CursorStyle {
135    /// All available cursor style options
136    pub const OPTIONS: &'static [&'static str] = &[
137        "default",
138        "blinking_block",
139        "steady_block",
140        "blinking_bar",
141        "steady_bar",
142        "blinking_underline",
143        "steady_underline",
144    ];
145
146    /// Human-readable descriptions for each cursor style
147    pub const DESCRIPTIONS: &'static [&'static str] = &[
148        "Terminal default",
149        "█ Blinking block",
150        "█ Solid block",
151        "│ Blinking bar",
152        "│ Solid bar",
153        "_ Blinking underline",
154        "_ Solid underline",
155    ];
156
157    /// Convert to crossterm cursor style (runtime only)
158    #[cfg(feature = "runtime")]
159    pub fn to_crossterm_style(self) -> crossterm::cursor::SetCursorStyle {
160        use crossterm::cursor::SetCursorStyle;
161        match self {
162            Self::Default => SetCursorStyle::DefaultUserShape,
163            Self::BlinkingBlock => SetCursorStyle::BlinkingBlock,
164            Self::SteadyBlock => SetCursorStyle::SteadyBlock,
165            Self::BlinkingBar => SetCursorStyle::BlinkingBar,
166            Self::SteadyBar => SetCursorStyle::SteadyBar,
167            Self::BlinkingUnderline => SetCursorStyle::BlinkingUnderScore,
168            Self::SteadyUnderline => SetCursorStyle::SteadyUnderScore,
169        }
170    }
171
172    /// Parse from string (for command palette)
173    pub fn parse(s: &str) -> Option<Self> {
174        match s {
175            "default" => Some(CursorStyle::Default),
176            "blinking_block" => Some(CursorStyle::BlinkingBlock),
177            "steady_block" => Some(CursorStyle::SteadyBlock),
178            "blinking_bar" => Some(CursorStyle::BlinkingBar),
179            "steady_bar" => Some(CursorStyle::SteadyBar),
180            "blinking_underline" => Some(CursorStyle::BlinkingUnderline),
181            "steady_underline" => Some(CursorStyle::SteadyUnderline),
182            _ => None,
183        }
184    }
185
186    /// Convert to string representation
187    pub fn as_str(self) -> &'static str {
188        match self {
189            Self::Default => "default",
190            Self::BlinkingBlock => "blinking_block",
191            Self::SteadyBlock => "steady_block",
192            Self::BlinkingBar => "blinking_bar",
193            Self::SteadyBar => "steady_bar",
194            Self::BlinkingUnderline => "blinking_underline",
195            Self::SteadyUnderline => "steady_underline",
196        }
197    }
198}
199
200impl JsonSchema for CursorStyle {
201    fn schema_name() -> Cow<'static, str> {
202        Cow::Borrowed("CursorStyle")
203    }
204
205    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
206        schemars::json_schema!({
207            "description": "Terminal cursor style",
208            "type": "string",
209            "enum": Self::OPTIONS
210        })
211    }
212}
213
214/// Newtype for keybinding map name that generates proper JSON Schema with enum options
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(transparent)]
217pub struct KeybindingMapName(pub String);
218
219impl KeybindingMapName {
220    /// Built-in keybinding map options shown in the settings dropdown
221    pub const BUILTIN_OPTIONS: &'static [&'static str] = &["default", "emacs", "vscode", "macos"];
222}
223
224impl Deref for KeybindingMapName {
225    type Target = str;
226    fn deref(&self) -> &Self::Target {
227        &self.0
228    }
229}
230
231impl From<String> for KeybindingMapName {
232    fn from(s: String) -> Self {
233        Self(s)
234    }
235}
236
237impl From<&str> for KeybindingMapName {
238    fn from(s: &str) -> Self {
239        Self(s.to_string())
240    }
241}
242
243impl PartialEq<str> for KeybindingMapName {
244    fn eq(&self, other: &str) -> bool {
245        self.0 == other
246    }
247}
248
249/// Line ending format for new files
250#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "lowercase")]
252pub enum LineEndingOption {
253    /// Unix/Linux/macOS format (LF)
254    #[default]
255    Lf,
256    /// Windows format (CRLF)
257    Crlf,
258    /// Classic Mac format (CR) - rare
259    Cr,
260}
261
262impl LineEndingOption {
263    /// Convert to the buffer's LineEnding type
264    pub fn to_line_ending(&self) -> crate::model::buffer::LineEnding {
265        match self {
266            Self::Lf => crate::model::buffer::LineEnding::LF,
267            Self::Crlf => crate::model::buffer::LineEnding::CRLF,
268            Self::Cr => crate::model::buffer::LineEnding::CR,
269        }
270    }
271}
272
273impl JsonSchema for LineEndingOption {
274    fn schema_name() -> Cow<'static, str> {
275        Cow::Borrowed("LineEndingOption")
276    }
277
278    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
279        schemars::json_schema!({
280            "description": "Default line ending format for new files",
281            "type": "string",
282            "enum": ["lf", "crlf", "cr"],
283            "default": "lf"
284        })
285    }
286}
287
288/// Controls whether Enter accepts a completion suggestion.
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
290#[serde(rename_all = "lowercase")]
291pub enum AcceptSuggestionOnEnter {
292    /// Enter always accepts the completion
293    #[default]
294    On,
295    /// Enter inserts a newline (use Tab to accept)
296    Off,
297    /// Enter accepts only if the completion differs from typed text
298    Smart,
299}
300
301impl JsonSchema for AcceptSuggestionOnEnter {
302    fn schema_name() -> Cow<'static, str> {
303        Cow::Borrowed("AcceptSuggestionOnEnter")
304    }
305
306    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
307        schemars::json_schema!({
308            "description": "Controls whether Enter accepts a completion suggestion",
309            "type": "string",
310            "enum": ["on", "off", "smart"],
311            "default": "on"
312        })
313    }
314}
315
316impl PartialEq<KeybindingMapName> for str {
317    fn eq(&self, other: &KeybindingMapName) -> bool {
318        self == other.0
319    }
320}
321
322impl JsonSchema for KeybindingMapName {
323    fn schema_name() -> Cow<'static, str> {
324        Cow::Borrowed("KeybindingMapOptions")
325    }
326
327    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
328        schemars::json_schema!({
329            "description": "Available keybinding maps",
330            "type": "string",
331            "enum": Self::BUILTIN_OPTIONS
332        })
333    }
334}
335
336/// Main configuration structure
337#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
338pub struct Config {
339    /// Configuration version (for migration support)
340    /// Configs without this field are treated as version 0
341    #[serde(default)]
342    pub version: u32,
343
344    /// Color theme name
345    #[serde(default = "default_theme_name")]
346    pub theme: ThemeName,
347
348    /// UI locale (language) for translations
349    /// If not set, auto-detected from environment (LC_ALL, LC_MESSAGES, LANG)
350    #[serde(default)]
351    pub locale: LocaleName,
352
353    /// Check for new versions on startup (default: true).
354    /// When enabled, also sends basic anonymous telemetry (version, OS, terminal type).
355    #[serde(default = "default_true")]
356    pub check_for_updates: bool,
357
358    /// Editor behavior settings (indentation, line numbers, wrapping, etc.)
359    #[serde(default)]
360    pub editor: EditorConfig,
361
362    /// File explorer panel settings
363    #[serde(default)]
364    pub file_explorer: FileExplorerConfig,
365
366    /// File browser settings (Open File dialog)
367    #[serde(default)]
368    pub file_browser: FileBrowserConfig,
369
370    /// Terminal settings
371    #[serde(default)]
372    pub terminal: TerminalConfig,
373
374    /// Custom keybindings (overrides for the active map)
375    #[serde(default)]
376    pub keybindings: Vec<Keybinding>,
377
378    /// Named keybinding maps (user can define custom maps here)
379    /// Each map can optionally inherit from another map
380    #[serde(default)]
381    pub keybinding_maps: HashMap<String, KeymapConfig>,
382
383    /// Active keybinding map name
384    #[serde(default = "default_keybinding_map_name")]
385    pub active_keybinding_map: KeybindingMapName,
386
387    /// Per-language configuration overrides (tab size, formatters, etc.)
388    #[serde(default)]
389    pub languages: HashMap<String, LanguageConfig>,
390
391    /// LSP server configurations by language
392    #[serde(default)]
393    pub lsp: HashMap<String, LspServerConfig>,
394
395    /// Warning notification settings
396    #[serde(default)]
397    pub warnings: WarningsConfig,
398
399    /// Plugin configurations by plugin name
400    /// Plugins are auto-discovered from the plugins directory.
401    /// Use this to enable/disable specific plugins.
402    #[serde(default)]
403    #[schemars(extend("x-standalone-category" = true, "x-no-add" = true))]
404    pub plugins: HashMap<String, PluginConfig>,
405
406    /// Package manager settings for plugin/theme installation
407    #[serde(default)]
408    pub packages: PackagesConfig,
409}
410
411fn default_keybinding_map_name() -> KeybindingMapName {
412    // On macOS, default to the macOS keymap which has Mac-specific bindings
413    // (Ctrl+A/E for Home/End, Ctrl+Shift+Z for redo, etc.)
414    if cfg!(target_os = "macos") {
415        KeybindingMapName("macos".to_string())
416    } else {
417        KeybindingMapName("default".to_string())
418    }
419}
420
421fn default_theme_name() -> ThemeName {
422    ThemeName("high-contrast".to_string())
423}
424
425/// Editor behavior configuration
426#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
427pub struct EditorConfig {
428    // ===== Display =====
429    /// Show line numbers in the gutter (default for new buffers)
430    #[serde(default = "default_true")]
431    #[schemars(extend("x-section" = "Display"))]
432    pub line_numbers: bool,
433
434    /// Show line numbers relative to cursor position
435    #[serde(default = "default_false")]
436    #[schemars(extend("x-section" = "Display"))]
437    pub relative_line_numbers: bool,
438
439    /// Wrap long lines to fit the window width (default for new views)
440    #[serde(default = "default_true")]
441    #[schemars(extend("x-section" = "Display"))]
442    pub line_wrap: bool,
443
444    /// Enable syntax highlighting for code files
445    #[serde(default = "default_true")]
446    #[schemars(extend("x-section" = "Display"))]
447    pub syntax_highlighting: bool,
448
449    /// Whether the menu bar is visible by default.
450    /// The menu bar provides access to menus (File, Edit, View, etc.) at the top of the screen.
451    /// Can be toggled at runtime via command palette or keybinding.
452    /// Default: true
453    #[serde(default = "default_true")]
454    #[schemars(extend("x-section" = "Display"))]
455    pub show_menu_bar: bool,
456
457    /// Whether the tab bar is visible by default.
458    /// The tab bar shows open files in each split pane.
459    /// Can be toggled at runtime via command palette or keybinding.
460    /// Default: true
461    #[serde(default = "default_true")]
462    #[schemars(extend("x-section" = "Display"))]
463    pub show_tab_bar: bool,
464
465    /// Use the terminal's default background color instead of the theme's editor background.
466    /// When enabled, the editor background inherits from the terminal emulator,
467    /// allowing transparency or custom terminal backgrounds to show through.
468    /// Default: false
469    #[serde(default = "default_false")]
470    #[schemars(extend("x-section" = "Display"))]
471    pub use_terminal_bg: bool,
472
473    /// Cursor style for the terminal cursor.
474    /// Options: blinking_block, steady_block, blinking_bar, steady_bar, blinking_underline, steady_underline
475    /// Default: blinking_block
476    #[serde(default)]
477    #[schemars(extend("x-section" = "Display"))]
478    pub cursor_style: CursorStyle,
479
480    // ===== Editing =====
481    /// Number of spaces per tab character
482    #[serde(default = "default_tab_size")]
483    #[schemars(extend("x-section" = "Editing"))]
484    pub tab_size: usize,
485
486    /// Automatically indent new lines based on the previous line
487    #[serde(default = "default_true")]
488    #[schemars(extend("x-section" = "Editing"))]
489    pub auto_indent: bool,
490
491    /// Minimum lines to keep visible above/below cursor when scrolling
492    #[serde(default = "default_scroll_offset")]
493    #[schemars(extend("x-section" = "Editing"))]
494    pub scroll_offset: usize,
495
496    /// Default line ending format for new files.
497    /// Files loaded from disk will use their detected line ending format.
498    /// Options: "lf" (Unix/Linux/macOS), "crlf" (Windows), "cr" (Classic Mac)
499    /// Default: "lf"
500    #[serde(default)]
501    #[schemars(extend("x-section" = "Editing"))]
502    pub default_line_ending: LineEndingOption,
503
504    /// Remove trailing whitespace from lines when saving.
505    /// Default: false
506    #[serde(default = "default_false")]
507    #[schemars(extend("x-section" = "Editing"))]
508    pub trim_trailing_whitespace_on_save: bool,
509
510    /// Ensure files end with a newline when saving.
511    /// Default: false
512    #[serde(default = "default_false")]
513    #[schemars(extend("x-section" = "Editing"))]
514    pub ensure_final_newline_on_save: bool,
515
516    // ===== Bracket Matching =====
517    /// Highlight matching bracket pairs when cursor is on a bracket.
518    /// Default: true
519    #[serde(default = "default_true")]
520    #[schemars(extend("x-section" = "Bracket Matching"))]
521    pub highlight_matching_brackets: bool,
522
523    /// Use rainbow colors for nested brackets based on nesting depth.
524    /// Requires highlight_matching_brackets to be enabled.
525    /// Default: true
526    #[serde(default = "default_true")]
527    #[schemars(extend("x-section" = "Bracket Matching"))]
528    pub rainbow_brackets: bool,
529
530    // ===== Completion =====
531    /// Enable quick suggestions (VS Code-like behavior).
532    /// When enabled, completion suggestions appear automatically while typing,
533    /// not just on trigger characters (like `.` or `::`).
534    /// Default: true
535    #[serde(default = "default_true")]
536    #[schemars(extend("x-section" = "Completion"))]
537    pub quick_suggestions: bool,
538
539    /// Delay in milliseconds before showing completion suggestions.
540    /// Lower values (10-50ms) feel more responsive but may be distracting.
541    /// Higher values (100-500ms) reduce noise while typing.
542    /// Trigger characters (like `.`) bypass this delay.
543    /// Default: 10 (matches VS Code)
544    #[serde(default = "default_quick_suggestions_delay")]
545    #[schemars(extend("x-section" = "Completion"))]
546    pub quick_suggestions_delay_ms: u64,
547
548    /// Whether trigger characters (like `.`, `::`, `->`) immediately show completions.
549    /// When true, typing a trigger character bypasses quick_suggestions_delay_ms.
550    /// Default: true
551    #[serde(default = "default_true")]
552    #[schemars(extend("x-section" = "Completion"))]
553    pub suggest_on_trigger_characters: bool,
554
555    /// Controls whether pressing Enter accepts the selected completion.
556    /// - "on": Enter always accepts the completion
557    /// - "off": Enter inserts a newline (use Tab to accept)
558    /// - "smart": Enter accepts only if the completion text differs from typed text
559    /// Default: "on"
560    #[serde(default = "default_accept_suggestion_on_enter")]
561    #[schemars(extend("x-section" = "Completion"))]
562    pub accept_suggestion_on_enter: AcceptSuggestionOnEnter,
563
564    // ===== LSP =====
565    /// Whether to enable LSP inlay hints (type hints, parameter hints, etc.)
566    #[serde(default = "default_true")]
567    #[schemars(extend("x-section" = "LSP"))]
568    pub enable_inlay_hints: bool,
569
570    /// Whether to request full-document LSP semantic tokens.
571    /// Range requests are still used when supported.
572    /// Default: false (range-only to avoid heavy full refreshes).
573    #[serde(default = "default_false")]
574    #[schemars(extend("x-section" = "LSP"))]
575    pub enable_semantic_tokens_full: bool,
576
577    // ===== Mouse =====
578    /// Whether mouse hover triggers LSP hover requests.
579    /// When enabled, hovering over code with the mouse will show documentation.
580    /// Default: true
581    #[serde(default = "default_true")]
582    #[schemars(extend("x-section" = "Mouse"))]
583    pub mouse_hover_enabled: bool,
584
585    /// Delay in milliseconds before a mouse hover triggers an LSP hover request.
586    /// Lower values show hover info faster but may cause more LSP server load.
587    /// Default: 500ms
588    #[serde(default = "default_mouse_hover_delay")]
589    #[schemars(extend("x-section" = "Mouse"))]
590    pub mouse_hover_delay_ms: u64,
591
592    /// Time window in milliseconds for detecting double-clicks.
593    /// Two clicks within this time are treated as a double-click (word selection).
594    /// Default: 500ms
595    #[serde(default = "default_double_click_time")]
596    #[schemars(extend("x-section" = "Mouse"))]
597    pub double_click_time_ms: u64,
598
599    // ===== Recovery =====
600    /// Whether to enable file recovery (Emacs-style auto-save)
601    /// When enabled, buffers are periodically saved to recovery files
602    /// so they can be recovered if the editor crashes.
603    #[serde(default = "default_true")]
604    #[schemars(extend("x-section" = "Recovery"))]
605    pub recovery_enabled: bool,
606
607    /// Auto-save interval in seconds for file recovery
608    /// Modified buffers are saved to recovery files at this interval.
609    /// Default: 2 seconds for fast recovery with minimal data loss.
610    /// Set to 0 to disable periodic auto-save (manual recovery only).
611    #[serde(default = "default_auto_save_interval")]
612    #[schemars(extend("x-section" = "Recovery"))]
613    pub auto_save_interval_secs: u32,
614
615    /// Poll interval in milliseconds for auto-reverting open buffers.
616    /// When auto-revert is enabled, file modification times are checked at this interval.
617    /// Lower values detect external changes faster but use more CPU.
618    /// Default: 2000ms (2 seconds)
619    #[serde(default = "default_auto_revert_poll_interval")]
620    #[schemars(extend("x-section" = "Recovery"))]
621    pub auto_revert_poll_interval_ms: u64,
622
623    // ===== Keyboard =====
624    /// Enable keyboard enhancement: disambiguate escape codes using CSI-u sequences.
625    /// This allows unambiguous reading of Escape and modified keys.
626    /// Requires terminal support (kitty keyboard protocol).
627    /// Default: true
628    #[serde(default = "default_true")]
629    #[schemars(extend("x-section" = "Keyboard"))]
630    pub keyboard_disambiguate_escape_codes: bool,
631
632    /// Enable keyboard enhancement: report key event types (repeat/release).
633    /// Adds extra events when keys are autorepeated or released.
634    /// Requires terminal support (kitty keyboard protocol).
635    /// Default: false
636    #[serde(default = "default_false")]
637    #[schemars(extend("x-section" = "Keyboard"))]
638    pub keyboard_report_event_types: bool,
639
640    /// Enable keyboard enhancement: report alternate keycodes.
641    /// Sends alternate keycodes in addition to the base keycode.
642    /// Requires terminal support (kitty keyboard protocol).
643    /// Default: true
644    #[serde(default = "default_true")]
645    #[schemars(extend("x-section" = "Keyboard"))]
646    pub keyboard_report_alternate_keys: bool,
647
648    /// Enable keyboard enhancement: report all keys as escape codes.
649    /// Represents all keyboard events as CSI-u sequences.
650    /// Required for repeat/release events on plain-text keys.
651    /// Requires terminal support (kitty keyboard protocol).
652    /// Default: false
653    #[serde(default = "default_false")]
654    #[schemars(extend("x-section" = "Keyboard"))]
655    pub keyboard_report_all_keys_as_escape_codes: bool,
656
657    // ===== Performance =====
658    /// Maximum time in milliseconds for syntax highlighting per frame
659    #[serde(default = "default_highlight_timeout")]
660    #[schemars(extend("x-section" = "Performance"))]
661    pub highlight_timeout_ms: u64,
662
663    /// Undo history snapshot interval (number of edits between snapshots)
664    #[serde(default = "default_snapshot_interval")]
665    #[schemars(extend("x-section" = "Performance"))]
666    pub snapshot_interval: usize,
667
668    /// Number of bytes to look back/forward from the viewport for syntax highlighting context.
669    /// Larger values improve accuracy for multi-line constructs (strings, comments, nested blocks)
670    /// but may slow down highlighting for very large files.
671    /// Default: 10KB (10000 bytes)
672    #[serde(default = "default_highlight_context_bytes")]
673    #[schemars(extend("x-section" = "Performance"))]
674    pub highlight_context_bytes: usize,
675
676    /// File size threshold in bytes for "large file" behavior
677    /// Files larger than this will:
678    /// - Skip LSP features
679    /// - Use constant-size scrollbar thumb (1 char)
680    ///
681    /// Files smaller will count actual lines for accurate scrollbar rendering
682    #[serde(default = "default_large_file_threshold")]
683    #[schemars(extend("x-section" = "Performance"))]
684    pub large_file_threshold_bytes: u64,
685
686    /// Estimated average line length in bytes (used for large file line estimation)
687    /// This is used by LineIterator to estimate line positions in large files
688    /// without line metadata. Typical values: 80-120 bytes.
689    #[serde(default = "default_estimated_line_length")]
690    #[schemars(extend("x-section" = "Performance"))]
691    pub estimated_line_length: usize,
692
693    /// Poll interval in milliseconds for refreshing expanded directories in the file explorer.
694    /// Directory modification times are checked at this interval to detect new/deleted files.
695    /// Lower values detect changes faster but use more CPU.
696    /// Default: 3000ms (3 seconds)
697    #[serde(default = "default_file_tree_poll_interval")]
698    #[schemars(extend("x-section" = "Performance"))]
699    pub file_tree_poll_interval_ms: u64,
700}
701
702fn default_tab_size() -> usize {
703    4
704}
705
706/// Large file threshold in bytes
707/// Files larger than this will use optimized algorithms (estimation, viewport-only parsing)
708/// Files smaller will use exact algorithms (full line tracking, complete parsing)
709pub const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; // 1MB
710
711fn default_large_file_threshold() -> u64 {
712    LARGE_FILE_THRESHOLD_BYTES
713}
714
715fn default_true() -> bool {
716    true
717}
718
719fn default_false() -> bool {
720    false
721}
722
723fn default_quick_suggestions_delay() -> u64 {
724    10 // 10ms like VS Code
725}
726
727fn default_accept_suggestion_on_enter() -> AcceptSuggestionOnEnter {
728    AcceptSuggestionOnEnter::On
729}
730
731fn default_scroll_offset() -> usize {
732    3
733}
734
735fn default_highlight_timeout() -> u64 {
736    5
737}
738
739fn default_snapshot_interval() -> usize {
740    100
741}
742
743fn default_estimated_line_length() -> usize {
744    80
745}
746
747fn default_auto_save_interval() -> u32 {
748    2 // Auto-save every 2 seconds for fast recovery
749}
750
751fn default_highlight_context_bytes() -> usize {
752    10_000 // 10KB context for accurate syntax highlighting
753}
754
755fn default_mouse_hover_delay() -> u64 {
756    500 // 500ms delay before showing hover info
757}
758
759fn default_double_click_time() -> u64 {
760    500 // 500ms window for detecting double-clicks
761}
762
763fn default_auto_revert_poll_interval() -> u64 {
764    2000 // 2 seconds between file mtime checks
765}
766
767fn default_file_tree_poll_interval() -> u64 {
768    3000 // 3 seconds between directory mtime checks
769}
770
771impl Default for EditorConfig {
772    fn default() -> Self {
773        Self {
774            tab_size: default_tab_size(),
775            auto_indent: true,
776            line_numbers: true,
777            relative_line_numbers: false,
778            scroll_offset: default_scroll_offset(),
779            syntax_highlighting: true,
780            line_wrap: true,
781            highlight_timeout_ms: default_highlight_timeout(),
782            snapshot_interval: default_snapshot_interval(),
783            large_file_threshold_bytes: default_large_file_threshold(),
784            estimated_line_length: default_estimated_line_length(),
785            enable_inlay_hints: true,
786            enable_semantic_tokens_full: false,
787            recovery_enabled: true,
788            auto_save_interval_secs: default_auto_save_interval(),
789            highlight_context_bytes: default_highlight_context_bytes(),
790            mouse_hover_enabled: true,
791            mouse_hover_delay_ms: default_mouse_hover_delay(),
792            double_click_time_ms: default_double_click_time(),
793            auto_revert_poll_interval_ms: default_auto_revert_poll_interval(),
794            file_tree_poll_interval_ms: default_file_tree_poll_interval(),
795            default_line_ending: LineEndingOption::default(),
796            trim_trailing_whitespace_on_save: false,
797            ensure_final_newline_on_save: false,
798            highlight_matching_brackets: true,
799            rainbow_brackets: true,
800            cursor_style: CursorStyle::default(),
801            keyboard_disambiguate_escape_codes: true,
802            keyboard_report_event_types: false,
803            keyboard_report_alternate_keys: true,
804            keyboard_report_all_keys_as_escape_codes: false,
805            quick_suggestions: true,
806            quick_suggestions_delay_ms: default_quick_suggestions_delay(),
807            suggest_on_trigger_characters: true,
808            accept_suggestion_on_enter: default_accept_suggestion_on_enter(),
809            show_menu_bar: true,
810            show_tab_bar: true,
811            use_terminal_bg: false,
812        }
813    }
814}
815
816/// File explorer configuration
817#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
818pub struct FileExplorerConfig {
819    /// Whether to respect .gitignore files
820    #[serde(default = "default_true")]
821    pub respect_gitignore: bool,
822
823    /// Whether to show hidden files (starting with .) by default
824    #[serde(default = "default_false")]
825    pub show_hidden: bool,
826
827    /// Whether to show gitignored files by default
828    #[serde(default = "default_false")]
829    pub show_gitignored: bool,
830
831    /// Custom patterns to ignore (in addition to .gitignore)
832    #[serde(default)]
833    pub custom_ignore_patterns: Vec<String>,
834
835    /// Width of file explorer as percentage (0.0 to 1.0)
836    #[serde(default = "default_explorer_width")]
837    pub width: f32,
838}
839
840fn default_explorer_width() -> f32 {
841    0.3 // 30% of screen width
842}
843
844/// Terminal configuration
845#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
846pub struct TerminalConfig {
847    /// When viewing terminal scrollback and new output arrives,
848    /// automatically jump back to terminal mode (default: true)
849    #[serde(default = "default_true")]
850    pub jump_to_end_on_output: bool,
851}
852
853impl Default for TerminalConfig {
854    fn default() -> Self {
855        Self {
856            jump_to_end_on_output: true,
857        }
858    }
859}
860
861/// Warning notification configuration
862#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
863pub struct WarningsConfig {
864    /// Show warning/error indicators in the status bar (default: true)
865    /// When enabled, displays a colored indicator for LSP errors and other warnings
866    #[serde(default = "default_true")]
867    pub show_status_indicator: bool,
868}
869
870impl Default for WarningsConfig {
871    fn default() -> Self {
872        Self {
873            show_status_indicator: true,
874        }
875    }
876}
877
878/// Package manager configuration for plugins and themes
879#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
880pub struct PackagesConfig {
881    /// Registry sources (git repository URLs containing plugin/theme indices)
882    /// Default: ["https://github.com/sinelaw/fresh-plugins-registry"]
883    #[serde(default = "default_package_sources")]
884    pub sources: Vec<String>,
885}
886
887fn default_package_sources() -> Vec<String> {
888    vec!["https://github.com/sinelaw/fresh-plugins-registry".to_string()]
889}
890
891impl Default for PackagesConfig {
892    fn default() -> Self {
893        Self {
894            sources: default_package_sources(),
895        }
896    }
897}
898
899// Re-export PluginConfig from fresh-core for shared type usage
900pub use fresh_core::config::PluginConfig;
901
902impl Default for FileExplorerConfig {
903    fn default() -> Self {
904        Self {
905            respect_gitignore: true,
906            show_hidden: false,
907            show_gitignored: false,
908            custom_ignore_patterns: Vec::new(),
909            width: default_explorer_width(),
910        }
911    }
912}
913
914/// File browser configuration (for Open File dialog)
915#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
916pub struct FileBrowserConfig {
917    /// Whether to show hidden files (starting with .) by default in Open File dialog
918    #[serde(default = "default_false")]
919    pub show_hidden: bool,
920}
921
922/// A single key in a sequence
923#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
924pub struct KeyPress {
925    /// Key name (e.g., "a", "Enter", "F1")
926    pub key: String,
927    /// Modifiers (e.g., ["ctrl"], ["ctrl", "shift"])
928    #[serde(default)]
929    pub modifiers: Vec<String>,
930}
931
932/// Keybinding definition
933#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
934#[schemars(extend("x-display-field" = "/action"))]
935pub struct Keybinding {
936    /// Key name (e.g., "a", "Enter", "F1") - for single-key bindings
937    #[serde(default, skip_serializing_if = "String::is_empty")]
938    pub key: String,
939
940    /// Modifiers (e.g., ["ctrl"], ["ctrl", "shift"]) - for single-key bindings
941    #[serde(default, skip_serializing_if = "Vec::is_empty")]
942    pub modifiers: Vec<String>,
943
944    /// Key sequence for chord bindings (e.g., [{"key": "x", "modifiers": ["ctrl"]}, {"key": "s", "modifiers": ["ctrl"]}])
945    /// If present, takes precedence over key + modifiers
946    #[serde(default, skip_serializing_if = "Vec::is_empty")]
947    pub keys: Vec<KeyPress>,
948
949    /// Action to perform (e.g., "insert_char", "move_left")
950    pub action: String,
951
952    /// Optional arguments for the action
953    #[serde(default)]
954    pub args: HashMap<String, serde_json::Value>,
955
956    /// Optional condition (e.g., "mode == insert")
957    #[serde(default)]
958    pub when: Option<String>,
959}
960
961/// Keymap configuration (for built-in and user-defined keymaps)
962#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
963#[schemars(extend("x-display-field" = "/inherits"))]
964pub struct KeymapConfig {
965    /// Optional parent keymap to inherit from
966    #[serde(default, skip_serializing_if = "Option::is_none")]
967    pub inherits: Option<String>,
968
969    /// Keybindings defined in this keymap
970    #[serde(default)]
971    pub bindings: Vec<Keybinding>,
972}
973
974/// Formatter configuration for a language
975#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
976#[schemars(extend("x-display-field" = "/command"))]
977pub struct FormatterConfig {
978    /// The formatter command to run (e.g., "rustfmt", "prettier")
979    pub command: String,
980
981    /// Arguments to pass to the formatter
982    /// Use "$FILE" to include the file path
983    #[serde(default)]
984    pub args: Vec<String>,
985
986    /// Whether to pass buffer content via stdin (default: true)
987    /// Most formatters read from stdin and write to stdout
988    #[serde(default = "default_true")]
989    pub stdin: bool,
990
991    /// Timeout in milliseconds (default: 10000)
992    #[serde(default = "default_on_save_timeout")]
993    pub timeout_ms: u64,
994}
995
996/// Action to run when a file is saved (for linters, etc.)
997#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
998#[schemars(extend("x-display-field" = "/command"))]
999pub struct OnSaveAction {
1000    /// The shell command to run
1001    /// The file path is available as $FILE or as an argument
1002    pub command: String,
1003
1004    /// Arguments to pass to the command
1005    /// Use "$FILE" to include the file path
1006    #[serde(default)]
1007    pub args: Vec<String>,
1008
1009    /// Working directory for the command (defaults to project root)
1010    #[serde(default)]
1011    pub working_dir: Option<String>,
1012
1013    /// Whether to use the buffer content as stdin
1014    #[serde(default)]
1015    pub stdin: bool,
1016
1017    /// Timeout in milliseconds (default: 10000)
1018    #[serde(default = "default_on_save_timeout")]
1019    pub timeout_ms: u64,
1020
1021    /// Whether this action is enabled (default: true)
1022    /// Set to false to disable an action without removing it from config
1023    #[serde(default = "default_true")]
1024    pub enabled: bool,
1025}
1026
1027fn default_on_save_timeout() -> u64 {
1028    10000
1029}
1030
1031/// Language-specific configuration
1032#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1033#[schemars(extend("x-display-field" = "/grammar"))]
1034pub struct LanguageConfig {
1035    /// File extensions for this language (e.g., ["rs"] for Rust)
1036    #[serde(default)]
1037    pub extensions: Vec<String>,
1038
1039    /// Exact filenames for this language (e.g., ["Makefile", "GNUmakefile"])
1040    #[serde(default)]
1041    pub filenames: Vec<String>,
1042
1043    /// Tree-sitter grammar name
1044    #[serde(default)]
1045    pub grammar: String,
1046
1047    /// Comment prefix
1048    #[serde(default)]
1049    pub comment_prefix: Option<String>,
1050
1051    /// Whether to auto-indent
1052    #[serde(default = "default_true")]
1053    pub auto_indent: bool,
1054
1055    /// Preferred highlighter backend (auto, tree-sitter, or textmate)
1056    #[serde(default)]
1057    pub highlighter: HighlighterPreference,
1058
1059    /// Path to custom TextMate grammar file (optional)
1060    /// If specified, this grammar will be used when highlighter is "textmate"
1061    #[serde(default)]
1062    pub textmate_grammar: Option<std::path::PathBuf>,
1063
1064    /// Whether to show whitespace tab indicators (→) for this language
1065    /// Defaults to true. Set to false for languages like Go that use tabs for indentation.
1066    #[serde(default = "default_true")]
1067    pub show_whitespace_tabs: bool,
1068
1069    /// Whether pressing Tab should insert a tab character instead of spaces.
1070    /// Defaults to false (insert spaces based on tab_size).
1071    /// Set to true for languages like Go and Makefile that require tabs.
1072    #[serde(default = "default_false")]
1073    pub use_tabs: bool,
1074
1075    /// Tab size (number of spaces per tab) for this language.
1076    /// If not specified, falls back to the global editor.tab_size setting.
1077    #[serde(default)]
1078    pub tab_size: Option<usize>,
1079
1080    /// The formatter for this language (used by format_buffer command)
1081    #[serde(default)]
1082    pub formatter: Option<FormatterConfig>,
1083
1084    /// Whether to automatically format on save (uses the formatter above)
1085    #[serde(default)]
1086    pub format_on_save: bool,
1087
1088    /// Actions to run when a file of this language is saved (linters, etc.)
1089    /// Actions are run in order; if any fails (non-zero exit), subsequent actions don't run
1090    /// Note: Use `formatter` + `format_on_save` for formatting, not on_save
1091    #[serde(default)]
1092    pub on_save: Vec<OnSaveAction>,
1093}
1094
1095/// Resolved editor configuration for a specific buffer.
1096///
1097/// This struct contains the effective settings for a buffer after applying
1098/// language-specific overrides on top of the global editor config.
1099///
1100/// Use `BufferConfig::resolve()` to create one from a Config and optional language ID.
1101#[derive(Debug, Clone)]
1102pub struct BufferConfig {
1103    /// Number of spaces per tab character
1104    pub tab_size: usize,
1105
1106    /// Whether to insert a tab character (true) or spaces (false) when pressing Tab
1107    pub use_tabs: bool,
1108
1109    /// Whether to auto-indent new lines
1110    pub auto_indent: bool,
1111
1112    /// Whether to show whitespace tab indicators (→)
1113    pub show_whitespace_tabs: bool,
1114
1115    /// Formatter command for this buffer
1116    pub formatter: Option<FormatterConfig>,
1117
1118    /// Whether to format on save
1119    pub format_on_save: bool,
1120
1121    /// Actions to run when saving
1122    pub on_save: Vec<OnSaveAction>,
1123
1124    /// Preferred highlighter backend
1125    pub highlighter: HighlighterPreference,
1126
1127    /// Path to custom TextMate grammar (if any)
1128    pub textmate_grammar: Option<std::path::PathBuf>,
1129}
1130
1131impl BufferConfig {
1132    /// Resolve the effective configuration for a buffer given its language.
1133    ///
1134    /// This merges the global editor settings with any language-specific overrides
1135    /// from `Config.languages`.
1136    ///
1137    /// # Arguments
1138    /// * `global_config` - The resolved global configuration
1139    /// * `language_id` - Optional language identifier (e.g., "rust", "python")
1140    pub fn resolve(global_config: &Config, language_id: Option<&str>) -> Self {
1141        let editor = &global_config.editor;
1142
1143        // Start with global editor settings
1144        let mut config = BufferConfig {
1145            tab_size: editor.tab_size,
1146            use_tabs: false, // Global default is spaces
1147            auto_indent: editor.auto_indent,
1148            show_whitespace_tabs: true, // Global default
1149            formatter: None,
1150            format_on_save: false,
1151            on_save: Vec::new(),
1152            highlighter: HighlighterPreference::Auto,
1153            textmate_grammar: None,
1154        };
1155
1156        // Apply language-specific overrides if available
1157        if let Some(lang_id) = language_id {
1158            if let Some(lang_config) = global_config.languages.get(lang_id) {
1159                // Tab size: use language setting if specified, else global
1160                if let Some(ts) = lang_config.tab_size {
1161                    config.tab_size = ts;
1162                }
1163
1164                // Use tabs: language override
1165                config.use_tabs = lang_config.use_tabs;
1166
1167                // Auto indent: language override
1168                config.auto_indent = lang_config.auto_indent;
1169
1170                // Show whitespace tabs: language override
1171                config.show_whitespace_tabs = lang_config.show_whitespace_tabs;
1172
1173                // Formatter: from language config
1174                config.formatter = lang_config.formatter.clone();
1175
1176                // Format on save: from language config
1177                config.format_on_save = lang_config.format_on_save;
1178
1179                // On save actions: from language config
1180                config.on_save = lang_config.on_save.clone();
1181
1182                // Highlighter preference: from language config
1183                config.highlighter = lang_config.highlighter;
1184
1185                // TextMate grammar path: from language config
1186                config.textmate_grammar = lang_config.textmate_grammar.clone();
1187            }
1188        }
1189
1190        config
1191    }
1192
1193    /// Get the effective indentation string for this buffer.
1194    ///
1195    /// Returns a tab character if `use_tabs` is true, otherwise returns
1196    /// `tab_size` spaces.
1197    pub fn indent_string(&self) -> String {
1198        if self.use_tabs {
1199            "\t".to_string()
1200        } else {
1201            " ".repeat(self.tab_size)
1202        }
1203    }
1204}
1205
1206/// Preference for which syntax highlighting backend to use
1207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
1208#[serde(rename_all = "lowercase")]
1209pub enum HighlighterPreference {
1210    /// Use tree-sitter if available, fall back to TextMate
1211    #[default]
1212    Auto,
1213    /// Force tree-sitter only (no highlighting if unavailable)
1214    #[serde(rename = "tree-sitter")]
1215    TreeSitter,
1216    /// Force TextMate grammar (skip tree-sitter even if available)
1217    #[serde(rename = "textmate")]
1218    TextMate,
1219}
1220
1221/// Menu bar configuration
1222#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1223pub struct MenuConfig {
1224    /// List of top-level menus in the menu bar
1225    #[serde(default)]
1226    pub menus: Vec<Menu>,
1227}
1228
1229// Re-export Menu and MenuItem from fresh-core for shared type usage
1230pub use fresh_core::menu::{Menu, MenuItem};
1231
1232/// Extension trait for Menu with editor-specific functionality
1233pub trait MenuExt {
1234    /// Get the identifier for matching (id if set, otherwise label).
1235    /// This is used for keybinding matching and should be stable across translations.
1236    fn match_id(&self) -> &str;
1237
1238    /// Expand all DynamicSubmenu items in this menu to regular Submenu items
1239    /// This should be called before the menu is used for rendering/navigation
1240    fn expand_dynamic_items(&mut self);
1241}
1242
1243impl MenuExt for Menu {
1244    fn match_id(&self) -> &str {
1245        self.id.as_deref().unwrap_or(&self.label)
1246    }
1247
1248    fn expand_dynamic_items(&mut self) {
1249        self.items = self
1250            .items
1251            .iter()
1252            .map(|item| item.expand_dynamic())
1253            .collect();
1254    }
1255}
1256
1257/// Extension trait for MenuItem with editor-specific functionality
1258pub trait MenuItemExt {
1259    /// Expand a DynamicSubmenu into a regular Submenu with generated items.
1260    /// Returns the original item if not a DynamicSubmenu.
1261    fn expand_dynamic(&self) -> MenuItem;
1262}
1263
1264impl MenuItemExt for MenuItem {
1265    fn expand_dynamic(&self) -> MenuItem {
1266        match self {
1267            MenuItem::DynamicSubmenu { label, source } => {
1268                let items = generate_dynamic_items(source);
1269                MenuItem::Submenu {
1270                    label: label.clone(),
1271                    items,
1272                }
1273            }
1274            other => other.clone(),
1275        }
1276    }
1277}
1278
1279/// Generate menu items for a dynamic source (runtime only - requires view::theme)
1280#[cfg(feature = "runtime")]
1281pub fn generate_dynamic_items(source: &str) -> Vec<MenuItem> {
1282    match source {
1283        "copy_with_theme" => {
1284            // Generate theme options from available themes
1285            let loader = crate::view::theme::ThemeLoader::new();
1286            let registry = loader.load_all();
1287            registry
1288                .list()
1289                .iter()
1290                .map(|info| {
1291                    let mut args = HashMap::new();
1292                    args.insert("theme".to_string(), serde_json::json!(info.name));
1293                    MenuItem::Action {
1294                        label: info.name.clone(),
1295                        action: "copy_with_theme".to_string(),
1296                        args,
1297                        when: Some(context_keys::HAS_SELECTION.to_string()),
1298                        checkbox: None,
1299                    }
1300                })
1301                .collect()
1302        }
1303        _ => vec![MenuItem::Label {
1304            info: format!("Unknown source: {}", source),
1305        }],
1306    }
1307}
1308
1309/// Generate menu items for a dynamic source (WASM stub - returns empty)
1310#[cfg(not(feature = "runtime"))]
1311pub fn generate_dynamic_items(_source: &str) -> Vec<MenuItem> {
1312    // Theme loading not available in WASM builds
1313    vec![]
1314}
1315
1316impl Default for Config {
1317    fn default() -> Self {
1318        Self {
1319            version: 0,
1320            theme: default_theme_name(),
1321            locale: LocaleName::default(),
1322            check_for_updates: true,
1323            editor: EditorConfig::default(),
1324            file_explorer: FileExplorerConfig::default(),
1325            file_browser: FileBrowserConfig::default(),
1326            terminal: TerminalConfig::default(),
1327            keybindings: vec![], // User customizations only; defaults come from active_keybinding_map
1328            keybinding_maps: HashMap::new(), // User-defined maps go here
1329            active_keybinding_map: default_keybinding_map_name(),
1330            languages: Self::default_languages(),
1331            lsp: Self::default_lsp_config(),
1332            warnings: WarningsConfig::default(),
1333            plugins: HashMap::new(), // Populated when scanning for plugins
1334            packages: PackagesConfig::default(),
1335        }
1336    }
1337}
1338
1339impl MenuConfig {
1340    /// Create a MenuConfig with translated menus using the current locale
1341    pub fn translated() -> Self {
1342        Self {
1343            menus: Self::translated_menus(),
1344        }
1345    }
1346
1347    /// Create default menu bar configuration with translated labels
1348    fn translated_menus() -> Vec<Menu> {
1349        vec![
1350            // File menu
1351            Menu {
1352                id: Some("File".to_string()),
1353                label: t!("menu.file").to_string(),
1354                when: None,
1355                items: vec![
1356                    MenuItem::Action {
1357                        label: t!("menu.file.new_file").to_string(),
1358                        action: "new".to_string(),
1359                        args: HashMap::new(),
1360                        when: None,
1361                        checkbox: None,
1362                    },
1363                    MenuItem::Action {
1364                        label: t!("menu.file.open_file").to_string(),
1365                        action: "open".to_string(),
1366                        args: HashMap::new(),
1367                        when: None,
1368                        checkbox: None,
1369                    },
1370                    MenuItem::Separator { separator: true },
1371                    MenuItem::Action {
1372                        label: t!("menu.file.save").to_string(),
1373                        action: "save".to_string(),
1374                        args: HashMap::new(),
1375                        when: None,
1376                        checkbox: None,
1377                    },
1378                    MenuItem::Action {
1379                        label: t!("menu.file.save_as").to_string(),
1380                        action: "save_as".to_string(),
1381                        args: HashMap::new(),
1382                        when: None,
1383                        checkbox: None,
1384                    },
1385                    MenuItem::Action {
1386                        label: t!("menu.file.revert").to_string(),
1387                        action: "revert".to_string(),
1388                        args: HashMap::new(),
1389                        when: None,
1390                        checkbox: None,
1391                    },
1392                    MenuItem::Separator { separator: true },
1393                    MenuItem::Action {
1394                        label: t!("menu.file.close_buffer").to_string(),
1395                        action: "close".to_string(),
1396                        args: HashMap::new(),
1397                        when: None,
1398                        checkbox: None,
1399                    },
1400                    MenuItem::Separator { separator: true },
1401                    MenuItem::Action {
1402                        label: t!("menu.file.switch_project").to_string(),
1403                        action: "switch_project".to_string(),
1404                        args: HashMap::new(),
1405                        when: None,
1406                        checkbox: None,
1407                    },
1408                    MenuItem::Action {
1409                        label: t!("menu.file.quit").to_string(),
1410                        action: "quit".to_string(),
1411                        args: HashMap::new(),
1412                        when: None,
1413                        checkbox: None,
1414                    },
1415                ],
1416            },
1417            // Edit menu
1418            Menu {
1419                id: Some("Edit".to_string()),
1420                label: t!("menu.edit").to_string(),
1421                when: None,
1422                items: vec![
1423                    MenuItem::Action {
1424                        label: t!("menu.edit.undo").to_string(),
1425                        action: "undo".to_string(),
1426                        args: HashMap::new(),
1427                        when: None,
1428                        checkbox: None,
1429                    },
1430                    MenuItem::Action {
1431                        label: t!("menu.edit.redo").to_string(),
1432                        action: "redo".to_string(),
1433                        args: HashMap::new(),
1434                        when: None,
1435                        checkbox: None,
1436                    },
1437                    MenuItem::Separator { separator: true },
1438                    MenuItem::Action {
1439                        label: t!("menu.edit.cut").to_string(),
1440                        action: "cut".to_string(),
1441                        args: HashMap::new(),
1442                        when: Some(context_keys::HAS_SELECTION.to_string()),
1443                        checkbox: None,
1444                    },
1445                    MenuItem::Action {
1446                        label: t!("menu.edit.copy").to_string(),
1447                        action: "copy".to_string(),
1448                        args: HashMap::new(),
1449                        when: Some(context_keys::HAS_SELECTION.to_string()),
1450                        checkbox: None,
1451                    },
1452                    MenuItem::DynamicSubmenu {
1453                        label: t!("menu.edit.copy_with_formatting").to_string(),
1454                        source: "copy_with_theme".to_string(),
1455                    },
1456                    MenuItem::Action {
1457                        label: t!("menu.edit.paste").to_string(),
1458                        action: "paste".to_string(),
1459                        args: HashMap::new(),
1460                        when: None,
1461                        checkbox: None,
1462                    },
1463                    MenuItem::Separator { separator: true },
1464                    MenuItem::Action {
1465                        label: t!("menu.edit.select_all").to_string(),
1466                        action: "select_all".to_string(),
1467                        args: HashMap::new(),
1468                        when: None,
1469                        checkbox: None,
1470                    },
1471                    MenuItem::Separator { separator: true },
1472                    MenuItem::Action {
1473                        label: t!("menu.edit.find").to_string(),
1474                        action: "search".to_string(),
1475                        args: HashMap::new(),
1476                        when: None,
1477                        checkbox: None,
1478                    },
1479                    MenuItem::Action {
1480                        label: t!("menu.edit.find_in_selection").to_string(),
1481                        action: "find_in_selection".to_string(),
1482                        args: HashMap::new(),
1483                        when: Some(context_keys::HAS_SELECTION.to_string()),
1484                        checkbox: None,
1485                    },
1486                    MenuItem::Action {
1487                        label: t!("menu.edit.find_next").to_string(),
1488                        action: "find_next".to_string(),
1489                        args: HashMap::new(),
1490                        when: None,
1491                        checkbox: None,
1492                    },
1493                    MenuItem::Action {
1494                        label: t!("menu.edit.find_previous").to_string(),
1495                        action: "find_previous".to_string(),
1496                        args: HashMap::new(),
1497                        when: None,
1498                        checkbox: None,
1499                    },
1500                    MenuItem::Action {
1501                        label: t!("menu.edit.replace").to_string(),
1502                        action: "query_replace".to_string(),
1503                        args: HashMap::new(),
1504                        when: None,
1505                        checkbox: None,
1506                    },
1507                    MenuItem::Separator { separator: true },
1508                    MenuItem::Action {
1509                        label: t!("menu.edit.delete_line").to_string(),
1510                        action: "delete_line".to_string(),
1511                        args: HashMap::new(),
1512                        when: None,
1513                        checkbox: None,
1514                    },
1515                    MenuItem::Action {
1516                        label: t!("menu.edit.format_buffer").to_string(),
1517                        action: "format_buffer".to_string(),
1518                        args: HashMap::new(),
1519                        when: Some(context_keys::FORMATTER_AVAILABLE.to_string()),
1520                        checkbox: None,
1521                    },
1522                    MenuItem::Separator { separator: true },
1523                    MenuItem::Action {
1524                        label: t!("menu.edit.settings").to_string(),
1525                        action: "open_settings".to_string(),
1526                        args: HashMap::new(),
1527                        when: None,
1528                        checkbox: None,
1529                    },
1530                ],
1531            },
1532            // View menu
1533            Menu {
1534                id: Some("View".to_string()),
1535                label: t!("menu.view").to_string(),
1536                when: None,
1537                items: vec![
1538                    MenuItem::Action {
1539                        label: t!("menu.view.file_explorer").to_string(),
1540                        action: "toggle_file_explorer".to_string(),
1541                        args: HashMap::new(),
1542                        when: None,
1543                        checkbox: Some(context_keys::FILE_EXPLORER.to_string()),
1544                    },
1545                    MenuItem::Separator { separator: true },
1546                    MenuItem::Action {
1547                        label: t!("menu.view.line_numbers").to_string(),
1548                        action: "toggle_line_numbers".to_string(),
1549                        args: HashMap::new(),
1550                        when: None,
1551                        checkbox: Some(context_keys::LINE_NUMBERS.to_string()),
1552                    },
1553                    MenuItem::Action {
1554                        label: t!("menu.view.line_wrap").to_string(),
1555                        action: "toggle_line_wrap".to_string(),
1556                        args: HashMap::new(),
1557                        when: None,
1558                        checkbox: Some(context_keys::LINE_WRAP.to_string()),
1559                    },
1560                    MenuItem::Action {
1561                        label: t!("menu.view.mouse_support").to_string(),
1562                        action: "toggle_mouse_capture".to_string(),
1563                        args: HashMap::new(),
1564                        when: None,
1565                        checkbox: Some(context_keys::MOUSE_CAPTURE.to_string()),
1566                    },
1567                    MenuItem::Separator { separator: true },
1568                    MenuItem::Action {
1569                        label: t!("menu.view.set_background").to_string(),
1570                        action: "set_background".to_string(),
1571                        args: HashMap::new(),
1572                        when: None,
1573                        checkbox: None,
1574                    },
1575                    MenuItem::Action {
1576                        label: t!("menu.view.set_background_blend").to_string(),
1577                        action: "set_background_blend".to_string(),
1578                        args: HashMap::new(),
1579                        when: None,
1580                        checkbox: None,
1581                    },
1582                    MenuItem::Action {
1583                        label: t!("menu.view.set_compose_width").to_string(),
1584                        action: "set_compose_width".to_string(),
1585                        args: HashMap::new(),
1586                        when: None,
1587                        checkbox: None,
1588                    },
1589                    MenuItem::Separator { separator: true },
1590                    MenuItem::Action {
1591                        label: t!("menu.view.select_theme").to_string(),
1592                        action: "select_theme".to_string(),
1593                        args: HashMap::new(),
1594                        when: None,
1595                        checkbox: None,
1596                    },
1597                    MenuItem::Action {
1598                        label: t!("menu.view.select_locale").to_string(),
1599                        action: "select_locale".to_string(),
1600                        args: HashMap::new(),
1601                        when: None,
1602                        checkbox: None,
1603                    },
1604                    MenuItem::Action {
1605                        label: t!("menu.view.settings").to_string(),
1606                        action: "open_settings".to_string(),
1607                        args: HashMap::new(),
1608                        when: None,
1609                        checkbox: None,
1610                    },
1611                    MenuItem::Action {
1612                        label: t!("menu.view.calibrate_input").to_string(),
1613                        action: "calibrate_input".to_string(),
1614                        args: HashMap::new(),
1615                        when: None,
1616                        checkbox: None,
1617                    },
1618                    MenuItem::Separator { separator: true },
1619                    MenuItem::Action {
1620                        label: t!("menu.view.split_horizontal").to_string(),
1621                        action: "split_horizontal".to_string(),
1622                        args: HashMap::new(),
1623                        when: None,
1624                        checkbox: None,
1625                    },
1626                    MenuItem::Action {
1627                        label: t!("menu.view.split_vertical").to_string(),
1628                        action: "split_vertical".to_string(),
1629                        args: HashMap::new(),
1630                        when: None,
1631                        checkbox: None,
1632                    },
1633                    MenuItem::Action {
1634                        label: t!("menu.view.close_split").to_string(),
1635                        action: "close_split".to_string(),
1636                        args: HashMap::new(),
1637                        when: None,
1638                        checkbox: None,
1639                    },
1640                    MenuItem::Action {
1641                        label: t!("menu.view.focus_next_split").to_string(),
1642                        action: "next_split".to_string(),
1643                        args: HashMap::new(),
1644                        when: None,
1645                        checkbox: None,
1646                    },
1647                    MenuItem::Action {
1648                        label: t!("menu.view.focus_prev_split").to_string(),
1649                        action: "prev_split".to_string(),
1650                        args: HashMap::new(),
1651                        when: None,
1652                        checkbox: None,
1653                    },
1654                    MenuItem::Action {
1655                        label: t!("menu.view.toggle_maximize_split").to_string(),
1656                        action: "toggle_maximize_split".to_string(),
1657                        args: HashMap::new(),
1658                        when: None,
1659                        checkbox: None,
1660                    },
1661                    MenuItem::Separator { separator: true },
1662                    MenuItem::Submenu {
1663                        label: t!("menu.terminal").to_string(),
1664                        items: vec![
1665                            MenuItem::Action {
1666                                label: t!("menu.terminal.open").to_string(),
1667                                action: "open_terminal".to_string(),
1668                                args: HashMap::new(),
1669                                when: None,
1670                                checkbox: None,
1671                            },
1672                            MenuItem::Action {
1673                                label: t!("menu.terminal.close").to_string(),
1674                                action: "close_terminal".to_string(),
1675                                args: HashMap::new(),
1676                                when: None,
1677                                checkbox: None,
1678                            },
1679                            MenuItem::Separator { separator: true },
1680                            MenuItem::Action {
1681                                label: t!("menu.terminal.toggle_keyboard_capture").to_string(),
1682                                action: "toggle_keyboard_capture".to_string(),
1683                                args: HashMap::new(),
1684                                when: None,
1685                                checkbox: None,
1686                            },
1687                        ],
1688                    },
1689                    MenuItem::Separator { separator: true },
1690                    MenuItem::Submenu {
1691                        label: t!("menu.view.keybinding_style").to_string(),
1692                        items: vec![
1693                            MenuItem::Action {
1694                                label: t!("menu.view.keybinding_default").to_string(),
1695                                action: "switch_keybinding_map".to_string(),
1696                                args: {
1697                                    let mut map = HashMap::new();
1698                                    map.insert("map".to_string(), serde_json::json!("default"));
1699                                    map
1700                                },
1701                                when: None,
1702                                checkbox: None,
1703                            },
1704                            MenuItem::Action {
1705                                label: t!("menu.view.keybinding_emacs").to_string(),
1706                                action: "switch_keybinding_map".to_string(),
1707                                args: {
1708                                    let mut map = HashMap::new();
1709                                    map.insert("map".to_string(), serde_json::json!("emacs"));
1710                                    map
1711                                },
1712                                when: None,
1713                                checkbox: None,
1714                            },
1715                            MenuItem::Action {
1716                                label: t!("menu.view.keybinding_vscode").to_string(),
1717                                action: "switch_keybinding_map".to_string(),
1718                                args: {
1719                                    let mut map = HashMap::new();
1720                                    map.insert("map".to_string(), serde_json::json!("vscode"));
1721                                    map
1722                                },
1723                                when: None,
1724                                checkbox: None,
1725                            },
1726                        ],
1727                    },
1728                ],
1729            },
1730            // Selection menu
1731            Menu {
1732                id: Some("Selection".to_string()),
1733                label: t!("menu.selection").to_string(),
1734                when: None,
1735                items: vec![
1736                    MenuItem::Action {
1737                        label: t!("menu.selection.select_all").to_string(),
1738                        action: "select_all".to_string(),
1739                        args: HashMap::new(),
1740                        when: None,
1741                        checkbox: None,
1742                    },
1743                    MenuItem::Action {
1744                        label: t!("menu.selection.select_word").to_string(),
1745                        action: "select_word".to_string(),
1746                        args: HashMap::new(),
1747                        when: None,
1748                        checkbox: None,
1749                    },
1750                    MenuItem::Action {
1751                        label: t!("menu.selection.select_line").to_string(),
1752                        action: "select_line".to_string(),
1753                        args: HashMap::new(),
1754                        when: None,
1755                        checkbox: None,
1756                    },
1757                    MenuItem::Action {
1758                        label: t!("menu.selection.expand_selection").to_string(),
1759                        action: "expand_selection".to_string(),
1760                        args: HashMap::new(),
1761                        when: None,
1762                        checkbox: None,
1763                    },
1764                    MenuItem::Separator { separator: true },
1765                    MenuItem::Action {
1766                        label: t!("menu.selection.add_cursor_above").to_string(),
1767                        action: "add_cursor_above".to_string(),
1768                        args: HashMap::new(),
1769                        when: None,
1770                        checkbox: None,
1771                    },
1772                    MenuItem::Action {
1773                        label: t!("menu.selection.add_cursor_below").to_string(),
1774                        action: "add_cursor_below".to_string(),
1775                        args: HashMap::new(),
1776                        when: None,
1777                        checkbox: None,
1778                    },
1779                    MenuItem::Action {
1780                        label: t!("menu.selection.add_cursor_next_match").to_string(),
1781                        action: "add_cursor_next_match".to_string(),
1782                        args: HashMap::new(),
1783                        when: None,
1784                        checkbox: None,
1785                    },
1786                    MenuItem::Action {
1787                        label: t!("menu.selection.remove_secondary_cursors").to_string(),
1788                        action: "remove_secondary_cursors".to_string(),
1789                        args: HashMap::new(),
1790                        when: None,
1791                        checkbox: None,
1792                    },
1793                ],
1794            },
1795            // Go menu
1796            Menu {
1797                id: Some("Go".to_string()),
1798                label: t!("menu.go").to_string(),
1799                when: None,
1800                items: vec![
1801                    MenuItem::Action {
1802                        label: t!("menu.go.goto_line").to_string(),
1803                        action: "goto_line".to_string(),
1804                        args: HashMap::new(),
1805                        when: None,
1806                        checkbox: None,
1807                    },
1808                    MenuItem::Action {
1809                        label: t!("menu.go.goto_definition").to_string(),
1810                        action: "lsp_goto_definition".to_string(),
1811                        args: HashMap::new(),
1812                        when: None,
1813                        checkbox: None,
1814                    },
1815                    MenuItem::Action {
1816                        label: t!("menu.go.find_references").to_string(),
1817                        action: "lsp_references".to_string(),
1818                        args: HashMap::new(),
1819                        when: None,
1820                        checkbox: None,
1821                    },
1822                    MenuItem::Separator { separator: true },
1823                    MenuItem::Action {
1824                        label: t!("menu.go.next_buffer").to_string(),
1825                        action: "next_buffer".to_string(),
1826                        args: HashMap::new(),
1827                        when: None,
1828                        checkbox: None,
1829                    },
1830                    MenuItem::Action {
1831                        label: t!("menu.go.prev_buffer").to_string(),
1832                        action: "prev_buffer".to_string(),
1833                        args: HashMap::new(),
1834                        when: None,
1835                        checkbox: None,
1836                    },
1837                    MenuItem::Separator { separator: true },
1838                    MenuItem::Action {
1839                        label: t!("menu.go.command_palette").to_string(),
1840                        action: "command_palette".to_string(),
1841                        args: HashMap::new(),
1842                        when: None,
1843                        checkbox: None,
1844                    },
1845                ],
1846            },
1847            // LSP menu
1848            Menu {
1849                id: Some("LSP".to_string()),
1850                label: t!("menu.lsp").to_string(),
1851                when: None,
1852                items: vec![
1853                    MenuItem::Action {
1854                        label: t!("menu.lsp.show_hover").to_string(),
1855                        action: "lsp_hover".to_string(),
1856                        args: HashMap::new(),
1857                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
1858                        checkbox: None,
1859                    },
1860                    MenuItem::Action {
1861                        label: t!("menu.lsp.goto_definition").to_string(),
1862                        action: "lsp_goto_definition".to_string(),
1863                        args: HashMap::new(),
1864                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
1865                        checkbox: None,
1866                    },
1867                    MenuItem::Action {
1868                        label: t!("menu.lsp.find_references").to_string(),
1869                        action: "lsp_references".to_string(),
1870                        args: HashMap::new(),
1871                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
1872                        checkbox: None,
1873                    },
1874                    MenuItem::Action {
1875                        label: t!("menu.lsp.rename_symbol").to_string(),
1876                        action: "lsp_rename".to_string(),
1877                        args: HashMap::new(),
1878                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
1879                        checkbox: None,
1880                    },
1881                    MenuItem::Separator { separator: true },
1882                    MenuItem::Action {
1883                        label: t!("menu.lsp.show_completions").to_string(),
1884                        action: "lsp_completion".to_string(),
1885                        args: HashMap::new(),
1886                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
1887                        checkbox: None,
1888                    },
1889                    MenuItem::Action {
1890                        label: t!("menu.lsp.show_signature").to_string(),
1891                        action: "lsp_signature_help".to_string(),
1892                        args: HashMap::new(),
1893                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
1894                        checkbox: None,
1895                    },
1896                    MenuItem::Action {
1897                        label: t!("menu.lsp.code_actions").to_string(),
1898                        action: "lsp_code_actions".to_string(),
1899                        args: HashMap::new(),
1900                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
1901                        checkbox: None,
1902                    },
1903                    MenuItem::Separator { separator: true },
1904                    MenuItem::Action {
1905                        label: t!("menu.lsp.toggle_inlay_hints").to_string(),
1906                        action: "toggle_inlay_hints".to_string(),
1907                        args: HashMap::new(),
1908                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
1909                        checkbox: Some(context_keys::INLAY_HINTS.to_string()),
1910                    },
1911                    MenuItem::Action {
1912                        label: t!("menu.lsp.toggle_mouse_hover").to_string(),
1913                        action: "toggle_mouse_hover".to_string(),
1914                        args: HashMap::new(),
1915                        when: None,
1916                        checkbox: Some(context_keys::MOUSE_HOVER.to_string()),
1917                    },
1918                    MenuItem::Separator { separator: true },
1919                    MenuItem::Action {
1920                        label: t!("menu.lsp.restart_server").to_string(),
1921                        action: "lsp_restart".to_string(),
1922                        args: HashMap::new(),
1923                        when: None,
1924                        checkbox: None,
1925                    },
1926                    MenuItem::Action {
1927                        label: t!("menu.lsp.stop_server").to_string(),
1928                        action: "lsp_stop".to_string(),
1929                        args: HashMap::new(),
1930                        when: None,
1931                        checkbox: None,
1932                    },
1933                ],
1934            },
1935            // Explorer menu (only visible when file explorer is focused)
1936            Menu {
1937                id: Some("Explorer".to_string()),
1938                label: t!("menu.explorer").to_string(),
1939                when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1940                items: vec![
1941                    MenuItem::Action {
1942                        label: t!("menu.explorer.new_file").to_string(),
1943                        action: "file_explorer_new_file".to_string(),
1944                        args: HashMap::new(),
1945                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1946                        checkbox: None,
1947                    },
1948                    MenuItem::Action {
1949                        label: t!("menu.explorer.new_folder").to_string(),
1950                        action: "file_explorer_new_directory".to_string(),
1951                        args: HashMap::new(),
1952                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1953                        checkbox: None,
1954                    },
1955                    MenuItem::Separator { separator: true },
1956                    MenuItem::Action {
1957                        label: t!("menu.explorer.open").to_string(),
1958                        action: "file_explorer_open".to_string(),
1959                        args: HashMap::new(),
1960                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1961                        checkbox: None,
1962                    },
1963                    MenuItem::Action {
1964                        label: t!("menu.explorer.rename").to_string(),
1965                        action: "file_explorer_rename".to_string(),
1966                        args: HashMap::new(),
1967                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1968                        checkbox: None,
1969                    },
1970                    MenuItem::Action {
1971                        label: t!("menu.explorer.delete").to_string(),
1972                        action: "file_explorer_delete".to_string(),
1973                        args: HashMap::new(),
1974                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1975                        checkbox: None,
1976                    },
1977                    MenuItem::Separator { separator: true },
1978                    MenuItem::Action {
1979                        label: t!("menu.explorer.refresh").to_string(),
1980                        action: "file_explorer_refresh".to_string(),
1981                        args: HashMap::new(),
1982                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1983                        checkbox: None,
1984                    },
1985                    MenuItem::Separator { separator: true },
1986                    MenuItem::Action {
1987                        label: t!("menu.explorer.show_hidden").to_string(),
1988                        action: "file_explorer_toggle_hidden".to_string(),
1989                        args: HashMap::new(),
1990                        when: Some(context_keys::FILE_EXPLORER.to_string()),
1991                        checkbox: Some(context_keys::FILE_EXPLORER_SHOW_HIDDEN.to_string()),
1992                    },
1993                    MenuItem::Action {
1994                        label: t!("menu.explorer.show_gitignored").to_string(),
1995                        action: "file_explorer_toggle_gitignored".to_string(),
1996                        args: HashMap::new(),
1997                        when: Some(context_keys::FILE_EXPLORER.to_string()),
1998                        checkbox: Some(context_keys::FILE_EXPLORER_SHOW_GITIGNORED.to_string()),
1999                    },
2000                ],
2001            },
2002            // Help menu
2003            Menu {
2004                id: Some("Help".to_string()),
2005                label: t!("menu.help").to_string(),
2006                when: None,
2007                items: vec![
2008                    MenuItem::Label {
2009                        info: format!("Fresh v{}", env!("CARGO_PKG_VERSION")),
2010                    },
2011                    MenuItem::Separator { separator: true },
2012                    MenuItem::Action {
2013                        label: t!("menu.help.show_manual").to_string(),
2014                        action: "show_help".to_string(),
2015                        args: HashMap::new(),
2016                        when: None,
2017                        checkbox: None,
2018                    },
2019                    MenuItem::Action {
2020                        label: t!("menu.help.keyboard_shortcuts").to_string(),
2021                        action: "keyboard_shortcuts".to_string(),
2022                        args: HashMap::new(),
2023                        when: None,
2024                        checkbox: None,
2025                    },
2026                    MenuItem::Separator { separator: true },
2027                    MenuItem::Action {
2028                        label: t!("menu.help.event_debug").to_string(),
2029                        action: "event_debug".to_string(),
2030                        args: HashMap::new(),
2031                        when: None,
2032                        checkbox: None,
2033                    },
2034                ],
2035            },
2036        ]
2037    }
2038}
2039
2040impl Config {
2041    /// The config filename used throughout the application
2042    pub(crate) const FILENAME: &'static str = "config.json";
2043
2044    /// Get the local config path (in the working directory)
2045    pub(crate) fn local_config_path(working_dir: &Path) -> std::path::PathBuf {
2046        working_dir.join(Self::FILENAME)
2047    }
2048
2049    /// Load configuration from a JSON file
2050    ///
2051    /// This deserializes the user's config file as a partial config and resolves
2052    /// it with system defaults. For HashMap fields like `lsp` and `languages`,
2053    /// entries from the user config are merged with the default entries.
2054    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
2055        let contents = std::fs::read_to_string(path.as_ref())
2056            .map_err(|e| ConfigError::IoError(e.to_string()))?;
2057
2058        // Deserialize as PartialConfig first, then resolve with defaults
2059        let partial: crate::partial_config::PartialConfig =
2060            serde_json::from_str(&contents).map_err(|e| ConfigError::ParseError(e.to_string()))?;
2061
2062        Ok(partial.resolve())
2063    }
2064
2065    /// Load a built-in keymap from embedded JSON
2066    fn load_builtin_keymap(name: &str) -> Option<KeymapConfig> {
2067        let json_content = match name {
2068            "default" => include_str!("../keymaps/default.json"),
2069            "emacs" => include_str!("../keymaps/emacs.json"),
2070            "vscode" => include_str!("../keymaps/vscode.json"),
2071            "macos" => include_str!("../keymaps/macos.json"),
2072            _ => return None,
2073        };
2074
2075        match serde_json::from_str(json_content) {
2076            Ok(config) => Some(config),
2077            Err(e) => {
2078                eprintln!("Failed to parse builtin keymap '{}': {}", name, e);
2079                None
2080            }
2081        }
2082    }
2083
2084    /// Resolve a keymap with inheritance
2085    /// Returns all bindings from the keymap and its parent chain
2086    pub fn resolve_keymap(&self, map_name: &str) -> Vec<Keybinding> {
2087        let mut visited = std::collections::HashSet::new();
2088        self.resolve_keymap_recursive(map_name, &mut visited)
2089    }
2090
2091    /// Recursive helper for resolve_keymap
2092    fn resolve_keymap_recursive(
2093        &self,
2094        map_name: &str,
2095        visited: &mut std::collections::HashSet<String>,
2096    ) -> Vec<Keybinding> {
2097        // Prevent infinite loops
2098        if visited.contains(map_name) {
2099            eprintln!(
2100                "Warning: Circular inheritance detected in keymap '{}'",
2101                map_name
2102            );
2103            return Vec::new();
2104        }
2105        visited.insert(map_name.to_string());
2106
2107        // Try to load the keymap (user-defined or built-in)
2108        let keymap = self
2109            .keybinding_maps
2110            .get(map_name)
2111            .cloned()
2112            .or_else(|| Self::load_builtin_keymap(map_name));
2113
2114        let Some(keymap) = keymap else {
2115            return Vec::new();
2116        };
2117
2118        // Start with parent bindings (if any)
2119        let mut all_bindings = if let Some(ref parent_name) = keymap.inherits {
2120            self.resolve_keymap_recursive(parent_name, visited)
2121        } else {
2122            Vec::new()
2123        };
2124
2125        // Add this keymap's bindings (they override parent bindings)
2126        all_bindings.extend(keymap.bindings);
2127
2128        all_bindings
2129    }
2130    /// Create default language configurations
2131    fn default_languages() -> HashMap<String, LanguageConfig> {
2132        let mut languages = HashMap::new();
2133
2134        languages.insert(
2135            "rust".to_string(),
2136            LanguageConfig {
2137                extensions: vec!["rs".to_string()],
2138                filenames: vec![],
2139                grammar: "rust".to_string(),
2140                comment_prefix: Some("//".to_string()),
2141                auto_indent: true,
2142                highlighter: HighlighterPreference::Auto,
2143                textmate_grammar: None,
2144                show_whitespace_tabs: true,
2145                use_tabs: false,
2146                tab_size: None,
2147                formatter: Some(FormatterConfig {
2148                    command: "rustfmt".to_string(),
2149                    args: vec!["--edition".to_string(), "2021".to_string()],
2150                    stdin: true,
2151                    timeout_ms: 10000,
2152                }),
2153                format_on_save: false,
2154                on_save: vec![],
2155            },
2156        );
2157
2158        languages.insert(
2159            "javascript".to_string(),
2160            LanguageConfig {
2161                extensions: vec!["js".to_string(), "jsx".to_string(), "mjs".to_string()],
2162                filenames: vec![],
2163                grammar: "javascript".to_string(),
2164                comment_prefix: Some("//".to_string()),
2165                auto_indent: true,
2166                highlighter: HighlighterPreference::Auto,
2167                textmate_grammar: None,
2168                show_whitespace_tabs: true,
2169                use_tabs: false,
2170                tab_size: None,
2171                formatter: Some(FormatterConfig {
2172                    command: "prettier".to_string(),
2173                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2174                    stdin: true,
2175                    timeout_ms: 10000,
2176                }),
2177                format_on_save: false,
2178                on_save: vec![],
2179            },
2180        );
2181
2182        languages.insert(
2183            "typescript".to_string(),
2184            LanguageConfig {
2185                extensions: vec!["ts".to_string(), "tsx".to_string(), "mts".to_string()],
2186                filenames: vec![],
2187                grammar: "typescript".to_string(),
2188                comment_prefix: Some("//".to_string()),
2189                auto_indent: true,
2190                highlighter: HighlighterPreference::Auto,
2191                textmate_grammar: None,
2192                show_whitespace_tabs: true,
2193                use_tabs: false,
2194                tab_size: None,
2195                formatter: Some(FormatterConfig {
2196                    command: "prettier".to_string(),
2197                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2198                    stdin: true,
2199                    timeout_ms: 10000,
2200                }),
2201                format_on_save: false,
2202                on_save: vec![],
2203            },
2204        );
2205
2206        languages.insert(
2207            "python".to_string(),
2208            LanguageConfig {
2209                extensions: vec!["py".to_string(), "pyi".to_string()],
2210                filenames: vec![],
2211                grammar: "python".to_string(),
2212                comment_prefix: Some("#".to_string()),
2213                auto_indent: true,
2214                highlighter: HighlighterPreference::Auto,
2215                textmate_grammar: None,
2216                show_whitespace_tabs: true,
2217                use_tabs: false,
2218                tab_size: None,
2219                formatter: Some(FormatterConfig {
2220                    command: "ruff".to_string(),
2221                    args: vec![
2222                        "format".to_string(),
2223                        "--stdin-filename".to_string(),
2224                        "$FILE".to_string(),
2225                    ],
2226                    stdin: true,
2227                    timeout_ms: 10000,
2228                }),
2229                format_on_save: false,
2230                on_save: vec![],
2231            },
2232        );
2233
2234        languages.insert(
2235            "c".to_string(),
2236            LanguageConfig {
2237                extensions: vec!["c".to_string(), "h".to_string()],
2238                filenames: vec![],
2239                grammar: "c".to_string(),
2240                comment_prefix: Some("//".to_string()),
2241                auto_indent: true,
2242                highlighter: HighlighterPreference::Auto,
2243                textmate_grammar: None,
2244                show_whitespace_tabs: true,
2245                use_tabs: false,
2246                tab_size: None,
2247                formatter: Some(FormatterConfig {
2248                    command: "clang-format".to_string(),
2249                    args: vec![],
2250                    stdin: true,
2251                    timeout_ms: 10000,
2252                }),
2253                format_on_save: false,
2254                on_save: vec![],
2255            },
2256        );
2257
2258        languages.insert(
2259            "cpp".to_string(),
2260            LanguageConfig {
2261                extensions: vec![
2262                    "cpp".to_string(),
2263                    "cc".to_string(),
2264                    "cxx".to_string(),
2265                    "hpp".to_string(),
2266                    "hh".to_string(),
2267                    "hxx".to_string(),
2268                ],
2269                filenames: vec![],
2270                grammar: "cpp".to_string(),
2271                comment_prefix: Some("//".to_string()),
2272                auto_indent: true,
2273                highlighter: HighlighterPreference::Auto,
2274                textmate_grammar: None,
2275                show_whitespace_tabs: true,
2276                use_tabs: false,
2277                tab_size: None,
2278                formatter: Some(FormatterConfig {
2279                    command: "clang-format".to_string(),
2280                    args: vec![],
2281                    stdin: true,
2282                    timeout_ms: 10000,
2283                }),
2284                format_on_save: false,
2285                on_save: vec![],
2286            },
2287        );
2288
2289        languages.insert(
2290            "csharp".to_string(),
2291            LanguageConfig {
2292                extensions: vec!["cs".to_string()],
2293                filenames: vec![],
2294                grammar: "c_sharp".to_string(),
2295                comment_prefix: Some("//".to_string()),
2296                auto_indent: true,
2297                highlighter: HighlighterPreference::Auto,
2298                textmate_grammar: None,
2299                show_whitespace_tabs: true,
2300                use_tabs: false,
2301                tab_size: None,
2302                formatter: None,
2303                format_on_save: false,
2304                on_save: vec![],
2305            },
2306        );
2307
2308        languages.insert(
2309            "bash".to_string(),
2310            LanguageConfig {
2311                extensions: vec!["sh".to_string(), "bash".to_string()],
2312                filenames: vec![
2313                    ".bash_aliases".to_string(),
2314                    ".bash_logout".to_string(),
2315                    ".bash_profile".to_string(),
2316                    ".bashrc".to_string(),
2317                    ".env".to_string(),
2318                    ".profile".to_string(),
2319                    ".zlogin".to_string(),
2320                    ".zlogout".to_string(),
2321                    ".zprofile".to_string(),
2322                    ".zshenv".to_string(),
2323                    ".zshrc".to_string(),
2324                    // Common shell script files without extensions
2325                    "PKGBUILD".to_string(),
2326                    "APKBUILD".to_string(),
2327                ],
2328                grammar: "bash".to_string(),
2329                comment_prefix: Some("#".to_string()),
2330                auto_indent: true,
2331                highlighter: HighlighterPreference::Auto,
2332                textmate_grammar: None,
2333                show_whitespace_tabs: true,
2334                use_tabs: false,
2335                tab_size: None,
2336                formatter: None,
2337                format_on_save: false,
2338                on_save: vec![],
2339            },
2340        );
2341
2342        languages.insert(
2343            "makefile".to_string(),
2344            LanguageConfig {
2345                extensions: vec!["mk".to_string()],
2346                filenames: vec![
2347                    "Makefile".to_string(),
2348                    "makefile".to_string(),
2349                    "GNUmakefile".to_string(),
2350                ],
2351                grammar: "make".to_string(),
2352                comment_prefix: Some("#".to_string()),
2353                auto_indent: false,
2354                highlighter: HighlighterPreference::Auto,
2355                textmate_grammar: None,
2356                show_whitespace_tabs: true,
2357                use_tabs: true,    // Makefiles require tabs for recipes
2358                tab_size: Some(8), // Makefiles traditionally use 8-space tabs
2359                formatter: None,
2360                format_on_save: false,
2361                on_save: vec![],
2362            },
2363        );
2364
2365        languages.insert(
2366            "dockerfile".to_string(),
2367            LanguageConfig {
2368                extensions: vec!["dockerfile".to_string()],
2369                filenames: vec!["Dockerfile".to_string(), "Containerfile".to_string()],
2370                grammar: "dockerfile".to_string(),
2371                comment_prefix: Some("#".to_string()),
2372                auto_indent: true,
2373                highlighter: HighlighterPreference::Auto,
2374                textmate_grammar: None,
2375                show_whitespace_tabs: true,
2376                use_tabs: false,
2377                tab_size: None,
2378                formatter: None,
2379                format_on_save: false,
2380                on_save: vec![],
2381            },
2382        );
2383
2384        languages.insert(
2385            "json".to_string(),
2386            LanguageConfig {
2387                extensions: vec!["json".to_string(), "jsonc".to_string()],
2388                filenames: vec![],
2389                grammar: "json".to_string(),
2390                comment_prefix: None,
2391                auto_indent: true,
2392                highlighter: HighlighterPreference::Auto,
2393                textmate_grammar: None,
2394                show_whitespace_tabs: true,
2395                use_tabs: false,
2396                tab_size: None,
2397                formatter: Some(FormatterConfig {
2398                    command: "prettier".to_string(),
2399                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2400                    stdin: true,
2401                    timeout_ms: 10000,
2402                }),
2403                format_on_save: false,
2404                on_save: vec![],
2405            },
2406        );
2407
2408        languages.insert(
2409            "toml".to_string(),
2410            LanguageConfig {
2411                extensions: vec!["toml".to_string()],
2412                filenames: vec!["Cargo.lock".to_string()],
2413                grammar: "toml".to_string(),
2414                comment_prefix: Some("#".to_string()),
2415                auto_indent: true,
2416                highlighter: HighlighterPreference::Auto,
2417                textmate_grammar: None,
2418                show_whitespace_tabs: true,
2419                use_tabs: false,
2420                tab_size: None,
2421                formatter: None,
2422                format_on_save: false,
2423                on_save: vec![],
2424            },
2425        );
2426
2427        languages.insert(
2428            "yaml".to_string(),
2429            LanguageConfig {
2430                extensions: vec!["yml".to_string(), "yaml".to_string()],
2431                filenames: vec![],
2432                grammar: "yaml".to_string(),
2433                comment_prefix: Some("#".to_string()),
2434                auto_indent: true,
2435                highlighter: HighlighterPreference::Auto,
2436                textmate_grammar: None,
2437                show_whitespace_tabs: true,
2438                use_tabs: false,
2439                tab_size: None,
2440                formatter: Some(FormatterConfig {
2441                    command: "prettier".to_string(),
2442                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2443                    stdin: true,
2444                    timeout_ms: 10000,
2445                }),
2446                format_on_save: false,
2447                on_save: vec![],
2448            },
2449        );
2450
2451        languages.insert(
2452            "markdown".to_string(),
2453            LanguageConfig {
2454                extensions: vec!["md".to_string(), "markdown".to_string()],
2455                filenames: vec!["README".to_string()],
2456                grammar: "markdown".to_string(),
2457                comment_prefix: None,
2458                auto_indent: false,
2459                highlighter: HighlighterPreference::Auto,
2460                textmate_grammar: None,
2461                show_whitespace_tabs: true,
2462                use_tabs: false,
2463                tab_size: None,
2464                formatter: None,
2465                format_on_save: false,
2466                on_save: vec![],
2467            },
2468        );
2469
2470        // Go uses tabs for indentation by convention, so hide tab indicators and use tabs
2471        languages.insert(
2472            "go".to_string(),
2473            LanguageConfig {
2474                extensions: vec!["go".to_string()],
2475                filenames: vec![],
2476                grammar: "go".to_string(),
2477                comment_prefix: Some("//".to_string()),
2478                auto_indent: true,
2479                highlighter: HighlighterPreference::Auto,
2480                textmate_grammar: None,
2481                show_whitespace_tabs: false,
2482                use_tabs: true,    // Go convention is to use tabs
2483                tab_size: Some(8), // Go convention is 8-space tab width
2484                formatter: Some(FormatterConfig {
2485                    command: "gofmt".to_string(),
2486                    args: vec![],
2487                    stdin: true,
2488                    timeout_ms: 10000,
2489                }),
2490                format_on_save: false,
2491                on_save: vec![],
2492            },
2493        );
2494
2495        languages.insert(
2496            "odin".to_string(),
2497            LanguageConfig {
2498                extensions: vec!["odin".to_string()],
2499                filenames: vec![],
2500                grammar: "odin".to_string(),
2501                comment_prefix: Some("//".to_string()),
2502                auto_indent: true,
2503                highlighter: HighlighterPreference::Auto,
2504                textmate_grammar: None,
2505                show_whitespace_tabs: false,
2506                use_tabs: true,
2507                tab_size: Some(8),
2508                formatter: None,
2509                format_on_save: false,
2510                on_save: vec![],
2511            },
2512        );
2513
2514        languages.insert(
2515            "zig".to_string(),
2516            LanguageConfig {
2517                extensions: vec!["zig".to_string(), "zon".to_string()],
2518                filenames: vec![],
2519                grammar: "zig".to_string(),
2520                comment_prefix: Some("//".to_string()),
2521                auto_indent: true,
2522                highlighter: HighlighterPreference::Auto,
2523                textmate_grammar: None,
2524                show_whitespace_tabs: true,
2525                use_tabs: false,
2526                tab_size: None,
2527                formatter: None,
2528                format_on_save: false,
2529                on_save: vec![],
2530            },
2531        );
2532
2533        languages.insert(
2534            "java".to_string(),
2535            LanguageConfig {
2536                extensions: vec!["java".to_string()],
2537                filenames: vec![],
2538                grammar: "java".to_string(),
2539                comment_prefix: Some("//".to_string()),
2540                auto_indent: true,
2541                highlighter: HighlighterPreference::Auto,
2542                textmate_grammar: None,
2543                show_whitespace_tabs: true,
2544                use_tabs: false,
2545                tab_size: None,
2546                formatter: None,
2547                format_on_save: false,
2548                on_save: vec![],
2549            },
2550        );
2551
2552        languages.insert(
2553            "latex".to_string(),
2554            LanguageConfig {
2555                extensions: vec![
2556                    "tex".to_string(),
2557                    "latex".to_string(),
2558                    "ltx".to_string(),
2559                    "sty".to_string(),
2560                    "cls".to_string(),
2561                    "bib".to_string(),
2562                ],
2563                filenames: vec![],
2564                grammar: "latex".to_string(),
2565                comment_prefix: Some("%".to_string()),
2566                auto_indent: true,
2567                highlighter: HighlighterPreference::Auto,
2568                textmate_grammar: None,
2569                show_whitespace_tabs: true,
2570                use_tabs: false,
2571                tab_size: None,
2572                formatter: None,
2573                format_on_save: false,
2574                on_save: vec![],
2575            },
2576        );
2577
2578        languages.insert(
2579            "templ".to_string(),
2580            LanguageConfig {
2581                extensions: vec!["templ".to_string()],
2582                filenames: vec![],
2583                grammar: "go".to_string(), // Templ uses Go-like syntax
2584                comment_prefix: Some("//".to_string()),
2585                auto_indent: true,
2586                highlighter: HighlighterPreference::Auto,
2587                textmate_grammar: None,
2588                show_whitespace_tabs: true,
2589                use_tabs: false,
2590                tab_size: None,
2591                formatter: None,
2592                format_on_save: false,
2593                on_save: vec![],
2594            },
2595        );
2596
2597        // Git-related file types
2598        languages.insert(
2599            "git-rebase".to_string(),
2600            LanguageConfig {
2601                extensions: vec![],
2602                filenames: vec!["git-rebase-todo".to_string()],
2603                grammar: "Git Rebase Todo".to_string(),
2604                comment_prefix: Some("#".to_string()),
2605                auto_indent: false,
2606                highlighter: HighlighterPreference::Auto,
2607                textmate_grammar: None,
2608                show_whitespace_tabs: true,
2609                use_tabs: false,
2610                tab_size: None,
2611                formatter: None,
2612                format_on_save: false,
2613                on_save: vec![],
2614            },
2615        );
2616
2617        languages.insert(
2618            "git-commit".to_string(),
2619            LanguageConfig {
2620                extensions: vec![],
2621                filenames: vec![
2622                    "COMMIT_EDITMSG".to_string(),
2623                    "MERGE_MSG".to_string(),
2624                    "SQUASH_MSG".to_string(),
2625                    "TAG_EDITMSG".to_string(),
2626                ],
2627                grammar: "Git Commit Message".to_string(),
2628                comment_prefix: Some("#".to_string()),
2629                auto_indent: false,
2630                highlighter: HighlighterPreference::Auto,
2631                textmate_grammar: None,
2632                show_whitespace_tabs: true,
2633                use_tabs: false,
2634                tab_size: None,
2635                formatter: None,
2636                format_on_save: false,
2637                on_save: vec![],
2638            },
2639        );
2640
2641        languages.insert(
2642            "gitignore".to_string(),
2643            LanguageConfig {
2644                extensions: vec!["gitignore".to_string()],
2645                filenames: vec![
2646                    ".gitignore".to_string(),
2647                    ".dockerignore".to_string(),
2648                    ".npmignore".to_string(),
2649                    ".hgignore".to_string(),
2650                ],
2651                grammar: "Gitignore".to_string(),
2652                comment_prefix: Some("#".to_string()),
2653                auto_indent: false,
2654                highlighter: HighlighterPreference::Auto,
2655                textmate_grammar: None,
2656                show_whitespace_tabs: true,
2657                use_tabs: false,
2658                tab_size: None,
2659                formatter: None,
2660                format_on_save: false,
2661                on_save: vec![],
2662            },
2663        );
2664
2665        languages.insert(
2666            "gitconfig".to_string(),
2667            LanguageConfig {
2668                extensions: vec!["gitconfig".to_string()],
2669                filenames: vec![".gitconfig".to_string(), ".gitmodules".to_string()],
2670                grammar: "Git Config".to_string(),
2671                comment_prefix: Some("#".to_string()),
2672                auto_indent: true,
2673                highlighter: HighlighterPreference::Auto,
2674                textmate_grammar: None,
2675                show_whitespace_tabs: true,
2676                use_tabs: false,
2677                tab_size: None,
2678                formatter: None,
2679                format_on_save: false,
2680                on_save: vec![],
2681            },
2682        );
2683
2684        languages.insert(
2685            "gitattributes".to_string(),
2686            LanguageConfig {
2687                extensions: vec!["gitattributes".to_string()],
2688                filenames: vec![".gitattributes".to_string()],
2689                grammar: "Git Attributes".to_string(),
2690                comment_prefix: Some("#".to_string()),
2691                auto_indent: false,
2692                highlighter: HighlighterPreference::Auto,
2693                textmate_grammar: None,
2694                show_whitespace_tabs: true,
2695                use_tabs: false,
2696                tab_size: None,
2697                formatter: None,
2698                format_on_save: false,
2699                on_save: vec![],
2700            },
2701        );
2702
2703        languages
2704    }
2705
2706    /// Create default LSP configurations
2707    #[cfg(feature = "runtime")]
2708    fn default_lsp_config() -> HashMap<String, LspServerConfig> {
2709        let mut lsp = HashMap::new();
2710
2711        // rust-analyzer (installed via rustup or package manager)
2712        // Enable logging to help debug LSP issues (stored in XDG state directory)
2713        let ra_log_path = crate::services::log_dirs::lsp_log_path("rust-analyzer")
2714            .to_string_lossy()
2715            .to_string();
2716
2717        Self::populate_lsp_config(&mut lsp, ra_log_path);
2718        lsp
2719    }
2720
2721    /// Create empty LSP configurations for WASM builds
2722    #[cfg(not(feature = "runtime"))]
2723    fn default_lsp_config() -> HashMap<String, LspServerConfig> {
2724        // LSP is not available in WASM builds
2725        HashMap::new()
2726    }
2727
2728    #[cfg(feature = "runtime")]
2729    fn populate_lsp_config(lsp: &mut HashMap<String, LspServerConfig>, ra_log_path: String) {
2730        // Minimal performance config for rust-analyzer:
2731        // - checkOnSave: false - disables cargo check on every save (the #1 cause of slowdowns)
2732        // - cachePriming.enable: false - disables background indexing of entire crate graph
2733        // - procMacro.enable: false - disables proc-macro expansion (saves CPU/RAM)
2734        // - cargo.buildScripts.enable: false - prevents running build.rs automatically
2735        // - cargo.autoreload: false - only reload manually
2736        // - diagnostics.enable: true - keeps basic syntax error reporting
2737        // - files.watcher: "server" - more efficient than editor-side watchers
2738        let ra_init_options = serde_json::json!({
2739            "checkOnSave": false,
2740            "cachePriming": { "enable": false },
2741            "procMacro": { "enable": false },
2742            "cargo": {
2743                "buildScripts": { "enable": false },
2744                "autoreload": false
2745            },
2746            "diagnostics": { "enable": true },
2747            "files": { "watcher": "server" }
2748        });
2749
2750        lsp.insert(
2751            "rust".to_string(),
2752            LspServerConfig {
2753                command: "rust-analyzer".to_string(),
2754                args: vec!["--log-file".to_string(), ra_log_path],
2755                enabled: true,
2756                auto_start: false,
2757                process_limits: ProcessLimits::default(),
2758                initialization_options: Some(ra_init_options),
2759            },
2760        );
2761
2762        // pylsp (installed via pip)
2763        lsp.insert(
2764            "python".to_string(),
2765            LspServerConfig {
2766                command: "pylsp".to_string(),
2767                args: vec![],
2768                enabled: true,
2769                auto_start: false,
2770                process_limits: ProcessLimits::default(),
2771                initialization_options: None,
2772            },
2773        );
2774
2775        // typescript-language-server (installed via npm)
2776        // Alternative: use "deno lsp" with initialization_options: {"enable": true}
2777        let ts_lsp = LspServerConfig {
2778            command: "typescript-language-server".to_string(),
2779            args: vec!["--stdio".to_string()],
2780            enabled: true,
2781            auto_start: false,
2782            process_limits: ProcessLimits::default(),
2783            initialization_options: None,
2784        };
2785        lsp.insert("javascript".to_string(), ts_lsp.clone());
2786        lsp.insert("typescript".to_string(), ts_lsp);
2787
2788        // vscode-html-language-server (installed via npm install -g vscode-langservers-extracted)
2789        lsp.insert(
2790            "html".to_string(),
2791            LspServerConfig {
2792                command: "vscode-html-language-server".to_string(),
2793                args: vec!["--stdio".to_string()],
2794                enabled: true,
2795                auto_start: false,
2796                process_limits: ProcessLimits::default(),
2797                initialization_options: None,
2798            },
2799        );
2800
2801        // vscode-css-language-server (installed via npm install -g vscode-langservers-extracted)
2802        lsp.insert(
2803            "css".to_string(),
2804            LspServerConfig {
2805                command: "vscode-css-language-server".to_string(),
2806                args: vec!["--stdio".to_string()],
2807                enabled: true,
2808                auto_start: false,
2809                process_limits: ProcessLimits::default(),
2810                initialization_options: None,
2811            },
2812        );
2813
2814        // clangd (installed via package manager)
2815        lsp.insert(
2816            "c".to_string(),
2817            LspServerConfig {
2818                command: "clangd".to_string(),
2819                args: vec![],
2820                enabled: true,
2821                auto_start: false,
2822                process_limits: ProcessLimits::default(),
2823                initialization_options: None,
2824            },
2825        );
2826        lsp.insert(
2827            "cpp".to_string(),
2828            LspServerConfig {
2829                command: "clangd".to_string(),
2830                args: vec![],
2831                enabled: true,
2832                auto_start: false,
2833                process_limits: ProcessLimits::default(),
2834                initialization_options: None,
2835            },
2836        );
2837
2838        // gopls (installed via go install)
2839        lsp.insert(
2840            "go".to_string(),
2841            LspServerConfig {
2842                command: "gopls".to_string(),
2843                args: vec![],
2844                enabled: true,
2845                auto_start: false,
2846                process_limits: ProcessLimits::default(),
2847                initialization_options: None,
2848            },
2849        );
2850
2851        // vscode-json-language-server (installed via npm install -g vscode-langservers-extracted)
2852        lsp.insert(
2853            "json".to_string(),
2854            LspServerConfig {
2855                command: "vscode-json-language-server".to_string(),
2856                args: vec!["--stdio".to_string()],
2857                enabled: true,
2858                auto_start: false,
2859                process_limits: ProcessLimits::default(),
2860                initialization_options: None,
2861            },
2862        );
2863
2864        // csharp-language-server (installed via dotnet tool install -g csharp-ls)
2865        lsp.insert(
2866            "csharp".to_string(),
2867            LspServerConfig {
2868                command: "csharp-ls".to_string(),
2869                args: vec![],
2870                enabled: true,
2871                auto_start: false,
2872                process_limits: ProcessLimits::default(),
2873                initialization_options: None,
2874            },
2875        );
2876
2877        // ols - Odin Language Server (https://github.com/DanielGavin/ols)
2878        // Build from source: cd ols && ./build.sh (Linux/macOS) or ./build.bat (Windows)
2879        lsp.insert(
2880            "odin".to_string(),
2881            LspServerConfig {
2882                command: "ols".to_string(),
2883                args: vec![],
2884                enabled: true,
2885                auto_start: false,
2886                process_limits: ProcessLimits::default(),
2887                initialization_options: None,
2888            },
2889        );
2890
2891        // zls - Zig Language Server (https://github.com/zigtools/zls)
2892        // Install via package manager or download from releases
2893        lsp.insert(
2894            "zig".to_string(),
2895            LspServerConfig {
2896                command: "zls".to_string(),
2897                args: vec![],
2898                enabled: true,
2899                auto_start: false,
2900                process_limits: ProcessLimits::default(),
2901                initialization_options: None,
2902            },
2903        );
2904
2905        // jdtls - Eclipse JDT Language Server for Java
2906        // Install via package manager or download from Eclipse
2907        lsp.insert(
2908            "java".to_string(),
2909            LspServerConfig {
2910                command: "jdtls".to_string(),
2911                args: vec![],
2912                enabled: true,
2913                auto_start: false,
2914                process_limits: ProcessLimits::default(),
2915                initialization_options: None,
2916            },
2917        );
2918
2919        // texlab - LaTeX Language Server (https://github.com/latex-lsp/texlab)
2920        // Install via cargo install texlab or package manager
2921        lsp.insert(
2922            "latex".to_string(),
2923            LspServerConfig {
2924                command: "texlab".to_string(),
2925                args: vec![],
2926                enabled: true,
2927                auto_start: false,
2928                process_limits: ProcessLimits::default(),
2929                initialization_options: None,
2930            },
2931        );
2932
2933        // marksman - Markdown Language Server (https://github.com/artempyanykh/marksman)
2934        // Install via package manager or download from releases
2935        lsp.insert(
2936            "markdown".to_string(),
2937            LspServerConfig {
2938                command: "marksman".to_string(),
2939                args: vec!["server".to_string()],
2940                enabled: true,
2941                auto_start: false,
2942                process_limits: ProcessLimits::default(),
2943                initialization_options: None,
2944            },
2945        );
2946
2947        // templ - Templ Language Server (https://templ.guide)
2948        // Install via go install github.com/a-h/templ/cmd/templ@latest
2949        lsp.insert(
2950            "templ".to_string(),
2951            LspServerConfig {
2952                command: "templ".to_string(),
2953                args: vec!["lsp".to_string()],
2954                enabled: true,
2955                auto_start: false,
2956                process_limits: ProcessLimits::default(),
2957                initialization_options: None,
2958            },
2959        );
2960    }
2961
2962    /// Validate the configuration
2963    pub fn validate(&self) -> Result<(), ConfigError> {
2964        // Validate tab size
2965        if self.editor.tab_size == 0 {
2966            return Err(ConfigError::ValidationError(
2967                "tab_size must be greater than 0".to_string(),
2968            ));
2969        }
2970
2971        // Validate scroll offset
2972        if self.editor.scroll_offset > 100 {
2973            return Err(ConfigError::ValidationError(
2974                "scroll_offset must be <= 100".to_string(),
2975            ));
2976        }
2977
2978        // Validate keybindings
2979        for binding in &self.keybindings {
2980            if binding.key.is_empty() {
2981                return Err(ConfigError::ValidationError(
2982                    "keybinding key cannot be empty".to_string(),
2983                ));
2984            }
2985            if binding.action.is_empty() {
2986                return Err(ConfigError::ValidationError(
2987                    "keybinding action cannot be empty".to_string(),
2988                ));
2989            }
2990        }
2991
2992        Ok(())
2993    }
2994}
2995
2996/// Configuration error types
2997#[derive(Debug)]
2998pub enum ConfigError {
2999    IoError(String),
3000    ParseError(String),
3001    SerializeError(String),
3002    ValidationError(String),
3003}
3004
3005impl std::fmt::Display for ConfigError {
3006    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3007        match self {
3008            Self::IoError(msg) => write!(f, "IO error: {msg}"),
3009            Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
3010            Self::SerializeError(msg) => write!(f, "Serialize error: {msg}"),
3011            Self::ValidationError(msg) => write!(f, "Validation error: {msg}"),
3012        }
3013    }
3014}
3015
3016impl std::error::Error for ConfigError {}
3017
3018#[cfg(test)]
3019mod tests {
3020    use super::*;
3021
3022    #[test]
3023    fn test_default_config() {
3024        let config = Config::default();
3025        assert_eq!(config.editor.tab_size, 4);
3026        assert!(config.editor.line_numbers);
3027        assert!(config.editor.syntax_highlighting);
3028        // keybindings is empty by design - it's for user customizations only
3029        // The actual keybindings come from resolve_keymap(active_keybinding_map)
3030        assert!(config.keybindings.is_empty());
3031        // But the resolved keymap should have bindings
3032        let resolved = config.resolve_keymap(&config.active_keybinding_map);
3033        assert!(!resolved.is_empty());
3034    }
3035
3036    #[test]
3037    fn test_all_builtin_keymaps_loadable() {
3038        for name in KeybindingMapName::BUILTIN_OPTIONS {
3039            let keymap = Config::load_builtin_keymap(name);
3040            assert!(keymap.is_some(), "Failed to load builtin keymap '{}'", name);
3041        }
3042    }
3043
3044    #[test]
3045    fn test_config_validation() {
3046        let mut config = Config::default();
3047        assert!(config.validate().is_ok());
3048
3049        config.editor.tab_size = 0;
3050        assert!(config.validate().is_err());
3051    }
3052
3053    #[test]
3054    fn test_macos_keymap_inherits_enter_bindings() {
3055        let config = Config::default();
3056        let bindings = config.resolve_keymap("macos");
3057
3058        let enter_bindings: Vec<_> = bindings.iter().filter(|b| b.key == "Enter").collect();
3059        assert!(
3060            !enter_bindings.is_empty(),
3061            "macos keymap should inherit Enter bindings from default, got {} Enter bindings",
3062            enter_bindings.len()
3063        );
3064        // Should have at least insert_newline for normal mode
3065        let has_insert_newline = enter_bindings.iter().any(|b| b.action == "insert_newline");
3066        assert!(
3067            has_insert_newline,
3068            "macos keymap should have insert_newline action for Enter key"
3069        );
3070    }
3071
3072    #[test]
3073    fn test_config_serialize_deserialize() {
3074        // Test that Config can be serialized and deserialized correctly
3075        let config = Config::default();
3076
3077        // Serialize to JSON
3078        let json = serde_json::to_string_pretty(&config).unwrap();
3079
3080        // Deserialize back
3081        let loaded: Config = serde_json::from_str(&json).unwrap();
3082
3083        assert_eq!(config.editor.tab_size, loaded.editor.tab_size);
3084        assert_eq!(config.theme, loaded.theme);
3085    }
3086
3087    #[test]
3088    fn test_config_with_custom_keybinding() {
3089        let json = r#"{
3090            "editor": {
3091                "tab_size": 2
3092            },
3093            "keybindings": [
3094                {
3095                    "key": "x",
3096                    "modifiers": ["ctrl", "shift"],
3097                    "action": "custom_action",
3098                    "args": {},
3099                    "when": null
3100                }
3101            ]
3102        }"#;
3103
3104        let config: Config = serde_json::from_str(json).unwrap();
3105        assert_eq!(config.editor.tab_size, 2);
3106        assert_eq!(config.keybindings.len(), 1);
3107        assert_eq!(config.keybindings[0].key, "x");
3108        assert_eq!(config.keybindings[0].modifiers.len(), 2);
3109    }
3110
3111    #[test]
3112    fn test_sparse_config_merges_with_defaults() {
3113        // User config that only specifies one LSP server
3114        let temp_dir = tempfile::tempdir().unwrap();
3115        let config_path = temp_dir.path().join("config.json");
3116
3117        // Write a sparse config - only overriding rust LSP
3118        let sparse_config = r#"{
3119            "lsp": {
3120                "rust": {
3121                    "command": "custom-rust-analyzer",
3122                    "args": ["--custom-arg"]
3123                }
3124            }
3125        }"#;
3126        std::fs::write(&config_path, sparse_config).unwrap();
3127
3128        // Load the config - should merge with defaults
3129        let loaded = Config::load_from_file(&config_path).unwrap();
3130
3131        // User's rust override should be present
3132        assert!(loaded.lsp.contains_key("rust"));
3133        assert_eq!(
3134            loaded.lsp["rust"].command,
3135            "custom-rust-analyzer".to_string()
3136        );
3137
3138        // Default LSP servers should also be present (merged from defaults)
3139        assert!(
3140            loaded.lsp.contains_key("python"),
3141            "python LSP should be merged from defaults"
3142        );
3143        assert!(
3144            loaded.lsp.contains_key("typescript"),
3145            "typescript LSP should be merged from defaults"
3146        );
3147        assert!(
3148            loaded.lsp.contains_key("javascript"),
3149            "javascript LSP should be merged from defaults"
3150        );
3151
3152        // Default language configs should also be present
3153        assert!(loaded.languages.contains_key("rust"));
3154        assert!(loaded.languages.contains_key("python"));
3155        assert!(loaded.languages.contains_key("typescript"));
3156    }
3157
3158    #[test]
3159    fn test_empty_config_gets_all_defaults() {
3160        let temp_dir = tempfile::tempdir().unwrap();
3161        let config_path = temp_dir.path().join("config.json");
3162
3163        // Write an empty config
3164        std::fs::write(&config_path, "{}").unwrap();
3165
3166        let loaded = Config::load_from_file(&config_path).unwrap();
3167        let defaults = Config::default();
3168
3169        // Should have all default LSP servers
3170        assert_eq!(loaded.lsp.len(), defaults.lsp.len());
3171
3172        // Should have all default languages
3173        assert_eq!(loaded.languages.len(), defaults.languages.len());
3174    }
3175
3176    #[test]
3177    fn test_dynamic_submenu_expansion() {
3178        // Test that DynamicSubmenu expands to Submenu with generated items
3179        let dynamic = MenuItem::DynamicSubmenu {
3180            label: "Test".to_string(),
3181            source: "copy_with_theme".to_string(),
3182        };
3183
3184        let expanded = dynamic.expand_dynamic();
3185
3186        // Should expand to a Submenu
3187        match expanded {
3188            MenuItem::Submenu { label, items } => {
3189                assert_eq!(label, "Test");
3190                // Should have items for each available theme
3191                let loader = crate::view::theme::ThemeLoader::new();
3192                let registry = loader.load_all();
3193                assert_eq!(items.len(), registry.len());
3194
3195                // Each item should be an Action with copy_with_theme
3196                for (item, theme_info) in items.iter().zip(registry.list().iter()) {
3197                    match item {
3198                        MenuItem::Action {
3199                            label,
3200                            action,
3201                            args,
3202                            ..
3203                        } => {
3204                            assert_eq!(label, &theme_info.name);
3205                            assert_eq!(action, "copy_with_theme");
3206                            assert_eq!(
3207                                args.get("theme").and_then(|v| v.as_str()),
3208                                Some(theme_info.name.as_str())
3209                            );
3210                        }
3211                        _ => panic!("Expected Action item"),
3212                    }
3213                }
3214            }
3215            _ => panic!("Expected Submenu after expansion"),
3216        }
3217    }
3218
3219    #[test]
3220    fn test_non_dynamic_item_unchanged() {
3221        // Non-DynamicSubmenu items should be unchanged by expand_dynamic
3222        let action = MenuItem::Action {
3223            label: "Test".to_string(),
3224            action: "test".to_string(),
3225            args: HashMap::new(),
3226            when: None,
3227            checkbox: None,
3228        };
3229
3230        let expanded = action.expand_dynamic();
3231        match expanded {
3232            MenuItem::Action { label, action, .. } => {
3233                assert_eq!(label, "Test");
3234                assert_eq!(action, "test");
3235            }
3236            _ => panic!("Action should remain Action after expand_dynamic"),
3237        }
3238    }
3239
3240    #[test]
3241    fn test_buffer_config_uses_global_defaults() {
3242        let config = Config::default();
3243        let buffer_config = BufferConfig::resolve(&config, None);
3244
3245        assert_eq!(buffer_config.tab_size, config.editor.tab_size);
3246        assert_eq!(buffer_config.auto_indent, config.editor.auto_indent);
3247        assert!(!buffer_config.use_tabs); // Default is spaces
3248        assert!(buffer_config.show_whitespace_tabs);
3249        assert!(buffer_config.formatter.is_none());
3250        assert!(!buffer_config.format_on_save);
3251    }
3252
3253    #[test]
3254    fn test_buffer_config_applies_language_overrides() {
3255        let mut config = Config::default();
3256
3257        // Add a language config with custom settings
3258        config.languages.insert(
3259            "go".to_string(),
3260            LanguageConfig {
3261                extensions: vec!["go".to_string()],
3262                filenames: vec![],
3263                grammar: "go".to_string(),
3264                comment_prefix: Some("//".to_string()),
3265                auto_indent: true,
3266                highlighter: HighlighterPreference::Auto,
3267                textmate_grammar: None,
3268                show_whitespace_tabs: false, // Go hides tab indicators
3269                use_tabs: true,              // Go uses tabs
3270                tab_size: Some(8),           // Go uses 8-space tabs
3271                formatter: Some(FormatterConfig {
3272                    command: "gofmt".to_string(),
3273                    args: vec![],
3274                    stdin: true,
3275                    timeout_ms: 10000,
3276                }),
3277                format_on_save: true,
3278                on_save: vec![],
3279            },
3280        );
3281
3282        let buffer_config = BufferConfig::resolve(&config, Some("go"));
3283
3284        assert_eq!(buffer_config.tab_size, 8);
3285        assert!(buffer_config.use_tabs);
3286        assert!(!buffer_config.show_whitespace_tabs);
3287        assert!(buffer_config.format_on_save);
3288        assert!(buffer_config.formatter.is_some());
3289        assert_eq!(buffer_config.formatter.as_ref().unwrap().command, "gofmt");
3290    }
3291
3292    #[test]
3293    fn test_buffer_config_unknown_language_uses_global() {
3294        let config = Config::default();
3295        let buffer_config = BufferConfig::resolve(&config, Some("unknown_lang"));
3296
3297        // Should fall back to global settings
3298        assert_eq!(buffer_config.tab_size, config.editor.tab_size);
3299        assert!(!buffer_config.use_tabs);
3300    }
3301
3302    #[test]
3303    fn test_buffer_config_indent_string() {
3304        let config = Config::default();
3305
3306        // Spaces indent
3307        let spaces_config = BufferConfig::resolve(&config, None);
3308        assert_eq!(spaces_config.indent_string(), "    "); // 4 spaces
3309
3310        // Tabs indent - create a language that uses tabs
3311        let mut config_with_tabs = Config::default();
3312        config_with_tabs.languages.insert(
3313            "makefile".to_string(),
3314            LanguageConfig {
3315                use_tabs: true,
3316                tab_size: Some(8),
3317                ..Default::default()
3318            },
3319        );
3320        let tabs_config = BufferConfig::resolve(&config_with_tabs, Some("makefile"));
3321        assert_eq!(tabs_config.indent_string(), "\t");
3322    }
3323}