Skip to main content

fresh/
config.rs

1use crate::types::{context_keys, LspLanguageConfig, LspServerConfig, ProcessLimits};
2
3use rust_i18n::t;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use std::borrow::Cow;
7use std::collections::HashMap;
8use std::ops::Deref;
9use std::path::Path;
10
11/// Newtype for theme name that generates proper JSON Schema with enum options
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct ThemeName(pub String);
15
16impl ThemeName {
17    /// Built-in theme options shown in the settings dropdown
18    pub const BUILTIN_OPTIONS: &'static [&'static str] =
19        &["dark", "light", "high-contrast", "nostalgia"];
20}
21
22impl Deref for ThemeName {
23    type Target = str;
24    fn deref(&self) -> &Self::Target {
25        &self.0
26    }
27}
28
29impl From<String> for ThemeName {
30    fn from(s: String) -> Self {
31        Self(s)
32    }
33}
34
35impl From<&str> for ThemeName {
36    fn from(s: &str) -> Self {
37        Self(s.to_string())
38    }
39}
40
41impl PartialEq<str> for ThemeName {
42    fn eq(&self, other: &str) -> bool {
43        self.0 == other
44    }
45}
46
47impl PartialEq<ThemeName> for str {
48    fn eq(&self, other: &ThemeName) -> bool {
49        self == other.0
50    }
51}
52
53impl JsonSchema for ThemeName {
54    fn schema_name() -> Cow<'static, str> {
55        Cow::Borrowed("ThemeOptions")
56    }
57
58    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
59        schemars::json_schema!({
60            "description": "Available color themes",
61            "type": "string",
62            "enum": Self::BUILTIN_OPTIONS
63        })
64    }
65}
66
67/// Newtype for locale name that generates proper JSON Schema with enum options
68/// Wraps Option<String> to allow null for auto-detection from environment
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
70#[serde(transparent)]
71pub struct LocaleName(pub Option<String>);
72
73// Include the generated locale options from build.rs
74include!(concat!(env!("OUT_DIR"), "/locale_options.rs"));
75
76impl LocaleName {
77    /// Available locale options shown in the settings dropdown
78    /// null means auto-detect from environment
79    /// This is auto-generated from the locales/*.json files by build.rs
80    pub const LOCALE_OPTIONS: &'static [Option<&'static str>] = GENERATED_LOCALE_OPTIONS;
81
82    /// Get the inner value as Option<&str>
83    pub fn as_option(&self) -> Option<&str> {
84        self.0.as_deref()
85    }
86}
87
88impl From<Option<String>> for LocaleName {
89    fn from(s: Option<String>) -> Self {
90        Self(s)
91    }
92}
93
94impl From<Option<&str>> for LocaleName {
95    fn from(s: Option<&str>) -> Self {
96        Self(s.map(|s| s.to_string()))
97    }
98}
99
100impl JsonSchema for LocaleName {
101    fn schema_name() -> Cow<'static, str> {
102        Cow::Borrowed("LocaleOptions")
103    }
104
105    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
106        schemars::json_schema!({
107            "description": "UI locale (language). Use null for auto-detection from environment.",
108            "enum": Self::LOCALE_OPTIONS
109        })
110    }
111}
112
113/// Cursor style options for the terminal cursor
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
115#[serde(rename_all = "snake_case")]
116pub enum CursorStyle {
117    /// Use the terminal's default cursor style
118    #[default]
119    Default,
120    /// Blinking block cursor (█)
121    BlinkingBlock,
122    /// Solid block cursor (█)
123    SteadyBlock,
124    /// Blinking vertical bar cursor (│)
125    BlinkingBar,
126    /// Solid vertical bar cursor (│)
127    SteadyBar,
128    /// Blinking underline cursor (_)
129    BlinkingUnderline,
130    /// Solid underline cursor (_)
131    SteadyUnderline,
132}
133
134impl CursorStyle {
135    /// All available cursor style options
136    pub const OPTIONS: &'static [&'static str] = &[
137        "default",
138        "blinking_block",
139        "steady_block",
140        "blinking_bar",
141        "steady_bar",
142        "blinking_underline",
143        "steady_underline",
144    ];
145
146    /// Human-readable descriptions for each cursor style
147    pub const DESCRIPTIONS: &'static [&'static str] = &[
148        "Terminal default",
149        "█ Blinking block",
150        "█ Solid block",
151        "│ Blinking bar",
152        "│ Solid bar",
153        "_ Blinking underline",
154        "_ Solid underline",
155    ];
156
157    /// Convert to crossterm cursor style (runtime only)
158    #[cfg(feature = "runtime")]
159    pub fn to_crossterm_style(self) -> crossterm::cursor::SetCursorStyle {
160        use crossterm::cursor::SetCursorStyle;
161        match self {
162            Self::Default => SetCursorStyle::DefaultUserShape,
163            Self::BlinkingBlock => SetCursorStyle::BlinkingBlock,
164            Self::SteadyBlock => SetCursorStyle::SteadyBlock,
165            Self::BlinkingBar => SetCursorStyle::BlinkingBar,
166            Self::SteadyBar => SetCursorStyle::SteadyBar,
167            Self::BlinkingUnderline => SetCursorStyle::BlinkingUnderScore,
168            Self::SteadyUnderline => SetCursorStyle::SteadyUnderScore,
169        }
170    }
171
172    /// Get the ANSI escape sequence for this cursor style (DECSCUSR)
173    /// Used for session mode where we can't write directly to terminal
174    pub fn to_escape_sequence(self) -> &'static [u8] {
175        match self {
176            Self::Default => b"\x1b[0 q",
177            Self::BlinkingBlock => b"\x1b[1 q",
178            Self::SteadyBlock => b"\x1b[2 q",
179            Self::BlinkingUnderline => b"\x1b[3 q",
180            Self::SteadyUnderline => b"\x1b[4 q",
181            Self::BlinkingBar => b"\x1b[5 q",
182            Self::SteadyBar => b"\x1b[6 q",
183        }
184    }
185
186    /// Parse from string (for command palette)
187    pub fn parse(s: &str) -> Option<Self> {
188        match s {
189            "default" => Some(CursorStyle::Default),
190            "blinking_block" => Some(CursorStyle::BlinkingBlock),
191            "steady_block" => Some(CursorStyle::SteadyBlock),
192            "blinking_bar" => Some(CursorStyle::BlinkingBar),
193            "steady_bar" => Some(CursorStyle::SteadyBar),
194            "blinking_underline" => Some(CursorStyle::BlinkingUnderline),
195            "steady_underline" => Some(CursorStyle::SteadyUnderline),
196            _ => None,
197        }
198    }
199
200    /// Convert to string representation
201    pub fn as_str(self) -> &'static str {
202        match self {
203            Self::Default => "default",
204            Self::BlinkingBlock => "blinking_block",
205            Self::SteadyBlock => "steady_block",
206            Self::BlinkingBar => "blinking_bar",
207            Self::SteadyBar => "steady_bar",
208            Self::BlinkingUnderline => "blinking_underline",
209            Self::SteadyUnderline => "steady_underline",
210        }
211    }
212}
213
214impl JsonSchema for CursorStyle {
215    fn schema_name() -> Cow<'static, str> {
216        Cow::Borrowed("CursorStyle")
217    }
218
219    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
220        schemars::json_schema!({
221            "description": "Terminal cursor style",
222            "type": "string",
223            "enum": Self::OPTIONS
224        })
225    }
226}
227
228/// Newtype for keybinding map name that generates proper JSON Schema with enum options
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(transparent)]
231pub struct KeybindingMapName(pub String);
232
233impl KeybindingMapName {
234    /// Built-in keybinding map options shown in the settings dropdown
235    pub const BUILTIN_OPTIONS: &'static [&'static str] =
236        &["default", "emacs", "vscode", "macos", "macos-gui"];
237}
238
239impl Deref for KeybindingMapName {
240    type Target = str;
241    fn deref(&self) -> &Self::Target {
242        &self.0
243    }
244}
245
246impl From<String> for KeybindingMapName {
247    fn from(s: String) -> Self {
248        Self(s)
249    }
250}
251
252impl From<&str> for KeybindingMapName {
253    fn from(s: &str) -> Self {
254        Self(s.to_string())
255    }
256}
257
258impl PartialEq<str> for KeybindingMapName {
259    fn eq(&self, other: &str) -> bool {
260        self.0 == other
261    }
262}
263
264/// Line ending format for new files
265#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
266#[serde(rename_all = "lowercase")]
267pub enum LineEndingOption {
268    /// Unix/Linux/macOS format (LF)
269    #[default]
270    Lf,
271    /// Windows format (CRLF)
272    Crlf,
273    /// Classic Mac format (CR) - rare
274    Cr,
275}
276
277impl LineEndingOption {
278    /// Convert to the buffer's LineEnding type
279    pub fn to_line_ending(&self) -> crate::model::buffer::LineEnding {
280        match self {
281            Self::Lf => crate::model::buffer::LineEnding::LF,
282            Self::Crlf => crate::model::buffer::LineEnding::CRLF,
283            Self::Cr => crate::model::buffer::LineEnding::CR,
284        }
285    }
286}
287
288impl JsonSchema for LineEndingOption {
289    fn schema_name() -> Cow<'static, str> {
290        Cow::Borrowed("LineEndingOption")
291    }
292
293    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
294        schemars::json_schema!({
295            "description": "Default line ending format for new files",
296            "type": "string",
297            "enum": ["lf", "crlf", "cr"],
298            "default": "lf"
299        })
300    }
301}
302
303impl PartialEq<KeybindingMapName> for str {
304    fn eq(&self, other: &KeybindingMapName) -> bool {
305        self == other.0
306    }
307}
308
309impl JsonSchema for KeybindingMapName {
310    fn schema_name() -> Cow<'static, str> {
311        Cow::Borrowed("KeybindingMapOptions")
312    }
313
314    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
315        schemars::json_schema!({
316            "description": "Available keybinding maps",
317            "type": "string",
318            "enum": Self::BUILTIN_OPTIONS
319        })
320    }
321}
322
323/// Main configuration structure
324#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
325pub struct Config {
326    /// Configuration version (for migration support)
327    /// Configs without this field are treated as version 0
328    #[serde(default)]
329    pub version: u32,
330
331    /// Color theme name
332    #[serde(default = "default_theme_name")]
333    pub theme: ThemeName,
334
335    /// UI locale (language) for translations
336    /// If not set, auto-detected from environment (LC_ALL, LC_MESSAGES, LANG)
337    #[serde(default)]
338    pub locale: LocaleName,
339
340    /// Check for new versions on startup (default: true).
341    /// When enabled, also sends basic anonymous telemetry (version, OS, terminal type).
342    #[serde(default = "default_true")]
343    pub check_for_updates: bool,
344
345    /// Editor behavior settings (indentation, line numbers, wrapping, etc.)
346    #[serde(default)]
347    pub editor: EditorConfig,
348
349    /// File explorer panel settings
350    #[serde(default)]
351    pub file_explorer: FileExplorerConfig,
352
353    /// File browser settings (Open File dialog)
354    #[serde(default)]
355    pub file_browser: FileBrowserConfig,
356
357    /// Clipboard settings (which clipboard methods to use)
358    #[serde(default)]
359    pub clipboard: ClipboardConfig,
360
361    /// Terminal settings
362    #[serde(default)]
363    pub terminal: TerminalConfig,
364
365    /// Custom keybindings (overrides for the active map)
366    #[serde(default)]
367    pub keybindings: Vec<Keybinding>,
368
369    /// Named keybinding maps (user can define custom maps here)
370    /// Each map can optionally inherit from another map
371    #[serde(default)]
372    pub keybinding_maps: HashMap<String, KeymapConfig>,
373
374    /// Active keybinding map name
375    #[serde(default = "default_keybinding_map_name")]
376    pub active_keybinding_map: KeybindingMapName,
377
378    /// Per-language configuration overrides (tab size, formatters, etc.)
379    #[serde(default)]
380    pub languages: HashMap<String, LanguageConfig>,
381
382    /// Fallback configuration for files whose type cannot be detected.
383    /// Applied when no extension, filename, glob, or built-in detection matches.
384    /// Useful for setting a default grammar (e.g., "bash") and comment_prefix
385    /// for unrecognized .conf, .rc, .rules, etc. files.
386    #[serde(default)]
387    pub fallback: Option<LanguageConfig>,
388
389    /// LSP server configurations by language.
390    /// Each language maps to one or more server configs (multi-LSP support).
391    /// Accepts both single-object and array forms for backwards compatibility.
392    #[serde(default)]
393    pub lsp: HashMap<String, LspLanguageConfig>,
394
395    /// Warning notification settings
396    #[serde(default)]
397    pub warnings: WarningsConfig,
398
399    /// Plugin configurations by plugin name
400    /// Plugins are auto-discovered from the plugins directory.
401    /// Use this to enable/disable specific plugins.
402    #[serde(default)]
403    #[schemars(extend("x-standalone-category" = true, "x-no-add" = true))]
404    pub plugins: HashMap<String, PluginConfig>,
405
406    /// Package manager settings for plugin/theme installation
407    #[serde(default)]
408    pub packages: PackagesConfig,
409}
410
411fn default_keybinding_map_name() -> KeybindingMapName {
412    // On macOS, default to the macOS keymap which has Mac-specific bindings
413    // (Ctrl+A/E for Home/End, Ctrl+Shift+Z for redo, etc.)
414    if cfg!(target_os = "macos") {
415        KeybindingMapName("macos".to_string())
416    } else {
417        KeybindingMapName("default".to_string())
418    }
419}
420
421fn default_theme_name() -> ThemeName {
422    ThemeName("high-contrast".to_string())
423}
424
425/// Resolved whitespace indicator visibility for a buffer.
426///
427/// These are the final resolved flags after applying master toggle,
428/// global config, and per-language overrides. Used directly by the renderer.
429#[derive(Debug, Clone, Copy)]
430pub struct WhitespaceVisibility {
431    pub spaces_leading: bool,
432    pub spaces_inner: bool,
433    pub spaces_trailing: bool,
434    pub tabs_leading: bool,
435    pub tabs_inner: bool,
436    pub tabs_trailing: bool,
437}
438
439impl Default for WhitespaceVisibility {
440    fn default() -> Self {
441        // Match EditorConfig defaults: tabs all on, spaces all off
442        Self {
443            spaces_leading: false,
444            spaces_inner: false,
445            spaces_trailing: false,
446            tabs_leading: true,
447            tabs_inner: true,
448            tabs_trailing: true,
449        }
450    }
451}
452
453impl WhitespaceVisibility {
454    /// Resolve from EditorConfig flat fields (applying master toggle)
455    pub fn from_editor_config(editor: &EditorConfig) -> Self {
456        if !editor.whitespace_show {
457            return Self {
458                spaces_leading: false,
459                spaces_inner: false,
460                spaces_trailing: false,
461                tabs_leading: false,
462                tabs_inner: false,
463                tabs_trailing: false,
464            };
465        }
466        Self {
467            spaces_leading: editor.whitespace_spaces_leading,
468            spaces_inner: editor.whitespace_spaces_inner,
469            spaces_trailing: editor.whitespace_spaces_trailing,
470            tabs_leading: editor.whitespace_tabs_leading,
471            tabs_inner: editor.whitespace_tabs_inner,
472            tabs_trailing: editor.whitespace_tabs_trailing,
473        }
474    }
475
476    /// Apply a language-level override for tab visibility.
477    /// When the language sets `show_whitespace_tabs: false`, all tab positions are disabled.
478    pub fn with_language_tab_override(mut self, show_whitespace_tabs: bool) -> Self {
479        if !show_whitespace_tabs {
480            self.tabs_leading = false;
481            self.tabs_inner = false;
482            self.tabs_trailing = false;
483        }
484        self
485    }
486
487    /// Returns true if any space indicator is enabled
488    pub fn any_spaces(&self) -> bool {
489        self.spaces_leading || self.spaces_inner || self.spaces_trailing
490    }
491
492    /// Returns true if any tab indicator is enabled
493    pub fn any_tabs(&self) -> bool {
494        self.tabs_leading || self.tabs_inner || self.tabs_trailing
495    }
496
497    /// Returns true if any indicator (space or tab) is enabled
498    pub fn any_visible(&self) -> bool {
499        self.any_spaces() || self.any_tabs()
500    }
501
502    /// Toggle all whitespace indicators on/off (master switch).
503    /// When turning off, all positions are disabled.
504    /// When turning on, restores to default visibility (tabs all on, spaces all off).
505    pub fn toggle_all(&mut self) {
506        if self.any_visible() {
507            *self = Self {
508                spaces_leading: false,
509                spaces_inner: false,
510                spaces_trailing: false,
511                tabs_leading: false,
512                tabs_inner: false,
513                tabs_trailing: false,
514            };
515        } else {
516            *self = Self::default();
517        }
518    }
519}
520
521/// Editor behavior configuration
522#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
523pub struct EditorConfig {
524    // ===== Display =====
525    /// Show line numbers in the gutter (default for new buffers)
526    #[serde(default = "default_true")]
527    #[schemars(extend("x-section" = "Display"))]
528    pub line_numbers: bool,
529
530    /// Show line numbers relative to cursor position
531    #[serde(default = "default_false")]
532    #[schemars(extend("x-section" = "Display"))]
533    pub relative_line_numbers: bool,
534
535    /// Highlight the line containing the cursor
536    #[serde(default = "default_true")]
537    #[schemars(extend("x-section" = "Display"))]
538    pub highlight_current_line: bool,
539
540    /// Wrap long lines to fit the window width (default for new views)
541    #[serde(default = "default_true")]
542    #[schemars(extend("x-section" = "Display"))]
543    pub line_wrap: bool,
544
545    /// Indent wrapped continuation lines to match the leading whitespace of the original line
546    #[serde(default = "default_true")]
547    #[schemars(extend("x-section" = "Display"))]
548    pub wrap_indent: bool,
549
550    /// Column at which to wrap lines when line wrapping is enabled.
551    /// If not specified (`null`), lines wrap at the viewport edge (default behavior).
552    /// Example: `80` wraps at column 80. The actual wrap column is clamped to the
553    /// viewport width (lines can't wrap beyond the visible area).
554    #[serde(default)]
555    #[schemars(extend("x-section" = "Display"))]
556    pub wrap_column: Option<usize>,
557
558    /// Width of the page in page view mode (in columns).
559    /// Controls the content width when page view is active, with centering margins.
560    /// Defaults to 80. Set to `null` to use the full viewport width.
561    #[serde(default = "default_page_width")]
562    #[schemars(extend("x-section" = "Display"))]
563    pub page_width: Option<usize>,
564
565    /// Enable syntax highlighting for code files
566    #[serde(default = "default_true")]
567    #[schemars(extend("x-section" = "Display"))]
568    pub syntax_highlighting: bool,
569
570    /// Whether the menu bar is visible by default.
571    /// The menu bar provides access to menus (File, Edit, View, etc.) at the top of the screen.
572    /// Can be toggled at runtime via command palette or keybinding.
573    /// Default: true
574    #[serde(default = "default_true")]
575    #[schemars(extend("x-section" = "Display"))]
576    pub show_menu_bar: bool,
577
578    /// Whether menu bar mnemonics (Alt+letter shortcuts) are enabled.
579    /// When enabled, pressing Alt+F opens the File menu, Alt+E opens Edit, etc.
580    /// Disabling this frees up Alt+letter keybindings for other actions.
581    /// Default: true
582    #[serde(default = "default_true")]
583    #[schemars(extend("x-section" = "Display"))]
584    pub menu_bar_mnemonics: bool,
585
586    /// Whether the tab bar is visible by default.
587    /// The tab bar shows open files in each split pane.
588    /// Can be toggled at runtime via command palette or keybinding.
589    /// Default: true
590    #[serde(default = "default_true")]
591    #[schemars(extend("x-section" = "Display"))]
592    pub show_tab_bar: bool,
593
594    /// Whether the status bar is visible by default.
595    /// The status bar shows file info, cursor position, and editor status at the bottom of the screen.
596    /// Can be toggled at runtime via command palette or keybinding.
597    /// Default: true
598    #[serde(default = "default_true")]
599    #[schemars(extend("x-section" = "Display"))]
600    pub show_status_bar: bool,
601
602    /// Whether the prompt line is visible by default.
603    /// The prompt line is the bottom-most line used for command input, search, file open, etc.
604    /// When hidden, the prompt line only appears when a prompt is active.
605    /// Can be toggled at runtime via command palette or keybinding.
606    /// Default: true
607    #[serde(default = "default_true")]
608    #[schemars(extend("x-section" = "Display"))]
609    pub show_prompt_line: bool,
610
611    /// Whether the vertical scrollbar is visible in each split pane.
612    /// Can be toggled at runtime via command palette or keybinding.
613    /// Default: true
614    #[serde(default = "default_true")]
615    #[schemars(extend("x-section" = "Display"))]
616    pub show_vertical_scrollbar: bool,
617
618    /// Whether the horizontal scrollbar is visible in each split pane.
619    /// The horizontal scrollbar appears when line wrap is disabled and content extends beyond the viewport.
620    /// Can be toggled at runtime via command palette or keybinding.
621    /// Default: false
622    #[serde(default = "default_false")]
623    #[schemars(extend("x-section" = "Display"))]
624    pub show_horizontal_scrollbar: bool,
625
626    /// Show tilde (~) markers on lines after the end of the file.
627    /// These vim-style markers indicate lines that are not part of the file content.
628    /// Default: true
629    #[serde(default = "default_true")]
630    #[schemars(extend("x-section" = "Display"))]
631    pub show_tilde: bool,
632
633    /// Use the terminal's default background color instead of the theme's editor background.
634    /// When enabled, the editor background inherits from the terminal emulator,
635    /// allowing transparency or custom terminal backgrounds to show through.
636    /// Default: false
637    #[serde(default = "default_false")]
638    #[schemars(extend("x-section" = "Display"))]
639    pub use_terminal_bg: bool,
640
641    /// Cursor style for the terminal cursor.
642    /// Options: blinking_block, steady_block, blinking_bar, steady_bar, blinking_underline, steady_underline
643    /// Default: blinking_block
644    #[serde(default)]
645    #[schemars(extend("x-section" = "Display"))]
646    pub cursor_style: CursorStyle,
647
648    /// Vertical ruler lines at specific column positions.
649    /// Draws subtle vertical lines to help with line length conventions.
650    /// Example: [80, 120] draws rulers at columns 80 and 120.
651    /// Default: [] (no rulers)
652    #[serde(default)]
653    #[schemars(extend("x-section" = "Display"))]
654    pub rulers: Vec<usize>,
655
656    // ===== Whitespace =====
657    /// Master toggle for whitespace indicator visibility.
658    /// When disabled, no whitespace indicators (·, →) are shown regardless
659    /// of the per-position settings below.
660    /// Default: true
661    #[serde(default = "default_true")]
662    #[schemars(extend("x-section" = "Whitespace"))]
663    pub whitespace_show: bool,
664
665    /// Show space indicators (·) for leading whitespace (indentation).
666    /// Leading whitespace is everything before the first non-space character on a line.
667    /// Default: false
668    #[serde(default = "default_false")]
669    #[schemars(extend("x-section" = "Whitespace"))]
670    pub whitespace_spaces_leading: bool,
671
672    /// Show space indicators (·) for inner whitespace (between words/tokens).
673    /// Inner whitespace is spaces between the first and last non-space characters.
674    /// Default: false
675    #[serde(default = "default_false")]
676    #[schemars(extend("x-section" = "Whitespace"))]
677    pub whitespace_spaces_inner: bool,
678
679    /// Show space indicators (·) for trailing whitespace.
680    /// Trailing whitespace is everything after the last non-space character on a line.
681    /// Default: false
682    #[serde(default = "default_false")]
683    #[schemars(extend("x-section" = "Whitespace"))]
684    pub whitespace_spaces_trailing: bool,
685
686    /// Show tab indicators (→) for leading tabs (indentation).
687    /// Can be overridden per-language via `show_whitespace_tabs` in language config.
688    /// Default: true
689    #[serde(default = "default_true")]
690    #[schemars(extend("x-section" = "Whitespace"))]
691    pub whitespace_tabs_leading: bool,
692
693    /// Show tab indicators (→) for inner tabs (between words/tokens).
694    /// Can be overridden per-language via `show_whitespace_tabs` in language config.
695    /// Default: true
696    #[serde(default = "default_true")]
697    #[schemars(extend("x-section" = "Whitespace"))]
698    pub whitespace_tabs_inner: bool,
699
700    /// Show tab indicators (→) for trailing tabs.
701    /// Can be overridden per-language via `show_whitespace_tabs` in language config.
702    /// Default: true
703    #[serde(default = "default_true")]
704    #[schemars(extend("x-section" = "Whitespace"))]
705    pub whitespace_tabs_trailing: bool,
706
707    // ===== Editing =====
708    /// Whether pressing Tab inserts a tab character instead of spaces.
709    /// This is the global default; individual languages can override it
710    /// via their own `use_tabs` setting.
711    /// Default: false (insert spaces)
712    #[serde(default = "default_false")]
713    #[schemars(extend("x-section" = "Editing"))]
714    pub use_tabs: bool,
715
716    /// Number of spaces per tab character
717    #[serde(default = "default_tab_size")]
718    #[schemars(extend("x-section" = "Editing"))]
719    pub tab_size: usize,
720
721    /// Automatically indent new lines based on the previous line
722    #[serde(default = "default_true")]
723    #[schemars(extend("x-section" = "Editing"))]
724    pub auto_indent: bool,
725
726    /// Automatically close brackets, parentheses, and quotes when typing.
727    /// When enabled, typing an opening delimiter like `(`, `[`, `{`, `"`, `'`, or `` ` ``
728    /// will automatically insert the matching closing delimiter.
729    /// Also enables skip-over (moving past existing closing delimiters) and
730    /// pair deletion (deleting both delimiters when backspacing between them).
731    /// Default: true
732    #[serde(default = "default_true")]
733    #[schemars(extend("x-section" = "Editing"))]
734    pub auto_close: bool,
735
736    /// Automatically surround selected text with matching pairs when typing
737    /// an opening delimiter. When enabled and text is selected, typing `(`, `[`,
738    /// `{`, `"`, `'`, or `` ` `` wraps the selection instead of replacing it.
739    /// Default: true
740    #[serde(default = "default_true")]
741    #[schemars(extend("x-section" = "Editing"))]
742    pub auto_surround: bool,
743
744    /// Minimum lines to keep visible above/below cursor when scrolling
745    #[serde(default = "default_scroll_offset")]
746    #[schemars(extend("x-section" = "Editing"))]
747    pub scroll_offset: usize,
748
749    /// Default line ending format for new files.
750    /// Files loaded from disk will use their detected line ending format.
751    /// Options: "lf" (Unix/Linux/macOS), "crlf" (Windows), "cr" (Classic Mac)
752    /// Default: "lf"
753    #[serde(default)]
754    #[schemars(extend("x-section" = "Editing"))]
755    pub default_line_ending: LineEndingOption,
756
757    /// Remove trailing whitespace from lines when saving.
758    /// Default: false
759    #[serde(default = "default_false")]
760    #[schemars(extend("x-section" = "Editing"))]
761    pub trim_trailing_whitespace_on_save: bool,
762
763    /// Ensure files end with a newline when saving.
764    /// Default: false
765    #[serde(default = "default_false")]
766    #[schemars(extend("x-section" = "Editing"))]
767    pub ensure_final_newline_on_save: bool,
768
769    // ===== Bracket Matching =====
770    /// Highlight matching bracket pairs when cursor is on a bracket.
771    /// Default: true
772    #[serde(default = "default_true")]
773    #[schemars(extend("x-section" = "Bracket Matching"))]
774    pub highlight_matching_brackets: bool,
775
776    /// Use rainbow colors for nested brackets based on nesting depth.
777    /// Requires highlight_matching_brackets to be enabled.
778    /// Default: true
779    #[serde(default = "default_true")]
780    #[schemars(extend("x-section" = "Bracket Matching"))]
781    pub rainbow_brackets: bool,
782
783    // ===== Completion =====
784    /// Automatically show the completion popup while typing.
785    /// When false (default), the popup only appears when explicitly invoked
786    /// (e.g. via Ctrl+Space). When true, it appears automatically after a
787    /// short delay while typing.
788    /// Default: false
789    #[serde(default = "default_false")]
790    #[schemars(extend("x-section" = "Completion"))]
791    pub completion_popup_auto_show: bool,
792
793    /// Enable quick suggestions (VS Code-like behavior).
794    /// When enabled, completion suggestions appear automatically while typing,
795    /// not just on trigger characters (like `.` or `::`).
796    /// Only takes effect when completion_popup_auto_show is true.
797    /// Default: true
798    #[serde(default = "default_true")]
799    #[schemars(extend("x-section" = "Completion"))]
800    pub quick_suggestions: bool,
801
802    /// Delay in milliseconds before showing completion suggestions.
803    /// Lower values (10-50ms) feel more responsive but may be distracting.
804    /// Higher values (100-500ms) reduce noise while typing.
805    /// Trigger characters (like `.`) bypass this delay.
806    /// Default: 150
807    #[serde(default = "default_quick_suggestions_delay")]
808    #[schemars(extend("x-section" = "Completion"))]
809    pub quick_suggestions_delay_ms: u64,
810
811    /// Whether trigger characters (like `.`, `::`, `->`) immediately show completions.
812    /// When true, typing a trigger character bypasses quick_suggestions_delay_ms.
813    /// Default: true
814    #[serde(default = "default_true")]
815    #[schemars(extend("x-section" = "Completion"))]
816    pub suggest_on_trigger_characters: bool,
817
818    // ===== LSP =====
819    /// Whether to enable LSP inlay hints (type hints, parameter hints, etc.)
820    #[serde(default = "default_true")]
821    #[schemars(extend("x-section" = "LSP"))]
822    pub enable_inlay_hints: bool,
823
824    /// Whether to request full-document LSP semantic tokens.
825    /// Range requests are still used when supported.
826    /// Default: false (range-only to avoid heavy full refreshes).
827    #[serde(default = "default_false")]
828    #[schemars(extend("x-section" = "LSP"))]
829    pub enable_semantic_tokens_full: bool,
830
831    /// Whether to show inline diagnostic text at the end of lines with errors/warnings.
832    /// When enabled, the highest-severity diagnostic message is rendered after the
833    /// source code on each affected line.
834    /// Default: false
835    #[serde(default = "default_false")]
836    #[schemars(extend("x-section" = "Diagnostics"))]
837    pub diagnostics_inline_text: bool,
838
839    // ===== Mouse =====
840    /// Whether mouse hover triggers LSP hover requests.
841    /// When enabled, hovering over code with the mouse will show documentation.
842    /// On Windows, this also controls the mouse tracking mode: when disabled,
843    /// the editor uses xterm mode 1002 (cell motion — click, drag, release only);
844    /// when enabled, it uses mode 1003 (all motion — full mouse movement tracking).
845    /// Mode 1003 generates high event volume on Windows and may cause input
846    /// corruption on some systems. On macOS and Linux this setting only controls
847    /// LSP hover; the mouse tracking mode is always full motion.
848    /// Default: true (macOS/Linux), false (Windows)
849    #[serde(default = "default_mouse_hover_enabled")]
850    #[schemars(extend("x-section" = "Mouse"))]
851    pub mouse_hover_enabled: bool,
852
853    /// Delay in milliseconds before a mouse hover triggers an LSP hover request.
854    /// Lower values show hover info faster but may cause more LSP server load.
855    /// Default: 500ms
856    #[serde(default = "default_mouse_hover_delay")]
857    #[schemars(extend("x-section" = "Mouse"))]
858    pub mouse_hover_delay_ms: u64,
859
860    /// Time window in milliseconds for detecting double-clicks.
861    /// Two clicks within this time are treated as a double-click (word selection).
862    /// Default: 500ms
863    #[serde(default = "default_double_click_time")]
864    #[schemars(extend("x-section" = "Mouse"))]
865    pub double_click_time_ms: u64,
866
867    /// Whether to enable persistent auto-save (save to original file on disk).
868    /// When enabled, modified buffers are saved to their original file path
869    /// at a configurable interval.
870    /// Default: false
871    #[serde(default = "default_false")]
872    #[schemars(extend("x-section" = "Recovery"))]
873    pub auto_save_enabled: bool,
874
875    /// Interval in seconds for persistent auto-save.
876    /// Modified buffers are saved to their original file at this interval.
877    /// Only effective when auto_save_enabled is true.
878    /// Default: 30 seconds
879    #[serde(default = "default_auto_save_interval")]
880    #[schemars(extend("x-section" = "Recovery"))]
881    pub auto_save_interval_secs: u32,
882
883    /// Whether to preserve unsaved changes in all buffers (file-backed and
884    /// unnamed) across editor sessions (VS Code "hot exit" behavior).
885    /// When enabled, modified buffers are backed up on clean exit and their
886    /// unsaved changes are restored on next startup.  Unnamed (scratch)
887    /// buffers are also persisted (Sublime Text / Notepad++ behavior).
888    /// Default: true
889    #[serde(default = "default_true", alias = "persist_unnamed_buffers")]
890    #[schemars(extend("x-section" = "Recovery"))]
891    pub hot_exit: bool,
892
893    // ===== Recovery =====
894    /// Whether to enable file recovery (Emacs-style auto-save)
895    /// When enabled, buffers are periodically saved to recovery files
896    /// so they can be recovered if the editor crashes.
897    #[serde(default = "default_true")]
898    #[schemars(extend("x-section" = "Recovery"))]
899    pub recovery_enabled: bool,
900
901    /// Interval in seconds for auto-recovery-save.
902    /// Modified buffers are saved to recovery files at this interval.
903    /// Only effective when recovery_enabled is true.
904    /// Default: 2 seconds
905    #[serde(default = "default_auto_recovery_save_interval")]
906    #[schemars(extend("x-section" = "Recovery"))]
907    pub auto_recovery_save_interval_secs: u32,
908
909    /// Poll interval in milliseconds for auto-reverting open buffers.
910    /// When auto-revert is enabled, file modification times are checked at this interval.
911    /// Lower values detect external changes faster but use more CPU.
912    /// Default: 2000ms (2 seconds)
913    #[serde(default = "default_auto_revert_poll_interval")]
914    #[schemars(extend("x-section" = "Recovery"))]
915    pub auto_revert_poll_interval_ms: u64,
916
917    // ===== Keyboard =====
918    /// Enable keyboard enhancement: disambiguate escape codes using CSI-u sequences.
919    /// This allows unambiguous reading of Escape and modified keys.
920    /// Requires terminal support (kitty keyboard protocol).
921    /// Default: true
922    #[serde(default = "default_true")]
923    #[schemars(extend("x-section" = "Keyboard"))]
924    pub keyboard_disambiguate_escape_codes: bool,
925
926    /// Enable keyboard enhancement: report key event types (repeat/release).
927    /// Adds extra events when keys are autorepeated or released.
928    /// Requires terminal support (kitty keyboard protocol).
929    /// Default: false
930    #[serde(default = "default_false")]
931    #[schemars(extend("x-section" = "Keyboard"))]
932    pub keyboard_report_event_types: bool,
933
934    /// Enable keyboard enhancement: report alternate keycodes.
935    /// Sends alternate keycodes in addition to the base keycode.
936    /// Requires terminal support (kitty keyboard protocol).
937    /// Default: true
938    #[serde(default = "default_true")]
939    #[schemars(extend("x-section" = "Keyboard"))]
940    pub keyboard_report_alternate_keys: bool,
941
942    /// Enable keyboard enhancement: report all keys as escape codes.
943    /// Represents all keyboard events as CSI-u sequences.
944    /// Required for repeat/release events on plain-text keys.
945    /// Requires terminal support (kitty keyboard protocol).
946    /// Default: false
947    #[serde(default = "default_false")]
948    #[schemars(extend("x-section" = "Keyboard"))]
949    pub keyboard_report_all_keys_as_escape_codes: bool,
950
951    // ===== Performance =====
952    /// Maximum time in milliseconds for syntax highlighting per frame
953    #[serde(default = "default_highlight_timeout")]
954    #[schemars(extend("x-section" = "Performance"))]
955    pub highlight_timeout_ms: u64,
956
957    /// Undo history snapshot interval (number of edits between snapshots)
958    #[serde(default = "default_snapshot_interval")]
959    #[schemars(extend("x-section" = "Performance"))]
960    pub snapshot_interval: usize,
961
962    /// Number of bytes to look back/forward from the viewport for syntax highlighting context.
963    /// Larger values improve accuracy for multi-line constructs (strings, comments, nested blocks)
964    /// but may slow down highlighting for very large files.
965    /// Default: 10KB (10000 bytes)
966    #[serde(default = "default_highlight_context_bytes")]
967    #[schemars(extend("x-section" = "Performance"))]
968    pub highlight_context_bytes: usize,
969
970    /// File size threshold in bytes for "large file" behavior
971    /// Files larger than this will:
972    /// - Skip LSP features
973    /// - Use constant-size scrollbar thumb (1 char)
974    ///
975    /// Files smaller will count actual lines for accurate scrollbar rendering
976    #[serde(default = "default_large_file_threshold")]
977    #[schemars(extend("x-section" = "Performance"))]
978    pub large_file_threshold_bytes: u64,
979
980    /// Estimated average line length in bytes (used for large file line estimation)
981    /// This is used by LineIterator to estimate line positions in large files
982    /// without line metadata. Typical values: 80-120 bytes.
983    #[serde(default = "default_estimated_line_length")]
984    #[schemars(extend("x-section" = "Performance"))]
985    pub estimated_line_length: usize,
986
987    /// Maximum number of concurrent filesystem read requests.
988    /// Used during line-feed scanning and other bulk I/O operations.
989    /// Higher values improve throughput, especially for remote filesystems.
990    /// Default: 64
991    #[serde(default = "default_read_concurrency")]
992    #[schemars(extend("x-section" = "Performance"))]
993    pub read_concurrency: usize,
994
995    /// Poll interval in milliseconds for refreshing expanded directories in the file explorer.
996    /// Directory modification times are checked at this interval to detect new/deleted files.
997    /// Lower values detect changes faster but use more CPU.
998    /// Default: 3000ms (3 seconds)
999    #[serde(default = "default_file_tree_poll_interval")]
1000    #[schemars(extend("x-section" = "Performance"))]
1001    pub file_tree_poll_interval_ms: u64,
1002}
1003
1004fn default_tab_size() -> usize {
1005    4
1006}
1007
1008/// Large file threshold in bytes
1009/// Files larger than this will use optimized algorithms (estimation, viewport-only parsing)
1010/// Files smaller will use exact algorithms (full line tracking, complete parsing)
1011pub const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; // 1MB
1012
1013fn default_large_file_threshold() -> u64 {
1014    LARGE_FILE_THRESHOLD_BYTES
1015}
1016
1017/// Maximum lines to scan forward when computing indent-based fold end
1018/// for the fold toggle action (user-triggered, infrequent).
1019pub const INDENT_FOLD_MAX_SCAN_LINES: usize = 10_000;
1020
1021/// Maximum lines to scan forward when checking foldability for gutter
1022/// indicators or click detection (called per-viewport-line during render).
1023pub const INDENT_FOLD_INDICATOR_MAX_SCAN: usize = 50;
1024
1025/// Maximum lines to walk backward when searching for a fold header
1026/// that contains the cursor (in the fold toggle action).
1027pub const INDENT_FOLD_MAX_UPWARD_SCAN: usize = 200;
1028
1029fn default_read_concurrency() -> usize {
1030    64
1031}
1032
1033fn default_true() -> bool {
1034    true
1035}
1036
1037fn default_false() -> bool {
1038    false
1039}
1040
1041fn default_quick_suggestions_delay() -> u64 {
1042    150 // 150ms — fast enough to feel responsive, slow enough to not interrupt typing
1043}
1044
1045fn default_scroll_offset() -> usize {
1046    3
1047}
1048
1049fn default_highlight_timeout() -> u64 {
1050    5
1051}
1052
1053fn default_snapshot_interval() -> usize {
1054    100
1055}
1056
1057fn default_estimated_line_length() -> usize {
1058    80
1059}
1060
1061fn default_auto_save_interval() -> u32 {
1062    30 // 30 seconds between persistent auto-saves
1063}
1064
1065fn default_auto_recovery_save_interval() -> u32 {
1066    2 // 2 seconds between recovery saves
1067}
1068
1069fn default_highlight_context_bytes() -> usize {
1070    10_000 // 10KB context for accurate syntax highlighting
1071}
1072
1073fn default_mouse_hover_enabled() -> bool {
1074    !cfg!(windows)
1075}
1076
1077fn default_mouse_hover_delay() -> u64 {
1078    500 // 500ms delay before showing hover info
1079}
1080
1081fn default_double_click_time() -> u64 {
1082    500 // 500ms window for detecting double-clicks
1083}
1084
1085fn default_auto_revert_poll_interval() -> u64 {
1086    2000 // 2 seconds between file mtime checks
1087}
1088
1089fn default_file_tree_poll_interval() -> u64 {
1090    3000 // 3 seconds between directory mtime checks
1091}
1092
1093impl Default for EditorConfig {
1094    fn default() -> Self {
1095        Self {
1096            use_tabs: false,
1097            tab_size: default_tab_size(),
1098            auto_indent: true,
1099            auto_close: true,
1100            auto_surround: true,
1101            line_numbers: true,
1102            relative_line_numbers: false,
1103            scroll_offset: default_scroll_offset(),
1104            syntax_highlighting: true,
1105            highlight_current_line: true,
1106            line_wrap: true,
1107            wrap_indent: true,
1108            wrap_column: None,
1109            page_width: default_page_width(),
1110            highlight_timeout_ms: default_highlight_timeout(),
1111            snapshot_interval: default_snapshot_interval(),
1112            large_file_threshold_bytes: default_large_file_threshold(),
1113            estimated_line_length: default_estimated_line_length(),
1114            enable_inlay_hints: true,
1115            enable_semantic_tokens_full: false,
1116            diagnostics_inline_text: false,
1117            auto_save_enabled: false,
1118            auto_save_interval_secs: default_auto_save_interval(),
1119            hot_exit: true,
1120            recovery_enabled: true,
1121            auto_recovery_save_interval_secs: default_auto_recovery_save_interval(),
1122            highlight_context_bytes: default_highlight_context_bytes(),
1123            mouse_hover_enabled: default_mouse_hover_enabled(),
1124            mouse_hover_delay_ms: default_mouse_hover_delay(),
1125            double_click_time_ms: default_double_click_time(),
1126            auto_revert_poll_interval_ms: default_auto_revert_poll_interval(),
1127            read_concurrency: default_read_concurrency(),
1128            file_tree_poll_interval_ms: default_file_tree_poll_interval(),
1129            default_line_ending: LineEndingOption::default(),
1130            trim_trailing_whitespace_on_save: false,
1131            ensure_final_newline_on_save: false,
1132            highlight_matching_brackets: true,
1133            rainbow_brackets: true,
1134            cursor_style: CursorStyle::default(),
1135            keyboard_disambiguate_escape_codes: true,
1136            keyboard_report_event_types: false,
1137            keyboard_report_alternate_keys: true,
1138            keyboard_report_all_keys_as_escape_codes: false,
1139            completion_popup_auto_show: false,
1140            quick_suggestions: true,
1141            quick_suggestions_delay_ms: default_quick_suggestions_delay(),
1142            suggest_on_trigger_characters: true,
1143            show_menu_bar: true,
1144            menu_bar_mnemonics: true,
1145            show_tab_bar: true,
1146            show_status_bar: true,
1147            show_prompt_line: true,
1148            show_vertical_scrollbar: true,
1149            show_horizontal_scrollbar: false,
1150            show_tilde: true,
1151            use_terminal_bg: false,
1152            rulers: Vec::new(),
1153            whitespace_show: true,
1154            whitespace_spaces_leading: false,
1155            whitespace_spaces_inner: false,
1156            whitespace_spaces_trailing: false,
1157            whitespace_tabs_leading: true,
1158            whitespace_tabs_inner: true,
1159            whitespace_tabs_trailing: true,
1160        }
1161    }
1162}
1163
1164/// File explorer configuration
1165#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1166pub struct FileExplorerConfig {
1167    /// Whether to respect .gitignore files
1168    #[serde(default = "default_true")]
1169    pub respect_gitignore: bool,
1170
1171    /// Whether to show hidden files (starting with .) by default
1172    #[serde(default = "default_false")]
1173    pub show_hidden: bool,
1174
1175    /// Whether to show gitignored files by default
1176    #[serde(default = "default_false")]
1177    pub show_gitignored: bool,
1178
1179    /// Custom patterns to ignore (in addition to .gitignore)
1180    #[serde(default)]
1181    pub custom_ignore_patterns: Vec<String>,
1182
1183    /// Width of file explorer as percentage (0.0 to 1.0)
1184    #[serde(default = "default_explorer_width")]
1185    pub width: f32,
1186}
1187
1188fn default_explorer_width() -> f32 {
1189    0.3 // 30% of screen width
1190}
1191
1192/// Clipboard configuration
1193///
1194/// Controls which clipboard methods are used for copy/paste operations.
1195/// By default, all methods are enabled and the editor tries them in order:
1196/// 1. OSC 52 escape sequences (works in modern terminals like Kitty, Alacritty, Wezterm)
1197/// 2. System clipboard via X11/Wayland APIs (works in Gnome Console, XFCE Terminal, etc.)
1198/// 3. Internal clipboard (always available as fallback)
1199///
1200/// If you experience hangs or issues (e.g., when using PuTTY or certain SSH setups),
1201/// you can disable specific methods.
1202#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1203pub struct ClipboardConfig {
1204    /// Enable OSC 52 escape sequences for clipboard access (default: true)
1205    /// Disable this if your terminal doesn't support OSC 52 or if it causes hangs
1206    #[serde(default = "default_true")]
1207    pub use_osc52: bool,
1208
1209    /// Enable system clipboard access via X11/Wayland APIs (default: true)
1210    /// Disable this if you don't have a display server or it causes issues
1211    #[serde(default = "default_true")]
1212    pub use_system_clipboard: bool,
1213}
1214
1215impl Default for ClipboardConfig {
1216    fn default() -> Self {
1217        Self {
1218            use_osc52: true,
1219            use_system_clipboard: true,
1220        }
1221    }
1222}
1223
1224/// Terminal configuration
1225#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1226pub struct TerminalConfig {
1227    /// When viewing terminal scrollback and new output arrives,
1228    /// automatically jump back to terminal mode (default: true)
1229    #[serde(default = "default_true")]
1230    pub jump_to_end_on_output: bool,
1231}
1232
1233impl Default for TerminalConfig {
1234    fn default() -> Self {
1235        Self {
1236            jump_to_end_on_output: true,
1237        }
1238    }
1239}
1240
1241/// Warning notification configuration
1242#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1243pub struct WarningsConfig {
1244    /// Show warning/error indicators in the status bar (default: true)
1245    /// When enabled, displays a colored indicator for LSP errors and other warnings
1246    #[serde(default = "default_true")]
1247    pub show_status_indicator: bool,
1248}
1249
1250impl Default for WarningsConfig {
1251    fn default() -> Self {
1252        Self {
1253            show_status_indicator: true,
1254        }
1255    }
1256}
1257
1258/// Package manager configuration for plugins and themes
1259#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1260pub struct PackagesConfig {
1261    /// Registry sources (git repository URLs containing plugin/theme indices)
1262    /// Default: ["https://github.com/sinelaw/fresh-plugins-registry"]
1263    #[serde(default = "default_package_sources")]
1264    pub sources: Vec<String>,
1265}
1266
1267fn default_package_sources() -> Vec<String> {
1268    vec!["https://github.com/sinelaw/fresh-plugins-registry".to_string()]
1269}
1270
1271impl Default for PackagesConfig {
1272    fn default() -> Self {
1273        Self {
1274            sources: default_package_sources(),
1275        }
1276    }
1277}
1278
1279// Re-export PluginConfig from fresh-core for shared type usage
1280pub use fresh_core::config::PluginConfig;
1281
1282impl Default for FileExplorerConfig {
1283    fn default() -> Self {
1284        Self {
1285            respect_gitignore: true,
1286            show_hidden: false,
1287            show_gitignored: false,
1288            custom_ignore_patterns: Vec::new(),
1289            width: default_explorer_width(),
1290        }
1291    }
1292}
1293
1294/// File browser configuration (for Open File dialog)
1295#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1296pub struct FileBrowserConfig {
1297    /// Whether to show hidden files (starting with .) by default in Open File dialog
1298    #[serde(default = "default_false")]
1299    pub show_hidden: bool,
1300}
1301
1302/// A single key in a sequence
1303#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1304pub struct KeyPress {
1305    /// Key name (e.g., "a", "Enter", "F1")
1306    pub key: String,
1307    /// Modifiers (e.g., ["ctrl"], ["ctrl", "shift"])
1308    #[serde(default)]
1309    pub modifiers: Vec<String>,
1310}
1311
1312/// Keybinding definition
1313#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1314#[schemars(extend("x-display-field" = "/action"))]
1315pub struct Keybinding {
1316    /// Key name (e.g., "a", "Enter", "F1") - for single-key bindings
1317    #[serde(default, skip_serializing_if = "String::is_empty")]
1318    pub key: String,
1319
1320    /// Modifiers (e.g., ["ctrl"], ["ctrl", "shift"]) - for single-key bindings
1321    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1322    pub modifiers: Vec<String>,
1323
1324    /// Key sequence for chord bindings (e.g., [{"key": "x", "modifiers": ["ctrl"]}, {"key": "s", "modifiers": ["ctrl"]}])
1325    /// If present, takes precedence over key + modifiers
1326    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1327    pub keys: Vec<KeyPress>,
1328
1329    /// Action to perform (e.g., "insert_char", "move_left")
1330    pub action: String,
1331
1332    /// Optional arguments for the action
1333    #[serde(default)]
1334    pub args: HashMap<String, serde_json::Value>,
1335
1336    /// Optional condition (e.g., "mode == insert")
1337    #[serde(default)]
1338    pub when: Option<String>,
1339}
1340
1341/// Keymap configuration (for built-in and user-defined keymaps)
1342#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1343#[schemars(extend("x-display-field" = "/inherits"))]
1344pub struct KeymapConfig {
1345    /// Optional parent keymap to inherit from
1346    #[serde(default, skip_serializing_if = "Option::is_none")]
1347    pub inherits: Option<String>,
1348
1349    /// Keybindings defined in this keymap
1350    #[serde(default)]
1351    pub bindings: Vec<Keybinding>,
1352}
1353
1354/// Formatter configuration for a language
1355#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1356#[schemars(extend("x-display-field" = "/command"))]
1357pub struct FormatterConfig {
1358    /// The formatter command to run (e.g., "rustfmt", "prettier")
1359    pub command: String,
1360
1361    /// Arguments to pass to the formatter
1362    /// Use "$FILE" to include the file path
1363    #[serde(default)]
1364    pub args: Vec<String>,
1365
1366    /// Whether to pass buffer content via stdin (default: true)
1367    /// Most formatters read from stdin and write to stdout
1368    #[serde(default = "default_true")]
1369    pub stdin: bool,
1370
1371    /// Timeout in milliseconds (default: 10000)
1372    #[serde(default = "default_on_save_timeout")]
1373    pub timeout_ms: u64,
1374}
1375
1376/// Action to run when a file is saved (for linters, etc.)
1377#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1378#[schemars(extend("x-display-field" = "/command"))]
1379pub struct OnSaveAction {
1380    /// The shell command to run
1381    /// The file path is available as $FILE or as an argument
1382    pub command: String,
1383
1384    /// Arguments to pass to the command
1385    /// Use "$FILE" to include the file path
1386    #[serde(default)]
1387    pub args: Vec<String>,
1388
1389    /// Working directory for the command (defaults to project root)
1390    #[serde(default)]
1391    pub working_dir: Option<String>,
1392
1393    /// Whether to use the buffer content as stdin
1394    #[serde(default)]
1395    pub stdin: bool,
1396
1397    /// Timeout in milliseconds (default: 10000)
1398    #[serde(default = "default_on_save_timeout")]
1399    pub timeout_ms: u64,
1400
1401    /// Whether this action is enabled (default: true)
1402    /// Set to false to disable an action without removing it from config
1403    #[serde(default = "default_true")]
1404    pub enabled: bool,
1405}
1406
1407fn default_on_save_timeout() -> u64 {
1408    10000
1409}
1410
1411fn default_page_width() -> Option<usize> {
1412    Some(80)
1413}
1414
1415/// Language-specific configuration
1416#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1417#[schemars(extend("x-display-field" = "/grammar"))]
1418pub struct LanguageConfig {
1419    /// File extensions for this language (e.g., ["rs"] for Rust)
1420    #[serde(default)]
1421    pub extensions: Vec<String>,
1422
1423    /// Exact filenames for this language (e.g., ["Makefile", "GNUmakefile"])
1424    #[serde(default)]
1425    pub filenames: Vec<String>,
1426
1427    /// Tree-sitter grammar name
1428    #[serde(default)]
1429    pub grammar: String,
1430
1431    /// Comment prefix
1432    #[serde(default)]
1433    pub comment_prefix: Option<String>,
1434
1435    /// Whether to auto-indent
1436    #[serde(default = "default_true")]
1437    pub auto_indent: bool,
1438
1439    /// Whether to auto-close brackets, parentheses, and quotes for this language.
1440    /// If not specified (`null`), falls back to the global `editor.auto_close` setting.
1441    #[serde(default)]
1442    pub auto_close: Option<bool>,
1443
1444    /// Whether to auto-surround selected text with matching pairs for this language.
1445    /// If not specified (`null`), falls back to the global `editor.auto_surround` setting.
1446    #[serde(default)]
1447    pub auto_surround: Option<bool>,
1448
1449    /// Preferred highlighter backend (auto, tree-sitter, or textmate)
1450    #[serde(default)]
1451    pub highlighter: HighlighterPreference,
1452
1453    /// Path to custom TextMate grammar file (optional)
1454    /// If specified, this grammar will be used when highlighter is "textmate"
1455    #[serde(default)]
1456    pub textmate_grammar: Option<std::path::PathBuf>,
1457
1458    /// Whether to show whitespace tab indicators (→) for this language
1459    /// Defaults to true. Set to false for languages like Go that use tabs for indentation.
1460    #[serde(default = "default_true")]
1461    pub show_whitespace_tabs: bool,
1462
1463    /// Whether to enable line wrapping for this language.
1464    /// If not specified (`null`), falls back to the global `editor.line_wrap` setting.
1465    /// Useful for prose-heavy languages like Markdown where wrapping is desirable
1466    /// even if globally disabled.
1467    #[serde(default)]
1468    pub line_wrap: Option<bool>,
1469
1470    /// Column at which to wrap lines for this language.
1471    /// If not specified (`null`), falls back to the global `editor.wrap_column` setting.
1472    #[serde(default)]
1473    pub wrap_column: Option<usize>,
1474
1475    /// Whether to automatically enable page view (compose mode) for this language.
1476    /// Page view provides a document-style layout with centered content,
1477    /// concealed formatting markers, and intelligent word wrapping.
1478    /// If not specified (`null`), page view is not auto-activated.
1479    #[serde(default)]
1480    pub page_view: Option<bool>,
1481
1482    /// Width of the page in page view mode (in columns).
1483    /// Controls the content width when page view is active, with centering margins.
1484    /// If not specified (`null`), falls back to the global `editor.page_width` setting.
1485    #[serde(default)]
1486    pub page_width: Option<usize>,
1487
1488    /// Whether pressing Tab should insert a tab character instead of spaces.
1489    /// If not specified (`null`), falls back to the global `editor.use_tabs` setting.
1490    /// Set to true for languages like Go and Makefile that require tabs.
1491    #[serde(default)]
1492    pub use_tabs: Option<bool>,
1493
1494    /// Tab size (number of spaces per tab) for this language.
1495    /// If not specified, falls back to the global editor.tab_size setting.
1496    #[serde(default)]
1497    pub tab_size: Option<usize>,
1498
1499    /// The formatter for this language (used by format_buffer command)
1500    #[serde(default)]
1501    pub formatter: Option<FormatterConfig>,
1502
1503    /// Whether to automatically format on save (uses the formatter above)
1504    #[serde(default)]
1505    pub format_on_save: bool,
1506
1507    /// Actions to run when a file of this language is saved (linters, etc.)
1508    /// Actions are run in order; if any fails (non-zero exit), subsequent actions don't run
1509    /// Note: Use `formatter` + `format_on_save` for formatting, not on_save
1510    #[serde(default)]
1511    pub on_save: Vec<OnSaveAction>,
1512
1513    /// Extra characters (beyond alphanumeric and `_`) considered part of
1514    /// identifiers for this language. Used by dabbrev and buffer-word
1515    /// completion to correctly tokenise language-specific naming conventions.
1516    ///
1517    /// Examples:
1518    /// - Lisp/Clojure/CSS: `"-"` (kebab-case identifiers)
1519    /// - PHP/Bash: `"$"` (variable sigils)
1520    /// - Ruby: `"?!"` (predicate/bang methods)
1521    /// - Rust (default): `""` (standard alphanumeric + underscore)
1522    #[serde(default)]
1523    pub word_characters: Option<String>,
1524}
1525
1526/// Resolved editor configuration for a specific buffer.
1527///
1528/// This struct contains the effective settings for a buffer after applying
1529/// language-specific overrides on top of the global editor config.
1530///
1531/// Use `BufferConfig::resolve()` to create one from a Config and optional language ID.
1532#[derive(Debug, Clone)]
1533pub struct BufferConfig {
1534    /// Number of spaces per tab character
1535    pub tab_size: usize,
1536
1537    /// Whether to insert a tab character (true) or spaces (false) when pressing Tab
1538    pub use_tabs: bool,
1539
1540    /// Whether to auto-indent new lines
1541    pub auto_indent: bool,
1542
1543    /// Whether to auto-close brackets, parentheses, and quotes
1544    pub auto_close: bool,
1545
1546    /// Whether to surround selected text with matching pairs
1547    pub auto_surround: bool,
1548
1549    /// Whether line wrapping is enabled for this buffer
1550    pub line_wrap: bool,
1551
1552    /// Column at which to wrap lines (None = viewport width)
1553    pub wrap_column: Option<usize>,
1554
1555    /// Resolved whitespace indicator visibility
1556    pub whitespace: WhitespaceVisibility,
1557
1558    /// Formatter command for this buffer
1559    pub formatter: Option<FormatterConfig>,
1560
1561    /// Whether to format on save
1562    pub format_on_save: bool,
1563
1564    /// Actions to run when saving
1565    pub on_save: Vec<OnSaveAction>,
1566
1567    /// Preferred highlighter backend
1568    pub highlighter: HighlighterPreference,
1569
1570    /// Path to custom TextMate grammar (if any)
1571    pub textmate_grammar: Option<std::path::PathBuf>,
1572
1573    /// Extra word-constituent characters for this language (for completion).
1574    /// Empty string means standard alphanumeric + underscore only.
1575    pub word_characters: String,
1576}
1577
1578impl BufferConfig {
1579    /// Resolve the effective configuration for a buffer given its language.
1580    ///
1581    /// This merges the global editor settings with any language-specific overrides
1582    /// from `Config.languages`.
1583    ///
1584    /// # Arguments
1585    /// * `global_config` - The resolved global configuration
1586    /// * `language_id` - Optional language identifier (e.g., "rust", "python")
1587    pub fn resolve(global_config: &Config, language_id: Option<&str>) -> Self {
1588        let editor = &global_config.editor;
1589
1590        // Start with global editor settings
1591        let mut whitespace = WhitespaceVisibility::from_editor_config(editor);
1592        let mut config = BufferConfig {
1593            tab_size: editor.tab_size,
1594            use_tabs: editor.use_tabs,
1595            auto_indent: editor.auto_indent,
1596            auto_close: editor.auto_close,
1597            auto_surround: editor.auto_surround,
1598            line_wrap: editor.line_wrap,
1599            wrap_column: editor.wrap_column,
1600            whitespace,
1601            formatter: None,
1602            format_on_save: false,
1603            on_save: Vec::new(),
1604            highlighter: HighlighterPreference::Auto,
1605            textmate_grammar: None,
1606            word_characters: String::new(),
1607        };
1608
1609        // Apply language-specific overrides if available.
1610        // If no language config matches and the language is "text" (undetected),
1611        // try the fallback config (#1219).
1612        let lang_config_ref = language_id
1613            .and_then(|id| global_config.languages.get(id))
1614            .or({
1615                // Apply fallback only when language is unknown ("text" or None)
1616                match language_id {
1617                    None | Some("text") => global_config.fallback.as_ref(),
1618                    _ => None,
1619                }
1620            });
1621        if let Some(lang_config) = lang_config_ref {
1622            // Tab size: use language setting if specified, else global
1623            if let Some(ts) = lang_config.tab_size {
1624                config.tab_size = ts;
1625            }
1626
1627            // Use tabs: language override (only if explicitly set)
1628            if let Some(use_tabs) = lang_config.use_tabs {
1629                config.use_tabs = use_tabs;
1630            }
1631
1632            // Line wrap: language override (only if explicitly set)
1633            if let Some(line_wrap) = lang_config.line_wrap {
1634                config.line_wrap = line_wrap;
1635            }
1636
1637            // Wrap column: language override (only if explicitly set)
1638            if lang_config.wrap_column.is_some() {
1639                config.wrap_column = lang_config.wrap_column;
1640            }
1641
1642            // Auto indent: language override
1643            config.auto_indent = lang_config.auto_indent;
1644
1645            // Auto close: language override (only if globally enabled)
1646            if config.auto_close {
1647                if let Some(lang_auto_close) = lang_config.auto_close {
1648                    config.auto_close = lang_auto_close;
1649                }
1650            }
1651
1652            // Auto surround: language override (only if globally enabled)
1653            if config.auto_surround {
1654                if let Some(lang_auto_surround) = lang_config.auto_surround {
1655                    config.auto_surround = lang_auto_surround;
1656                }
1657            }
1658
1659            // Whitespace tabs: language override can disable tab indicators
1660            whitespace = whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
1661            config.whitespace = whitespace;
1662
1663            // Formatter: from language config
1664            config.formatter = lang_config.formatter.clone();
1665
1666            // Format on save: from language config
1667            config.format_on_save = lang_config.format_on_save;
1668
1669            // On save actions: from language config
1670            config.on_save = lang_config.on_save.clone();
1671
1672            // Highlighter preference: from language config
1673            config.highlighter = lang_config.highlighter;
1674
1675            // TextMate grammar path: from language config
1676            config.textmate_grammar = lang_config.textmate_grammar.clone();
1677
1678            // Word characters: from language config
1679            if let Some(ref wc) = lang_config.word_characters {
1680                config.word_characters = wc.clone();
1681            }
1682        }
1683
1684        config
1685    }
1686
1687    /// Get the effective indentation string for this buffer.
1688    ///
1689    /// Returns a tab character if `use_tabs` is true, otherwise returns
1690    /// `tab_size` spaces.
1691    pub fn indent_string(&self) -> String {
1692        if self.use_tabs {
1693            "\t".to_string()
1694        } else {
1695            " ".repeat(self.tab_size)
1696        }
1697    }
1698}
1699
1700/// Preference for which syntax highlighting backend to use
1701#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
1702#[serde(rename_all = "lowercase")]
1703pub enum HighlighterPreference {
1704    /// Use tree-sitter if available, fall back to TextMate
1705    #[default]
1706    Auto,
1707    /// Force tree-sitter only (no highlighting if unavailable)
1708    #[serde(rename = "tree-sitter")]
1709    TreeSitter,
1710    /// Force TextMate grammar (skip tree-sitter even if available)
1711    #[serde(rename = "textmate")]
1712    TextMate,
1713}
1714
1715/// Menu bar configuration
1716#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1717pub struct MenuConfig {
1718    /// List of top-level menus in the menu bar
1719    #[serde(default)]
1720    pub menus: Vec<Menu>,
1721}
1722
1723// Re-export Menu and MenuItem from fresh-core for shared type usage
1724pub use fresh_core::menu::{Menu, MenuItem};
1725
1726/// Extension trait for Menu with editor-specific functionality
1727pub trait MenuExt {
1728    /// Get the identifier for matching (id if set, otherwise label).
1729    /// This is used for keybinding matching and should be stable across translations.
1730    fn match_id(&self) -> &str;
1731
1732    /// Expand all DynamicSubmenu items in this menu to regular Submenu items
1733    /// This should be called before the menu is used for rendering/navigation
1734    fn expand_dynamic_items(&mut self, themes_dir: &std::path::Path);
1735}
1736
1737impl MenuExt for Menu {
1738    fn match_id(&self) -> &str {
1739        self.id.as_deref().unwrap_or(&self.label)
1740    }
1741
1742    fn expand_dynamic_items(&mut self, themes_dir: &std::path::Path) {
1743        self.items = self
1744            .items
1745            .iter()
1746            .map(|item| item.expand_dynamic(themes_dir))
1747            .collect();
1748    }
1749}
1750
1751/// Extension trait for MenuItem with editor-specific functionality
1752pub trait MenuItemExt {
1753    /// Expand a DynamicSubmenu into a regular Submenu with generated items.
1754    /// Returns the original item if not a DynamicSubmenu.
1755    fn expand_dynamic(&self, themes_dir: &std::path::Path) -> MenuItem;
1756}
1757
1758impl MenuItemExt for MenuItem {
1759    fn expand_dynamic(&self, themes_dir: &std::path::Path) -> MenuItem {
1760        match self {
1761            MenuItem::DynamicSubmenu { label, source } => {
1762                let items = generate_dynamic_items(source, themes_dir);
1763                MenuItem::Submenu {
1764                    label: label.clone(),
1765                    items,
1766                }
1767            }
1768            other => other.clone(),
1769        }
1770    }
1771}
1772
1773/// Generate menu items for a dynamic source (runtime only - requires view::theme)
1774#[cfg(feature = "runtime")]
1775pub fn generate_dynamic_items(source: &str, themes_dir: &std::path::Path) -> Vec<MenuItem> {
1776    match source {
1777        "copy_with_theme" => {
1778            // Generate theme options from available themes
1779            let loader = crate::view::theme::ThemeLoader::new(themes_dir.to_path_buf());
1780            let registry = loader.load_all(&[]);
1781            registry
1782                .list()
1783                .iter()
1784                .map(|info| {
1785                    let mut args = HashMap::new();
1786                    args.insert("theme".to_string(), serde_json::json!(info.name));
1787                    MenuItem::Action {
1788                        label: info.name.clone(),
1789                        action: "copy_with_theme".to_string(),
1790                        args,
1791                        when: Some(context_keys::HAS_SELECTION.to_string()),
1792                        checkbox: None,
1793                    }
1794                })
1795                .collect()
1796        }
1797        _ => vec![MenuItem::Label {
1798            info: format!("Unknown source: {}", source),
1799        }],
1800    }
1801}
1802
1803/// Generate menu items for a dynamic source (WASM stub - returns empty)
1804#[cfg(not(feature = "runtime"))]
1805pub fn generate_dynamic_items(_source: &str, _themes_dir: &std::path::Path) -> Vec<MenuItem> {
1806    // Theme loading not available in WASM builds
1807    vec![]
1808}
1809
1810impl Default for Config {
1811    fn default() -> Self {
1812        Self {
1813            version: 0,
1814            theme: default_theme_name(),
1815            locale: LocaleName::default(),
1816            check_for_updates: true,
1817            editor: EditorConfig::default(),
1818            file_explorer: FileExplorerConfig::default(),
1819            file_browser: FileBrowserConfig::default(),
1820            clipboard: ClipboardConfig::default(),
1821            terminal: TerminalConfig::default(),
1822            keybindings: vec![], // User customizations only; defaults come from active_keybinding_map
1823            keybinding_maps: HashMap::new(), // User-defined maps go here
1824            active_keybinding_map: default_keybinding_map_name(),
1825            languages: Self::default_languages(),
1826            fallback: None,
1827            lsp: Self::default_lsp_config(),
1828            warnings: WarningsConfig::default(),
1829            plugins: HashMap::new(), // Populated when scanning for plugins
1830            packages: PackagesConfig::default(),
1831        }
1832    }
1833}
1834
1835impl MenuConfig {
1836    /// Create a MenuConfig with translated menus using the current locale
1837    pub fn translated() -> Self {
1838        Self {
1839            menus: Self::translated_menus(),
1840        }
1841    }
1842
1843    /// Create default menu bar configuration with translated labels.
1844    ///
1845    /// This is the single source of truth for the editor's menu structure.
1846    /// Both the built-in TUI menu bar and the native GUI menu bar (e.g. macOS)
1847    /// are built from this definition.
1848    pub fn translated_menus() -> Vec<Menu> {
1849        vec![
1850            // File menu
1851            Menu {
1852                id: Some("File".to_string()),
1853                label: t!("menu.file").to_string(),
1854                when: None,
1855                items: vec![
1856                    MenuItem::Action {
1857                        label: t!("menu.file.new_file").to_string(),
1858                        action: "new".to_string(),
1859                        args: HashMap::new(),
1860                        when: None,
1861                        checkbox: None,
1862                    },
1863                    MenuItem::Action {
1864                        label: t!("menu.file.open_file").to_string(),
1865                        action: "open".to_string(),
1866                        args: HashMap::new(),
1867                        when: None,
1868                        checkbox: None,
1869                    },
1870                    MenuItem::Separator { separator: true },
1871                    MenuItem::Action {
1872                        label: t!("menu.file.save").to_string(),
1873                        action: "save".to_string(),
1874                        args: HashMap::new(),
1875                        when: None,
1876                        checkbox: None,
1877                    },
1878                    MenuItem::Action {
1879                        label: t!("menu.file.save_as").to_string(),
1880                        action: "save_as".to_string(),
1881                        args: HashMap::new(),
1882                        when: None,
1883                        checkbox: None,
1884                    },
1885                    MenuItem::Action {
1886                        label: t!("menu.file.revert").to_string(),
1887                        action: "revert".to_string(),
1888                        args: HashMap::new(),
1889                        when: None,
1890                        checkbox: None,
1891                    },
1892                    MenuItem::Action {
1893                        label: t!("menu.file.reload_with_encoding").to_string(),
1894                        action: "reload_with_encoding".to_string(),
1895                        args: HashMap::new(),
1896                        when: None,
1897                        checkbox: None,
1898                    },
1899                    MenuItem::Separator { separator: true },
1900                    MenuItem::Action {
1901                        label: t!("menu.file.close_buffer").to_string(),
1902                        action: "close".to_string(),
1903                        args: HashMap::new(),
1904                        when: None,
1905                        checkbox: None,
1906                    },
1907                    MenuItem::Separator { separator: true },
1908                    MenuItem::Action {
1909                        label: t!("menu.file.switch_project").to_string(),
1910                        action: "switch_project".to_string(),
1911                        args: HashMap::new(),
1912                        when: None,
1913                        checkbox: None,
1914                    },
1915                    MenuItem::Separator { separator: true },
1916                    MenuItem::Action {
1917                        label: t!("menu.file.detach").to_string(),
1918                        action: "detach".to_string(),
1919                        args: HashMap::new(),
1920                        when: Some(context_keys::SESSION_MODE.to_string()),
1921                        checkbox: None,
1922                    },
1923                    MenuItem::Action {
1924                        label: t!("menu.file.quit").to_string(),
1925                        action: "quit".to_string(),
1926                        args: HashMap::new(),
1927                        when: None,
1928                        checkbox: None,
1929                    },
1930                ],
1931            },
1932            // Edit menu
1933            Menu {
1934                id: Some("Edit".to_string()),
1935                label: t!("menu.edit").to_string(),
1936                when: None,
1937                items: vec![
1938                    MenuItem::Action {
1939                        label: t!("menu.edit.undo").to_string(),
1940                        action: "undo".to_string(),
1941                        args: HashMap::new(),
1942                        when: None,
1943                        checkbox: None,
1944                    },
1945                    MenuItem::Action {
1946                        label: t!("menu.edit.redo").to_string(),
1947                        action: "redo".to_string(),
1948                        args: HashMap::new(),
1949                        when: None,
1950                        checkbox: None,
1951                    },
1952                    MenuItem::Separator { separator: true },
1953                    MenuItem::Action {
1954                        label: t!("menu.edit.cut").to_string(),
1955                        action: "cut".to_string(),
1956                        args: HashMap::new(),
1957                        when: Some(context_keys::HAS_SELECTION.to_string()),
1958                        checkbox: None,
1959                    },
1960                    MenuItem::Action {
1961                        label: t!("menu.edit.copy").to_string(),
1962                        action: "copy".to_string(),
1963                        args: HashMap::new(),
1964                        when: Some(context_keys::HAS_SELECTION.to_string()),
1965                        checkbox: None,
1966                    },
1967                    MenuItem::DynamicSubmenu {
1968                        label: t!("menu.edit.copy_with_formatting").to_string(),
1969                        source: "copy_with_theme".to_string(),
1970                    },
1971                    MenuItem::Action {
1972                        label: t!("menu.edit.paste").to_string(),
1973                        action: "paste".to_string(),
1974                        args: HashMap::new(),
1975                        when: None,
1976                        checkbox: None,
1977                    },
1978                    MenuItem::Separator { separator: true },
1979                    MenuItem::Action {
1980                        label: t!("menu.edit.select_all").to_string(),
1981                        action: "select_all".to_string(),
1982                        args: HashMap::new(),
1983                        when: None,
1984                        checkbox: None,
1985                    },
1986                    MenuItem::Separator { separator: true },
1987                    MenuItem::Action {
1988                        label: t!("menu.edit.find").to_string(),
1989                        action: "search".to_string(),
1990                        args: HashMap::new(),
1991                        when: None,
1992                        checkbox: None,
1993                    },
1994                    MenuItem::Action {
1995                        label: t!("menu.edit.find_in_selection").to_string(),
1996                        action: "find_in_selection".to_string(),
1997                        args: HashMap::new(),
1998                        when: Some(context_keys::HAS_SELECTION.to_string()),
1999                        checkbox: None,
2000                    },
2001                    MenuItem::Action {
2002                        label: t!("menu.edit.find_next").to_string(),
2003                        action: "find_next".to_string(),
2004                        args: HashMap::new(),
2005                        when: None,
2006                        checkbox: None,
2007                    },
2008                    MenuItem::Action {
2009                        label: t!("menu.edit.find_previous").to_string(),
2010                        action: "find_previous".to_string(),
2011                        args: HashMap::new(),
2012                        when: None,
2013                        checkbox: None,
2014                    },
2015                    MenuItem::Action {
2016                        label: t!("menu.edit.replace").to_string(),
2017                        action: "query_replace".to_string(),
2018                        args: HashMap::new(),
2019                        when: None,
2020                        checkbox: None,
2021                    },
2022                    MenuItem::Separator { separator: true },
2023                    MenuItem::Action {
2024                        label: t!("menu.edit.delete_line").to_string(),
2025                        action: "delete_line".to_string(),
2026                        args: HashMap::new(),
2027                        when: None,
2028                        checkbox: None,
2029                    },
2030                    MenuItem::Action {
2031                        label: t!("menu.edit.format_buffer").to_string(),
2032                        action: "format_buffer".to_string(),
2033                        args: HashMap::new(),
2034                        when: Some(context_keys::FORMATTER_AVAILABLE.to_string()),
2035                        checkbox: None,
2036                    },
2037                    MenuItem::Separator { separator: true },
2038                    MenuItem::Action {
2039                        label: t!("menu.edit.settings").to_string(),
2040                        action: "open_settings".to_string(),
2041                        args: HashMap::new(),
2042                        when: None,
2043                        checkbox: None,
2044                    },
2045                    MenuItem::Action {
2046                        label: t!("menu.edit.keybinding_editor").to_string(),
2047                        action: "open_keybinding_editor".to_string(),
2048                        args: HashMap::new(),
2049                        when: None,
2050                        checkbox: None,
2051                    },
2052                ],
2053            },
2054            // View menu
2055            Menu {
2056                id: Some("View".to_string()),
2057                label: t!("menu.view").to_string(),
2058                when: None,
2059                items: vec![
2060                    MenuItem::Action {
2061                        label: t!("menu.view.file_explorer").to_string(),
2062                        action: "toggle_file_explorer".to_string(),
2063                        args: HashMap::new(),
2064                        when: None,
2065                        checkbox: Some(context_keys::FILE_EXPLORER.to_string()),
2066                    },
2067                    MenuItem::Separator { separator: true },
2068                    MenuItem::Action {
2069                        label: t!("menu.view.line_numbers").to_string(),
2070                        action: "toggle_line_numbers".to_string(),
2071                        args: HashMap::new(),
2072                        when: None,
2073                        checkbox: Some(context_keys::LINE_NUMBERS.to_string()),
2074                    },
2075                    MenuItem::Action {
2076                        label: t!("menu.view.line_wrap").to_string(),
2077                        action: "toggle_line_wrap".to_string(),
2078                        args: HashMap::new(),
2079                        when: None,
2080                        checkbox: Some(context_keys::LINE_WRAP.to_string()),
2081                    },
2082                    MenuItem::Action {
2083                        label: t!("menu.view.mouse_support").to_string(),
2084                        action: "toggle_mouse_capture".to_string(),
2085                        args: HashMap::new(),
2086                        when: None,
2087                        checkbox: Some(context_keys::MOUSE_CAPTURE.to_string()),
2088                    },
2089                    MenuItem::Separator { separator: true },
2090                    MenuItem::Action {
2091                        label: t!("menu.view.vertical_scrollbar").to_string(),
2092                        action: "toggle_vertical_scrollbar".to_string(),
2093                        args: HashMap::new(),
2094                        when: None,
2095                        checkbox: Some(context_keys::VERTICAL_SCROLLBAR.to_string()),
2096                    },
2097                    MenuItem::Action {
2098                        label: t!("menu.view.horizontal_scrollbar").to_string(),
2099                        action: "toggle_horizontal_scrollbar".to_string(),
2100                        args: HashMap::new(),
2101                        when: None,
2102                        checkbox: Some(context_keys::HORIZONTAL_SCROLLBAR.to_string()),
2103                    },
2104                    MenuItem::Separator { separator: true },
2105                    MenuItem::Action {
2106                        label: t!("menu.view.set_background").to_string(),
2107                        action: "set_background".to_string(),
2108                        args: HashMap::new(),
2109                        when: None,
2110                        checkbox: None,
2111                    },
2112                    MenuItem::Action {
2113                        label: t!("menu.view.set_background_blend").to_string(),
2114                        action: "set_background_blend".to_string(),
2115                        args: HashMap::new(),
2116                        when: None,
2117                        checkbox: None,
2118                    },
2119                    MenuItem::Action {
2120                        label: t!("menu.view.set_page_width").to_string(),
2121                        action: "set_page_width".to_string(),
2122                        args: HashMap::new(),
2123                        when: None,
2124                        checkbox: None,
2125                    },
2126                    MenuItem::Separator { separator: true },
2127                    MenuItem::Action {
2128                        label: t!("menu.view.select_theme").to_string(),
2129                        action: "select_theme".to_string(),
2130                        args: HashMap::new(),
2131                        when: None,
2132                        checkbox: None,
2133                    },
2134                    MenuItem::Action {
2135                        label: t!("menu.view.select_locale").to_string(),
2136                        action: "select_locale".to_string(),
2137                        args: HashMap::new(),
2138                        when: None,
2139                        checkbox: None,
2140                    },
2141                    MenuItem::Action {
2142                        label: t!("menu.view.settings").to_string(),
2143                        action: "open_settings".to_string(),
2144                        args: HashMap::new(),
2145                        when: None,
2146                        checkbox: None,
2147                    },
2148                    MenuItem::Action {
2149                        label: t!("menu.view.calibrate_input").to_string(),
2150                        action: "calibrate_input".to_string(),
2151                        args: HashMap::new(),
2152                        when: None,
2153                        checkbox: None,
2154                    },
2155                    MenuItem::Separator { separator: true },
2156                    MenuItem::Action {
2157                        label: t!("menu.view.split_horizontal").to_string(),
2158                        action: "split_horizontal".to_string(),
2159                        args: HashMap::new(),
2160                        when: None,
2161                        checkbox: None,
2162                    },
2163                    MenuItem::Action {
2164                        label: t!("menu.view.split_vertical").to_string(),
2165                        action: "split_vertical".to_string(),
2166                        args: HashMap::new(),
2167                        when: None,
2168                        checkbox: None,
2169                    },
2170                    MenuItem::Action {
2171                        label: t!("menu.view.close_split").to_string(),
2172                        action: "close_split".to_string(),
2173                        args: HashMap::new(),
2174                        when: None,
2175                        checkbox: None,
2176                    },
2177                    MenuItem::Action {
2178                        label: t!("menu.view.scroll_sync").to_string(),
2179                        action: "toggle_scroll_sync".to_string(),
2180                        args: HashMap::new(),
2181                        when: Some(context_keys::HAS_SAME_BUFFER_SPLITS.to_string()),
2182                        checkbox: Some(context_keys::SCROLL_SYNC.to_string()),
2183                    },
2184                    MenuItem::Action {
2185                        label: t!("menu.view.focus_next_split").to_string(),
2186                        action: "next_split".to_string(),
2187                        args: HashMap::new(),
2188                        when: None,
2189                        checkbox: None,
2190                    },
2191                    MenuItem::Action {
2192                        label: t!("menu.view.focus_prev_split").to_string(),
2193                        action: "prev_split".to_string(),
2194                        args: HashMap::new(),
2195                        when: None,
2196                        checkbox: None,
2197                    },
2198                    MenuItem::Action {
2199                        label: t!("menu.view.toggle_maximize_split").to_string(),
2200                        action: "toggle_maximize_split".to_string(),
2201                        args: HashMap::new(),
2202                        when: None,
2203                        checkbox: None,
2204                    },
2205                    MenuItem::Separator { separator: true },
2206                    MenuItem::Submenu {
2207                        label: t!("menu.terminal").to_string(),
2208                        items: vec![
2209                            MenuItem::Action {
2210                                label: t!("menu.terminal.open").to_string(),
2211                                action: "open_terminal".to_string(),
2212                                args: HashMap::new(),
2213                                when: None,
2214                                checkbox: None,
2215                            },
2216                            MenuItem::Action {
2217                                label: t!("menu.terminal.close").to_string(),
2218                                action: "close_terminal".to_string(),
2219                                args: HashMap::new(),
2220                                when: None,
2221                                checkbox: None,
2222                            },
2223                            MenuItem::Separator { separator: true },
2224                            MenuItem::Action {
2225                                label: t!("menu.terminal.toggle_keyboard_capture").to_string(),
2226                                action: "toggle_keyboard_capture".to_string(),
2227                                args: HashMap::new(),
2228                                when: None,
2229                                checkbox: None,
2230                            },
2231                        ],
2232                    },
2233                    MenuItem::Separator { separator: true },
2234                    MenuItem::Submenu {
2235                        label: t!("menu.view.keybinding_style").to_string(),
2236                        items: vec![
2237                            MenuItem::Action {
2238                                label: t!("menu.view.keybinding_default").to_string(),
2239                                action: "switch_keybinding_map".to_string(),
2240                                args: {
2241                                    let mut map = HashMap::new();
2242                                    map.insert("map".to_string(), serde_json::json!("default"));
2243                                    map
2244                                },
2245                                when: None,
2246                                checkbox: Some(context_keys::KEYMAP_DEFAULT.to_string()),
2247                            },
2248                            MenuItem::Action {
2249                                label: t!("menu.view.keybinding_emacs").to_string(),
2250                                action: "switch_keybinding_map".to_string(),
2251                                args: {
2252                                    let mut map = HashMap::new();
2253                                    map.insert("map".to_string(), serde_json::json!("emacs"));
2254                                    map
2255                                },
2256                                when: None,
2257                                checkbox: Some(context_keys::KEYMAP_EMACS.to_string()),
2258                            },
2259                            MenuItem::Action {
2260                                label: t!("menu.view.keybinding_vscode").to_string(),
2261                                action: "switch_keybinding_map".to_string(),
2262                                args: {
2263                                    let mut map = HashMap::new();
2264                                    map.insert("map".to_string(), serde_json::json!("vscode"));
2265                                    map
2266                                },
2267                                when: None,
2268                                checkbox: Some(context_keys::KEYMAP_VSCODE.to_string()),
2269                            },
2270                            MenuItem::Action {
2271                                label: "macOS GUI (⌘)".to_string(),
2272                                action: "switch_keybinding_map".to_string(),
2273                                args: {
2274                                    let mut map = HashMap::new();
2275                                    map.insert("map".to_string(), serde_json::json!("macos-gui"));
2276                                    map
2277                                },
2278                                when: None,
2279                                checkbox: Some(context_keys::KEYMAP_MACOS_GUI.to_string()),
2280                            },
2281                        ],
2282                    },
2283                ],
2284            },
2285            // Selection menu
2286            Menu {
2287                id: Some("Selection".to_string()),
2288                label: t!("menu.selection").to_string(),
2289                when: None,
2290                items: vec![
2291                    MenuItem::Action {
2292                        label: t!("menu.selection.select_all").to_string(),
2293                        action: "select_all".to_string(),
2294                        args: HashMap::new(),
2295                        when: None,
2296                        checkbox: None,
2297                    },
2298                    MenuItem::Action {
2299                        label: t!("menu.selection.select_word").to_string(),
2300                        action: "select_word".to_string(),
2301                        args: HashMap::new(),
2302                        when: None,
2303                        checkbox: None,
2304                    },
2305                    MenuItem::Action {
2306                        label: t!("menu.selection.select_line").to_string(),
2307                        action: "select_line".to_string(),
2308                        args: HashMap::new(),
2309                        when: None,
2310                        checkbox: None,
2311                    },
2312                    MenuItem::Action {
2313                        label: t!("menu.selection.expand_selection").to_string(),
2314                        action: "expand_selection".to_string(),
2315                        args: HashMap::new(),
2316                        when: None,
2317                        checkbox: None,
2318                    },
2319                    MenuItem::Separator { separator: true },
2320                    MenuItem::Action {
2321                        label: t!("menu.selection.add_cursor_above").to_string(),
2322                        action: "add_cursor_above".to_string(),
2323                        args: HashMap::new(),
2324                        when: None,
2325                        checkbox: None,
2326                    },
2327                    MenuItem::Action {
2328                        label: t!("menu.selection.add_cursor_below").to_string(),
2329                        action: "add_cursor_below".to_string(),
2330                        args: HashMap::new(),
2331                        when: None,
2332                        checkbox: None,
2333                    },
2334                    MenuItem::Action {
2335                        label: t!("menu.selection.add_cursor_next_match").to_string(),
2336                        action: "add_cursor_next_match".to_string(),
2337                        args: HashMap::new(),
2338                        when: None,
2339                        checkbox: None,
2340                    },
2341                    MenuItem::Action {
2342                        label: t!("menu.selection.remove_secondary_cursors").to_string(),
2343                        action: "remove_secondary_cursors".to_string(),
2344                        args: HashMap::new(),
2345                        when: None,
2346                        checkbox: None,
2347                    },
2348                ],
2349            },
2350            // Go menu
2351            Menu {
2352                id: Some("Go".to_string()),
2353                label: t!("menu.go").to_string(),
2354                when: None,
2355                items: vec![
2356                    MenuItem::Action {
2357                        label: t!("menu.go.goto_line").to_string(),
2358                        action: "goto_line".to_string(),
2359                        args: HashMap::new(),
2360                        when: None,
2361                        checkbox: None,
2362                    },
2363                    MenuItem::Action {
2364                        label: t!("menu.go.goto_definition").to_string(),
2365                        action: "lsp_goto_definition".to_string(),
2366                        args: HashMap::new(),
2367                        when: None,
2368                        checkbox: None,
2369                    },
2370                    MenuItem::Action {
2371                        label: t!("menu.go.find_references").to_string(),
2372                        action: "lsp_references".to_string(),
2373                        args: HashMap::new(),
2374                        when: None,
2375                        checkbox: None,
2376                    },
2377                    MenuItem::Separator { separator: true },
2378                    MenuItem::Action {
2379                        label: t!("menu.go.next_buffer").to_string(),
2380                        action: "next_buffer".to_string(),
2381                        args: HashMap::new(),
2382                        when: None,
2383                        checkbox: None,
2384                    },
2385                    MenuItem::Action {
2386                        label: t!("menu.go.prev_buffer").to_string(),
2387                        action: "prev_buffer".to_string(),
2388                        args: HashMap::new(),
2389                        when: None,
2390                        checkbox: None,
2391                    },
2392                    MenuItem::Separator { separator: true },
2393                    MenuItem::Action {
2394                        label: t!("menu.go.command_palette").to_string(),
2395                        action: "command_palette".to_string(),
2396                        args: HashMap::new(),
2397                        when: None,
2398                        checkbox: None,
2399                    },
2400                ],
2401            },
2402            // LSP menu
2403            Menu {
2404                id: Some("LSP".to_string()),
2405                label: t!("menu.lsp").to_string(),
2406                when: None,
2407                items: vec![
2408                    MenuItem::Action {
2409                        label: t!("menu.lsp.show_hover").to_string(),
2410                        action: "lsp_hover".to_string(),
2411                        args: HashMap::new(),
2412                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2413                        checkbox: None,
2414                    },
2415                    MenuItem::Action {
2416                        label: t!("menu.lsp.goto_definition").to_string(),
2417                        action: "lsp_goto_definition".to_string(),
2418                        args: HashMap::new(),
2419                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2420                        checkbox: None,
2421                    },
2422                    MenuItem::Action {
2423                        label: t!("menu.lsp.find_references").to_string(),
2424                        action: "lsp_references".to_string(),
2425                        args: HashMap::new(),
2426                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2427                        checkbox: None,
2428                    },
2429                    MenuItem::Action {
2430                        label: t!("menu.lsp.rename_symbol").to_string(),
2431                        action: "lsp_rename".to_string(),
2432                        args: HashMap::new(),
2433                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2434                        checkbox: None,
2435                    },
2436                    MenuItem::Separator { separator: true },
2437                    MenuItem::Action {
2438                        label: t!("menu.lsp.show_completions").to_string(),
2439                        action: "lsp_completion".to_string(),
2440                        args: HashMap::new(),
2441                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2442                        checkbox: None,
2443                    },
2444                    MenuItem::Action {
2445                        label: t!("menu.lsp.show_signature").to_string(),
2446                        action: "lsp_signature_help".to_string(),
2447                        args: HashMap::new(),
2448                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2449                        checkbox: None,
2450                    },
2451                    MenuItem::Action {
2452                        label: t!("menu.lsp.code_actions").to_string(),
2453                        action: "lsp_code_actions".to_string(),
2454                        args: HashMap::new(),
2455                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2456                        checkbox: None,
2457                    },
2458                    MenuItem::Separator { separator: true },
2459                    MenuItem::Action {
2460                        label: t!("menu.lsp.toggle_inlay_hints").to_string(),
2461                        action: "toggle_inlay_hints".to_string(),
2462                        args: HashMap::new(),
2463                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2464                        checkbox: Some(context_keys::INLAY_HINTS.to_string()),
2465                    },
2466                    MenuItem::Action {
2467                        label: t!("menu.lsp.toggle_mouse_hover").to_string(),
2468                        action: "toggle_mouse_hover".to_string(),
2469                        args: HashMap::new(),
2470                        when: None,
2471                        checkbox: Some(context_keys::MOUSE_HOVER.to_string()),
2472                    },
2473                    MenuItem::Separator { separator: true },
2474                    MenuItem::Action {
2475                        label: t!("menu.lsp.restart_server").to_string(),
2476                        action: "lsp_restart".to_string(),
2477                        args: HashMap::new(),
2478                        when: None,
2479                        checkbox: None,
2480                    },
2481                    MenuItem::Action {
2482                        label: t!("menu.lsp.stop_server").to_string(),
2483                        action: "lsp_stop".to_string(),
2484                        args: HashMap::new(),
2485                        when: None,
2486                        checkbox: None,
2487                    },
2488                    MenuItem::Separator { separator: true },
2489                    MenuItem::Action {
2490                        label: t!("menu.lsp.toggle_for_buffer").to_string(),
2491                        action: "lsp_toggle_for_buffer".to_string(),
2492                        args: HashMap::new(),
2493                        when: None,
2494                        checkbox: None,
2495                    },
2496                ],
2497            },
2498            // Explorer menu (only visible when file explorer is focused)
2499            Menu {
2500                id: Some("Explorer".to_string()),
2501                label: t!("menu.explorer").to_string(),
2502                when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2503                items: vec![
2504                    MenuItem::Action {
2505                        label: t!("menu.explorer.new_file").to_string(),
2506                        action: "file_explorer_new_file".to_string(),
2507                        args: HashMap::new(),
2508                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2509                        checkbox: None,
2510                    },
2511                    MenuItem::Action {
2512                        label: t!("menu.explorer.new_folder").to_string(),
2513                        action: "file_explorer_new_directory".to_string(),
2514                        args: HashMap::new(),
2515                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2516                        checkbox: None,
2517                    },
2518                    MenuItem::Separator { separator: true },
2519                    MenuItem::Action {
2520                        label: t!("menu.explorer.open").to_string(),
2521                        action: "file_explorer_open".to_string(),
2522                        args: HashMap::new(),
2523                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2524                        checkbox: None,
2525                    },
2526                    MenuItem::Action {
2527                        label: t!("menu.explorer.rename").to_string(),
2528                        action: "file_explorer_rename".to_string(),
2529                        args: HashMap::new(),
2530                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2531                        checkbox: None,
2532                    },
2533                    MenuItem::Action {
2534                        label: t!("menu.explorer.delete").to_string(),
2535                        action: "file_explorer_delete".to_string(),
2536                        args: HashMap::new(),
2537                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2538                        checkbox: None,
2539                    },
2540                    MenuItem::Separator { separator: true },
2541                    MenuItem::Action {
2542                        label: t!("menu.explorer.refresh").to_string(),
2543                        action: "file_explorer_refresh".to_string(),
2544                        args: HashMap::new(),
2545                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2546                        checkbox: None,
2547                    },
2548                    MenuItem::Separator { separator: true },
2549                    MenuItem::Action {
2550                        label: t!("menu.explorer.show_hidden").to_string(),
2551                        action: "file_explorer_toggle_hidden".to_string(),
2552                        args: HashMap::new(),
2553                        when: Some(context_keys::FILE_EXPLORER.to_string()),
2554                        checkbox: Some(context_keys::FILE_EXPLORER_SHOW_HIDDEN.to_string()),
2555                    },
2556                    MenuItem::Action {
2557                        label: t!("menu.explorer.show_gitignored").to_string(),
2558                        action: "file_explorer_toggle_gitignored".to_string(),
2559                        args: HashMap::new(),
2560                        when: Some(context_keys::FILE_EXPLORER.to_string()),
2561                        checkbox: Some(context_keys::FILE_EXPLORER_SHOW_GITIGNORED.to_string()),
2562                    },
2563                ],
2564            },
2565            // Help menu
2566            Menu {
2567                id: Some("Help".to_string()),
2568                label: t!("menu.help").to_string(),
2569                when: None,
2570                items: vec![
2571                    MenuItem::Label {
2572                        info: format!("Fresh v{}", env!("CARGO_PKG_VERSION")),
2573                    },
2574                    MenuItem::Separator { separator: true },
2575                    MenuItem::Action {
2576                        label: t!("menu.help.show_manual").to_string(),
2577                        action: "show_help".to_string(),
2578                        args: HashMap::new(),
2579                        when: None,
2580                        checkbox: None,
2581                    },
2582                    MenuItem::Action {
2583                        label: t!("menu.help.keyboard_shortcuts").to_string(),
2584                        action: "keyboard_shortcuts".to_string(),
2585                        args: HashMap::new(),
2586                        when: None,
2587                        checkbox: None,
2588                    },
2589                    MenuItem::Separator { separator: true },
2590                    MenuItem::Action {
2591                        label: t!("menu.help.event_debug").to_string(),
2592                        action: "event_debug".to_string(),
2593                        args: HashMap::new(),
2594                        when: None,
2595                        checkbox: None,
2596                    },
2597                ],
2598            },
2599        ]
2600    }
2601}
2602
2603impl Config {
2604    /// The config filename used throughout the application
2605    pub(crate) const FILENAME: &'static str = "config.json";
2606
2607    /// Get the local config path (in the working directory)
2608    pub(crate) fn local_config_path(working_dir: &Path) -> std::path::PathBuf {
2609        working_dir.join(Self::FILENAME)
2610    }
2611
2612    /// Load configuration from a JSON file
2613    ///
2614    /// This deserializes the user's config file as a partial config and resolves
2615    /// it with system defaults. For HashMap fields like `lsp` and `languages`,
2616    /// entries from the user config are merged with the default entries.
2617    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
2618        let contents = std::fs::read_to_string(path.as_ref())
2619            .map_err(|e| ConfigError::IoError(e.to_string()))?;
2620
2621        // Deserialize as PartialConfig first, then resolve with defaults
2622        let partial: crate::partial_config::PartialConfig =
2623            serde_json::from_str(&contents).map_err(|e| ConfigError::ParseError(e.to_string()))?;
2624
2625        Ok(partial.resolve())
2626    }
2627
2628    /// Load a built-in keymap from embedded JSON
2629    fn load_builtin_keymap(name: &str) -> Option<KeymapConfig> {
2630        let json_content = match name {
2631            "default" => include_str!("../keymaps/default.json"),
2632            "emacs" => include_str!("../keymaps/emacs.json"),
2633            "vscode" => include_str!("../keymaps/vscode.json"),
2634            "macos" => include_str!("../keymaps/macos.json"),
2635            "macos-gui" => include_str!("../keymaps/macos-gui.json"),
2636            _ => return None,
2637        };
2638
2639        match serde_json::from_str(json_content) {
2640            Ok(config) => Some(config),
2641            Err(e) => {
2642                eprintln!("Failed to parse builtin keymap '{}': {}", name, e);
2643                None
2644            }
2645        }
2646    }
2647
2648    /// Resolve a keymap with inheritance
2649    /// Returns all bindings from the keymap and its parent chain
2650    pub fn resolve_keymap(&self, map_name: &str) -> Vec<Keybinding> {
2651        let mut visited = std::collections::HashSet::new();
2652        self.resolve_keymap_recursive(map_name, &mut visited)
2653    }
2654
2655    /// Recursive helper for resolve_keymap
2656    fn resolve_keymap_recursive(
2657        &self,
2658        map_name: &str,
2659        visited: &mut std::collections::HashSet<String>,
2660    ) -> Vec<Keybinding> {
2661        // Prevent infinite loops
2662        if visited.contains(map_name) {
2663            eprintln!(
2664                "Warning: Circular inheritance detected in keymap '{}'",
2665                map_name
2666            );
2667            return Vec::new();
2668        }
2669        visited.insert(map_name.to_string());
2670
2671        // Try to load the keymap (user-defined or built-in)
2672        let keymap = self
2673            .keybinding_maps
2674            .get(map_name)
2675            .cloned()
2676            .or_else(|| Self::load_builtin_keymap(map_name));
2677
2678        let Some(keymap) = keymap else {
2679            return Vec::new();
2680        };
2681
2682        // Start with parent bindings (if any)
2683        let mut all_bindings = if let Some(ref parent_name) = keymap.inherits {
2684            self.resolve_keymap_recursive(parent_name, visited)
2685        } else {
2686            Vec::new()
2687        };
2688
2689        // Add this keymap's bindings (they override parent bindings)
2690        all_bindings.extend(keymap.bindings);
2691
2692        all_bindings
2693    }
2694    /// Create default language configurations
2695    fn default_languages() -> HashMap<String, LanguageConfig> {
2696        let mut languages = HashMap::new();
2697
2698        languages.insert(
2699            "rust".to_string(),
2700            LanguageConfig {
2701                extensions: vec!["rs".to_string()],
2702                filenames: vec![],
2703                grammar: "rust".to_string(),
2704                comment_prefix: Some("//".to_string()),
2705                auto_indent: true,
2706                auto_close: None,
2707                auto_surround: None,
2708                highlighter: HighlighterPreference::Auto,
2709                textmate_grammar: None,
2710                show_whitespace_tabs: true,
2711                line_wrap: None,
2712                wrap_column: None,
2713                page_view: None,
2714                page_width: None,
2715                use_tabs: None,
2716                tab_size: None,
2717                formatter: Some(FormatterConfig {
2718                    command: "rustfmt".to_string(),
2719                    args: vec!["--edition".to_string(), "2021".to_string()],
2720                    stdin: true,
2721                    timeout_ms: 10000,
2722                }),
2723                format_on_save: false,
2724                on_save: vec![],
2725                word_characters: None,
2726            },
2727        );
2728
2729        languages.insert(
2730            "javascript".to_string(),
2731            LanguageConfig {
2732                extensions: vec!["js".to_string(), "jsx".to_string(), "mjs".to_string()],
2733                filenames: vec![],
2734                grammar: "javascript".to_string(),
2735                comment_prefix: Some("//".to_string()),
2736                auto_indent: true,
2737                auto_close: None,
2738                auto_surround: None,
2739                highlighter: HighlighterPreference::Auto,
2740                textmate_grammar: None,
2741                show_whitespace_tabs: true,
2742                line_wrap: None,
2743                wrap_column: None,
2744                page_view: None,
2745                page_width: None,
2746                use_tabs: None,
2747                tab_size: None,
2748                formatter: Some(FormatterConfig {
2749                    command: "prettier".to_string(),
2750                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2751                    stdin: true,
2752                    timeout_ms: 10000,
2753                }),
2754                format_on_save: false,
2755                on_save: vec![],
2756                word_characters: None,
2757            },
2758        );
2759
2760        languages.insert(
2761            "typescript".to_string(),
2762            LanguageConfig {
2763                extensions: vec!["ts".to_string(), "tsx".to_string(), "mts".to_string()],
2764                filenames: vec![],
2765                grammar: "typescript".to_string(),
2766                comment_prefix: Some("//".to_string()),
2767                auto_indent: true,
2768                auto_close: None,
2769                auto_surround: None,
2770                highlighter: HighlighterPreference::Auto,
2771                textmate_grammar: None,
2772                show_whitespace_tabs: true,
2773                line_wrap: None,
2774                wrap_column: None,
2775                page_view: None,
2776                page_width: None,
2777                use_tabs: None,
2778                tab_size: None,
2779                formatter: Some(FormatterConfig {
2780                    command: "prettier".to_string(),
2781                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2782                    stdin: true,
2783                    timeout_ms: 10000,
2784                }),
2785                format_on_save: false,
2786                on_save: vec![],
2787                word_characters: None,
2788            },
2789        );
2790
2791        languages.insert(
2792            "python".to_string(),
2793            LanguageConfig {
2794                extensions: vec!["py".to_string(), "pyi".to_string()],
2795                filenames: vec![],
2796                grammar: "python".to_string(),
2797                comment_prefix: Some("#".to_string()),
2798                auto_indent: true,
2799                auto_close: None,
2800                auto_surround: None,
2801                highlighter: HighlighterPreference::Auto,
2802                textmate_grammar: None,
2803                show_whitespace_tabs: true,
2804                line_wrap: None,
2805                wrap_column: None,
2806                page_view: None,
2807                page_width: None,
2808                use_tabs: None,
2809                tab_size: None,
2810                formatter: Some(FormatterConfig {
2811                    command: "ruff".to_string(),
2812                    args: vec![
2813                        "format".to_string(),
2814                        "--stdin-filename".to_string(),
2815                        "$FILE".to_string(),
2816                    ],
2817                    stdin: true,
2818                    timeout_ms: 10000,
2819                }),
2820                format_on_save: false,
2821                on_save: vec![],
2822                word_characters: None,
2823            },
2824        );
2825
2826        languages.insert(
2827            "c".to_string(),
2828            LanguageConfig {
2829                extensions: vec!["c".to_string(), "h".to_string()],
2830                filenames: vec![],
2831                grammar: "c".to_string(),
2832                comment_prefix: Some("//".to_string()),
2833                auto_indent: true,
2834                auto_close: None,
2835                auto_surround: None,
2836                highlighter: HighlighterPreference::Auto,
2837                textmate_grammar: None,
2838                show_whitespace_tabs: true,
2839                line_wrap: None,
2840                wrap_column: None,
2841                page_view: None,
2842                page_width: None,
2843                use_tabs: None,
2844                tab_size: None,
2845                formatter: Some(FormatterConfig {
2846                    command: "clang-format".to_string(),
2847                    args: vec![],
2848                    stdin: true,
2849                    timeout_ms: 10000,
2850                }),
2851                format_on_save: false,
2852                on_save: vec![],
2853                word_characters: None,
2854            },
2855        );
2856
2857        languages.insert(
2858            "cpp".to_string(),
2859            LanguageConfig {
2860                extensions: vec![
2861                    "cpp".to_string(),
2862                    "cc".to_string(),
2863                    "cxx".to_string(),
2864                    "hpp".to_string(),
2865                    "hh".to_string(),
2866                    "hxx".to_string(),
2867                ],
2868                filenames: vec![],
2869                grammar: "cpp".to_string(),
2870                comment_prefix: Some("//".to_string()),
2871                auto_indent: true,
2872                auto_close: None,
2873                auto_surround: None,
2874                highlighter: HighlighterPreference::Auto,
2875                textmate_grammar: None,
2876                show_whitespace_tabs: true,
2877                line_wrap: None,
2878                wrap_column: None,
2879                page_view: None,
2880                page_width: None,
2881                use_tabs: None,
2882                tab_size: None,
2883                formatter: Some(FormatterConfig {
2884                    command: "clang-format".to_string(),
2885                    args: vec![],
2886                    stdin: true,
2887                    timeout_ms: 10000,
2888                }),
2889                format_on_save: false,
2890                on_save: vec![],
2891                word_characters: None,
2892            },
2893        );
2894
2895        languages.insert(
2896            "csharp".to_string(),
2897            LanguageConfig {
2898                extensions: vec!["cs".to_string()],
2899                filenames: vec![],
2900                grammar: "C#".to_string(),
2901                comment_prefix: Some("//".to_string()),
2902                auto_indent: true,
2903                auto_close: None,
2904                auto_surround: None,
2905                highlighter: HighlighterPreference::Auto,
2906                textmate_grammar: None,
2907                show_whitespace_tabs: true,
2908                line_wrap: None,
2909                wrap_column: None,
2910                page_view: None,
2911                page_width: None,
2912                use_tabs: None,
2913                tab_size: None,
2914                formatter: None,
2915                format_on_save: false,
2916                on_save: vec![],
2917                word_characters: None,
2918            },
2919        );
2920
2921        languages.insert(
2922            "bash".to_string(),
2923            LanguageConfig {
2924                extensions: vec!["sh".to_string(), "bash".to_string()],
2925                filenames: vec![
2926                    ".bash_aliases".to_string(),
2927                    ".bash_logout".to_string(),
2928                    ".bash_profile".to_string(),
2929                    ".bashrc".to_string(),
2930                    ".env".to_string(),
2931                    ".profile".to_string(),
2932                    ".zlogin".to_string(),
2933                    ".zlogout".to_string(),
2934                    ".zprofile".to_string(),
2935                    ".zshenv".to_string(),
2936                    ".zshrc".to_string(),
2937                    // Common shell script files without extensions
2938                    "PKGBUILD".to_string(),
2939                    "APKBUILD".to_string(),
2940                ],
2941                grammar: "bash".to_string(),
2942                comment_prefix: Some("#".to_string()),
2943                auto_indent: true,
2944                auto_close: None,
2945                auto_surround: None,
2946                highlighter: HighlighterPreference::Auto,
2947                textmate_grammar: None,
2948                show_whitespace_tabs: true,
2949                line_wrap: None,
2950                wrap_column: None,
2951                page_view: None,
2952                page_width: None,
2953                use_tabs: None,
2954                tab_size: None,
2955                formatter: None,
2956                format_on_save: false,
2957                on_save: vec![],
2958                word_characters: None,
2959            },
2960        );
2961
2962        languages.insert(
2963            "makefile".to_string(),
2964            LanguageConfig {
2965                extensions: vec!["mk".to_string()],
2966                filenames: vec![
2967                    "Makefile".to_string(),
2968                    "makefile".to_string(),
2969                    "GNUmakefile".to_string(),
2970                ],
2971                grammar: "Makefile".to_string(),
2972                comment_prefix: Some("#".to_string()),
2973                auto_indent: false,
2974                auto_close: None,
2975                auto_surround: None,
2976                highlighter: HighlighterPreference::Auto,
2977                textmate_grammar: None,
2978                show_whitespace_tabs: true,
2979                line_wrap: None,
2980                wrap_column: None,
2981                page_view: None,
2982                page_width: None,
2983                use_tabs: Some(true), // Makefiles require tabs for recipes
2984                tab_size: Some(8),    // Makefiles traditionally use 8-space tabs
2985                formatter: None,
2986                format_on_save: false,
2987                on_save: vec![],
2988                word_characters: None,
2989            },
2990        );
2991
2992        languages.insert(
2993            "dockerfile".to_string(),
2994            LanguageConfig {
2995                extensions: vec!["dockerfile".to_string()],
2996                filenames: vec!["Dockerfile".to_string(), "Containerfile".to_string()],
2997                grammar: "dockerfile".to_string(),
2998                comment_prefix: Some("#".to_string()),
2999                auto_indent: true,
3000                auto_close: None,
3001                auto_surround: None,
3002                highlighter: HighlighterPreference::Auto,
3003                textmate_grammar: None,
3004                show_whitespace_tabs: true,
3005                line_wrap: None,
3006                wrap_column: None,
3007                page_view: None,
3008                page_width: None,
3009                use_tabs: None,
3010                tab_size: None,
3011                formatter: None,
3012                format_on_save: false,
3013                on_save: vec![],
3014                word_characters: None,
3015            },
3016        );
3017
3018        languages.insert(
3019            "json".to_string(),
3020            LanguageConfig {
3021                extensions: vec!["json".to_string(), "jsonc".to_string()],
3022                filenames: vec![],
3023                grammar: "json".to_string(),
3024                comment_prefix: None,
3025                auto_indent: true,
3026                auto_close: None,
3027                auto_surround: None,
3028                highlighter: HighlighterPreference::Auto,
3029                textmate_grammar: None,
3030                show_whitespace_tabs: true,
3031                line_wrap: None,
3032                wrap_column: None,
3033                page_view: None,
3034                page_width: None,
3035                use_tabs: None,
3036                tab_size: None,
3037                formatter: Some(FormatterConfig {
3038                    command: "prettier".to_string(),
3039                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
3040                    stdin: true,
3041                    timeout_ms: 10000,
3042                }),
3043                format_on_save: false,
3044                on_save: vec![],
3045                word_characters: None,
3046            },
3047        );
3048
3049        languages.insert(
3050            "toml".to_string(),
3051            LanguageConfig {
3052                extensions: vec!["toml".to_string()],
3053                filenames: vec!["Cargo.lock".to_string()],
3054                grammar: "toml".to_string(),
3055                comment_prefix: Some("#".to_string()),
3056                auto_indent: true,
3057                auto_close: None,
3058                auto_surround: None,
3059                highlighter: HighlighterPreference::Auto,
3060                textmate_grammar: None,
3061                show_whitespace_tabs: true,
3062                line_wrap: None,
3063                wrap_column: None,
3064                page_view: None,
3065                page_width: None,
3066                use_tabs: None,
3067                tab_size: None,
3068                formatter: None,
3069                format_on_save: false,
3070                on_save: vec![],
3071                word_characters: None,
3072            },
3073        );
3074
3075        languages.insert(
3076            "yaml".to_string(),
3077            LanguageConfig {
3078                extensions: vec!["yml".to_string(), "yaml".to_string()],
3079                filenames: vec![],
3080                grammar: "yaml".to_string(),
3081                comment_prefix: Some("#".to_string()),
3082                auto_indent: true,
3083                auto_close: None,
3084                auto_surround: None,
3085                highlighter: HighlighterPreference::Auto,
3086                textmate_grammar: None,
3087                show_whitespace_tabs: true,
3088                line_wrap: None,
3089                wrap_column: None,
3090                page_view: None,
3091                page_width: None,
3092                use_tabs: None,
3093                tab_size: None,
3094                formatter: Some(FormatterConfig {
3095                    command: "prettier".to_string(),
3096                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
3097                    stdin: true,
3098                    timeout_ms: 10000,
3099                }),
3100                format_on_save: false,
3101                on_save: vec![],
3102                word_characters: None,
3103            },
3104        );
3105
3106        languages.insert(
3107            "markdown".to_string(),
3108            LanguageConfig {
3109                extensions: vec!["md".to_string(), "markdown".to_string()],
3110                filenames: vec!["README".to_string()],
3111                grammar: "markdown".to_string(),
3112                comment_prefix: None,
3113                auto_indent: false,
3114                auto_close: None,
3115                auto_surround: None,
3116                highlighter: HighlighterPreference::Auto,
3117                textmate_grammar: None,
3118                show_whitespace_tabs: true,
3119                line_wrap: None,
3120                wrap_column: None,
3121                page_view: None,
3122                page_width: None,
3123                use_tabs: None,
3124                tab_size: None,
3125                formatter: None,
3126                format_on_save: false,
3127                on_save: vec![],
3128                word_characters: None,
3129            },
3130        );
3131
3132        // Go uses tabs for indentation by convention, so hide tab indicators and use tabs
3133        languages.insert(
3134            "go".to_string(),
3135            LanguageConfig {
3136                extensions: vec!["go".to_string()],
3137                filenames: vec![],
3138                grammar: "go".to_string(),
3139                comment_prefix: Some("//".to_string()),
3140                auto_indent: true,
3141                auto_close: None,
3142                auto_surround: None,
3143                highlighter: HighlighterPreference::Auto,
3144                textmate_grammar: None,
3145                show_whitespace_tabs: false,
3146                line_wrap: None,
3147                wrap_column: None,
3148                page_view: None,
3149                page_width: None,
3150                use_tabs: Some(true), // Go convention is to use tabs
3151                tab_size: Some(8),    // Go convention is 8-space tab width
3152                formatter: Some(FormatterConfig {
3153                    command: "gofmt".to_string(),
3154                    args: vec![],
3155                    stdin: true,
3156                    timeout_ms: 10000,
3157                }),
3158                format_on_save: false,
3159                on_save: vec![],
3160                word_characters: None,
3161            },
3162        );
3163
3164        languages.insert(
3165            "odin".to_string(),
3166            LanguageConfig {
3167                extensions: vec!["odin".to_string()],
3168                filenames: vec![],
3169                grammar: "odin".to_string(),
3170                comment_prefix: Some("//".to_string()),
3171                auto_indent: true,
3172                auto_close: None,
3173                auto_surround: None,
3174                highlighter: HighlighterPreference::Auto,
3175                textmate_grammar: None,
3176                show_whitespace_tabs: false,
3177                line_wrap: None,
3178                wrap_column: None,
3179                page_view: None,
3180                page_width: None,
3181                use_tabs: Some(true),
3182                tab_size: Some(8),
3183                formatter: None,
3184                format_on_save: false,
3185                on_save: vec![],
3186                word_characters: None,
3187            },
3188        );
3189
3190        languages.insert(
3191            "zig".to_string(),
3192            LanguageConfig {
3193                extensions: vec!["zig".to_string(), "zon".to_string()],
3194                filenames: vec![],
3195                grammar: "zig".to_string(),
3196                comment_prefix: Some("//".to_string()),
3197                auto_indent: true,
3198                auto_close: None,
3199                auto_surround: None,
3200                highlighter: HighlighterPreference::Auto,
3201                textmate_grammar: None,
3202                show_whitespace_tabs: true,
3203                line_wrap: None,
3204                wrap_column: None,
3205                page_view: None,
3206                page_width: None,
3207                use_tabs: None,
3208                tab_size: None,
3209                formatter: None,
3210                format_on_save: false,
3211                on_save: vec![],
3212                word_characters: None,
3213            },
3214        );
3215
3216        languages.insert(
3217            "java".to_string(),
3218            LanguageConfig {
3219                extensions: vec!["java".to_string()],
3220                filenames: vec![],
3221                grammar: "java".to_string(),
3222                comment_prefix: Some("//".to_string()),
3223                auto_indent: true,
3224                auto_close: None,
3225                auto_surround: None,
3226                highlighter: HighlighterPreference::Auto,
3227                textmate_grammar: None,
3228                show_whitespace_tabs: true,
3229                line_wrap: None,
3230                wrap_column: None,
3231                page_view: None,
3232                page_width: None,
3233                use_tabs: None,
3234                tab_size: None,
3235                formatter: None,
3236                format_on_save: false,
3237                on_save: vec![],
3238                word_characters: None,
3239            },
3240        );
3241
3242        languages.insert(
3243            "latex".to_string(),
3244            LanguageConfig {
3245                extensions: vec![
3246                    "tex".to_string(),
3247                    "latex".to_string(),
3248                    "ltx".to_string(),
3249                    "sty".to_string(),
3250                    "cls".to_string(),
3251                    "bib".to_string(),
3252                ],
3253                filenames: vec![],
3254                grammar: "latex".to_string(),
3255                comment_prefix: Some("%".to_string()),
3256                auto_indent: true,
3257                auto_close: None,
3258                auto_surround: None,
3259                highlighter: HighlighterPreference::Auto,
3260                textmate_grammar: None,
3261                show_whitespace_tabs: true,
3262                line_wrap: None,
3263                wrap_column: None,
3264                page_view: None,
3265                page_width: None,
3266                use_tabs: None,
3267                tab_size: None,
3268                formatter: None,
3269                format_on_save: false,
3270                on_save: vec![],
3271                word_characters: None,
3272            },
3273        );
3274
3275        languages.insert(
3276            "templ".to_string(),
3277            LanguageConfig {
3278                extensions: vec!["templ".to_string()],
3279                filenames: vec![],
3280                grammar: "go".to_string(), // Templ uses Go-like syntax
3281                comment_prefix: Some("//".to_string()),
3282                auto_indent: true,
3283                auto_close: None,
3284                auto_surround: None,
3285                highlighter: HighlighterPreference::Auto,
3286                textmate_grammar: None,
3287                show_whitespace_tabs: true,
3288                line_wrap: None,
3289                wrap_column: None,
3290                page_view: None,
3291                page_width: None,
3292                use_tabs: None,
3293                tab_size: None,
3294                formatter: None,
3295                format_on_save: false,
3296                on_save: vec![],
3297                word_characters: None,
3298            },
3299        );
3300
3301        // Git-related file types
3302        languages.insert(
3303            "git-rebase".to_string(),
3304            LanguageConfig {
3305                extensions: vec![],
3306                filenames: vec!["git-rebase-todo".to_string()],
3307                grammar: "Git Rebase Todo".to_string(),
3308                comment_prefix: Some("#".to_string()),
3309                auto_indent: false,
3310                auto_close: None,
3311                auto_surround: None,
3312                highlighter: HighlighterPreference::Auto,
3313                textmate_grammar: None,
3314                show_whitespace_tabs: true,
3315                line_wrap: None,
3316                wrap_column: None,
3317                page_view: None,
3318                page_width: None,
3319                use_tabs: None,
3320                tab_size: None,
3321                formatter: None,
3322                format_on_save: false,
3323                on_save: vec![],
3324                word_characters: None,
3325            },
3326        );
3327
3328        languages.insert(
3329            "git-commit".to_string(),
3330            LanguageConfig {
3331                extensions: vec![],
3332                filenames: vec![
3333                    "COMMIT_EDITMSG".to_string(),
3334                    "MERGE_MSG".to_string(),
3335                    "SQUASH_MSG".to_string(),
3336                    "TAG_EDITMSG".to_string(),
3337                ],
3338                grammar: "Git Commit Message".to_string(),
3339                comment_prefix: Some("#".to_string()),
3340                auto_indent: false,
3341                auto_close: None,
3342                auto_surround: None,
3343                highlighter: HighlighterPreference::Auto,
3344                textmate_grammar: None,
3345                show_whitespace_tabs: true,
3346                line_wrap: None,
3347                wrap_column: None,
3348                page_view: None,
3349                page_width: None,
3350                use_tabs: None,
3351                tab_size: None,
3352                formatter: None,
3353                format_on_save: false,
3354                on_save: vec![],
3355                word_characters: None,
3356            },
3357        );
3358
3359        languages.insert(
3360            "gitignore".to_string(),
3361            LanguageConfig {
3362                extensions: vec!["gitignore".to_string()],
3363                filenames: vec![
3364                    ".gitignore".to_string(),
3365                    ".dockerignore".to_string(),
3366                    ".npmignore".to_string(),
3367                    ".hgignore".to_string(),
3368                ],
3369                grammar: "Gitignore".to_string(),
3370                comment_prefix: Some("#".to_string()),
3371                auto_indent: false,
3372                auto_close: None,
3373                auto_surround: None,
3374                highlighter: HighlighterPreference::Auto,
3375                textmate_grammar: None,
3376                show_whitespace_tabs: true,
3377                line_wrap: None,
3378                wrap_column: None,
3379                page_view: None,
3380                page_width: None,
3381                use_tabs: None,
3382                tab_size: None,
3383                formatter: None,
3384                format_on_save: false,
3385                on_save: vec![],
3386                word_characters: None,
3387            },
3388        );
3389
3390        languages.insert(
3391            "gitconfig".to_string(),
3392            LanguageConfig {
3393                extensions: vec!["gitconfig".to_string()],
3394                filenames: vec![".gitconfig".to_string(), ".gitmodules".to_string()],
3395                grammar: "Git Config".to_string(),
3396                comment_prefix: Some("#".to_string()),
3397                auto_indent: true,
3398                auto_close: None,
3399                auto_surround: None,
3400                highlighter: HighlighterPreference::Auto,
3401                textmate_grammar: None,
3402                show_whitespace_tabs: true,
3403                line_wrap: None,
3404                wrap_column: None,
3405                page_view: None,
3406                page_width: None,
3407                use_tabs: None,
3408                tab_size: None,
3409                formatter: None,
3410                format_on_save: false,
3411                on_save: vec![],
3412                word_characters: None,
3413            },
3414        );
3415
3416        languages.insert(
3417            "gitattributes".to_string(),
3418            LanguageConfig {
3419                extensions: vec!["gitattributes".to_string()],
3420                filenames: vec![".gitattributes".to_string()],
3421                grammar: "Git Attributes".to_string(),
3422                comment_prefix: Some("#".to_string()),
3423                auto_indent: false,
3424                auto_close: None,
3425                auto_surround: None,
3426                highlighter: HighlighterPreference::Auto,
3427                textmate_grammar: None,
3428                show_whitespace_tabs: true,
3429                line_wrap: None,
3430                wrap_column: None,
3431                page_view: None,
3432                page_width: None,
3433                use_tabs: None,
3434                tab_size: None,
3435                formatter: None,
3436                format_on_save: false,
3437                on_save: vec![],
3438                word_characters: None,
3439            },
3440        );
3441
3442        languages.insert(
3443            "typst".to_string(),
3444            LanguageConfig {
3445                extensions: vec!["typ".to_string()],
3446                filenames: vec![],
3447                grammar: "Typst".to_string(),
3448                comment_prefix: Some("//".to_string()),
3449                auto_indent: true,
3450                auto_close: None,
3451                auto_surround: None,
3452                highlighter: HighlighterPreference::Auto,
3453                textmate_grammar: None,
3454                show_whitespace_tabs: true,
3455                line_wrap: None,
3456                wrap_column: None,
3457                page_view: None,
3458                page_width: None,
3459                use_tabs: None,
3460                tab_size: None,
3461                formatter: None,
3462                format_on_save: false,
3463                on_save: vec![],
3464                word_characters: None,
3465            },
3466        );
3467
3468        // --- Languages added for LSP support ---
3469        // These entries ensure detect_language() maps file extensions to language
3470        // names that match the LSP config keys in default_lsp_config().
3471
3472        languages.insert(
3473            "kotlin".to_string(),
3474            LanguageConfig {
3475                extensions: vec!["kt".to_string(), "kts".to_string()],
3476                filenames: vec![],
3477                grammar: "Kotlin".to_string(),
3478                comment_prefix: Some("//".to_string()),
3479                auto_indent: true,
3480                auto_close: None,
3481                auto_surround: None,
3482                highlighter: HighlighterPreference::Auto,
3483                textmate_grammar: None,
3484                show_whitespace_tabs: true,
3485                line_wrap: None,
3486                wrap_column: None,
3487                page_view: None,
3488                page_width: None,
3489                use_tabs: None,
3490                tab_size: None,
3491                formatter: None,
3492                format_on_save: false,
3493                on_save: vec![],
3494                word_characters: None,
3495            },
3496        );
3497
3498        languages.insert(
3499            "swift".to_string(),
3500            LanguageConfig {
3501                extensions: vec!["swift".to_string()],
3502                filenames: vec![],
3503                grammar: "Swift".to_string(),
3504                comment_prefix: Some("//".to_string()),
3505                auto_indent: true,
3506                auto_close: None,
3507                auto_surround: None,
3508                highlighter: HighlighterPreference::Auto,
3509                textmate_grammar: None,
3510                show_whitespace_tabs: true,
3511                line_wrap: None,
3512                wrap_column: None,
3513                page_view: None,
3514                page_width: None,
3515                use_tabs: None,
3516                tab_size: None,
3517                formatter: None,
3518                format_on_save: false,
3519                on_save: vec![],
3520                word_characters: None,
3521            },
3522        );
3523
3524        languages.insert(
3525            "scala".to_string(),
3526            LanguageConfig {
3527                extensions: vec!["scala".to_string(), "sc".to_string()],
3528                filenames: vec![],
3529                grammar: "Scala".to_string(),
3530                comment_prefix: Some("//".to_string()),
3531                auto_indent: true,
3532                auto_close: None,
3533                auto_surround: None,
3534                highlighter: HighlighterPreference::Auto,
3535                textmate_grammar: None,
3536                show_whitespace_tabs: true,
3537                line_wrap: None,
3538                wrap_column: None,
3539                page_view: None,
3540                page_width: None,
3541                use_tabs: None,
3542                tab_size: None,
3543                formatter: None,
3544                format_on_save: false,
3545                on_save: vec![],
3546                word_characters: None,
3547            },
3548        );
3549
3550        languages.insert(
3551            "dart".to_string(),
3552            LanguageConfig {
3553                extensions: vec!["dart".to_string()],
3554                filenames: vec![],
3555                grammar: "Dart".to_string(),
3556                comment_prefix: Some("//".to_string()),
3557                auto_indent: true,
3558                auto_close: None,
3559                auto_surround: None,
3560                highlighter: HighlighterPreference::Auto,
3561                textmate_grammar: None,
3562                show_whitespace_tabs: true,
3563                line_wrap: None,
3564                wrap_column: None,
3565                page_view: None,
3566                page_width: None,
3567                use_tabs: None,
3568                tab_size: None,
3569                formatter: None,
3570                format_on_save: false,
3571                on_save: vec![],
3572                word_characters: None,
3573            },
3574        );
3575
3576        languages.insert(
3577            "elixir".to_string(),
3578            LanguageConfig {
3579                extensions: vec!["ex".to_string(), "exs".to_string()],
3580                filenames: vec![],
3581                grammar: "Elixir".to_string(),
3582                comment_prefix: Some("#".to_string()),
3583                auto_indent: true,
3584                auto_close: None,
3585                auto_surround: None,
3586                highlighter: HighlighterPreference::Auto,
3587                textmate_grammar: None,
3588                show_whitespace_tabs: true,
3589                line_wrap: None,
3590                wrap_column: None,
3591                page_view: None,
3592                page_width: None,
3593                use_tabs: None,
3594                tab_size: None,
3595                formatter: None,
3596                format_on_save: false,
3597                on_save: vec![],
3598                word_characters: None,
3599            },
3600        );
3601
3602        languages.insert(
3603            "erlang".to_string(),
3604            LanguageConfig {
3605                extensions: vec!["erl".to_string(), "hrl".to_string()],
3606                filenames: vec![],
3607                grammar: "Erlang".to_string(),
3608                comment_prefix: Some("%".to_string()),
3609                auto_indent: true,
3610                auto_close: None,
3611                auto_surround: None,
3612                highlighter: HighlighterPreference::Auto,
3613                textmate_grammar: None,
3614                show_whitespace_tabs: true,
3615                line_wrap: None,
3616                wrap_column: None,
3617                page_view: None,
3618                page_width: None,
3619                use_tabs: None,
3620                tab_size: None,
3621                formatter: None,
3622                format_on_save: false,
3623                on_save: vec![],
3624                word_characters: None,
3625            },
3626        );
3627
3628        languages.insert(
3629            "haskell".to_string(),
3630            LanguageConfig {
3631                extensions: vec!["hs".to_string(), "lhs".to_string()],
3632                filenames: vec![],
3633                grammar: "Haskell".to_string(),
3634                comment_prefix: Some("--".to_string()),
3635                auto_indent: true,
3636                auto_close: None,
3637                auto_surround: None,
3638                highlighter: HighlighterPreference::Auto,
3639                textmate_grammar: None,
3640                show_whitespace_tabs: true,
3641                line_wrap: None,
3642                wrap_column: None,
3643                page_view: None,
3644                page_width: None,
3645                use_tabs: None,
3646                tab_size: None,
3647                formatter: None,
3648                format_on_save: false,
3649                on_save: vec![],
3650                word_characters: None,
3651            },
3652        );
3653
3654        languages.insert(
3655            "ocaml".to_string(),
3656            LanguageConfig {
3657                extensions: vec!["ml".to_string(), "mli".to_string()],
3658                filenames: vec![],
3659                grammar: "OCaml".to_string(),
3660                comment_prefix: None,
3661                auto_indent: true,
3662                auto_close: None,
3663                auto_surround: None,
3664                highlighter: HighlighterPreference::Auto,
3665                textmate_grammar: None,
3666                show_whitespace_tabs: true,
3667                line_wrap: None,
3668                wrap_column: None,
3669                page_view: None,
3670                page_width: None,
3671                use_tabs: None,
3672                tab_size: None,
3673                formatter: None,
3674                format_on_save: false,
3675                on_save: vec![],
3676                word_characters: None,
3677            },
3678        );
3679
3680        languages.insert(
3681            "clojure".to_string(),
3682            LanguageConfig {
3683                extensions: vec![
3684                    "clj".to_string(),
3685                    "cljs".to_string(),
3686                    "cljc".to_string(),
3687                    "edn".to_string(),
3688                ],
3689                filenames: vec![],
3690                grammar: "Clojure".to_string(),
3691                comment_prefix: Some(";".to_string()),
3692                auto_indent: true,
3693                auto_close: None,
3694                auto_surround: None,
3695                highlighter: HighlighterPreference::Auto,
3696                textmate_grammar: None,
3697                show_whitespace_tabs: true,
3698                line_wrap: None,
3699                wrap_column: None,
3700                page_view: None,
3701                page_width: None,
3702                use_tabs: None,
3703                tab_size: None,
3704                formatter: None,
3705                format_on_save: false,
3706                on_save: vec![],
3707                word_characters: None,
3708            },
3709        );
3710
3711        languages.insert(
3712            "r".to_string(),
3713            LanguageConfig {
3714                extensions: vec!["r".to_string(), "R".to_string(), "rmd".to_string()],
3715                filenames: vec![],
3716                grammar: "R".to_string(),
3717                comment_prefix: Some("#".to_string()),
3718                auto_indent: true,
3719                auto_close: None,
3720                auto_surround: None,
3721                highlighter: HighlighterPreference::Auto,
3722                textmate_grammar: None,
3723                show_whitespace_tabs: true,
3724                line_wrap: None,
3725                wrap_column: None,
3726                page_view: None,
3727                page_width: None,
3728                use_tabs: None,
3729                tab_size: None,
3730                formatter: None,
3731                format_on_save: false,
3732                on_save: vec![],
3733                word_characters: None,
3734            },
3735        );
3736
3737        languages.insert(
3738            "julia".to_string(),
3739            LanguageConfig {
3740                extensions: vec!["jl".to_string()],
3741                filenames: vec![],
3742                grammar: "Julia".to_string(),
3743                comment_prefix: Some("#".to_string()),
3744                auto_indent: true,
3745                auto_close: None,
3746                auto_surround: None,
3747                highlighter: HighlighterPreference::Auto,
3748                textmate_grammar: None,
3749                show_whitespace_tabs: true,
3750                line_wrap: None,
3751                wrap_column: None,
3752                page_view: None,
3753                page_width: None,
3754                use_tabs: None,
3755                tab_size: None,
3756                formatter: None,
3757                format_on_save: false,
3758                on_save: vec![],
3759                word_characters: None,
3760            },
3761        );
3762
3763        languages.insert(
3764            "perl".to_string(),
3765            LanguageConfig {
3766                extensions: vec!["pl".to_string(), "pm".to_string(), "t".to_string()],
3767                filenames: vec![],
3768                grammar: "Perl".to_string(),
3769                comment_prefix: Some("#".to_string()),
3770                auto_indent: true,
3771                auto_close: None,
3772                auto_surround: None,
3773                highlighter: HighlighterPreference::Auto,
3774                textmate_grammar: None,
3775                show_whitespace_tabs: true,
3776                line_wrap: None,
3777                wrap_column: None,
3778                page_view: None,
3779                page_width: None,
3780                use_tabs: None,
3781                tab_size: None,
3782                formatter: None,
3783                format_on_save: false,
3784                on_save: vec![],
3785                word_characters: None,
3786            },
3787        );
3788
3789        languages.insert(
3790            "nim".to_string(),
3791            LanguageConfig {
3792                extensions: vec!["nim".to_string(), "nims".to_string(), "nimble".to_string()],
3793                filenames: vec![],
3794                grammar: "Nim".to_string(),
3795                comment_prefix: Some("#".to_string()),
3796                auto_indent: true,
3797                auto_close: None,
3798                auto_surround: None,
3799                highlighter: HighlighterPreference::Auto,
3800                textmate_grammar: None,
3801                show_whitespace_tabs: true,
3802                line_wrap: None,
3803                wrap_column: None,
3804                page_view: None,
3805                page_width: None,
3806                use_tabs: None,
3807                tab_size: None,
3808                formatter: None,
3809                format_on_save: false,
3810                on_save: vec![],
3811                word_characters: None,
3812            },
3813        );
3814
3815        languages.insert(
3816            "gleam".to_string(),
3817            LanguageConfig {
3818                extensions: vec!["gleam".to_string()],
3819                filenames: vec![],
3820                grammar: "Gleam".to_string(),
3821                comment_prefix: Some("//".to_string()),
3822                auto_indent: true,
3823                auto_close: None,
3824                auto_surround: None,
3825                highlighter: HighlighterPreference::Auto,
3826                textmate_grammar: None,
3827                show_whitespace_tabs: true,
3828                line_wrap: None,
3829                wrap_column: None,
3830                page_view: None,
3831                page_width: None,
3832                use_tabs: None,
3833                tab_size: None,
3834                formatter: None,
3835                format_on_save: false,
3836                on_save: vec![],
3837                word_characters: None,
3838            },
3839        );
3840
3841        languages.insert(
3842            "fsharp".to_string(),
3843            LanguageConfig {
3844                extensions: vec!["fs".to_string(), "fsi".to_string(), "fsx".to_string()],
3845                filenames: vec![],
3846                grammar: "FSharp".to_string(),
3847                comment_prefix: Some("//".to_string()),
3848                auto_indent: true,
3849                auto_close: None,
3850                auto_surround: None,
3851                highlighter: HighlighterPreference::Auto,
3852                textmate_grammar: None,
3853                show_whitespace_tabs: true,
3854                line_wrap: None,
3855                wrap_column: None,
3856                page_view: None,
3857                page_width: None,
3858                use_tabs: None,
3859                tab_size: None,
3860                formatter: None,
3861                format_on_save: false,
3862                on_save: vec![],
3863                word_characters: None,
3864            },
3865        );
3866
3867        languages.insert(
3868            "nix".to_string(),
3869            LanguageConfig {
3870                extensions: vec!["nix".to_string()],
3871                filenames: vec![],
3872                grammar: "Nix".to_string(),
3873                comment_prefix: Some("#".to_string()),
3874                auto_indent: true,
3875                auto_close: None,
3876                auto_surround: None,
3877                highlighter: HighlighterPreference::Auto,
3878                textmate_grammar: None,
3879                show_whitespace_tabs: true,
3880                line_wrap: None,
3881                wrap_column: None,
3882                page_view: None,
3883                page_width: None,
3884                use_tabs: None,
3885                tab_size: None,
3886                formatter: None,
3887                format_on_save: false,
3888                on_save: vec![],
3889                word_characters: None,
3890            },
3891        );
3892
3893        languages.insert(
3894            "nushell".to_string(),
3895            LanguageConfig {
3896                extensions: vec!["nu".to_string()],
3897                filenames: vec![],
3898                grammar: "Nushell".to_string(),
3899                comment_prefix: Some("#".to_string()),
3900                auto_indent: true,
3901                auto_close: None,
3902                auto_surround: None,
3903                highlighter: HighlighterPreference::Auto,
3904                textmate_grammar: None,
3905                show_whitespace_tabs: true,
3906                line_wrap: None,
3907                wrap_column: None,
3908                page_view: None,
3909                page_width: None,
3910                use_tabs: None,
3911                tab_size: None,
3912                formatter: None,
3913                format_on_save: false,
3914                on_save: vec![],
3915                word_characters: None,
3916            },
3917        );
3918
3919        languages.insert(
3920            "solidity".to_string(),
3921            LanguageConfig {
3922                extensions: vec!["sol".to_string()],
3923                filenames: vec![],
3924                grammar: "Solidity".to_string(),
3925                comment_prefix: Some("//".to_string()),
3926                auto_indent: true,
3927                auto_close: None,
3928                auto_surround: None,
3929                highlighter: HighlighterPreference::Auto,
3930                textmate_grammar: None,
3931                show_whitespace_tabs: true,
3932                line_wrap: None,
3933                wrap_column: None,
3934                page_view: None,
3935                page_width: None,
3936                use_tabs: None,
3937                tab_size: None,
3938                formatter: None,
3939                format_on_save: false,
3940                on_save: vec![],
3941                word_characters: None,
3942            },
3943        );
3944
3945        languages.insert(
3946            "ruby".to_string(),
3947            LanguageConfig {
3948                extensions: vec!["rb".to_string(), "rake".to_string(), "gemspec".to_string()],
3949                filenames: vec![
3950                    "Gemfile".to_string(),
3951                    "Rakefile".to_string(),
3952                    "Guardfile".to_string(),
3953                ],
3954                grammar: "Ruby".to_string(),
3955                comment_prefix: Some("#".to_string()),
3956                auto_indent: true,
3957                auto_close: None,
3958                auto_surround: None,
3959                highlighter: HighlighterPreference::Auto,
3960                textmate_grammar: None,
3961                show_whitespace_tabs: true,
3962                line_wrap: None,
3963                wrap_column: None,
3964                page_view: None,
3965                page_width: None,
3966                use_tabs: None,
3967                tab_size: None,
3968                formatter: None,
3969                format_on_save: false,
3970                on_save: vec![],
3971                word_characters: None,
3972            },
3973        );
3974
3975        languages.insert(
3976            "php".to_string(),
3977            LanguageConfig {
3978                extensions: vec!["php".to_string(), "phtml".to_string()],
3979                filenames: vec![],
3980                grammar: "PHP".to_string(),
3981                comment_prefix: Some("//".to_string()),
3982                auto_indent: true,
3983                auto_close: None,
3984                auto_surround: None,
3985                highlighter: HighlighterPreference::Auto,
3986                textmate_grammar: None,
3987                show_whitespace_tabs: true,
3988                line_wrap: None,
3989                wrap_column: None,
3990                page_view: None,
3991                page_width: None,
3992                use_tabs: None,
3993                tab_size: None,
3994                formatter: None,
3995                format_on_save: false,
3996                on_save: vec![],
3997                word_characters: None,
3998            },
3999        );
4000
4001        languages.insert(
4002            "lua".to_string(),
4003            LanguageConfig {
4004                extensions: vec!["lua".to_string()],
4005                filenames: vec![],
4006                grammar: "Lua".to_string(),
4007                comment_prefix: Some("--".to_string()),
4008                auto_indent: true,
4009                auto_close: None,
4010                auto_surround: None,
4011                highlighter: HighlighterPreference::Auto,
4012                textmate_grammar: None,
4013                show_whitespace_tabs: true,
4014                line_wrap: None,
4015                wrap_column: None,
4016                page_view: None,
4017                page_width: None,
4018                use_tabs: None,
4019                tab_size: None,
4020                formatter: None,
4021                format_on_save: false,
4022                on_save: vec![],
4023                word_characters: None,
4024            },
4025        );
4026
4027        languages.insert(
4028            "html".to_string(),
4029            LanguageConfig {
4030                extensions: vec!["html".to_string(), "htm".to_string()],
4031                filenames: vec![],
4032                grammar: "HTML".to_string(),
4033                comment_prefix: None,
4034                auto_indent: true,
4035                auto_close: None,
4036                auto_surround: None,
4037                highlighter: HighlighterPreference::Auto,
4038                textmate_grammar: None,
4039                show_whitespace_tabs: true,
4040                line_wrap: None,
4041                wrap_column: None,
4042                page_view: None,
4043                page_width: None,
4044                use_tabs: None,
4045                tab_size: None,
4046                formatter: None,
4047                format_on_save: false,
4048                on_save: vec![],
4049                word_characters: None,
4050            },
4051        );
4052
4053        languages.insert(
4054            "css".to_string(),
4055            LanguageConfig {
4056                extensions: vec!["css".to_string()],
4057                filenames: vec![],
4058                grammar: "CSS".to_string(),
4059                comment_prefix: None,
4060                auto_indent: true,
4061                auto_close: None,
4062                auto_surround: None,
4063                highlighter: HighlighterPreference::Auto,
4064                textmate_grammar: None,
4065                show_whitespace_tabs: true,
4066                line_wrap: None,
4067                wrap_column: None,
4068                page_view: None,
4069                page_width: None,
4070                use_tabs: None,
4071                tab_size: None,
4072                formatter: None,
4073                format_on_save: false,
4074                on_save: vec![],
4075                word_characters: None,
4076            },
4077        );
4078
4079        languages.insert(
4080            "sql".to_string(),
4081            LanguageConfig {
4082                extensions: vec!["sql".to_string()],
4083                filenames: vec![],
4084                grammar: "SQL".to_string(),
4085                comment_prefix: Some("--".to_string()),
4086                auto_indent: true,
4087                auto_close: None,
4088                auto_surround: None,
4089                highlighter: HighlighterPreference::Auto,
4090                textmate_grammar: None,
4091                show_whitespace_tabs: true,
4092                line_wrap: None,
4093                wrap_column: None,
4094                page_view: None,
4095                page_width: None,
4096                use_tabs: None,
4097                tab_size: None,
4098                formatter: None,
4099                format_on_save: false,
4100                on_save: vec![],
4101                word_characters: None,
4102            },
4103        );
4104
4105        languages.insert(
4106            "graphql".to_string(),
4107            LanguageConfig {
4108                extensions: vec!["graphql".to_string(), "gql".to_string()],
4109                filenames: vec![],
4110                grammar: "GraphQL".to_string(),
4111                comment_prefix: Some("#".to_string()),
4112                auto_indent: true,
4113                auto_close: None,
4114                auto_surround: None,
4115                highlighter: HighlighterPreference::Auto,
4116                textmate_grammar: None,
4117                show_whitespace_tabs: true,
4118                line_wrap: None,
4119                wrap_column: None,
4120                page_view: None,
4121                page_width: None,
4122                use_tabs: None,
4123                tab_size: None,
4124                formatter: None,
4125                format_on_save: false,
4126                on_save: vec![],
4127                word_characters: None,
4128            },
4129        );
4130
4131        languages.insert(
4132            "protobuf".to_string(),
4133            LanguageConfig {
4134                extensions: vec!["proto".to_string()],
4135                filenames: vec![],
4136                grammar: "Protocol Buffers".to_string(),
4137                comment_prefix: Some("//".to_string()),
4138                auto_indent: true,
4139                auto_close: None,
4140                auto_surround: None,
4141                highlighter: HighlighterPreference::Auto,
4142                textmate_grammar: None,
4143                show_whitespace_tabs: true,
4144                line_wrap: None,
4145                wrap_column: None,
4146                page_view: None,
4147                page_width: None,
4148                use_tabs: None,
4149                tab_size: None,
4150                formatter: None,
4151                format_on_save: false,
4152                on_save: vec![],
4153                word_characters: None,
4154            },
4155        );
4156
4157        languages.insert(
4158            "cmake".to_string(),
4159            LanguageConfig {
4160                extensions: vec!["cmake".to_string()],
4161                filenames: vec!["CMakeLists.txt".to_string()],
4162                grammar: "CMake".to_string(),
4163                comment_prefix: Some("#".to_string()),
4164                auto_indent: true,
4165                auto_close: None,
4166                auto_surround: None,
4167                highlighter: HighlighterPreference::Auto,
4168                textmate_grammar: None,
4169                show_whitespace_tabs: true,
4170                line_wrap: None,
4171                wrap_column: None,
4172                page_view: None,
4173                page_width: None,
4174                use_tabs: None,
4175                tab_size: None,
4176                formatter: None,
4177                format_on_save: false,
4178                on_save: vec![],
4179                word_characters: None,
4180            },
4181        );
4182
4183        languages.insert(
4184            "terraform".to_string(),
4185            LanguageConfig {
4186                extensions: vec!["tf".to_string(), "tfvars".to_string(), "hcl".to_string()],
4187                filenames: vec![],
4188                grammar: "HCL".to_string(),
4189                comment_prefix: Some("#".to_string()),
4190                auto_indent: true,
4191                auto_close: None,
4192                auto_surround: None,
4193                highlighter: HighlighterPreference::Auto,
4194                textmate_grammar: None,
4195                show_whitespace_tabs: true,
4196                line_wrap: None,
4197                wrap_column: None,
4198                page_view: None,
4199                page_width: None,
4200                use_tabs: None,
4201                tab_size: None,
4202                formatter: None,
4203                format_on_save: false,
4204                on_save: vec![],
4205                word_characters: None,
4206            },
4207        );
4208
4209        languages.insert(
4210            "vue".to_string(),
4211            LanguageConfig {
4212                extensions: vec!["vue".to_string()],
4213                filenames: vec![],
4214                grammar: "Vue".to_string(),
4215                comment_prefix: None,
4216                auto_indent: true,
4217                auto_close: None,
4218                auto_surround: None,
4219                highlighter: HighlighterPreference::Auto,
4220                textmate_grammar: None,
4221                show_whitespace_tabs: true,
4222                line_wrap: None,
4223                wrap_column: None,
4224                page_view: None,
4225                page_width: None,
4226                use_tabs: None,
4227                tab_size: None,
4228                formatter: None,
4229                format_on_save: false,
4230                on_save: vec![],
4231                word_characters: None,
4232            },
4233        );
4234
4235        languages.insert(
4236            "svelte".to_string(),
4237            LanguageConfig {
4238                extensions: vec!["svelte".to_string()],
4239                filenames: vec![],
4240                grammar: "Svelte".to_string(),
4241                comment_prefix: None,
4242                auto_indent: true,
4243                auto_close: None,
4244                auto_surround: None,
4245                highlighter: HighlighterPreference::Auto,
4246                textmate_grammar: None,
4247                show_whitespace_tabs: true,
4248                line_wrap: None,
4249                wrap_column: None,
4250                page_view: None,
4251                page_width: None,
4252                use_tabs: None,
4253                tab_size: None,
4254                formatter: None,
4255                format_on_save: false,
4256                on_save: vec![],
4257                word_characters: None,
4258            },
4259        );
4260
4261        languages.insert(
4262            "astro".to_string(),
4263            LanguageConfig {
4264                extensions: vec!["astro".to_string()],
4265                filenames: vec![],
4266                grammar: "Astro".to_string(),
4267                comment_prefix: None,
4268                auto_indent: true,
4269                auto_close: None,
4270                auto_surround: None,
4271                highlighter: HighlighterPreference::Auto,
4272                textmate_grammar: None,
4273                show_whitespace_tabs: true,
4274                line_wrap: None,
4275                wrap_column: None,
4276                page_view: None,
4277                page_width: None,
4278                use_tabs: None,
4279                tab_size: None,
4280                formatter: None,
4281                format_on_save: false,
4282                on_save: vec![],
4283                word_characters: None,
4284            },
4285        );
4286
4287        // --- Languages for embedded grammars (syntax highlighting only) ---
4288
4289        languages.insert(
4290            "scss".to_string(),
4291            LanguageConfig {
4292                extensions: vec!["scss".to_string()],
4293                filenames: vec![],
4294                grammar: "SCSS".to_string(),
4295                comment_prefix: Some("//".to_string()),
4296                auto_indent: true,
4297                auto_close: None,
4298                auto_surround: None,
4299                highlighter: HighlighterPreference::Auto,
4300                textmate_grammar: None,
4301                show_whitespace_tabs: true,
4302                line_wrap: None,
4303                wrap_column: None,
4304                page_view: None,
4305                page_width: None,
4306                use_tabs: None,
4307                tab_size: None,
4308                formatter: None,
4309                format_on_save: false,
4310                on_save: vec![],
4311                word_characters: None,
4312            },
4313        );
4314
4315        languages.insert(
4316            "less".to_string(),
4317            LanguageConfig {
4318                extensions: vec!["less".to_string()],
4319                filenames: vec![],
4320                grammar: "LESS".to_string(),
4321                comment_prefix: Some("//".to_string()),
4322                auto_indent: true,
4323                auto_close: None,
4324                auto_surround: None,
4325                highlighter: HighlighterPreference::Auto,
4326                textmate_grammar: None,
4327                show_whitespace_tabs: true,
4328                line_wrap: None,
4329                wrap_column: None,
4330                page_view: None,
4331                page_width: None,
4332                use_tabs: None,
4333                tab_size: None,
4334                formatter: None,
4335                format_on_save: false,
4336                on_save: vec![],
4337                word_characters: None,
4338            },
4339        );
4340
4341        languages.insert(
4342            "powershell".to_string(),
4343            LanguageConfig {
4344                extensions: vec!["ps1".to_string(), "psm1".to_string(), "psd1".to_string()],
4345                filenames: vec![],
4346                grammar: "PowerShell".to_string(),
4347                comment_prefix: Some("#".to_string()),
4348                auto_indent: true,
4349                auto_close: None,
4350                auto_surround: None,
4351                highlighter: HighlighterPreference::Auto,
4352                textmate_grammar: None,
4353                show_whitespace_tabs: true,
4354                line_wrap: None,
4355                wrap_column: None,
4356                page_view: None,
4357                page_width: None,
4358                use_tabs: None,
4359                tab_size: None,
4360                formatter: None,
4361                format_on_save: false,
4362                on_save: vec![],
4363                word_characters: None,
4364            },
4365        );
4366
4367        languages.insert(
4368            "kdl".to_string(),
4369            LanguageConfig {
4370                extensions: vec!["kdl".to_string()],
4371                filenames: vec![],
4372                grammar: "KDL".to_string(),
4373                comment_prefix: Some("//".to_string()),
4374                auto_indent: true,
4375                auto_close: None,
4376                auto_surround: None,
4377                highlighter: HighlighterPreference::Auto,
4378                textmate_grammar: None,
4379                show_whitespace_tabs: true,
4380                line_wrap: None,
4381                wrap_column: None,
4382                page_view: None,
4383                page_width: None,
4384                use_tabs: None,
4385                tab_size: None,
4386                formatter: None,
4387                format_on_save: false,
4388                on_save: vec![],
4389                word_characters: None,
4390            },
4391        );
4392
4393        languages.insert(
4394            "starlark".to_string(),
4395            LanguageConfig {
4396                extensions: vec!["bzl".to_string(), "star".to_string()],
4397                filenames: vec!["BUILD".to_string(), "WORKSPACE".to_string()],
4398                grammar: "Starlark".to_string(),
4399                comment_prefix: Some("#".to_string()),
4400                auto_indent: true,
4401                auto_close: None,
4402                auto_surround: None,
4403                highlighter: HighlighterPreference::Auto,
4404                textmate_grammar: None,
4405                show_whitespace_tabs: true,
4406                line_wrap: None,
4407                wrap_column: None,
4408                page_view: None,
4409                page_width: None,
4410                use_tabs: None,
4411                tab_size: None,
4412                formatter: None,
4413                format_on_save: false,
4414                on_save: vec![],
4415                word_characters: None,
4416            },
4417        );
4418
4419        languages.insert(
4420            "justfile".to_string(),
4421            LanguageConfig {
4422                extensions: vec![],
4423                filenames: vec![
4424                    "justfile".to_string(),
4425                    "Justfile".to_string(),
4426                    ".justfile".to_string(),
4427                ],
4428                grammar: "Justfile".to_string(),
4429                comment_prefix: Some("#".to_string()),
4430                auto_indent: true,
4431                auto_close: None,
4432                auto_surround: None,
4433                highlighter: HighlighterPreference::Auto,
4434                textmate_grammar: None,
4435                show_whitespace_tabs: true,
4436                line_wrap: None,
4437                wrap_column: None,
4438                page_view: None,
4439                page_width: None,
4440                use_tabs: Some(true),
4441                tab_size: None,
4442                formatter: None,
4443                format_on_save: false,
4444                on_save: vec![],
4445                word_characters: None,
4446            },
4447        );
4448
4449        languages.insert(
4450            "earthfile".to_string(),
4451            LanguageConfig {
4452                extensions: vec!["earth".to_string()],
4453                filenames: vec!["Earthfile".to_string()],
4454                grammar: "Earthfile".to_string(),
4455                comment_prefix: Some("#".to_string()),
4456                auto_indent: true,
4457                auto_close: None,
4458                auto_surround: None,
4459                highlighter: HighlighterPreference::Auto,
4460                textmate_grammar: None,
4461                show_whitespace_tabs: true,
4462                line_wrap: None,
4463                wrap_column: None,
4464                page_view: None,
4465                page_width: None,
4466                use_tabs: None,
4467                tab_size: None,
4468                formatter: None,
4469                format_on_save: false,
4470                on_save: vec![],
4471                word_characters: None,
4472            },
4473        );
4474
4475        languages.insert(
4476            "gomod".to_string(),
4477            LanguageConfig {
4478                extensions: vec![],
4479                filenames: vec!["go.mod".to_string(), "go.sum".to_string()],
4480                grammar: "Go Module".to_string(),
4481                comment_prefix: Some("//".to_string()),
4482                auto_indent: true,
4483                auto_close: None,
4484                auto_surround: None,
4485                highlighter: HighlighterPreference::Auto,
4486                textmate_grammar: None,
4487                show_whitespace_tabs: true,
4488                line_wrap: None,
4489                wrap_column: None,
4490                page_view: None,
4491                page_width: None,
4492                use_tabs: Some(true),
4493                tab_size: None,
4494                formatter: None,
4495                format_on_save: false,
4496                on_save: vec![],
4497                word_characters: None,
4498            },
4499        );
4500
4501        languages.insert(
4502            "vlang".to_string(),
4503            LanguageConfig {
4504                extensions: vec!["v".to_string(), "vv".to_string()],
4505                filenames: vec![],
4506                grammar: "V".to_string(),
4507                comment_prefix: Some("//".to_string()),
4508                auto_indent: true,
4509                auto_close: None,
4510                auto_surround: None,
4511                highlighter: HighlighterPreference::Auto,
4512                textmate_grammar: None,
4513                show_whitespace_tabs: true,
4514                line_wrap: None,
4515                wrap_column: None,
4516                page_view: None,
4517                page_width: None,
4518                use_tabs: None,
4519                tab_size: None,
4520                formatter: None,
4521                format_on_save: false,
4522                on_save: vec![],
4523                word_characters: None,
4524            },
4525        );
4526
4527        languages.insert(
4528            "ini".to_string(),
4529            LanguageConfig {
4530                extensions: vec!["ini".to_string(), "cfg".to_string()],
4531                filenames: vec![],
4532                grammar: "INI".to_string(),
4533                comment_prefix: Some(";".to_string()),
4534                auto_indent: false,
4535                auto_close: None,
4536                auto_surround: None,
4537                highlighter: HighlighterPreference::Auto,
4538                textmate_grammar: None,
4539                show_whitespace_tabs: true,
4540                line_wrap: None,
4541                wrap_column: None,
4542                page_view: None,
4543                page_width: None,
4544                use_tabs: None,
4545                tab_size: None,
4546                formatter: None,
4547                format_on_save: false,
4548                on_save: vec![],
4549                word_characters: None,
4550            },
4551        );
4552
4553        languages.insert(
4554            "hyprlang".to_string(),
4555            LanguageConfig {
4556                extensions: vec!["hl".to_string()],
4557                filenames: vec!["hyprland.conf".to_string()],
4558                grammar: "Hyprlang".to_string(),
4559                comment_prefix: Some("#".to_string()),
4560                auto_indent: true,
4561                auto_close: None,
4562                auto_surround: None,
4563                highlighter: HighlighterPreference::Auto,
4564                textmate_grammar: None,
4565                show_whitespace_tabs: true,
4566                line_wrap: None,
4567                wrap_column: None,
4568                page_view: None,
4569                page_width: None,
4570                use_tabs: None,
4571                tab_size: None,
4572                formatter: None,
4573                format_on_save: false,
4574                on_save: vec![],
4575                word_characters: None,
4576            },
4577        );
4578
4579        languages
4580    }
4581
4582    /// Create default LSP configurations
4583    #[cfg(feature = "runtime")]
4584    fn default_lsp_config() -> HashMap<String, LspLanguageConfig> {
4585        let mut lsp = HashMap::new();
4586
4587        // rust-analyzer (installed via rustup or package manager)
4588        // Enable logging to help debug LSP issues (stored in XDG state directory)
4589        let ra_log_path = crate::services::log_dirs::lsp_log_path("rust-analyzer")
4590            .to_string_lossy()
4591            .to_string();
4592
4593        Self::populate_lsp_config(&mut lsp, ra_log_path);
4594        lsp
4595    }
4596
4597    /// Create empty LSP configurations for WASM builds
4598    #[cfg(not(feature = "runtime"))]
4599    fn default_lsp_config() -> HashMap<String, LspLanguageConfig> {
4600        // LSP is not available in WASM builds
4601        HashMap::new()
4602    }
4603
4604    #[cfg(feature = "runtime")]
4605    fn populate_lsp_config(lsp: &mut HashMap<String, LspLanguageConfig>, ra_log_path: String) {
4606        // rust-analyzer: full mode by default (no init param restrictions, no process limits).
4607        // Users can switch to reduced-memory mode via the "Rust LSP: Reduced Memory Mode"
4608        // command palette command (provided by the rust-lsp plugin).
4609        lsp.insert(
4610            "rust".to_string(),
4611            LspLanguageConfig::Multi(vec![LspServerConfig {
4612                command: "rust-analyzer".to_string(),
4613                args: vec!["--log-file".to_string(), ra_log_path],
4614                enabled: true,
4615                auto_start: false,
4616                process_limits: ProcessLimits::unlimited(),
4617                initialization_options: None,
4618                env: Default::default(),
4619                language_id_overrides: Default::default(),
4620                name: None,
4621                only_features: None,
4622                except_features: None,
4623                root_markers: vec![
4624                    "Cargo.toml".to_string(),
4625                    "rust-project.json".to_string(),
4626                    ".git".to_string(),
4627                ],
4628            }]),
4629        );
4630
4631        // pylsp (installed via pip)
4632        lsp.insert(
4633            "python".to_string(),
4634            LspLanguageConfig::Multi(vec![LspServerConfig {
4635                command: "pylsp".to_string(),
4636                args: vec![],
4637                enabled: true,
4638                auto_start: false,
4639                process_limits: ProcessLimits::default(),
4640                initialization_options: None,
4641                env: Default::default(),
4642                language_id_overrides: Default::default(),
4643                name: None,
4644                only_features: None,
4645                except_features: None,
4646                root_markers: vec![
4647                    "pyproject.toml".to_string(),
4648                    "setup.py".to_string(),
4649                    "setup.cfg".to_string(),
4650                    "pyrightconfig.json".to_string(),
4651                    ".git".to_string(),
4652                ],
4653            }]),
4654        );
4655
4656        // typescript-language-server (installed via npm)
4657        // Alternative: use "deno lsp" with initialization_options: {"enable": true}
4658        lsp.insert(
4659            "javascript".to_string(),
4660            LspLanguageConfig::Multi(vec![LspServerConfig {
4661                command: "typescript-language-server".to_string(),
4662                args: vec!["--stdio".to_string()],
4663                enabled: true,
4664                auto_start: false,
4665                process_limits: ProcessLimits::default(),
4666                initialization_options: None,
4667                env: Default::default(),
4668                language_id_overrides: HashMap::from([(
4669                    "jsx".to_string(),
4670                    "javascriptreact".to_string(),
4671                )]),
4672                name: None,
4673                only_features: None,
4674                except_features: None,
4675                root_markers: vec![
4676                    "tsconfig.json".to_string(),
4677                    "jsconfig.json".to_string(),
4678                    "package.json".to_string(),
4679                    ".git".to_string(),
4680                ],
4681            }]),
4682        );
4683        lsp.insert(
4684            "typescript".to_string(),
4685            LspLanguageConfig::Multi(vec![LspServerConfig {
4686                command: "typescript-language-server".to_string(),
4687                args: vec!["--stdio".to_string()],
4688                enabled: true,
4689                auto_start: false,
4690                process_limits: ProcessLimits::default(),
4691                initialization_options: None,
4692                env: Default::default(),
4693                language_id_overrides: HashMap::from([(
4694                    "tsx".to_string(),
4695                    "typescriptreact".to_string(),
4696                )]),
4697                name: None,
4698                only_features: None,
4699                except_features: None,
4700                root_markers: vec![
4701                    "tsconfig.json".to_string(),
4702                    "jsconfig.json".to_string(),
4703                    "package.json".to_string(),
4704                    ".git".to_string(),
4705                ],
4706            }]),
4707        );
4708
4709        // vscode-html-language-server (installed via npm install -g vscode-langservers-extracted)
4710        lsp.insert(
4711            "html".to_string(),
4712            LspLanguageConfig::Multi(vec![LspServerConfig {
4713                command: "vscode-html-language-server".to_string(),
4714                args: vec!["--stdio".to_string()],
4715                enabled: true,
4716                auto_start: false,
4717                process_limits: ProcessLimits::default(),
4718                initialization_options: None,
4719                env: Default::default(),
4720                language_id_overrides: Default::default(),
4721                name: None,
4722                only_features: None,
4723                except_features: None,
4724                root_markers: Default::default(),
4725            }]),
4726        );
4727
4728        // vscode-css-language-server (installed via npm install -g vscode-langservers-extracted)
4729        lsp.insert(
4730            "css".to_string(),
4731            LspLanguageConfig::Multi(vec![LspServerConfig {
4732                command: "vscode-css-language-server".to_string(),
4733                args: vec!["--stdio".to_string()],
4734                enabled: true,
4735                auto_start: false,
4736                process_limits: ProcessLimits::default(),
4737                initialization_options: None,
4738                env: Default::default(),
4739                language_id_overrides: Default::default(),
4740                name: None,
4741                only_features: None,
4742                except_features: None,
4743                root_markers: Default::default(),
4744            }]),
4745        );
4746
4747        // clangd (installed via package manager)
4748        lsp.insert(
4749            "c".to_string(),
4750            LspLanguageConfig::Multi(vec![LspServerConfig {
4751                command: "clangd".to_string(),
4752                args: vec![],
4753                enabled: true,
4754                auto_start: false,
4755                process_limits: ProcessLimits::default(),
4756                initialization_options: None,
4757                env: Default::default(),
4758                language_id_overrides: Default::default(),
4759                name: None,
4760                only_features: None,
4761                except_features: None,
4762                root_markers: vec![
4763                    "compile_commands.json".to_string(),
4764                    "CMakeLists.txt".to_string(),
4765                    "Makefile".to_string(),
4766                    ".git".to_string(),
4767                ],
4768            }]),
4769        );
4770        lsp.insert(
4771            "cpp".to_string(),
4772            LspLanguageConfig::Multi(vec![LspServerConfig {
4773                command: "clangd".to_string(),
4774                args: vec![],
4775                enabled: true,
4776                auto_start: false,
4777                process_limits: ProcessLimits::default(),
4778                initialization_options: None,
4779                env: Default::default(),
4780                language_id_overrides: Default::default(),
4781                name: None,
4782                only_features: None,
4783                except_features: None,
4784                root_markers: vec![
4785                    "compile_commands.json".to_string(),
4786                    "CMakeLists.txt".to_string(),
4787                    "Makefile".to_string(),
4788                    ".git".to_string(),
4789                ],
4790            }]),
4791        );
4792
4793        // gopls (installed via go install)
4794        lsp.insert(
4795            "go".to_string(),
4796            LspLanguageConfig::Multi(vec![LspServerConfig {
4797                command: "gopls".to_string(),
4798                args: vec![],
4799                enabled: true,
4800                auto_start: false,
4801                process_limits: ProcessLimits::default(),
4802                initialization_options: None,
4803                env: Default::default(),
4804                language_id_overrides: Default::default(),
4805                name: None,
4806                only_features: None,
4807                except_features: None,
4808                root_markers: vec![
4809                    "go.mod".to_string(),
4810                    "go.work".to_string(),
4811                    ".git".to_string(),
4812                ],
4813            }]),
4814        );
4815
4816        // vscode-json-language-server (installed via npm install -g vscode-langservers-extracted)
4817        lsp.insert(
4818            "json".to_string(),
4819            LspLanguageConfig::Multi(vec![LspServerConfig {
4820                command: "vscode-json-language-server".to_string(),
4821                args: vec!["--stdio".to_string()],
4822                enabled: true,
4823                auto_start: false,
4824                process_limits: ProcessLimits::default(),
4825                initialization_options: None,
4826                env: Default::default(),
4827                language_id_overrides: Default::default(),
4828                name: None,
4829                only_features: None,
4830                except_features: None,
4831                root_markers: Default::default(),
4832            }]),
4833        );
4834
4835        // csharp-language-server (installed via dotnet tool install -g csharp-ls)
4836        lsp.insert(
4837            "csharp".to_string(),
4838            LspLanguageConfig::Multi(vec![LspServerConfig {
4839                command: "csharp-ls".to_string(),
4840                args: vec![],
4841                enabled: true,
4842                auto_start: false,
4843                process_limits: ProcessLimits::default(),
4844                initialization_options: None,
4845                env: Default::default(),
4846                language_id_overrides: Default::default(),
4847                name: None,
4848                only_features: None,
4849                except_features: None,
4850                root_markers: vec![
4851                    "*.csproj".to_string(),
4852                    "*.sln".to_string(),
4853                    ".git".to_string(),
4854                ],
4855            }]),
4856        );
4857
4858        // ols - Odin Language Server (https://github.com/DanielGavin/ols)
4859        // Build from source: cd ols && ./build.sh (Linux/macOS) or ./build.bat (Windows)
4860        lsp.insert(
4861            "odin".to_string(),
4862            LspLanguageConfig::Multi(vec![LspServerConfig {
4863                command: "ols".to_string(),
4864                args: vec![],
4865                enabled: true,
4866                auto_start: false,
4867                process_limits: ProcessLimits::default(),
4868                initialization_options: None,
4869                env: Default::default(),
4870                language_id_overrides: Default::default(),
4871                name: None,
4872                only_features: None,
4873                except_features: None,
4874                root_markers: Default::default(),
4875            }]),
4876        );
4877
4878        // zls - Zig Language Server (https://github.com/zigtools/zls)
4879        // Install via package manager or download from releases
4880        lsp.insert(
4881            "zig".to_string(),
4882            LspLanguageConfig::Multi(vec![LspServerConfig {
4883                command: "zls".to_string(),
4884                args: vec![],
4885                enabled: true,
4886                auto_start: false,
4887                process_limits: ProcessLimits::default(),
4888                initialization_options: None,
4889                env: Default::default(),
4890                language_id_overrides: Default::default(),
4891                name: None,
4892                only_features: None,
4893                except_features: None,
4894                root_markers: Default::default(),
4895            }]),
4896        );
4897
4898        // jdtls - Eclipse JDT Language Server for Java
4899        // Install via package manager or download from Eclipse
4900        lsp.insert(
4901            "java".to_string(),
4902            LspLanguageConfig::Multi(vec![LspServerConfig {
4903                command: "jdtls".to_string(),
4904                args: vec![],
4905                enabled: true,
4906                auto_start: false,
4907                process_limits: ProcessLimits::default(),
4908                initialization_options: None,
4909                env: Default::default(),
4910                language_id_overrides: Default::default(),
4911                name: None,
4912                only_features: None,
4913                except_features: None,
4914                root_markers: vec![
4915                    "pom.xml".to_string(),
4916                    "build.gradle".to_string(),
4917                    "build.gradle.kts".to_string(),
4918                    ".git".to_string(),
4919                ],
4920            }]),
4921        );
4922
4923        // texlab - LaTeX Language Server (https://github.com/latex-lsp/texlab)
4924        // Install via cargo install texlab or package manager
4925        lsp.insert(
4926            "latex".to_string(),
4927            LspLanguageConfig::Multi(vec![LspServerConfig {
4928                command: "texlab".to_string(),
4929                args: vec![],
4930                enabled: true,
4931                auto_start: false,
4932                process_limits: ProcessLimits::default(),
4933                initialization_options: None,
4934                env: Default::default(),
4935                language_id_overrides: Default::default(),
4936                name: None,
4937                only_features: None,
4938                except_features: None,
4939                root_markers: Default::default(),
4940            }]),
4941        );
4942
4943        // marksman - Markdown Language Server (https://github.com/artempyanykh/marksman)
4944        // Install via package manager or download from releases
4945        lsp.insert(
4946            "markdown".to_string(),
4947            LspLanguageConfig::Multi(vec![LspServerConfig {
4948                command: "marksman".to_string(),
4949                args: vec!["server".to_string()],
4950                enabled: true,
4951                auto_start: false,
4952                process_limits: ProcessLimits::default(),
4953                initialization_options: None,
4954                env: Default::default(),
4955                language_id_overrides: Default::default(),
4956                name: None,
4957                only_features: None,
4958                except_features: None,
4959                root_markers: Default::default(),
4960            }]),
4961        );
4962
4963        // templ - Templ Language Server (https://templ.guide)
4964        // Install via go install github.com/a-h/templ/cmd/templ@latest
4965        lsp.insert(
4966            "templ".to_string(),
4967            LspLanguageConfig::Multi(vec![LspServerConfig {
4968                command: "templ".to_string(),
4969                args: vec!["lsp".to_string()],
4970                enabled: true,
4971                auto_start: false,
4972                process_limits: ProcessLimits::default(),
4973                initialization_options: None,
4974                env: Default::default(),
4975                language_id_overrides: Default::default(),
4976                name: None,
4977                only_features: None,
4978                except_features: None,
4979                root_markers: Default::default(),
4980            }]),
4981        );
4982
4983        // tinymist - Typst Language Server (https://github.com/Myriad-Dreamin/tinymist)
4984        // Install via cargo install tinymist or download from releases
4985        lsp.insert(
4986            "typst".to_string(),
4987            LspLanguageConfig::Multi(vec![LspServerConfig {
4988                command: "tinymist".to_string(),
4989                args: vec![],
4990                enabled: true,
4991                auto_start: false,
4992                process_limits: ProcessLimits::default(),
4993                initialization_options: None,
4994                env: Default::default(),
4995                language_id_overrides: Default::default(),
4996                name: None,
4997                only_features: None,
4998                except_features: None,
4999                root_markers: Default::default(),
5000            }]),
5001        );
5002
5003        // bash-language-server (installed via npm install -g bash-language-server)
5004        lsp.insert(
5005            "bash".to_string(),
5006            LspLanguageConfig::Multi(vec![LspServerConfig {
5007                command: "bash-language-server".to_string(),
5008                args: vec!["start".to_string()],
5009                enabled: true,
5010                auto_start: false,
5011                process_limits: ProcessLimits::default(),
5012                initialization_options: None,
5013                env: Default::default(),
5014                language_id_overrides: Default::default(),
5015                name: None,
5016                only_features: None,
5017                except_features: None,
5018                root_markers: Default::default(),
5019            }]),
5020        );
5021
5022        // lua-language-server (https://github.com/LuaLS/lua-language-server)
5023        // Install via package manager or download from releases
5024        lsp.insert(
5025            "lua".to_string(),
5026            LspLanguageConfig::Multi(vec![LspServerConfig {
5027                command: "lua-language-server".to_string(),
5028                args: vec![],
5029                enabled: true,
5030                auto_start: false,
5031                process_limits: ProcessLimits::default(),
5032                initialization_options: None,
5033                env: Default::default(),
5034                language_id_overrides: Default::default(),
5035                name: None,
5036                only_features: None,
5037                except_features: None,
5038                root_markers: vec![
5039                    ".luarc.json".to_string(),
5040                    ".luarc.jsonc".to_string(),
5041                    ".luacheckrc".to_string(),
5042                    ".stylua.toml".to_string(),
5043                    ".git".to_string(),
5044                ],
5045            }]),
5046        );
5047
5048        // solargraph - Ruby Language Server (installed via gem install solargraph)
5049        lsp.insert(
5050            "ruby".to_string(),
5051            LspLanguageConfig::Multi(vec![LspServerConfig {
5052                command: "solargraph".to_string(),
5053                args: vec!["stdio".to_string()],
5054                enabled: true,
5055                auto_start: false,
5056                process_limits: ProcessLimits::default(),
5057                initialization_options: None,
5058                env: Default::default(),
5059                language_id_overrides: Default::default(),
5060                name: None,
5061                only_features: None,
5062                except_features: None,
5063                root_markers: vec![
5064                    "Gemfile".to_string(),
5065                    ".ruby-version".to_string(),
5066                    ".git".to_string(),
5067                ],
5068            }]),
5069        );
5070
5071        // phpactor - PHP Language Server (https://phpactor.readthedocs.io)
5072        // Install via composer global require phpactor/phpactor
5073        lsp.insert(
5074            "php".to_string(),
5075            LspLanguageConfig::Multi(vec![LspServerConfig {
5076                command: "phpactor".to_string(),
5077                args: vec!["language-server".to_string()],
5078                enabled: true,
5079                auto_start: false,
5080                process_limits: ProcessLimits::default(),
5081                initialization_options: None,
5082                env: Default::default(),
5083                language_id_overrides: Default::default(),
5084                name: None,
5085                only_features: None,
5086                except_features: None,
5087                root_markers: vec!["composer.json".to_string(), ".git".to_string()],
5088            }]),
5089        );
5090
5091        // yaml-language-server (installed via npm install -g yaml-language-server)
5092        lsp.insert(
5093            "yaml".to_string(),
5094            LspLanguageConfig::Multi(vec![LspServerConfig {
5095                command: "yaml-language-server".to_string(),
5096                args: vec!["--stdio".to_string()],
5097                enabled: true,
5098                auto_start: false,
5099                process_limits: ProcessLimits::default(),
5100                initialization_options: None,
5101                env: Default::default(),
5102                language_id_overrides: Default::default(),
5103                name: None,
5104                only_features: None,
5105                except_features: None,
5106                root_markers: Default::default(),
5107            }]),
5108        );
5109
5110        // taplo - TOML Language Server (https://taplo.tamasfe.dev)
5111        // Install via cargo install taplo-cli or npm install -g @taplo/cli
5112        lsp.insert(
5113            "toml".to_string(),
5114            LspLanguageConfig::Multi(vec![LspServerConfig {
5115                command: "taplo".to_string(),
5116                args: vec!["lsp".to_string(), "stdio".to_string()],
5117                enabled: true,
5118                auto_start: false,
5119                process_limits: ProcessLimits::default(),
5120                initialization_options: None,
5121                env: Default::default(),
5122                language_id_overrides: Default::default(),
5123                name: None,
5124                only_features: None,
5125                except_features: None,
5126                root_markers: Default::default(),
5127            }]),
5128        );
5129
5130        // dart - Dart Language Server (#1252)
5131        // Included with the Dart SDK
5132        lsp.insert(
5133            "dart".to_string(),
5134            LspLanguageConfig::Multi(vec![LspServerConfig {
5135                command: "dart".to_string(),
5136                args: vec!["language-server".to_string(), "--protocol=lsp".to_string()],
5137                enabled: true,
5138                auto_start: false,
5139                process_limits: ProcessLimits::default(),
5140                initialization_options: None,
5141                env: Default::default(),
5142                language_id_overrides: Default::default(),
5143                name: None,
5144                only_features: None,
5145                except_features: None,
5146                root_markers: vec!["pubspec.yaml".to_string(), ".git".to_string()],
5147            }]),
5148        );
5149
5150        // nu - Nushell Language Server (#1031)
5151        // Built into the Nushell binary
5152        lsp.insert(
5153            "nushell".to_string(),
5154            LspLanguageConfig::Multi(vec![LspServerConfig {
5155                command: "nu".to_string(),
5156                args: vec!["--lsp".to_string()],
5157                enabled: true,
5158                auto_start: false,
5159                process_limits: ProcessLimits::default(),
5160                initialization_options: None,
5161                env: Default::default(),
5162                language_id_overrides: Default::default(),
5163                name: None,
5164                only_features: None,
5165                except_features: None,
5166                root_markers: Default::default(),
5167            }]),
5168        );
5169
5170        // solc - Solidity Language Server (#857)
5171        // Install via npm install -g @nomicfoundation/solidity-language-server
5172        lsp.insert(
5173            "solidity".to_string(),
5174            LspLanguageConfig::Multi(vec![LspServerConfig {
5175                command: "nomicfoundation-solidity-language-server".to_string(),
5176                args: vec!["--stdio".to_string()],
5177                enabled: true,
5178                auto_start: false,
5179                process_limits: ProcessLimits::default(),
5180                initialization_options: None,
5181                env: Default::default(),
5182                language_id_overrides: Default::default(),
5183                name: None,
5184                only_features: None,
5185                except_features: None,
5186                root_markers: Default::default(),
5187            }]),
5188        );
5189
5190        // --- DevOps / infrastructure LSP servers ---
5191
5192        // terraform-ls - Terraform Language Server (https://github.com/hashicorp/terraform-ls)
5193        // Install via package manager or download from releases
5194        lsp.insert(
5195            "terraform".to_string(),
5196            LspLanguageConfig::Multi(vec![LspServerConfig {
5197                command: "terraform-ls".to_string(),
5198                args: vec!["serve".to_string()],
5199                enabled: true,
5200                auto_start: false,
5201                process_limits: ProcessLimits::default(),
5202                initialization_options: None,
5203                env: Default::default(),
5204                language_id_overrides: Default::default(),
5205                name: None,
5206                only_features: None,
5207                except_features: None,
5208                root_markers: vec![
5209                    "*.tf".to_string(),
5210                    ".terraform".to_string(),
5211                    ".git".to_string(),
5212                ],
5213            }]),
5214        );
5215
5216        // cmake-language-server (https://github.com/regen100/cmake-language-server)
5217        // Install via pip: pip install cmake-language-server
5218        lsp.insert(
5219            "cmake".to_string(),
5220            LspLanguageConfig::Multi(vec![LspServerConfig {
5221                command: "cmake-language-server".to_string(),
5222                args: vec![],
5223                enabled: true,
5224                auto_start: false,
5225                process_limits: ProcessLimits::default(),
5226                initialization_options: None,
5227                env: Default::default(),
5228                language_id_overrides: Default::default(),
5229                name: None,
5230                only_features: None,
5231                except_features: None,
5232                root_markers: vec!["CMakeLists.txt".to_string(), ".git".to_string()],
5233            }]),
5234        );
5235
5236        // buf - Protobuf Language Server (https://buf.build)
5237        // Install via package manager or curl
5238        lsp.insert(
5239            "protobuf".to_string(),
5240            LspLanguageConfig::Multi(vec![LspServerConfig {
5241                command: "buf".to_string(),
5242                args: vec!["beta".to_string(), "lsp".to_string()],
5243                enabled: true,
5244                auto_start: false,
5245                process_limits: ProcessLimits::default(),
5246                initialization_options: None,
5247                env: Default::default(),
5248                language_id_overrides: Default::default(),
5249                name: None,
5250                only_features: None,
5251                except_features: None,
5252                root_markers: Default::default(),
5253            }]),
5254        );
5255
5256        // graphql-lsp (https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service-cli)
5257        // Install via npm: npm install -g graphql-language-service-cli
5258        lsp.insert(
5259            "graphql".to_string(),
5260            LspLanguageConfig::Multi(vec![LspServerConfig {
5261                command: "graphql-lsp".to_string(),
5262                args: vec!["server".to_string(), "-m".to_string(), "stream".to_string()],
5263                enabled: true,
5264                auto_start: false,
5265                process_limits: ProcessLimits::default(),
5266                initialization_options: None,
5267                env: Default::default(),
5268                language_id_overrides: Default::default(),
5269                name: None,
5270                only_features: None,
5271                except_features: None,
5272                root_markers: Default::default(),
5273            }]),
5274        );
5275
5276        // sqls - SQL Language Server (https://github.com/sqls-server/sqls)
5277        // Install via go: go install github.com/sqls-server/sqls@latest
5278        lsp.insert(
5279            "sql".to_string(),
5280            LspLanguageConfig::Multi(vec![LspServerConfig {
5281                command: "sqls".to_string(),
5282                args: vec![],
5283                enabled: true,
5284                auto_start: false,
5285                process_limits: ProcessLimits::default(),
5286                initialization_options: None,
5287                env: Default::default(),
5288                language_id_overrides: Default::default(),
5289                name: None,
5290                only_features: None,
5291                except_features: None,
5292                root_markers: Default::default(),
5293            }]),
5294        );
5295
5296        // --- Web framework LSP servers ---
5297
5298        // vue-language-server (installed via npm install -g @vue/language-server)
5299        lsp.insert(
5300            "vue".to_string(),
5301            LspLanguageConfig::Multi(vec![LspServerConfig {
5302                command: "vue-language-server".to_string(),
5303                args: vec!["--stdio".to_string()],
5304                enabled: true,
5305                auto_start: false,
5306                process_limits: ProcessLimits::default(),
5307                initialization_options: None,
5308                env: Default::default(),
5309                language_id_overrides: Default::default(),
5310                name: None,
5311                only_features: None,
5312                except_features: None,
5313                root_markers: Default::default(),
5314            }]),
5315        );
5316
5317        // svelte-language-server (installed via npm install -g svelte-language-server)
5318        lsp.insert(
5319            "svelte".to_string(),
5320            LspLanguageConfig::Multi(vec![LspServerConfig {
5321                command: "svelteserver".to_string(),
5322                args: vec!["--stdio".to_string()],
5323                enabled: true,
5324                auto_start: false,
5325                process_limits: ProcessLimits::default(),
5326                initialization_options: None,
5327                env: Default::default(),
5328                language_id_overrides: Default::default(),
5329                name: None,
5330                only_features: None,
5331                except_features: None,
5332                root_markers: Default::default(),
5333            }]),
5334        );
5335
5336        // astro-ls - Astro Language Server (installed via npm install -g @astrojs/language-server)
5337        lsp.insert(
5338            "astro".to_string(),
5339            LspLanguageConfig::Multi(vec![LspServerConfig {
5340                command: "astro-ls".to_string(),
5341                args: vec!["--stdio".to_string()],
5342                enabled: true,
5343                auto_start: false,
5344                process_limits: ProcessLimits::default(),
5345                initialization_options: None,
5346                env: Default::default(),
5347                language_id_overrides: Default::default(),
5348                name: None,
5349                only_features: None,
5350                except_features: None,
5351                root_markers: Default::default(),
5352            }]),
5353        );
5354
5355        // tailwindcss-language-server (installed via npm install -g @tailwindcss/language-server)
5356        lsp.insert(
5357            "tailwindcss".to_string(),
5358            LspLanguageConfig::Multi(vec![LspServerConfig {
5359                command: "tailwindcss-language-server".to_string(),
5360                args: vec!["--stdio".to_string()],
5361                enabled: true,
5362                auto_start: false,
5363                process_limits: ProcessLimits::default(),
5364                initialization_options: None,
5365                env: Default::default(),
5366                language_id_overrides: Default::default(),
5367                name: None,
5368                only_features: None,
5369                except_features: None,
5370                root_markers: Default::default(),
5371            }]),
5372        );
5373
5374        // --- Programming language LSP servers ---
5375
5376        // nil - Nix Language Server (https://github.com/oxalica/nil)
5377        // Install via nix profile install github:oxalica/nil
5378        lsp.insert(
5379            "nix".to_string(),
5380            LspLanguageConfig::Multi(vec![LspServerConfig {
5381                command: "nil".to_string(),
5382                args: vec![],
5383                enabled: true,
5384                auto_start: false,
5385                process_limits: ProcessLimits::default(),
5386                initialization_options: None,
5387                env: Default::default(),
5388                language_id_overrides: Default::default(),
5389                name: None,
5390                only_features: None,
5391                except_features: None,
5392                root_markers: Default::default(),
5393            }]),
5394        );
5395
5396        // kotlin-language-server (https://github.com/fwcd/kotlin-language-server)
5397        // Install via package manager or build from source
5398        lsp.insert(
5399            "kotlin".to_string(),
5400            LspLanguageConfig::Multi(vec![LspServerConfig {
5401                command: "kotlin-language-server".to_string(),
5402                args: vec![],
5403                enabled: true,
5404                auto_start: false,
5405                process_limits: ProcessLimits::default(),
5406                initialization_options: None,
5407                env: Default::default(),
5408                language_id_overrides: Default::default(),
5409                name: None,
5410                only_features: None,
5411                except_features: None,
5412                root_markers: Default::default(),
5413            }]),
5414        );
5415
5416        // sourcekit-lsp - Swift Language Server (included with Swift toolchain)
5417        lsp.insert(
5418            "swift".to_string(),
5419            LspLanguageConfig::Multi(vec![LspServerConfig {
5420                command: "sourcekit-lsp".to_string(),
5421                args: vec![],
5422                enabled: true,
5423                auto_start: false,
5424                process_limits: ProcessLimits::default(),
5425                initialization_options: None,
5426                env: Default::default(),
5427                language_id_overrides: Default::default(),
5428                name: None,
5429                only_features: None,
5430                except_features: None,
5431                root_markers: Default::default(),
5432            }]),
5433        );
5434
5435        // metals - Scala Language Server (https://scalameta.org/metals/)
5436        // Install via coursier: cs install metals
5437        lsp.insert(
5438            "scala".to_string(),
5439            LspLanguageConfig::Multi(vec![LspServerConfig {
5440                command: "metals".to_string(),
5441                args: vec![],
5442                enabled: true,
5443                auto_start: false,
5444                process_limits: ProcessLimits::default(),
5445                initialization_options: None,
5446                env: Default::default(),
5447                language_id_overrides: Default::default(),
5448                name: None,
5449                only_features: None,
5450                except_features: None,
5451                root_markers: Default::default(),
5452            }]),
5453        );
5454
5455        // elixir-ls - Elixir Language Server (https://github.com/elixir-lsp/elixir-ls)
5456        // Install via mix: mix escript.install hex elixir_ls
5457        lsp.insert(
5458            "elixir".to_string(),
5459            LspLanguageConfig::Multi(vec![LspServerConfig {
5460                command: "elixir-ls".to_string(),
5461                args: vec![],
5462                enabled: true,
5463                auto_start: false,
5464                process_limits: ProcessLimits::default(),
5465                initialization_options: None,
5466                env: Default::default(),
5467                language_id_overrides: Default::default(),
5468                name: None,
5469                only_features: None,
5470                except_features: None,
5471                root_markers: Default::default(),
5472            }]),
5473        );
5474
5475        // erlang_ls - Erlang Language Server (https://github.com/erlang-ls/erlang_ls)
5476        lsp.insert(
5477            "erlang".to_string(),
5478            LspLanguageConfig::Multi(vec![LspServerConfig {
5479                command: "erlang_ls".to_string(),
5480                args: vec![],
5481                enabled: true,
5482                auto_start: false,
5483                process_limits: ProcessLimits::default(),
5484                initialization_options: None,
5485                env: Default::default(),
5486                language_id_overrides: Default::default(),
5487                name: None,
5488                only_features: None,
5489                except_features: None,
5490                root_markers: Default::default(),
5491            }]),
5492        );
5493
5494        // haskell-language-server (https://github.com/haskell/haskell-language-server)
5495        // Install via ghcup: ghcup install hls
5496        lsp.insert(
5497            "haskell".to_string(),
5498            LspLanguageConfig::Multi(vec![LspServerConfig {
5499                command: "haskell-language-server-wrapper".to_string(),
5500                args: vec!["--lsp".to_string()],
5501                enabled: true,
5502                auto_start: false,
5503                process_limits: ProcessLimits::default(),
5504                initialization_options: None,
5505                env: Default::default(),
5506                language_id_overrides: Default::default(),
5507                name: None,
5508                only_features: None,
5509                except_features: None,
5510                root_markers: Default::default(),
5511            }]),
5512        );
5513
5514        // ocamllsp - OCaml Language Server (https://github.com/ocaml/ocaml-lsp)
5515        // Install via opam: opam install ocaml-lsp-server
5516        lsp.insert(
5517            "ocaml".to_string(),
5518            LspLanguageConfig::Multi(vec![LspServerConfig {
5519                command: "ocamllsp".to_string(),
5520                args: vec![],
5521                enabled: true,
5522                auto_start: false,
5523                process_limits: ProcessLimits::default(),
5524                initialization_options: None,
5525                env: Default::default(),
5526                language_id_overrides: Default::default(),
5527                name: None,
5528                only_features: None,
5529                except_features: None,
5530                root_markers: Default::default(),
5531            }]),
5532        );
5533
5534        // clojure-lsp (https://github.com/clojure-lsp/clojure-lsp)
5535        // Install via package manager or download from releases
5536        lsp.insert(
5537            "clojure".to_string(),
5538            LspLanguageConfig::Multi(vec![LspServerConfig {
5539                command: "clojure-lsp".to_string(),
5540                args: vec![],
5541                enabled: true,
5542                auto_start: false,
5543                process_limits: ProcessLimits::default(),
5544                initialization_options: None,
5545                env: Default::default(),
5546                language_id_overrides: Default::default(),
5547                name: None,
5548                only_features: None,
5549                except_features: None,
5550                root_markers: Default::default(),
5551            }]),
5552        );
5553
5554        // r-languageserver (https://github.com/REditorSupport/languageserver)
5555        // Install via R: install.packages("languageserver")
5556        lsp.insert(
5557            "r".to_string(),
5558            LspLanguageConfig::Multi(vec![LspServerConfig {
5559                command: "R".to_string(),
5560                args: vec![
5561                    "--vanilla".to_string(),
5562                    "-e".to_string(),
5563                    "languageserver::run()".to_string(),
5564                ],
5565                enabled: true,
5566                auto_start: false,
5567                process_limits: ProcessLimits::default(),
5568                initialization_options: None,
5569                env: Default::default(),
5570                language_id_overrides: Default::default(),
5571                name: None,
5572                only_features: None,
5573                except_features: None,
5574                root_markers: Default::default(),
5575            }]),
5576        );
5577
5578        // julia LanguageServer.jl (https://github.com/julia-vscode/LanguageServer.jl)
5579        // Install via Julia: using Pkg; Pkg.add("LanguageServer")
5580        lsp.insert(
5581            "julia".to_string(),
5582            LspLanguageConfig::Multi(vec![LspServerConfig {
5583                command: "julia".to_string(),
5584                args: vec![
5585                    "--startup-file=no".to_string(),
5586                    "--history-file=no".to_string(),
5587                    "-e".to_string(),
5588                    "using LanguageServer; runserver()".to_string(),
5589                ],
5590                enabled: true,
5591                auto_start: false,
5592                process_limits: ProcessLimits::default(),
5593                initialization_options: None,
5594                env: Default::default(),
5595                language_id_overrides: Default::default(),
5596                name: None,
5597                only_features: None,
5598                except_features: None,
5599                root_markers: Default::default(),
5600            }]),
5601        );
5602
5603        // PerlNavigator (https://github.com/bscan/PerlNavigator)
5604        // Install via npm: npm install -g perlnavigator-server
5605        lsp.insert(
5606            "perl".to_string(),
5607            LspLanguageConfig::Multi(vec![LspServerConfig {
5608                command: "perlnavigator".to_string(),
5609                args: vec!["--stdio".to_string()],
5610                enabled: true,
5611                auto_start: false,
5612                process_limits: ProcessLimits::default(),
5613                initialization_options: None,
5614                env: Default::default(),
5615                language_id_overrides: Default::default(),
5616                name: None,
5617                only_features: None,
5618                except_features: None,
5619                root_markers: Default::default(),
5620            }]),
5621        );
5622
5623        // nimlangserver - Nim Language Server (https://github.com/nim-lang/langserver)
5624        // Install via nimble: nimble install nimlangserver
5625        lsp.insert(
5626            "nim".to_string(),
5627            LspLanguageConfig::Multi(vec![LspServerConfig {
5628                command: "nimlangserver".to_string(),
5629                args: vec![],
5630                enabled: true,
5631                auto_start: false,
5632                process_limits: ProcessLimits::default(),
5633                initialization_options: None,
5634                env: Default::default(),
5635                language_id_overrides: Default::default(),
5636                name: None,
5637                only_features: None,
5638                except_features: None,
5639                root_markers: Default::default(),
5640            }]),
5641        );
5642
5643        // gleam lsp - Gleam Language Server (built into the gleam binary)
5644        lsp.insert(
5645            "gleam".to_string(),
5646            LspLanguageConfig::Multi(vec![LspServerConfig {
5647                command: "gleam".to_string(),
5648                args: vec!["lsp".to_string()],
5649                enabled: true,
5650                auto_start: false,
5651                process_limits: ProcessLimits::default(),
5652                initialization_options: None,
5653                env: Default::default(),
5654                language_id_overrides: Default::default(),
5655                name: None,
5656                only_features: None,
5657                except_features: None,
5658                root_markers: Default::default(),
5659            }]),
5660        );
5661
5662        // fsharp - F# Language Server (https://github.com/fsharp/FsAutoComplete)
5663        // Install via dotnet: dotnet tool install -g fsautocomplete
5664        lsp.insert(
5665            "fsharp".to_string(),
5666            LspLanguageConfig::Multi(vec![LspServerConfig {
5667                command: "fsautocomplete".to_string(),
5668                args: vec!["--adaptive-lsp-server-enabled".to_string()],
5669                enabled: true,
5670                auto_start: false,
5671                process_limits: ProcessLimits::default(),
5672                initialization_options: None,
5673                env: Default::default(),
5674                language_id_overrides: Default::default(),
5675                name: None,
5676                only_features: None,
5677                except_features: None,
5678                root_markers: Default::default(),
5679            }]),
5680        );
5681    }
5682    pub fn validate(&self) -> Result<(), ConfigError> {
5683        // Validate tab size
5684        if self.editor.tab_size == 0 {
5685            return Err(ConfigError::ValidationError(
5686                "tab_size must be greater than 0".to_string(),
5687            ));
5688        }
5689
5690        // Validate scroll offset
5691        if self.editor.scroll_offset > 100 {
5692            return Err(ConfigError::ValidationError(
5693                "scroll_offset must be <= 100".to_string(),
5694            ));
5695        }
5696
5697        // Validate keybindings
5698        for binding in &self.keybindings {
5699            if binding.key.is_empty() {
5700                return Err(ConfigError::ValidationError(
5701                    "keybinding key cannot be empty".to_string(),
5702                ));
5703            }
5704            if binding.action.is_empty() {
5705                return Err(ConfigError::ValidationError(
5706                    "keybinding action cannot be empty".to_string(),
5707                ));
5708            }
5709        }
5710
5711        Ok(())
5712    }
5713}
5714
5715/// Configuration error types
5716#[derive(Debug)]
5717pub enum ConfigError {
5718    IoError(String),
5719    ParseError(String),
5720    SerializeError(String),
5721    ValidationError(String),
5722}
5723
5724impl std::fmt::Display for ConfigError {
5725    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5726        match self {
5727            Self::IoError(msg) => write!(f, "IO error: {msg}"),
5728            Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
5729            Self::SerializeError(msg) => write!(f, "Serialize error: {msg}"),
5730            Self::ValidationError(msg) => write!(f, "Validation error: {msg}"),
5731        }
5732    }
5733}
5734
5735impl std::error::Error for ConfigError {}
5736
5737#[cfg(test)]
5738mod tests {
5739    use super::*;
5740
5741    #[test]
5742    fn test_default_config() {
5743        let config = Config::default();
5744        assert_eq!(config.editor.tab_size, 4);
5745        assert!(config.editor.line_numbers);
5746        assert!(config.editor.syntax_highlighting);
5747        // keybindings is empty by design - it's for user customizations only
5748        // The actual keybindings come from resolve_keymap(active_keybinding_map)
5749        assert!(config.keybindings.is_empty());
5750        // But the resolved keymap should have bindings
5751        let resolved = config.resolve_keymap(&config.active_keybinding_map);
5752        assert!(!resolved.is_empty());
5753    }
5754
5755    #[test]
5756    fn test_all_builtin_keymaps_loadable() {
5757        for name in KeybindingMapName::BUILTIN_OPTIONS {
5758            let keymap = Config::load_builtin_keymap(name);
5759            assert!(keymap.is_some(), "Failed to load builtin keymap '{}'", name);
5760        }
5761    }
5762
5763    #[test]
5764    fn test_config_validation() {
5765        let mut config = Config::default();
5766        assert!(config.validate().is_ok());
5767
5768        config.editor.tab_size = 0;
5769        assert!(config.validate().is_err());
5770    }
5771
5772    #[test]
5773    fn test_macos_keymap_inherits_enter_bindings() {
5774        let config = Config::default();
5775        let bindings = config.resolve_keymap("macos");
5776
5777        let enter_bindings: Vec<_> = bindings.iter().filter(|b| b.key == "Enter").collect();
5778        assert!(
5779            !enter_bindings.is_empty(),
5780            "macos keymap should inherit Enter bindings from default, got {} Enter bindings",
5781            enter_bindings.len()
5782        );
5783        // Should have at least insert_newline for normal mode
5784        let has_insert_newline = enter_bindings.iter().any(|b| b.action == "insert_newline");
5785        assert!(
5786            has_insert_newline,
5787            "macos keymap should have insert_newline action for Enter key"
5788        );
5789    }
5790
5791    #[test]
5792    fn test_config_serialize_deserialize() {
5793        // Test that Config can be serialized and deserialized correctly
5794        let config = Config::default();
5795
5796        // Serialize to JSON
5797        let json = serde_json::to_string_pretty(&config).unwrap();
5798
5799        // Deserialize back
5800        let loaded: Config = serde_json::from_str(&json).unwrap();
5801
5802        assert_eq!(config.editor.tab_size, loaded.editor.tab_size);
5803        assert_eq!(config.theme, loaded.theme);
5804    }
5805
5806    #[test]
5807    fn test_config_with_custom_keybinding() {
5808        let json = r#"{
5809            "editor": {
5810                "tab_size": 2
5811            },
5812            "keybindings": [
5813                {
5814                    "key": "x",
5815                    "modifiers": ["ctrl", "shift"],
5816                    "action": "custom_action",
5817                    "args": {},
5818                    "when": null
5819                }
5820            ]
5821        }"#;
5822
5823        let config: Config = serde_json::from_str(json).unwrap();
5824        assert_eq!(config.editor.tab_size, 2);
5825        assert_eq!(config.keybindings.len(), 1);
5826        assert_eq!(config.keybindings[0].key, "x");
5827        assert_eq!(config.keybindings[0].modifiers.len(), 2);
5828    }
5829
5830    #[test]
5831    fn test_sparse_config_merges_with_defaults() {
5832        // User config that only specifies one LSP server
5833        let temp_dir = tempfile::tempdir().unwrap();
5834        let config_path = temp_dir.path().join("config.json");
5835
5836        // Write a sparse config - only overriding rust LSP
5837        let sparse_config = r#"{
5838            "lsp": {
5839                "rust": {
5840                    "command": "custom-rust-analyzer",
5841                    "args": ["--custom-arg"]
5842                }
5843            }
5844        }"#;
5845        std::fs::write(&config_path, sparse_config).unwrap();
5846
5847        // Load the config - should merge with defaults
5848        let loaded = Config::load_from_file(&config_path).unwrap();
5849
5850        // User's rust override should be present
5851        assert!(loaded.lsp.contains_key("rust"));
5852        assert_eq!(
5853            loaded.lsp["rust"].as_slice()[0].command,
5854            "custom-rust-analyzer".to_string()
5855        );
5856
5857        // Default LSP servers should also be present (merged from defaults)
5858        assert!(
5859            loaded.lsp.contains_key("python"),
5860            "python LSP should be merged from defaults"
5861        );
5862        assert!(
5863            loaded.lsp.contains_key("typescript"),
5864            "typescript LSP should be merged from defaults"
5865        );
5866        assert!(
5867            loaded.lsp.contains_key("javascript"),
5868            "javascript LSP should be merged from defaults"
5869        );
5870
5871        // Default language configs should also be present
5872        assert!(loaded.languages.contains_key("rust"));
5873        assert!(loaded.languages.contains_key("python"));
5874        assert!(loaded.languages.contains_key("typescript"));
5875    }
5876
5877    #[test]
5878    fn test_empty_config_gets_all_defaults() {
5879        let temp_dir = tempfile::tempdir().unwrap();
5880        let config_path = temp_dir.path().join("config.json");
5881
5882        // Write an empty config
5883        std::fs::write(&config_path, "{}").unwrap();
5884
5885        let loaded = Config::load_from_file(&config_path).unwrap();
5886        let defaults = Config::default();
5887
5888        // Should have all default LSP servers
5889        assert_eq!(loaded.lsp.len(), defaults.lsp.len());
5890
5891        // Should have all default languages
5892        assert_eq!(loaded.languages.len(), defaults.languages.len());
5893    }
5894
5895    #[test]
5896    fn test_dynamic_submenu_expansion() {
5897        // Test that DynamicSubmenu expands to Submenu with generated items
5898        let temp_dir = tempfile::tempdir().unwrap();
5899        let themes_dir = temp_dir.path().to_path_buf();
5900
5901        let dynamic = MenuItem::DynamicSubmenu {
5902            label: "Test".to_string(),
5903            source: "copy_with_theme".to_string(),
5904        };
5905
5906        let expanded = dynamic.expand_dynamic(&themes_dir);
5907
5908        // Should expand to a Submenu
5909        match expanded {
5910            MenuItem::Submenu { label, items } => {
5911                assert_eq!(label, "Test");
5912                // Should have items for each available theme (embedded themes only, no user themes in temp dir)
5913                let loader = crate::view::theme::ThemeLoader::embedded_only();
5914                let registry = loader.load_all(&[]);
5915                assert_eq!(items.len(), registry.len());
5916
5917                // Each item should be an Action with copy_with_theme
5918                for (item, theme_info) in items.iter().zip(registry.list().iter()) {
5919                    match item {
5920                        MenuItem::Action {
5921                            label,
5922                            action,
5923                            args,
5924                            ..
5925                        } => {
5926                            assert_eq!(label, &theme_info.name);
5927                            assert_eq!(action, "copy_with_theme");
5928                            assert_eq!(
5929                                args.get("theme").and_then(|v| v.as_str()),
5930                                Some(theme_info.name.as_str())
5931                            );
5932                        }
5933                        _ => panic!("Expected Action item"),
5934                    }
5935                }
5936            }
5937            _ => panic!("Expected Submenu after expansion"),
5938        }
5939    }
5940
5941    #[test]
5942    fn test_non_dynamic_item_unchanged() {
5943        // Non-DynamicSubmenu items should be unchanged by expand_dynamic
5944        let temp_dir = tempfile::tempdir().unwrap();
5945        let themes_dir = temp_dir.path();
5946
5947        let action = MenuItem::Action {
5948            label: "Test".to_string(),
5949            action: "test".to_string(),
5950            args: HashMap::new(),
5951            when: None,
5952            checkbox: None,
5953        };
5954
5955        let expanded = action.expand_dynamic(themes_dir);
5956        match expanded {
5957            MenuItem::Action { label, action, .. } => {
5958                assert_eq!(label, "Test");
5959                assert_eq!(action, "test");
5960            }
5961            _ => panic!("Action should remain Action after expand_dynamic"),
5962        }
5963    }
5964
5965    #[test]
5966    fn test_buffer_config_uses_global_defaults() {
5967        let config = Config::default();
5968        let buffer_config = BufferConfig::resolve(&config, None);
5969
5970        assert_eq!(buffer_config.tab_size, config.editor.tab_size);
5971        assert_eq!(buffer_config.auto_indent, config.editor.auto_indent);
5972        assert!(!buffer_config.use_tabs); // Default is spaces
5973        assert!(buffer_config.whitespace.any_tabs()); // Tabs visible by default
5974        assert!(buffer_config.formatter.is_none());
5975        assert!(!buffer_config.format_on_save);
5976    }
5977
5978    #[test]
5979    fn test_buffer_config_applies_language_overrides() {
5980        let mut config = Config::default();
5981
5982        // Add a language config with custom settings
5983        config.languages.insert(
5984            "go".to_string(),
5985            LanguageConfig {
5986                extensions: vec!["go".to_string()],
5987                filenames: vec![],
5988                grammar: "go".to_string(),
5989                comment_prefix: Some("//".to_string()),
5990                auto_indent: true,
5991                auto_close: None,
5992                auto_surround: None,
5993                highlighter: HighlighterPreference::Auto,
5994                textmate_grammar: None,
5995                show_whitespace_tabs: false, // Go hides tab indicators
5996                line_wrap: None,
5997                wrap_column: None,
5998                page_view: None,
5999                page_width: None,
6000                use_tabs: Some(true), // Go uses tabs
6001                tab_size: Some(8),    // Go uses 8-space tabs
6002                formatter: Some(FormatterConfig {
6003                    command: "gofmt".to_string(),
6004                    args: vec![],
6005                    stdin: true,
6006                    timeout_ms: 10000,
6007                }),
6008                format_on_save: true,
6009                on_save: vec![],
6010                word_characters: None,
6011            },
6012        );
6013
6014        let buffer_config = BufferConfig::resolve(&config, Some("go"));
6015
6016        assert_eq!(buffer_config.tab_size, 8);
6017        assert!(buffer_config.use_tabs);
6018        assert!(!buffer_config.whitespace.any_tabs()); // Go disables tab indicators
6019        assert!(buffer_config.format_on_save);
6020        assert!(buffer_config.formatter.is_some());
6021        assert_eq!(buffer_config.formatter.as_ref().unwrap().command, "gofmt");
6022    }
6023
6024    #[test]
6025    fn test_buffer_config_unknown_language_uses_global() {
6026        let config = Config::default();
6027        let buffer_config = BufferConfig::resolve(&config, Some("unknown_lang"));
6028
6029        // Should fall back to global settings
6030        assert_eq!(buffer_config.tab_size, config.editor.tab_size);
6031        assert!(!buffer_config.use_tabs);
6032    }
6033
6034    #[test]
6035    fn test_buffer_config_per_language_line_wrap() {
6036        let mut config = Config::default();
6037        config.editor.line_wrap = false;
6038
6039        // Add markdown with line_wrap override
6040        config.languages.insert(
6041            "markdown".to_string(),
6042            LanguageConfig {
6043                extensions: vec!["md".to_string()],
6044                line_wrap: Some(true),
6045                ..Default::default()
6046            },
6047        );
6048
6049        // Markdown should override global line_wrap=false
6050        let md_config = BufferConfig::resolve(&config, Some("markdown"));
6051        assert!(md_config.line_wrap, "Markdown should have line_wrap=true");
6052
6053        // Other languages should use global default (false)
6054        let other_config = BufferConfig::resolve(&config, Some("rust"));
6055        assert!(
6056            !other_config.line_wrap,
6057            "Non-configured languages should use global line_wrap=false"
6058        );
6059
6060        // No language should use global default
6061        let no_lang_config = BufferConfig::resolve(&config, None);
6062        assert!(
6063            !no_lang_config.line_wrap,
6064            "No language should use global line_wrap=false"
6065        );
6066    }
6067
6068    #[test]
6069    fn test_buffer_config_per_language_wrap_column() {
6070        let mut config = Config::default();
6071        config.editor.wrap_column = Some(120);
6072
6073        // Add markdown with wrap_column override
6074        config.languages.insert(
6075            "markdown".to_string(),
6076            LanguageConfig {
6077                extensions: vec!["md".to_string()],
6078                wrap_column: Some(80),
6079                ..Default::default()
6080            },
6081        );
6082
6083        // Markdown should use its own wrap_column
6084        let md_config = BufferConfig::resolve(&config, Some("markdown"));
6085        assert_eq!(md_config.wrap_column, Some(80));
6086
6087        // Other languages should use global wrap_column
6088        let other_config = BufferConfig::resolve(&config, Some("rust"));
6089        assert_eq!(other_config.wrap_column, Some(120));
6090
6091        // No language should use global wrap_column
6092        let no_lang_config = BufferConfig::resolve(&config, None);
6093        assert_eq!(no_lang_config.wrap_column, Some(120));
6094    }
6095
6096    #[test]
6097    fn test_buffer_config_indent_string() {
6098        let config = Config::default();
6099
6100        // Spaces indent
6101        let spaces_config = BufferConfig::resolve(&config, None);
6102        assert_eq!(spaces_config.indent_string(), "    "); // 4 spaces
6103
6104        // Tabs indent - create a language that uses tabs
6105        let mut config_with_tabs = Config::default();
6106        config_with_tabs.languages.insert(
6107            "makefile".to_string(),
6108            LanguageConfig {
6109                use_tabs: Some(true),
6110                tab_size: Some(8),
6111                ..Default::default()
6112            },
6113        );
6114        let tabs_config = BufferConfig::resolve(&config_with_tabs, Some("makefile"));
6115        assert_eq!(tabs_config.indent_string(), "\t");
6116    }
6117
6118    #[test]
6119    fn test_buffer_config_global_use_tabs_inherited() {
6120        // When editor.use_tabs is true, buffers without a language-specific
6121        // override should inherit the global setting.
6122        let mut config = Config::default();
6123        config.editor.use_tabs = true;
6124
6125        // Unknown language inherits global
6126        let buffer_config = BufferConfig::resolve(&config, Some("unknown_lang"));
6127        assert!(buffer_config.use_tabs);
6128
6129        // No language inherits global
6130        let buffer_config = BufferConfig::resolve(&config, None);
6131        assert!(buffer_config.use_tabs);
6132
6133        // Language with explicit use_tabs: Some(false) overrides global
6134        config.languages.insert(
6135            "python".to_string(),
6136            LanguageConfig {
6137                use_tabs: Some(false),
6138                ..Default::default()
6139            },
6140        );
6141        let buffer_config = BufferConfig::resolve(&config, Some("python"));
6142        assert!(!buffer_config.use_tabs);
6143
6144        // Language with use_tabs: None inherits global true
6145        config.languages.insert(
6146            "rust".to_string(),
6147            LanguageConfig {
6148                use_tabs: None,
6149                ..Default::default()
6150            },
6151        );
6152        let buffer_config = BufferConfig::resolve(&config, Some("rust"));
6153        assert!(buffer_config.use_tabs);
6154    }
6155
6156    /// Verify that every LSP config key has a matching entry in default_languages().
6157    /// Without this, detect_language() won't map file extensions to the language name,
6158    /// causing "No LSP server configured for this file type" even though the LSP config
6159    /// exists. The only exception is "tailwindcss" which attaches to CSS/HTML/JS files
6160    /// rather than having its own file type.
6161    #[test]
6162    #[cfg(feature = "runtime")]
6163    fn test_lsp_languages_have_language_config() {
6164        let config = Config::default();
6165        let exceptions = ["tailwindcss"];
6166        for lsp_key in config.lsp.keys() {
6167            if exceptions.contains(&lsp_key.as_str()) {
6168                continue;
6169            }
6170            assert!(
6171                config.languages.contains_key(lsp_key),
6172                "LSP config key '{}' has no matching entry in default_languages(). \
6173                 Add a LanguageConfig with the correct file extensions so detect_language() \
6174                 can map files to this language.",
6175                lsp_key
6176            );
6177        }
6178    }
6179}