Skip to main content

fresh/
config.rs

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