Skip to main content

fresh/
config.rs

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