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