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    /// Open files in a "preview" (ephemeral) tab on single-click in the
1395    /// file explorer. The preview tab is replaced by the next single-click
1396    /// instead of accumulating tabs. Editing the file, double-clicking
1397    /// (or pressing Enter) on it in the explorer, or dragging its tab
1398    /// promotes the tab to a permanent tab.
1399    /// Default: true
1400    #[serde(default = "default_true")]
1401    pub preview_tabs: bool,
1402}
1403
1404fn default_explorer_width() -> f32 {
1405    0.3 // 30% of screen width
1406}
1407
1408/// Clipboard configuration
1409///
1410/// Controls which clipboard methods are used for copy/paste operations.
1411/// By default, all methods are enabled and the editor tries them in order:
1412/// 1. OSC 52 escape sequences (works in modern terminals like Kitty, Alacritty, Wezterm)
1413/// 2. System clipboard via X11/Wayland APIs (works in Gnome Console, XFCE Terminal, etc.)
1414/// 3. Internal clipboard (always available as fallback)
1415///
1416/// If you experience hangs or issues (e.g., when using PuTTY or certain SSH setups),
1417/// you can disable specific methods.
1418#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1419pub struct ClipboardConfig {
1420    /// Enable OSC 52 escape sequences for clipboard access (default: true)
1421    /// Disable this if your terminal doesn't support OSC 52 or if it causes hangs
1422    #[serde(default = "default_true")]
1423    pub use_osc52: bool,
1424
1425    /// Enable system clipboard access via X11/Wayland APIs (default: true)
1426    /// Disable this if you don't have a display server or it causes issues
1427    #[serde(default = "default_true")]
1428    pub use_system_clipboard: bool,
1429}
1430
1431impl Default for ClipboardConfig {
1432    fn default() -> Self {
1433        Self {
1434            use_osc52: true,
1435            use_system_clipboard: true,
1436        }
1437    }
1438}
1439
1440/// Terminal configuration
1441#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1442pub struct TerminalConfig {
1443    /// When viewing terminal scrollback and new output arrives,
1444    /// automatically jump back to terminal mode (default: true)
1445    #[serde(default = "default_true")]
1446    pub jump_to_end_on_output: bool,
1447}
1448
1449impl Default for TerminalConfig {
1450    fn default() -> Self {
1451        Self {
1452            jump_to_end_on_output: true,
1453        }
1454    }
1455}
1456
1457/// Warning notification configuration
1458#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1459pub struct WarningsConfig {
1460    /// Show warning/error indicators in the status bar (default: true)
1461    /// When enabled, displays a colored indicator for LSP errors and other warnings
1462    #[serde(default = "default_true")]
1463    pub show_status_indicator: bool,
1464}
1465
1466impl Default for WarningsConfig {
1467    fn default() -> Self {
1468        Self {
1469            show_status_indicator: true,
1470        }
1471    }
1472}
1473
1474/// Package manager configuration for plugins and themes
1475#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1476pub struct PackagesConfig {
1477    /// Registry sources (git repository URLs containing plugin/theme indices)
1478    /// Default: ["https://github.com/sinelaw/fresh-plugins-registry"]
1479    #[serde(default = "default_package_sources")]
1480    pub sources: Vec<String>,
1481}
1482
1483fn default_package_sources() -> Vec<String> {
1484    vec!["https://github.com/sinelaw/fresh-plugins-registry".to_string()]
1485}
1486
1487impl Default for PackagesConfig {
1488    fn default() -> Self {
1489        Self {
1490            sources: default_package_sources(),
1491        }
1492    }
1493}
1494
1495// Re-export PluginConfig from fresh-core for shared type usage
1496pub use fresh_core::config::PluginConfig;
1497
1498impl Default for FileExplorerConfig {
1499    fn default() -> Self {
1500        Self {
1501            respect_gitignore: true,
1502            show_hidden: false,
1503            show_gitignored: false,
1504            custom_ignore_patterns: Vec::new(),
1505            width: default_explorer_width(),
1506            preview_tabs: true,
1507        }
1508    }
1509}
1510
1511/// File browser configuration (for Open File dialog)
1512#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1513pub struct FileBrowserConfig {
1514    /// Whether to show hidden files (starting with .) by default in Open File dialog
1515    #[serde(default = "default_false")]
1516    pub show_hidden: bool,
1517}
1518
1519/// A single key in a sequence
1520#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1521pub struct KeyPress {
1522    /// Key name (e.g., "a", "Enter", "F1")
1523    pub key: String,
1524    /// Modifiers (e.g., ["ctrl"], ["ctrl", "shift"])
1525    #[serde(default)]
1526    pub modifiers: Vec<String>,
1527}
1528
1529/// Keybinding definition
1530#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1531#[schemars(extend("x-display-field" = "/action"))]
1532pub struct Keybinding {
1533    /// Key name (e.g., "a", "Enter", "F1") - for single-key bindings
1534    #[serde(default, skip_serializing_if = "String::is_empty")]
1535    pub key: String,
1536
1537    /// Modifiers (e.g., ["ctrl"], ["ctrl", "shift"]) - for single-key bindings
1538    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1539    pub modifiers: Vec<String>,
1540
1541    /// Key sequence for chord bindings (e.g., [{"key": "x", "modifiers": ["ctrl"]}, {"key": "s", "modifiers": ["ctrl"]}])
1542    /// If present, takes precedence over key + modifiers
1543    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1544    pub keys: Vec<KeyPress>,
1545
1546    /// Action to perform (e.g., "insert_char", "move_left")
1547    pub action: String,
1548
1549    /// Optional arguments for the action
1550    #[serde(default)]
1551    pub args: HashMap<String, serde_json::Value>,
1552
1553    /// Optional condition (e.g., "mode == insert")
1554    #[serde(default)]
1555    pub when: Option<String>,
1556}
1557
1558/// Keymap configuration (for built-in and user-defined keymaps)
1559#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1560#[schemars(extend("x-display-field" = "/inherits"))]
1561pub struct KeymapConfig {
1562    /// Optional parent keymap to inherit from
1563    #[serde(default, skip_serializing_if = "Option::is_none")]
1564    pub inherits: Option<String>,
1565
1566    /// Keybindings defined in this keymap
1567    #[serde(default)]
1568    pub bindings: Vec<Keybinding>,
1569}
1570
1571/// Formatter configuration for a language
1572#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1573#[schemars(extend("x-display-field" = "/command"))]
1574pub struct FormatterConfig {
1575    /// The formatter command to run (e.g., "rustfmt", "prettier")
1576    pub command: String,
1577
1578    /// Arguments to pass to the formatter
1579    /// Use "$FILE" to include the file path
1580    #[serde(default)]
1581    pub args: Vec<String>,
1582
1583    /// Whether to pass buffer content via stdin (default: true)
1584    /// Most formatters read from stdin and write to stdout
1585    #[serde(default = "default_true")]
1586    pub stdin: bool,
1587
1588    /// Timeout in milliseconds (default: 10000)
1589    #[serde(default = "default_on_save_timeout")]
1590    pub timeout_ms: u64,
1591}
1592
1593/// Action to run when a file is saved (for linters, etc.)
1594#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1595#[schemars(extend("x-display-field" = "/command"))]
1596pub struct OnSaveAction {
1597    /// The shell command to run
1598    /// The file path is available as $FILE or as an argument
1599    pub command: String,
1600
1601    /// Arguments to pass to the command
1602    /// Use "$FILE" to include the file path
1603    #[serde(default)]
1604    pub args: Vec<String>,
1605
1606    /// Working directory for the command (defaults to project root)
1607    #[serde(default)]
1608    pub working_dir: Option<String>,
1609
1610    /// Whether to use the buffer content as stdin
1611    #[serde(default)]
1612    pub stdin: bool,
1613
1614    /// Timeout in milliseconds (default: 10000)
1615    #[serde(default = "default_on_save_timeout")]
1616    pub timeout_ms: u64,
1617
1618    /// Whether this action is enabled (default: true)
1619    /// Set to false to disable an action without removing it from config
1620    #[serde(default = "default_true")]
1621    pub enabled: bool,
1622}
1623
1624fn default_on_save_timeout() -> u64 {
1625    10000
1626}
1627
1628fn default_page_width() -> Option<usize> {
1629    Some(80)
1630}
1631
1632/// Language-specific configuration
1633#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1634#[schemars(extend("x-display-field" = "/grammar"))]
1635pub struct LanguageConfig {
1636    /// File extensions for this language (e.g., ["rs"] for Rust)
1637    #[serde(default)]
1638    pub extensions: Vec<String>,
1639
1640    /// Exact filenames for this language (e.g., ["Makefile", "GNUmakefile"])
1641    #[serde(default)]
1642    pub filenames: Vec<String>,
1643
1644    /// Tree-sitter grammar name
1645    #[serde(default)]
1646    pub grammar: String,
1647
1648    /// Comment prefix
1649    #[serde(default)]
1650    pub comment_prefix: Option<String>,
1651
1652    /// Whether to auto-indent
1653    #[serde(default = "default_true")]
1654    pub auto_indent: bool,
1655
1656    /// Whether to auto-close brackets, parentheses, and quotes for this language.
1657    /// If not specified (`null`), falls back to the global `editor.auto_close` setting.
1658    #[serde(default)]
1659    pub auto_close: Option<bool>,
1660
1661    /// Whether to auto-surround selected text with matching pairs for this language.
1662    /// If not specified (`null`), falls back to the global `editor.auto_surround` setting.
1663    #[serde(default)]
1664    pub auto_surround: Option<bool>,
1665
1666    /// Path to custom TextMate grammar file (optional)
1667    /// If specified, this grammar will be used when highlighter is "textmate"
1668    #[serde(default)]
1669    pub textmate_grammar: Option<std::path::PathBuf>,
1670
1671    /// Whether to show whitespace tab indicators (→) for this language
1672    /// Defaults to true. Set to false for languages like Go that use tabs for indentation.
1673    #[serde(default = "default_true")]
1674    pub show_whitespace_tabs: bool,
1675
1676    /// Whether to enable line wrapping for this language.
1677    /// If not specified (`null`), falls back to the global `editor.line_wrap` setting.
1678    /// Useful for prose-heavy languages like Markdown where wrapping is desirable
1679    /// even if globally disabled.
1680    #[serde(default)]
1681    pub line_wrap: Option<bool>,
1682
1683    /// Column at which to wrap lines for this language.
1684    /// If not specified (`null`), falls back to the global `editor.wrap_column` setting.
1685    #[serde(default)]
1686    pub wrap_column: Option<usize>,
1687
1688    /// Whether to automatically enable page view (compose mode) for this language.
1689    /// Page view provides a document-style layout with centered content,
1690    /// concealed formatting markers, and intelligent word wrapping.
1691    /// If not specified (`null`), page view is not auto-activated.
1692    #[serde(default)]
1693    pub page_view: Option<bool>,
1694
1695    /// Width of the page in page view mode (in columns).
1696    /// Controls the content width when page view is active, with centering margins.
1697    /// If not specified (`null`), falls back to the global `editor.page_width` setting.
1698    #[serde(default)]
1699    pub page_width: Option<usize>,
1700
1701    /// Whether pressing Tab should insert a tab character instead of spaces.
1702    /// If not specified (`null`), falls back to the global `editor.use_tabs` setting.
1703    /// Set to true for languages like Go and Makefile that require tabs.
1704    #[serde(default)]
1705    pub use_tabs: Option<bool>,
1706
1707    /// Tab size (number of spaces per tab) for this language.
1708    /// If not specified, falls back to the global editor.tab_size setting.
1709    #[serde(default)]
1710    pub tab_size: Option<usize>,
1711
1712    /// The formatter for this language (used by format_buffer command)
1713    #[serde(default)]
1714    pub formatter: Option<FormatterConfig>,
1715
1716    /// Whether to automatically format on save (uses the formatter above)
1717    #[serde(default)]
1718    pub format_on_save: bool,
1719
1720    /// Actions to run when a file of this language is saved (linters, etc.)
1721    /// Actions are run in order; if any fails (non-zero exit), subsequent actions don't run
1722    /// Note: Use `formatter` + `format_on_save` for formatting, not on_save
1723    #[serde(default)]
1724    pub on_save: Vec<OnSaveAction>,
1725
1726    /// Extra characters (beyond alphanumeric and `_`) considered part of
1727    /// identifiers for this language. Used by dabbrev and buffer-word
1728    /// completion to correctly tokenise language-specific naming conventions.
1729    ///
1730    /// Examples:
1731    /// - Lisp/Clojure/CSS: `"-"` (kebab-case identifiers)
1732    /// - PHP/Bash: `"$"` (variable sigils)
1733    /// - Ruby: `"?!"` (predicate/bang methods)
1734    /// - Rust (default): `""` (standard alphanumeric + underscore)
1735    #[serde(default)]
1736    pub word_characters: Option<String>,
1737}
1738
1739/// Resolved editor configuration for a specific buffer.
1740///
1741/// This struct contains the effective settings for a buffer after applying
1742/// language-specific overrides on top of the global editor config.
1743///
1744/// Use `BufferConfig::resolve()` to create one from a Config and optional language ID.
1745#[derive(Debug, Clone)]
1746pub struct BufferConfig {
1747    /// Number of spaces per tab character
1748    pub tab_size: usize,
1749
1750    /// Whether to insert a tab character (true) or spaces (false) when pressing Tab
1751    pub use_tabs: bool,
1752
1753    /// Whether to auto-indent new lines
1754    pub auto_indent: bool,
1755
1756    /// Whether to auto-close brackets, parentheses, and quotes
1757    pub auto_close: bool,
1758
1759    /// Whether to surround selected text with matching pairs
1760    pub auto_surround: bool,
1761
1762    /// Whether line wrapping is enabled for this buffer
1763    pub line_wrap: bool,
1764
1765    /// Column at which to wrap lines (None = viewport width)
1766    pub wrap_column: Option<usize>,
1767
1768    /// Resolved whitespace indicator visibility
1769    pub whitespace: WhitespaceVisibility,
1770
1771    /// Formatter command for this buffer
1772    pub formatter: Option<FormatterConfig>,
1773
1774    /// Whether to format on save
1775    pub format_on_save: bool,
1776
1777    /// Actions to run when saving
1778    pub on_save: Vec<OnSaveAction>,
1779
1780    /// Path to custom TextMate grammar (if any)
1781    pub textmate_grammar: Option<std::path::PathBuf>,
1782
1783    /// Extra word-constituent characters for this language (for completion).
1784    /// Empty string means standard alphanumeric + underscore only.
1785    pub word_characters: String,
1786}
1787
1788impl BufferConfig {
1789    /// Resolve the effective configuration for a buffer given its language.
1790    ///
1791    /// This merges the global editor settings with any language-specific overrides
1792    /// from `Config.languages`.
1793    ///
1794    /// # Arguments
1795    /// * `global_config` - The resolved global configuration
1796    /// * `language_id` - Optional language identifier (e.g., "rust", "python")
1797    pub fn resolve(global_config: &Config, language_id: Option<&str>) -> Self {
1798        let editor = &global_config.editor;
1799
1800        // Start with global editor settings
1801        let mut whitespace = WhitespaceVisibility::from_editor_config(editor);
1802        let mut config = BufferConfig {
1803            tab_size: editor.tab_size,
1804            use_tabs: editor.use_tabs,
1805            auto_indent: editor.auto_indent,
1806            auto_close: editor.auto_close,
1807            auto_surround: editor.auto_surround,
1808            line_wrap: editor.line_wrap,
1809            wrap_column: editor.wrap_column,
1810            whitespace,
1811            formatter: None,
1812            format_on_save: false,
1813            on_save: Vec::new(),
1814            textmate_grammar: None,
1815            word_characters: String::new(),
1816        };
1817
1818        // Apply language-specific overrides if available.
1819        // If no language config matches and the language is "text" (undetected),
1820        // try the default_language config (#1219).
1821        let lang_config_ref = language_id
1822            .and_then(|id| global_config.languages.get(id))
1823            .or_else(|| {
1824                // Apply default_language only when language is unknown ("text" or None)
1825                match language_id {
1826                    None | Some("text") => global_config
1827                        .default_language
1828                        .as_deref()
1829                        .and_then(|lang| global_config.languages.get(lang)),
1830                    _ => None,
1831                }
1832            });
1833        if let Some(lang_config) = lang_config_ref {
1834            // Tab size: use language setting if specified, else global
1835            if let Some(ts) = lang_config.tab_size {
1836                config.tab_size = ts;
1837            }
1838
1839            // Use tabs: language override (only if explicitly set)
1840            if let Some(use_tabs) = lang_config.use_tabs {
1841                config.use_tabs = use_tabs;
1842            }
1843
1844            // Line wrap: language override (only if explicitly set)
1845            if let Some(line_wrap) = lang_config.line_wrap {
1846                config.line_wrap = line_wrap;
1847            }
1848
1849            // Wrap column: language override (only if explicitly set)
1850            if lang_config.wrap_column.is_some() {
1851                config.wrap_column = lang_config.wrap_column;
1852            }
1853
1854            // Auto indent: language override
1855            config.auto_indent = lang_config.auto_indent;
1856
1857            // Auto close: language override (only if globally enabled)
1858            if config.auto_close {
1859                if let Some(lang_auto_close) = lang_config.auto_close {
1860                    config.auto_close = lang_auto_close;
1861                }
1862            }
1863
1864            // Auto surround: language override (only if globally enabled)
1865            if config.auto_surround {
1866                if let Some(lang_auto_surround) = lang_config.auto_surround {
1867                    config.auto_surround = lang_auto_surround;
1868                }
1869            }
1870
1871            // Whitespace tabs: language override can disable tab indicators
1872            whitespace = whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
1873            config.whitespace = whitespace;
1874
1875            // Formatter: from language config
1876            config.formatter = lang_config.formatter.clone();
1877
1878            // Format on save: from language config
1879            config.format_on_save = lang_config.format_on_save;
1880
1881            // On save actions: from language config
1882            config.on_save = lang_config.on_save.clone();
1883
1884            // TextMate grammar path: from language config
1885            config.textmate_grammar = lang_config.textmate_grammar.clone();
1886
1887            // Word characters: from language config
1888            if let Some(ref wc) = lang_config.word_characters {
1889                config.word_characters = wc.clone();
1890            }
1891        }
1892
1893        config
1894    }
1895
1896    /// Get the effective indentation string for this buffer.
1897    ///
1898    /// Returns a tab character if `use_tabs` is true, otherwise returns
1899    /// `tab_size` spaces.
1900    pub fn indent_string(&self) -> String {
1901        if self.use_tabs {
1902            "\t".to_string()
1903        } else {
1904            " ".repeat(self.tab_size)
1905        }
1906    }
1907}
1908
1909/// Menu bar configuration
1910#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1911pub struct MenuConfig {
1912    /// List of top-level menus in the menu bar
1913    #[serde(default)]
1914    pub menus: Vec<Menu>,
1915}
1916
1917// Re-export Menu and MenuItem from fresh-core for shared type usage
1918pub use fresh_core::menu::{Menu, MenuItem};
1919
1920/// Extension trait for Menu with editor-specific functionality
1921pub trait MenuExt {
1922    /// Get the identifier for matching (id if set, otherwise label).
1923    /// This is used for keybinding matching and should be stable across translations.
1924    fn match_id(&self) -> &str;
1925
1926    /// Expand all DynamicSubmenu items in this menu to regular Submenu items
1927    /// This should be called before the menu is used for rendering/navigation
1928    fn expand_dynamic_items(&mut self, themes_dir: &std::path::Path);
1929}
1930
1931impl MenuExt for Menu {
1932    fn match_id(&self) -> &str {
1933        self.id.as_deref().unwrap_or(&self.label)
1934    }
1935
1936    fn expand_dynamic_items(&mut self, themes_dir: &std::path::Path) {
1937        self.items = self
1938            .items
1939            .iter()
1940            .map(|item| item.expand_dynamic(themes_dir))
1941            .collect();
1942    }
1943}
1944
1945/// Extension trait for MenuItem with editor-specific functionality
1946pub trait MenuItemExt {
1947    /// Expand a DynamicSubmenu into a regular Submenu with generated items.
1948    /// Returns the original item if not a DynamicSubmenu.
1949    fn expand_dynamic(&self, themes_dir: &std::path::Path) -> MenuItem;
1950}
1951
1952impl MenuItemExt for MenuItem {
1953    fn expand_dynamic(&self, themes_dir: &std::path::Path) -> MenuItem {
1954        match self {
1955            MenuItem::DynamicSubmenu { label, source } => {
1956                let items = generate_dynamic_items(source, themes_dir);
1957                MenuItem::Submenu {
1958                    label: label.clone(),
1959                    items,
1960                }
1961            }
1962            other => other.clone(),
1963        }
1964    }
1965}
1966
1967/// Generate menu items for a dynamic source (runtime only - requires view::theme)
1968#[cfg(feature = "runtime")]
1969pub fn generate_dynamic_items(source: &str, themes_dir: &std::path::Path) -> Vec<MenuItem> {
1970    match source {
1971        "copy_with_theme" => {
1972            // Generate theme options from available themes
1973            let loader = crate::view::theme::ThemeLoader::new(themes_dir.to_path_buf());
1974            let registry = loader.load_all(&[]);
1975            registry
1976                .list()
1977                .iter()
1978                .map(|info| {
1979                    let mut args = HashMap::new();
1980                    args.insert("theme".to_string(), serde_json::json!(info.key));
1981                    MenuItem::Action {
1982                        label: info.name.clone(),
1983                        action: "copy_with_theme".to_string(),
1984                        args,
1985                        when: Some(context_keys::HAS_SELECTION.to_string()),
1986                        checkbox: None,
1987                    }
1988                })
1989                .collect()
1990        }
1991        _ => vec![MenuItem::Label {
1992            info: format!("Unknown source: {}", source),
1993        }],
1994    }
1995}
1996
1997/// Generate menu items for a dynamic source (WASM stub - returns empty)
1998#[cfg(not(feature = "runtime"))]
1999pub fn generate_dynamic_items(_source: &str, _themes_dir: &std::path::Path) -> Vec<MenuItem> {
2000    // Theme loading not available in WASM builds
2001    vec![]
2002}
2003
2004impl Default for Config {
2005    fn default() -> Self {
2006        Self {
2007            version: 0,
2008            theme: default_theme_name(),
2009            locale: LocaleName::default(),
2010            check_for_updates: true,
2011            editor: EditorConfig::default(),
2012            file_explorer: FileExplorerConfig::default(),
2013            file_browser: FileBrowserConfig::default(),
2014            clipboard: ClipboardConfig::default(),
2015            terminal: TerminalConfig::default(),
2016            keybindings: vec![], // User customizations only; defaults come from active_keybinding_map
2017            keybinding_maps: HashMap::new(), // User-defined maps go here
2018            active_keybinding_map: default_keybinding_map_name(),
2019            languages: Self::default_languages(),
2020            default_language: None,
2021            lsp: Self::default_lsp_config(),
2022            universal_lsp: Self::default_universal_lsp_config(),
2023            warnings: WarningsConfig::default(),
2024            plugins: HashMap::new(), // Populated when scanning for plugins
2025            packages: PackagesConfig::default(),
2026        }
2027    }
2028}
2029
2030impl MenuConfig {
2031    /// Create a MenuConfig with translated menus using the current locale
2032    pub fn translated() -> Self {
2033        Self {
2034            menus: Self::translated_menus(),
2035        }
2036    }
2037
2038    /// Create default menu bar configuration with translated labels.
2039    ///
2040    /// This is the single source of truth for the editor's menu structure.
2041    /// Both the built-in TUI menu bar and the native GUI menu bar (e.g. macOS)
2042    /// are built from this definition.
2043    pub fn translated_menus() -> Vec<Menu> {
2044        vec![
2045            // File menu
2046            Menu {
2047                id: Some("File".to_string()),
2048                label: t!("menu.file").to_string(),
2049                when: None,
2050                items: vec![
2051                    MenuItem::Action {
2052                        label: t!("menu.file.new_file").to_string(),
2053                        action: "new".to_string(),
2054                        args: HashMap::new(),
2055                        when: None,
2056                        checkbox: None,
2057                    },
2058                    MenuItem::Action {
2059                        label: t!("menu.file.open_file").to_string(),
2060                        action: "open".to_string(),
2061                        args: HashMap::new(),
2062                        when: None,
2063                        checkbox: None,
2064                    },
2065                    MenuItem::Separator { separator: true },
2066                    MenuItem::Action {
2067                        label: t!("menu.file.save").to_string(),
2068                        action: "save".to_string(),
2069                        args: HashMap::new(),
2070                        when: None,
2071                        checkbox: None,
2072                    },
2073                    MenuItem::Action {
2074                        label: t!("menu.file.save_as").to_string(),
2075                        action: "save_as".to_string(),
2076                        args: HashMap::new(),
2077                        when: None,
2078                        checkbox: None,
2079                    },
2080                    MenuItem::Action {
2081                        label: t!("menu.file.revert").to_string(),
2082                        action: "revert".to_string(),
2083                        args: HashMap::new(),
2084                        when: None,
2085                        checkbox: None,
2086                    },
2087                    MenuItem::Action {
2088                        label: t!("menu.file.reload_with_encoding").to_string(),
2089                        action: "reload_with_encoding".to_string(),
2090                        args: HashMap::new(),
2091                        when: None,
2092                        checkbox: None,
2093                    },
2094                    MenuItem::Separator { separator: true },
2095                    MenuItem::Action {
2096                        label: t!("menu.file.close_buffer").to_string(),
2097                        action: "close".to_string(),
2098                        args: HashMap::new(),
2099                        when: None,
2100                        checkbox: None,
2101                    },
2102                    MenuItem::Separator { separator: true },
2103                    MenuItem::Action {
2104                        label: t!("menu.file.switch_project").to_string(),
2105                        action: "switch_project".to_string(),
2106                        args: HashMap::new(),
2107                        when: None,
2108                        checkbox: None,
2109                    },
2110                    MenuItem::Separator { separator: true },
2111                    MenuItem::Action {
2112                        label: t!("menu.file.detach").to_string(),
2113                        action: "detach".to_string(),
2114                        args: HashMap::new(),
2115                        when: Some(context_keys::SESSION_MODE.to_string()),
2116                        checkbox: None,
2117                    },
2118                    MenuItem::Action {
2119                        label: t!("menu.file.quit").to_string(),
2120                        action: "quit".to_string(),
2121                        args: HashMap::new(),
2122                        when: None,
2123                        checkbox: None,
2124                    },
2125                ],
2126            },
2127            // Edit menu
2128            Menu {
2129                id: Some("Edit".to_string()),
2130                label: t!("menu.edit").to_string(),
2131                when: None,
2132                items: vec![
2133                    MenuItem::Action {
2134                        label: t!("menu.edit.undo").to_string(),
2135                        action: "undo".to_string(),
2136                        args: HashMap::new(),
2137                        when: None,
2138                        checkbox: None,
2139                    },
2140                    MenuItem::Action {
2141                        label: t!("menu.edit.redo").to_string(),
2142                        action: "redo".to_string(),
2143                        args: HashMap::new(),
2144                        when: None,
2145                        checkbox: None,
2146                    },
2147                    MenuItem::Separator { separator: true },
2148                    MenuItem::Action {
2149                        label: t!("menu.edit.cut").to_string(),
2150                        action: "cut".to_string(),
2151                        args: HashMap::new(),
2152                        when: Some(context_keys::HAS_SELECTION.to_string()),
2153                        checkbox: None,
2154                    },
2155                    MenuItem::Action {
2156                        label: t!("menu.edit.copy").to_string(),
2157                        action: "copy".to_string(),
2158                        args: HashMap::new(),
2159                        when: Some(context_keys::HAS_SELECTION.to_string()),
2160                        checkbox: None,
2161                    },
2162                    MenuItem::DynamicSubmenu {
2163                        label: t!("menu.edit.copy_with_formatting").to_string(),
2164                        source: "copy_with_theme".to_string(),
2165                    },
2166                    MenuItem::Action {
2167                        label: t!("menu.edit.paste").to_string(),
2168                        action: "paste".to_string(),
2169                        args: HashMap::new(),
2170                        when: None,
2171                        checkbox: None,
2172                    },
2173                    MenuItem::Separator { separator: true },
2174                    MenuItem::Action {
2175                        label: t!("menu.edit.select_all").to_string(),
2176                        action: "select_all".to_string(),
2177                        args: HashMap::new(),
2178                        when: None,
2179                        checkbox: None,
2180                    },
2181                    MenuItem::Separator { separator: true },
2182                    MenuItem::Action {
2183                        label: t!("menu.edit.find").to_string(),
2184                        action: "search".to_string(),
2185                        args: HashMap::new(),
2186                        when: None,
2187                        checkbox: None,
2188                    },
2189                    MenuItem::Action {
2190                        label: t!("menu.edit.find_in_selection").to_string(),
2191                        action: "find_in_selection".to_string(),
2192                        args: HashMap::new(),
2193                        when: Some(context_keys::HAS_SELECTION.to_string()),
2194                        checkbox: None,
2195                    },
2196                    MenuItem::Action {
2197                        label: t!("menu.edit.find_next").to_string(),
2198                        action: "find_next".to_string(),
2199                        args: HashMap::new(),
2200                        when: None,
2201                        checkbox: None,
2202                    },
2203                    MenuItem::Action {
2204                        label: t!("menu.edit.find_previous").to_string(),
2205                        action: "find_previous".to_string(),
2206                        args: HashMap::new(),
2207                        when: None,
2208                        checkbox: None,
2209                    },
2210                    MenuItem::Action {
2211                        label: t!("menu.edit.replace").to_string(),
2212                        action: "query_replace".to_string(),
2213                        args: HashMap::new(),
2214                        when: None,
2215                        checkbox: None,
2216                    },
2217                    MenuItem::Separator { separator: true },
2218                    MenuItem::Action {
2219                        label: t!("menu.edit.delete_line").to_string(),
2220                        action: "delete_line".to_string(),
2221                        args: HashMap::new(),
2222                        when: None,
2223                        checkbox: None,
2224                    },
2225                    MenuItem::Action {
2226                        label: t!("menu.edit.format_buffer").to_string(),
2227                        action: "format_buffer".to_string(),
2228                        args: HashMap::new(),
2229                        when: Some(context_keys::FORMATTER_AVAILABLE.to_string()),
2230                        checkbox: None,
2231                    },
2232                    MenuItem::Separator { separator: true },
2233                    MenuItem::Action {
2234                        label: t!("menu.edit.settings").to_string(),
2235                        action: "open_settings".to_string(),
2236                        args: HashMap::new(),
2237                        when: None,
2238                        checkbox: None,
2239                    },
2240                    MenuItem::Action {
2241                        label: t!("menu.edit.keybinding_editor").to_string(),
2242                        action: "open_keybinding_editor".to_string(),
2243                        args: HashMap::new(),
2244                        when: None,
2245                        checkbox: None,
2246                    },
2247                ],
2248            },
2249            // View menu
2250            Menu {
2251                id: Some("View".to_string()),
2252                label: t!("menu.view").to_string(),
2253                when: None,
2254                items: vec![
2255                    MenuItem::Action {
2256                        label: t!("menu.view.file_explorer").to_string(),
2257                        action: "toggle_file_explorer".to_string(),
2258                        args: HashMap::new(),
2259                        when: None,
2260                        checkbox: Some(context_keys::FILE_EXPLORER.to_string()),
2261                    },
2262                    MenuItem::Separator { separator: true },
2263                    MenuItem::Action {
2264                        label: t!("menu.view.line_numbers").to_string(),
2265                        action: "toggle_line_numbers".to_string(),
2266                        args: HashMap::new(),
2267                        when: None,
2268                        checkbox: Some(context_keys::LINE_NUMBERS.to_string()),
2269                    },
2270                    MenuItem::Action {
2271                        label: t!("menu.view.line_wrap").to_string(),
2272                        action: "toggle_line_wrap".to_string(),
2273                        args: HashMap::new(),
2274                        when: None,
2275                        checkbox: Some(context_keys::LINE_WRAP.to_string()),
2276                    },
2277                    MenuItem::Action {
2278                        label: t!("menu.view.mouse_support").to_string(),
2279                        action: "toggle_mouse_capture".to_string(),
2280                        args: HashMap::new(),
2281                        when: None,
2282                        checkbox: Some(context_keys::MOUSE_CAPTURE.to_string()),
2283                    },
2284                    MenuItem::Separator { separator: true },
2285                    MenuItem::Action {
2286                        label: t!("menu.view.vertical_scrollbar").to_string(),
2287                        action: "toggle_vertical_scrollbar".to_string(),
2288                        args: HashMap::new(),
2289                        when: None,
2290                        checkbox: Some(context_keys::VERTICAL_SCROLLBAR.to_string()),
2291                    },
2292                    MenuItem::Action {
2293                        label: t!("menu.view.horizontal_scrollbar").to_string(),
2294                        action: "toggle_horizontal_scrollbar".to_string(),
2295                        args: HashMap::new(),
2296                        when: None,
2297                        checkbox: Some(context_keys::HORIZONTAL_SCROLLBAR.to_string()),
2298                    },
2299                    MenuItem::Separator { separator: true },
2300                    MenuItem::Action {
2301                        label: t!("menu.view.set_background").to_string(),
2302                        action: "set_background".to_string(),
2303                        args: HashMap::new(),
2304                        when: None,
2305                        checkbox: None,
2306                    },
2307                    MenuItem::Action {
2308                        label: t!("menu.view.set_background_blend").to_string(),
2309                        action: "set_background_blend".to_string(),
2310                        args: HashMap::new(),
2311                        when: None,
2312                        checkbox: None,
2313                    },
2314                    MenuItem::Action {
2315                        label: t!("menu.view.set_page_width").to_string(),
2316                        action: "set_page_width".to_string(),
2317                        args: HashMap::new(),
2318                        when: None,
2319                        checkbox: None,
2320                    },
2321                    MenuItem::Separator { separator: true },
2322                    MenuItem::Action {
2323                        label: t!("menu.view.select_theme").to_string(),
2324                        action: "select_theme".to_string(),
2325                        args: HashMap::new(),
2326                        when: None,
2327                        checkbox: None,
2328                    },
2329                    MenuItem::Action {
2330                        label: t!("menu.view.select_locale").to_string(),
2331                        action: "select_locale".to_string(),
2332                        args: HashMap::new(),
2333                        when: None,
2334                        checkbox: None,
2335                    },
2336                    MenuItem::Action {
2337                        label: t!("menu.view.settings").to_string(),
2338                        action: "open_settings".to_string(),
2339                        args: HashMap::new(),
2340                        when: None,
2341                        checkbox: None,
2342                    },
2343                    MenuItem::Action {
2344                        label: t!("menu.view.calibrate_input").to_string(),
2345                        action: "calibrate_input".to_string(),
2346                        args: HashMap::new(),
2347                        when: None,
2348                        checkbox: None,
2349                    },
2350                    MenuItem::Separator { separator: true },
2351                    MenuItem::Action {
2352                        label: t!("menu.view.split_horizontal").to_string(),
2353                        action: "split_horizontal".to_string(),
2354                        args: HashMap::new(),
2355                        when: None,
2356                        checkbox: None,
2357                    },
2358                    MenuItem::Action {
2359                        label: t!("menu.view.split_vertical").to_string(),
2360                        action: "split_vertical".to_string(),
2361                        args: HashMap::new(),
2362                        when: None,
2363                        checkbox: None,
2364                    },
2365                    MenuItem::Action {
2366                        label: t!("menu.view.close_split").to_string(),
2367                        action: "close_split".to_string(),
2368                        args: HashMap::new(),
2369                        when: None,
2370                        checkbox: None,
2371                    },
2372                    MenuItem::Action {
2373                        label: t!("menu.view.scroll_sync").to_string(),
2374                        action: "toggle_scroll_sync".to_string(),
2375                        args: HashMap::new(),
2376                        when: Some(context_keys::HAS_SAME_BUFFER_SPLITS.to_string()),
2377                        checkbox: Some(context_keys::SCROLL_SYNC.to_string()),
2378                    },
2379                    MenuItem::Action {
2380                        label: t!("menu.view.focus_next_split").to_string(),
2381                        action: "next_split".to_string(),
2382                        args: HashMap::new(),
2383                        when: None,
2384                        checkbox: None,
2385                    },
2386                    MenuItem::Action {
2387                        label: t!("menu.view.focus_prev_split").to_string(),
2388                        action: "prev_split".to_string(),
2389                        args: HashMap::new(),
2390                        when: None,
2391                        checkbox: None,
2392                    },
2393                    MenuItem::Action {
2394                        label: t!("menu.view.toggle_maximize_split").to_string(),
2395                        action: "toggle_maximize_split".to_string(),
2396                        args: HashMap::new(),
2397                        when: None,
2398                        checkbox: None,
2399                    },
2400                    MenuItem::Separator { separator: true },
2401                    MenuItem::Submenu {
2402                        label: t!("menu.terminal").to_string(),
2403                        items: vec![
2404                            MenuItem::Action {
2405                                label: t!("menu.terminal.open").to_string(),
2406                                action: "open_terminal".to_string(),
2407                                args: HashMap::new(),
2408                                when: None,
2409                                checkbox: None,
2410                            },
2411                            MenuItem::Action {
2412                                label: t!("menu.terminal.close").to_string(),
2413                                action: "close_terminal".to_string(),
2414                                args: HashMap::new(),
2415                                when: None,
2416                                checkbox: None,
2417                            },
2418                            MenuItem::Separator { separator: true },
2419                            MenuItem::Action {
2420                                label: t!("menu.terminal.toggle_keyboard_capture").to_string(),
2421                                action: "toggle_keyboard_capture".to_string(),
2422                                args: HashMap::new(),
2423                                when: None,
2424                                checkbox: None,
2425                            },
2426                        ],
2427                    },
2428                    MenuItem::Separator { separator: true },
2429                    MenuItem::Submenu {
2430                        label: t!("menu.view.keybinding_style").to_string(),
2431                        items: vec![
2432                            MenuItem::Action {
2433                                label: t!("menu.view.keybinding_default").to_string(),
2434                                action: "switch_keybinding_map".to_string(),
2435                                args: {
2436                                    let mut map = HashMap::new();
2437                                    map.insert("map".to_string(), serde_json::json!("default"));
2438                                    map
2439                                },
2440                                when: None,
2441                                checkbox: Some(context_keys::KEYMAP_DEFAULT.to_string()),
2442                            },
2443                            MenuItem::Action {
2444                                label: t!("menu.view.keybinding_emacs").to_string(),
2445                                action: "switch_keybinding_map".to_string(),
2446                                args: {
2447                                    let mut map = HashMap::new();
2448                                    map.insert("map".to_string(), serde_json::json!("emacs"));
2449                                    map
2450                                },
2451                                when: None,
2452                                checkbox: Some(context_keys::KEYMAP_EMACS.to_string()),
2453                            },
2454                            MenuItem::Action {
2455                                label: t!("menu.view.keybinding_vscode").to_string(),
2456                                action: "switch_keybinding_map".to_string(),
2457                                args: {
2458                                    let mut map = HashMap::new();
2459                                    map.insert("map".to_string(), serde_json::json!("vscode"));
2460                                    map
2461                                },
2462                                when: None,
2463                                checkbox: Some(context_keys::KEYMAP_VSCODE.to_string()),
2464                            },
2465                            MenuItem::Action {
2466                                label: "macOS GUI (⌘)".to_string(),
2467                                action: "switch_keybinding_map".to_string(),
2468                                args: {
2469                                    let mut map = HashMap::new();
2470                                    map.insert("map".to_string(), serde_json::json!("macos-gui"));
2471                                    map
2472                                },
2473                                when: None,
2474                                checkbox: Some(context_keys::KEYMAP_MACOS_GUI.to_string()),
2475                            },
2476                        ],
2477                    },
2478                ],
2479            },
2480            // Selection menu
2481            Menu {
2482                id: Some("Selection".to_string()),
2483                label: t!("menu.selection").to_string(),
2484                when: None,
2485                items: vec![
2486                    MenuItem::Action {
2487                        label: t!("menu.selection.select_all").to_string(),
2488                        action: "select_all".to_string(),
2489                        args: HashMap::new(),
2490                        when: None,
2491                        checkbox: None,
2492                    },
2493                    MenuItem::Action {
2494                        label: t!("menu.selection.select_word").to_string(),
2495                        action: "select_word".to_string(),
2496                        args: HashMap::new(),
2497                        when: None,
2498                        checkbox: None,
2499                    },
2500                    MenuItem::Action {
2501                        label: t!("menu.selection.select_line").to_string(),
2502                        action: "select_line".to_string(),
2503                        args: HashMap::new(),
2504                        when: None,
2505                        checkbox: None,
2506                    },
2507                    MenuItem::Action {
2508                        label: t!("menu.selection.expand_selection").to_string(),
2509                        action: "expand_selection".to_string(),
2510                        args: HashMap::new(),
2511                        when: None,
2512                        checkbox: None,
2513                    },
2514                    MenuItem::Separator { separator: true },
2515                    MenuItem::Action {
2516                        label: t!("menu.selection.add_cursor_above").to_string(),
2517                        action: "add_cursor_above".to_string(),
2518                        args: HashMap::new(),
2519                        when: None,
2520                        checkbox: None,
2521                    },
2522                    MenuItem::Action {
2523                        label: t!("menu.selection.add_cursor_below").to_string(),
2524                        action: "add_cursor_below".to_string(),
2525                        args: HashMap::new(),
2526                        when: None,
2527                        checkbox: None,
2528                    },
2529                    MenuItem::Action {
2530                        label: t!("menu.selection.add_cursor_next_match").to_string(),
2531                        action: "add_cursor_next_match".to_string(),
2532                        args: HashMap::new(),
2533                        when: None,
2534                        checkbox: None,
2535                    },
2536                    MenuItem::Action {
2537                        label: t!("menu.selection.remove_secondary_cursors").to_string(),
2538                        action: "remove_secondary_cursors".to_string(),
2539                        args: HashMap::new(),
2540                        when: None,
2541                        checkbox: None,
2542                    },
2543                ],
2544            },
2545            // Go menu
2546            Menu {
2547                id: Some("Go".to_string()),
2548                label: t!("menu.go").to_string(),
2549                when: None,
2550                items: vec![
2551                    MenuItem::Action {
2552                        label: t!("menu.go.goto_line").to_string(),
2553                        action: "goto_line".to_string(),
2554                        args: HashMap::new(),
2555                        when: None,
2556                        checkbox: None,
2557                    },
2558                    MenuItem::Action {
2559                        label: t!("menu.go.goto_definition").to_string(),
2560                        action: "lsp_goto_definition".to_string(),
2561                        args: HashMap::new(),
2562                        when: None,
2563                        checkbox: None,
2564                    },
2565                    MenuItem::Action {
2566                        label: t!("menu.go.find_references").to_string(),
2567                        action: "lsp_references".to_string(),
2568                        args: HashMap::new(),
2569                        when: None,
2570                        checkbox: None,
2571                    },
2572                    MenuItem::Separator { separator: true },
2573                    MenuItem::Action {
2574                        label: t!("menu.go.next_buffer").to_string(),
2575                        action: "next_buffer".to_string(),
2576                        args: HashMap::new(),
2577                        when: None,
2578                        checkbox: None,
2579                    },
2580                    MenuItem::Action {
2581                        label: t!("menu.go.prev_buffer").to_string(),
2582                        action: "prev_buffer".to_string(),
2583                        args: HashMap::new(),
2584                        when: None,
2585                        checkbox: None,
2586                    },
2587                    MenuItem::Separator { separator: true },
2588                    MenuItem::Action {
2589                        label: t!("menu.go.command_palette").to_string(),
2590                        action: "command_palette".to_string(),
2591                        args: HashMap::new(),
2592                        when: None,
2593                        checkbox: None,
2594                    },
2595                ],
2596            },
2597            // LSP menu
2598            Menu {
2599                id: Some("LSP".to_string()),
2600                label: t!("menu.lsp").to_string(),
2601                when: None,
2602                items: vec![
2603                    MenuItem::Action {
2604                        label: t!("menu.lsp.show_hover").to_string(),
2605                        action: "lsp_hover".to_string(),
2606                        args: HashMap::new(),
2607                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2608                        checkbox: None,
2609                    },
2610                    MenuItem::Action {
2611                        label: t!("menu.lsp.goto_definition").to_string(),
2612                        action: "lsp_goto_definition".to_string(),
2613                        args: HashMap::new(),
2614                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2615                        checkbox: None,
2616                    },
2617                    MenuItem::Action {
2618                        label: t!("menu.lsp.find_references").to_string(),
2619                        action: "lsp_references".to_string(),
2620                        args: HashMap::new(),
2621                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2622                        checkbox: None,
2623                    },
2624                    MenuItem::Action {
2625                        label: t!("menu.lsp.rename_symbol").to_string(),
2626                        action: "lsp_rename".to_string(),
2627                        args: HashMap::new(),
2628                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2629                        checkbox: None,
2630                    },
2631                    MenuItem::Separator { separator: true },
2632                    MenuItem::Action {
2633                        label: t!("menu.lsp.show_completions").to_string(),
2634                        action: "lsp_completion".to_string(),
2635                        args: HashMap::new(),
2636                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2637                        checkbox: None,
2638                    },
2639                    MenuItem::Action {
2640                        label: t!("menu.lsp.show_signature").to_string(),
2641                        action: "lsp_signature_help".to_string(),
2642                        args: HashMap::new(),
2643                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2644                        checkbox: None,
2645                    },
2646                    MenuItem::Action {
2647                        label: t!("menu.lsp.code_actions").to_string(),
2648                        action: "lsp_code_actions".to_string(),
2649                        args: HashMap::new(),
2650                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2651                        checkbox: None,
2652                    },
2653                    MenuItem::Separator { separator: true },
2654                    MenuItem::Action {
2655                        label: t!("menu.lsp.toggle_inlay_hints").to_string(),
2656                        action: "toggle_inlay_hints".to_string(),
2657                        args: HashMap::new(),
2658                        when: Some(context_keys::LSP_AVAILABLE.to_string()),
2659                        checkbox: Some(context_keys::INLAY_HINTS.to_string()),
2660                    },
2661                    MenuItem::Action {
2662                        label: t!("menu.lsp.toggle_mouse_hover").to_string(),
2663                        action: "toggle_mouse_hover".to_string(),
2664                        args: HashMap::new(),
2665                        when: None,
2666                        checkbox: Some(context_keys::MOUSE_HOVER.to_string()),
2667                    },
2668                    MenuItem::Separator { separator: true },
2669                    MenuItem::Action {
2670                        label: t!("menu.lsp.restart_server").to_string(),
2671                        action: "lsp_restart".to_string(),
2672                        args: HashMap::new(),
2673                        when: None,
2674                        checkbox: None,
2675                    },
2676                    MenuItem::Action {
2677                        label: t!("menu.lsp.stop_server").to_string(),
2678                        action: "lsp_stop".to_string(),
2679                        args: HashMap::new(),
2680                        when: None,
2681                        checkbox: None,
2682                    },
2683                    MenuItem::Separator { separator: true },
2684                    MenuItem::Action {
2685                        label: t!("menu.lsp.toggle_for_buffer").to_string(),
2686                        action: "lsp_toggle_for_buffer".to_string(),
2687                        args: HashMap::new(),
2688                        when: None,
2689                        checkbox: None,
2690                    },
2691                ],
2692            },
2693            // Explorer menu (only visible when file explorer is focused)
2694            Menu {
2695                id: Some("Explorer".to_string()),
2696                label: t!("menu.explorer").to_string(),
2697                when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2698                items: vec![
2699                    MenuItem::Action {
2700                        label: t!("menu.explorer.new_file").to_string(),
2701                        action: "file_explorer_new_file".to_string(),
2702                        args: HashMap::new(),
2703                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2704                        checkbox: None,
2705                    },
2706                    MenuItem::Action {
2707                        label: t!("menu.explorer.new_folder").to_string(),
2708                        action: "file_explorer_new_directory".to_string(),
2709                        args: HashMap::new(),
2710                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2711                        checkbox: None,
2712                    },
2713                    MenuItem::Separator { separator: true },
2714                    MenuItem::Action {
2715                        label: t!("menu.explorer.open").to_string(),
2716                        action: "file_explorer_open".to_string(),
2717                        args: HashMap::new(),
2718                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2719                        checkbox: None,
2720                    },
2721                    MenuItem::Action {
2722                        label: t!("menu.explorer.rename").to_string(),
2723                        action: "file_explorer_rename".to_string(),
2724                        args: HashMap::new(),
2725                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2726                        checkbox: None,
2727                    },
2728                    MenuItem::Action {
2729                        label: t!("menu.explorer.delete").to_string(),
2730                        action: "file_explorer_delete".to_string(),
2731                        args: HashMap::new(),
2732                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2733                        checkbox: None,
2734                    },
2735                    MenuItem::Separator { separator: true },
2736                    MenuItem::Action {
2737                        label: t!("menu.explorer.refresh").to_string(),
2738                        action: "file_explorer_refresh".to_string(),
2739                        args: HashMap::new(),
2740                        when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2741                        checkbox: None,
2742                    },
2743                    MenuItem::Separator { separator: true },
2744                    MenuItem::Action {
2745                        label: t!("menu.explorer.show_hidden").to_string(),
2746                        action: "file_explorer_toggle_hidden".to_string(),
2747                        args: HashMap::new(),
2748                        when: Some(context_keys::FILE_EXPLORER.to_string()),
2749                        checkbox: Some(context_keys::FILE_EXPLORER_SHOW_HIDDEN.to_string()),
2750                    },
2751                    MenuItem::Action {
2752                        label: t!("menu.explorer.show_gitignored").to_string(),
2753                        action: "file_explorer_toggle_gitignored".to_string(),
2754                        args: HashMap::new(),
2755                        when: Some(context_keys::FILE_EXPLORER.to_string()),
2756                        checkbox: Some(context_keys::FILE_EXPLORER_SHOW_GITIGNORED.to_string()),
2757                    },
2758                ],
2759            },
2760            // Help menu
2761            Menu {
2762                id: Some("Help".to_string()),
2763                label: t!("menu.help").to_string(),
2764                when: None,
2765                items: vec![
2766                    MenuItem::Label {
2767                        info: format!("Fresh v{}", env!("CARGO_PKG_VERSION")),
2768                    },
2769                    MenuItem::Separator { separator: true },
2770                    MenuItem::Action {
2771                        label: t!("menu.help.show_manual").to_string(),
2772                        action: "show_help".to_string(),
2773                        args: HashMap::new(),
2774                        when: None,
2775                        checkbox: None,
2776                    },
2777                    MenuItem::Action {
2778                        label: t!("menu.help.keyboard_shortcuts").to_string(),
2779                        action: "keyboard_shortcuts".to_string(),
2780                        args: HashMap::new(),
2781                        when: None,
2782                        checkbox: None,
2783                    },
2784                    MenuItem::Separator { separator: true },
2785                    MenuItem::Action {
2786                        label: t!("menu.help.event_debug").to_string(),
2787                        action: "event_debug".to_string(),
2788                        args: HashMap::new(),
2789                        when: None,
2790                        checkbox: None,
2791                    },
2792                ],
2793            },
2794        ]
2795    }
2796}
2797
2798impl Config {
2799    /// The config filename used throughout the application
2800    pub(crate) const FILENAME: &'static str = "config.json";
2801
2802    /// Get the local config path (in the working directory)
2803    pub(crate) fn local_config_path(working_dir: &Path) -> std::path::PathBuf {
2804        working_dir.join(Self::FILENAME)
2805    }
2806
2807    /// Load configuration from a JSON file
2808    ///
2809    /// This deserializes the user's config file as a partial config and resolves
2810    /// it with system defaults. For HashMap fields like `lsp` and `languages`,
2811    /// entries from the user config are merged with the default entries.
2812    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
2813        let contents = std::fs::read_to_string(path.as_ref())
2814            .map_err(|e| ConfigError::IoError(e.to_string()))?;
2815
2816        // Deserialize as PartialConfig first, then resolve with defaults
2817        let partial: crate::partial_config::PartialConfig =
2818            serde_json::from_str(&contents).map_err(|e| ConfigError::ParseError(e.to_string()))?;
2819
2820        Ok(partial.resolve())
2821    }
2822
2823    /// Load a built-in keymap from embedded JSON
2824    fn load_builtin_keymap(name: &str) -> Option<KeymapConfig> {
2825        let json_content = match name {
2826            "default" => include_str!("../keymaps/default.json"),
2827            "emacs" => include_str!("../keymaps/emacs.json"),
2828            "vscode" => include_str!("../keymaps/vscode.json"),
2829            "macos" => include_str!("../keymaps/macos.json"),
2830            "macos-gui" => include_str!("../keymaps/macos-gui.json"),
2831            _ => return None,
2832        };
2833
2834        match serde_json::from_str(json_content) {
2835            Ok(config) => Some(config),
2836            Err(e) => {
2837                eprintln!("Failed to parse builtin keymap '{}': {}", name, e);
2838                None
2839            }
2840        }
2841    }
2842
2843    /// Resolve a keymap with inheritance
2844    /// Returns all bindings from the keymap and its parent chain
2845    pub fn resolve_keymap(&self, map_name: &str) -> Vec<Keybinding> {
2846        let mut visited = std::collections::HashSet::new();
2847        self.resolve_keymap_recursive(map_name, &mut visited)
2848    }
2849
2850    /// Recursive helper for resolve_keymap
2851    fn resolve_keymap_recursive(
2852        &self,
2853        map_name: &str,
2854        visited: &mut std::collections::HashSet<String>,
2855    ) -> Vec<Keybinding> {
2856        // Prevent infinite loops
2857        if visited.contains(map_name) {
2858            eprintln!(
2859                "Warning: Circular inheritance detected in keymap '{}'",
2860                map_name
2861            );
2862            return Vec::new();
2863        }
2864        visited.insert(map_name.to_string());
2865
2866        // Try to load the keymap (user-defined or built-in)
2867        let keymap = self
2868            .keybinding_maps
2869            .get(map_name)
2870            .cloned()
2871            .or_else(|| Self::load_builtin_keymap(map_name));
2872
2873        let Some(keymap) = keymap else {
2874            return Vec::new();
2875        };
2876
2877        // Start with parent bindings (if any)
2878        let mut all_bindings = if let Some(ref parent_name) = keymap.inherits {
2879            self.resolve_keymap_recursive(parent_name, visited)
2880        } else {
2881            Vec::new()
2882        };
2883
2884        // Add this keymap's bindings (they override parent bindings)
2885        all_bindings.extend(keymap.bindings);
2886
2887        all_bindings
2888    }
2889    /// Create default language configurations
2890    fn default_languages() -> HashMap<String, LanguageConfig> {
2891        let mut languages = HashMap::new();
2892
2893        languages.insert(
2894            "rust".to_string(),
2895            LanguageConfig {
2896                extensions: vec!["rs".to_string()],
2897                filenames: vec![],
2898                grammar: "rust".to_string(),
2899                comment_prefix: Some("//".to_string()),
2900                auto_indent: true,
2901                auto_close: None,
2902                auto_surround: None,
2903                textmate_grammar: None,
2904                show_whitespace_tabs: true,
2905                line_wrap: None,
2906                wrap_column: None,
2907                page_view: None,
2908                page_width: None,
2909                use_tabs: None,
2910                tab_size: None,
2911                formatter: Some(FormatterConfig {
2912                    command: "rustfmt".to_string(),
2913                    args: vec!["--edition".to_string(), "2021".to_string()],
2914                    stdin: true,
2915                    timeout_ms: 10000,
2916                }),
2917                format_on_save: false,
2918                on_save: vec![],
2919                word_characters: None,
2920            },
2921        );
2922
2923        languages.insert(
2924            "javascript".to_string(),
2925            LanguageConfig {
2926                extensions: vec!["js".to_string(), "jsx".to_string(), "mjs".to_string()],
2927                filenames: vec![],
2928                grammar: "javascript".to_string(),
2929                comment_prefix: Some("//".to_string()),
2930                auto_indent: true,
2931                auto_close: None,
2932                auto_surround: None,
2933                textmate_grammar: None,
2934                show_whitespace_tabs: true,
2935                line_wrap: None,
2936                wrap_column: None,
2937                page_view: None,
2938                page_width: None,
2939                use_tabs: None,
2940                tab_size: None,
2941                formatter: Some(FormatterConfig {
2942                    command: "prettier".to_string(),
2943                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2944                    stdin: true,
2945                    timeout_ms: 10000,
2946                }),
2947                format_on_save: false,
2948                on_save: vec![],
2949                word_characters: None,
2950            },
2951        );
2952
2953        languages.insert(
2954            "typescript".to_string(),
2955            LanguageConfig {
2956                extensions: vec!["ts".to_string(), "tsx".to_string(), "mts".to_string()],
2957                filenames: vec![],
2958                grammar: "typescript".to_string(),
2959                comment_prefix: Some("//".to_string()),
2960                auto_indent: true,
2961                auto_close: None,
2962                auto_surround: None,
2963                textmate_grammar: None,
2964                show_whitespace_tabs: true,
2965                line_wrap: None,
2966                wrap_column: None,
2967                page_view: None,
2968                page_width: None,
2969                use_tabs: None,
2970                tab_size: None,
2971                formatter: Some(FormatterConfig {
2972                    command: "prettier".to_string(),
2973                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2974                    stdin: true,
2975                    timeout_ms: 10000,
2976                }),
2977                format_on_save: false,
2978                on_save: vec![],
2979                word_characters: None,
2980            },
2981        );
2982
2983        languages.insert(
2984            "python".to_string(),
2985            LanguageConfig {
2986                extensions: vec!["py".to_string(), "pyi".to_string()],
2987                filenames: vec![],
2988                grammar: "python".to_string(),
2989                comment_prefix: Some("#".to_string()),
2990                auto_indent: true,
2991                auto_close: None,
2992                auto_surround: None,
2993                textmate_grammar: None,
2994                show_whitespace_tabs: true,
2995                line_wrap: None,
2996                wrap_column: None,
2997                page_view: None,
2998                page_width: None,
2999                use_tabs: None,
3000                tab_size: None,
3001                formatter: Some(FormatterConfig {
3002                    command: "ruff".to_string(),
3003                    args: vec![
3004                        "format".to_string(),
3005                        "--stdin-filename".to_string(),
3006                        "$FILE".to_string(),
3007                    ],
3008                    stdin: true,
3009                    timeout_ms: 10000,
3010                }),
3011                format_on_save: false,
3012                on_save: vec![],
3013                word_characters: None,
3014            },
3015        );
3016
3017        languages.insert(
3018            "c".to_string(),
3019            LanguageConfig {
3020                extensions: vec!["c".to_string(), "h".to_string()],
3021                filenames: vec![],
3022                grammar: "c".to_string(),
3023                comment_prefix: Some("//".to_string()),
3024                auto_indent: true,
3025                auto_close: None,
3026                auto_surround: None,
3027                textmate_grammar: None,
3028                show_whitespace_tabs: true,
3029                line_wrap: None,
3030                wrap_column: None,
3031                page_view: None,
3032                page_width: None,
3033                use_tabs: None,
3034                tab_size: None,
3035                formatter: Some(FormatterConfig {
3036                    command: "clang-format".to_string(),
3037                    args: vec![],
3038                    stdin: true,
3039                    timeout_ms: 10000,
3040                }),
3041                format_on_save: false,
3042                on_save: vec![],
3043                word_characters: None,
3044            },
3045        );
3046
3047        languages.insert(
3048            "cpp".to_string(),
3049            LanguageConfig {
3050                extensions: vec![
3051                    "cpp".to_string(),
3052                    "cc".to_string(),
3053                    "cxx".to_string(),
3054                    "hpp".to_string(),
3055                    "hh".to_string(),
3056                    "hxx".to_string(),
3057                ],
3058                filenames: vec![],
3059                grammar: "cpp".to_string(),
3060                comment_prefix: Some("//".to_string()),
3061                auto_indent: true,
3062                auto_close: None,
3063                auto_surround: None,
3064                textmate_grammar: None,
3065                show_whitespace_tabs: true,
3066                line_wrap: None,
3067                wrap_column: None,
3068                page_view: None,
3069                page_width: None,
3070                use_tabs: None,
3071                tab_size: None,
3072                formatter: Some(FormatterConfig {
3073                    command: "clang-format".to_string(),
3074                    args: vec![],
3075                    stdin: true,
3076                    timeout_ms: 10000,
3077                }),
3078                format_on_save: false,
3079                on_save: vec![],
3080                word_characters: None,
3081            },
3082        );
3083
3084        languages.insert(
3085            "csharp".to_string(),
3086            LanguageConfig {
3087                extensions: vec!["cs".to_string()],
3088                filenames: vec![],
3089                grammar: "C#".to_string(),
3090                comment_prefix: Some("//".to_string()),
3091                auto_indent: true,
3092                auto_close: None,
3093                auto_surround: None,
3094                textmate_grammar: None,
3095                show_whitespace_tabs: true,
3096                line_wrap: None,
3097                wrap_column: None,
3098                page_view: None,
3099                page_width: None,
3100                use_tabs: None,
3101                tab_size: None,
3102                formatter: None,
3103                format_on_save: false,
3104                on_save: vec![],
3105                word_characters: None,
3106            },
3107        );
3108
3109        languages.insert(
3110            "bash".to_string(),
3111            LanguageConfig {
3112                extensions: vec!["sh".to_string(), "bash".to_string()],
3113                filenames: vec![
3114                    ".bash_aliases".to_string(),
3115                    ".bash_logout".to_string(),
3116                    ".bash_profile".to_string(),
3117                    ".bashrc".to_string(),
3118                    ".env".to_string(),
3119                    ".profile".to_string(),
3120                    ".zlogin".to_string(),
3121                    ".zlogout".to_string(),
3122                    ".zprofile".to_string(),
3123                    ".zshenv".to_string(),
3124                    ".zshrc".to_string(),
3125                    // Common shell script files without extensions
3126                    "PKGBUILD".to_string(),
3127                    "APKBUILD".to_string(),
3128                ],
3129                grammar: "bash".to_string(),
3130                comment_prefix: Some("#".to_string()),
3131                auto_indent: true,
3132                auto_close: None,
3133                auto_surround: None,
3134                textmate_grammar: None,
3135                show_whitespace_tabs: true,
3136                line_wrap: None,
3137                wrap_column: None,
3138                page_view: None,
3139                page_width: None,
3140                use_tabs: None,
3141                tab_size: None,
3142                formatter: None,
3143                format_on_save: false,
3144                on_save: vec![],
3145                word_characters: None,
3146            },
3147        );
3148
3149        languages.insert(
3150            "makefile".to_string(),
3151            LanguageConfig {
3152                extensions: vec!["mk".to_string()],
3153                filenames: vec![
3154                    "Makefile".to_string(),
3155                    "makefile".to_string(),
3156                    "GNUmakefile".to_string(),
3157                ],
3158                grammar: "Makefile".to_string(),
3159                comment_prefix: Some("#".to_string()),
3160                auto_indent: false,
3161                auto_close: None,
3162                auto_surround: None,
3163                textmate_grammar: None,
3164                show_whitespace_tabs: true,
3165                line_wrap: None,
3166                wrap_column: None,
3167                page_view: None,
3168                page_width: None,
3169                use_tabs: Some(true), // Makefiles require tabs for recipes
3170                tab_size: Some(8),    // Makefiles traditionally use 8-space tabs
3171                formatter: None,
3172                format_on_save: false,
3173                on_save: vec![],
3174                word_characters: None,
3175            },
3176        );
3177
3178        languages.insert(
3179            "dockerfile".to_string(),
3180            LanguageConfig {
3181                extensions: vec!["dockerfile".to_string()],
3182                filenames: vec!["Dockerfile".to_string(), "Containerfile".to_string()],
3183                grammar: "dockerfile".to_string(),
3184                comment_prefix: Some("#".to_string()),
3185                auto_indent: true,
3186                auto_close: None,
3187                auto_surround: None,
3188                textmate_grammar: None,
3189                show_whitespace_tabs: true,
3190                line_wrap: None,
3191                wrap_column: None,
3192                page_view: None,
3193                page_width: None,
3194                use_tabs: None,
3195                tab_size: None,
3196                formatter: None,
3197                format_on_save: false,
3198                on_save: vec![],
3199                word_characters: None,
3200            },
3201        );
3202
3203        languages.insert(
3204            "json".to_string(),
3205            LanguageConfig {
3206                extensions: vec!["json".to_string(), "jsonc".to_string()],
3207                filenames: vec![],
3208                grammar: "json".to_string(),
3209                comment_prefix: None,
3210                auto_indent: true,
3211                auto_close: None,
3212                auto_surround: None,
3213                textmate_grammar: None,
3214                show_whitespace_tabs: true,
3215                line_wrap: None,
3216                wrap_column: None,
3217                page_view: None,
3218                page_width: None,
3219                use_tabs: None,
3220                tab_size: None,
3221                formatter: Some(FormatterConfig {
3222                    command: "prettier".to_string(),
3223                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
3224                    stdin: true,
3225                    timeout_ms: 10000,
3226                }),
3227                format_on_save: false,
3228                on_save: vec![],
3229                word_characters: None,
3230            },
3231        );
3232
3233        languages.insert(
3234            "toml".to_string(),
3235            LanguageConfig {
3236                extensions: vec!["toml".to_string()],
3237                filenames: vec!["Cargo.lock".to_string()],
3238                grammar: "toml".to_string(),
3239                comment_prefix: Some("#".to_string()),
3240                auto_indent: true,
3241                auto_close: None,
3242                auto_surround: None,
3243                textmate_grammar: None,
3244                show_whitespace_tabs: true,
3245                line_wrap: None,
3246                wrap_column: None,
3247                page_view: None,
3248                page_width: None,
3249                use_tabs: None,
3250                tab_size: None,
3251                formatter: None,
3252                format_on_save: false,
3253                on_save: vec![],
3254                word_characters: None,
3255            },
3256        );
3257
3258        languages.insert(
3259            "yaml".to_string(),
3260            LanguageConfig {
3261                extensions: vec!["yml".to_string(), "yaml".to_string()],
3262                filenames: vec![],
3263                grammar: "yaml".to_string(),
3264                comment_prefix: Some("#".to_string()),
3265                auto_indent: true,
3266                auto_close: None,
3267                auto_surround: None,
3268                textmate_grammar: None,
3269                show_whitespace_tabs: true,
3270                line_wrap: None,
3271                wrap_column: None,
3272                page_view: None,
3273                page_width: None,
3274                use_tabs: None,
3275                tab_size: None,
3276                formatter: Some(FormatterConfig {
3277                    command: "prettier".to_string(),
3278                    args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
3279                    stdin: true,
3280                    timeout_ms: 10000,
3281                }),
3282                format_on_save: false,
3283                on_save: vec![],
3284                word_characters: None,
3285            },
3286        );
3287
3288        languages.insert(
3289            "markdown".to_string(),
3290            LanguageConfig {
3291                extensions: vec!["md".to_string(), "markdown".to_string()],
3292                filenames: vec!["README".to_string()],
3293                grammar: "markdown".to_string(),
3294                comment_prefix: None,
3295                auto_indent: false,
3296                auto_close: None,
3297                auto_surround: None,
3298                textmate_grammar: None,
3299                show_whitespace_tabs: true,
3300                line_wrap: None,
3301                wrap_column: None,
3302                page_view: None,
3303                page_width: None,
3304                use_tabs: None,
3305                tab_size: None,
3306                formatter: None,
3307                format_on_save: false,
3308                on_save: vec![],
3309                word_characters: None,
3310            },
3311        );
3312
3313        // Go uses tabs for indentation by convention, so hide tab indicators and use tabs
3314        languages.insert(
3315            "go".to_string(),
3316            LanguageConfig {
3317                extensions: vec!["go".to_string()],
3318                filenames: vec![],
3319                grammar: "go".to_string(),
3320                comment_prefix: Some("//".to_string()),
3321                auto_indent: true,
3322                auto_close: None,
3323                auto_surround: None,
3324                textmate_grammar: None,
3325                show_whitespace_tabs: false,
3326                line_wrap: None,
3327                wrap_column: None,
3328                page_view: None,
3329                page_width: None,
3330                use_tabs: Some(true), // Go convention is to use tabs
3331                tab_size: Some(8),    // Go convention is 8-space tab width
3332                formatter: Some(FormatterConfig {
3333                    command: "gofmt".to_string(),
3334                    args: vec![],
3335                    stdin: true,
3336                    timeout_ms: 10000,
3337                }),
3338                format_on_save: false,
3339                on_save: vec![],
3340                word_characters: None,
3341            },
3342        );
3343
3344        languages.insert(
3345            "odin".to_string(),
3346            LanguageConfig {
3347                extensions: vec!["odin".to_string()],
3348                filenames: vec![],
3349                grammar: "odin".to_string(),
3350                comment_prefix: Some("//".to_string()),
3351                auto_indent: true,
3352                auto_close: None,
3353                auto_surround: None,
3354                textmate_grammar: None,
3355                show_whitespace_tabs: false,
3356                line_wrap: None,
3357                wrap_column: None,
3358                page_view: None,
3359                page_width: None,
3360                use_tabs: Some(true),
3361                tab_size: Some(8),
3362                formatter: None,
3363                format_on_save: false,
3364                on_save: vec![],
3365                word_characters: None,
3366            },
3367        );
3368
3369        languages.insert(
3370            "zig".to_string(),
3371            LanguageConfig {
3372                extensions: vec!["zig".to_string(), "zon".to_string()],
3373                filenames: vec![],
3374                grammar: "zig".to_string(),
3375                comment_prefix: Some("//".to_string()),
3376                auto_indent: true,
3377                auto_close: None,
3378                auto_surround: None,
3379                textmate_grammar: None,
3380                show_whitespace_tabs: true,
3381                line_wrap: None,
3382                wrap_column: None,
3383                page_view: None,
3384                page_width: None,
3385                use_tabs: None,
3386                tab_size: None,
3387                formatter: None,
3388                format_on_save: false,
3389                on_save: vec![],
3390                word_characters: None,
3391            },
3392        );
3393
3394        languages.insert(
3395            "java".to_string(),
3396            LanguageConfig {
3397                extensions: vec!["java".to_string()],
3398                filenames: vec![],
3399                grammar: "java".to_string(),
3400                comment_prefix: Some("//".to_string()),
3401                auto_indent: true,
3402                auto_close: None,
3403                auto_surround: None,
3404                textmate_grammar: None,
3405                show_whitespace_tabs: true,
3406                line_wrap: None,
3407                wrap_column: None,
3408                page_view: None,
3409                page_width: None,
3410                use_tabs: None,
3411                tab_size: None,
3412                formatter: None,
3413                format_on_save: false,
3414                on_save: vec![],
3415                word_characters: None,
3416            },
3417        );
3418
3419        languages.insert(
3420            "latex".to_string(),
3421            LanguageConfig {
3422                extensions: vec![
3423                    "tex".to_string(),
3424                    "latex".to_string(),
3425                    "ltx".to_string(),
3426                    "sty".to_string(),
3427                    "cls".to_string(),
3428                    "bib".to_string(),
3429                ],
3430                filenames: vec![],
3431                grammar: "latex".to_string(),
3432                comment_prefix: Some("%".to_string()),
3433                auto_indent: true,
3434                auto_close: None,
3435                auto_surround: None,
3436                textmate_grammar: None,
3437                show_whitespace_tabs: true,
3438                line_wrap: None,
3439                wrap_column: None,
3440                page_view: None,
3441                page_width: None,
3442                use_tabs: None,
3443                tab_size: None,
3444                formatter: None,
3445                format_on_save: false,
3446                on_save: vec![],
3447                word_characters: None,
3448            },
3449        );
3450
3451        languages.insert(
3452            "templ".to_string(),
3453            LanguageConfig {
3454                extensions: vec!["templ".to_string()],
3455                filenames: vec![],
3456                grammar: "go".to_string(), // Templ uses Go-like syntax
3457                comment_prefix: Some("//".to_string()),
3458                auto_indent: true,
3459                auto_close: None,
3460                auto_surround: None,
3461                textmate_grammar: None,
3462                show_whitespace_tabs: true,
3463                line_wrap: None,
3464                wrap_column: None,
3465                page_view: None,
3466                page_width: None,
3467                use_tabs: None,
3468                tab_size: None,
3469                formatter: None,
3470                format_on_save: false,
3471                on_save: vec![],
3472                word_characters: None,
3473            },
3474        );
3475
3476        // Git-related file types
3477        languages.insert(
3478            "git-rebase".to_string(),
3479            LanguageConfig {
3480                extensions: vec![],
3481                filenames: vec!["git-rebase-todo".to_string()],
3482                grammar: "Git Rebase Todo".to_string(),
3483                comment_prefix: Some("#".to_string()),
3484                auto_indent: false,
3485                auto_close: None,
3486                auto_surround: None,
3487                textmate_grammar: None,
3488                show_whitespace_tabs: true,
3489                line_wrap: None,
3490                wrap_column: None,
3491                page_view: None,
3492                page_width: None,
3493                use_tabs: None,
3494                tab_size: None,
3495                formatter: None,
3496                format_on_save: false,
3497                on_save: vec![],
3498                word_characters: None,
3499            },
3500        );
3501
3502        languages.insert(
3503            "git-commit".to_string(),
3504            LanguageConfig {
3505                extensions: vec![],
3506                filenames: vec![
3507                    "COMMIT_EDITMSG".to_string(),
3508                    "MERGE_MSG".to_string(),
3509                    "SQUASH_MSG".to_string(),
3510                    "TAG_EDITMSG".to_string(),
3511                ],
3512                grammar: "Git Commit Message".to_string(),
3513                comment_prefix: Some("#".to_string()),
3514                auto_indent: false,
3515                auto_close: None,
3516                auto_surround: None,
3517                textmate_grammar: None,
3518                show_whitespace_tabs: true,
3519                line_wrap: None,
3520                wrap_column: None,
3521                page_view: None,
3522                page_width: None,
3523                use_tabs: None,
3524                tab_size: None,
3525                formatter: None,
3526                format_on_save: false,
3527                on_save: vec![],
3528                word_characters: None,
3529            },
3530        );
3531
3532        languages.insert(
3533            "gitignore".to_string(),
3534            LanguageConfig {
3535                extensions: vec!["gitignore".to_string()],
3536                filenames: vec![
3537                    ".gitignore".to_string(),
3538                    ".dockerignore".to_string(),
3539                    ".npmignore".to_string(),
3540                    ".hgignore".to_string(),
3541                ],
3542                grammar: "Gitignore".to_string(),
3543                comment_prefix: Some("#".to_string()),
3544                auto_indent: false,
3545                auto_close: None,
3546                auto_surround: None,
3547                textmate_grammar: None,
3548                show_whitespace_tabs: true,
3549                line_wrap: None,
3550                wrap_column: None,
3551                page_view: None,
3552                page_width: None,
3553                use_tabs: None,
3554                tab_size: None,
3555                formatter: None,
3556                format_on_save: false,
3557                on_save: vec![],
3558                word_characters: None,
3559            },
3560        );
3561
3562        languages.insert(
3563            "gitconfig".to_string(),
3564            LanguageConfig {
3565                extensions: vec!["gitconfig".to_string()],
3566                filenames: vec![".gitconfig".to_string(), ".gitmodules".to_string()],
3567                grammar: "Git Config".to_string(),
3568                comment_prefix: Some("#".to_string()),
3569                auto_indent: true,
3570                auto_close: None,
3571                auto_surround: None,
3572                textmate_grammar: None,
3573                show_whitespace_tabs: true,
3574                line_wrap: None,
3575                wrap_column: None,
3576                page_view: None,
3577                page_width: None,
3578                use_tabs: None,
3579                tab_size: None,
3580                formatter: None,
3581                format_on_save: false,
3582                on_save: vec![],
3583                word_characters: None,
3584            },
3585        );
3586
3587        languages.insert(
3588            "gitattributes".to_string(),
3589            LanguageConfig {
3590                extensions: vec!["gitattributes".to_string()],
3591                filenames: vec![".gitattributes".to_string()],
3592                grammar: "Git Attributes".to_string(),
3593                comment_prefix: Some("#".to_string()),
3594                auto_indent: false,
3595                auto_close: None,
3596                auto_surround: None,
3597                textmate_grammar: None,
3598                show_whitespace_tabs: true,
3599                line_wrap: None,
3600                wrap_column: None,
3601                page_view: None,
3602                page_width: None,
3603                use_tabs: None,
3604                tab_size: None,
3605                formatter: None,
3606                format_on_save: false,
3607                on_save: vec![],
3608                word_characters: None,
3609            },
3610        );
3611
3612        languages.insert(
3613            "typst".to_string(),
3614            LanguageConfig {
3615                extensions: vec!["typ".to_string()],
3616                filenames: vec![],
3617                grammar: "Typst".to_string(),
3618                comment_prefix: Some("//".to_string()),
3619                auto_indent: true,
3620                auto_close: None,
3621                auto_surround: None,
3622                textmate_grammar: None,
3623                show_whitespace_tabs: true,
3624                line_wrap: None,
3625                wrap_column: None,
3626                page_view: None,
3627                page_width: None,
3628                use_tabs: None,
3629                tab_size: None,
3630                formatter: None,
3631                format_on_save: false,
3632                on_save: vec![],
3633                word_characters: None,
3634            },
3635        );
3636
3637        // --- Languages added for LSP support ---
3638        // These entries ensure detect_language() maps file extensions to language
3639        // names that match the LSP config keys in default_lsp_config().
3640
3641        languages.insert(
3642            "kotlin".to_string(),
3643            LanguageConfig {
3644                extensions: vec!["kt".to_string(), "kts".to_string()],
3645                filenames: vec![],
3646                grammar: "Kotlin".to_string(),
3647                comment_prefix: Some("//".to_string()),
3648                auto_indent: true,
3649                auto_close: None,
3650                auto_surround: None,
3651                textmate_grammar: None,
3652                show_whitespace_tabs: true,
3653                line_wrap: None,
3654                wrap_column: None,
3655                page_view: None,
3656                page_width: None,
3657                use_tabs: None,
3658                tab_size: None,
3659                formatter: None,
3660                format_on_save: false,
3661                on_save: vec![],
3662                word_characters: None,
3663            },
3664        );
3665
3666        languages.insert(
3667            "swift".to_string(),
3668            LanguageConfig {
3669                extensions: vec!["swift".to_string()],
3670                filenames: vec![],
3671                grammar: "Swift".to_string(),
3672                comment_prefix: Some("//".to_string()),
3673                auto_indent: true,
3674                auto_close: None,
3675                auto_surround: None,
3676                textmate_grammar: None,
3677                show_whitespace_tabs: true,
3678                line_wrap: None,
3679                wrap_column: None,
3680                page_view: None,
3681                page_width: None,
3682                use_tabs: None,
3683                tab_size: None,
3684                formatter: None,
3685                format_on_save: false,
3686                on_save: vec![],
3687                word_characters: None,
3688            },
3689        );
3690
3691        languages.insert(
3692            "scala".to_string(),
3693            LanguageConfig {
3694                extensions: vec!["scala".to_string(), "sc".to_string()],
3695                filenames: vec![],
3696                grammar: "Scala".to_string(),
3697                comment_prefix: Some("//".to_string()),
3698                auto_indent: true,
3699                auto_close: None,
3700                auto_surround: None,
3701                textmate_grammar: None,
3702                show_whitespace_tabs: true,
3703                line_wrap: None,
3704                wrap_column: None,
3705                page_view: None,
3706                page_width: None,
3707                use_tabs: None,
3708                tab_size: None,
3709                formatter: None,
3710                format_on_save: false,
3711                on_save: vec![],
3712                word_characters: None,
3713            },
3714        );
3715
3716        languages.insert(
3717            "dart".to_string(),
3718            LanguageConfig {
3719                extensions: vec!["dart".to_string()],
3720                filenames: vec![],
3721                grammar: "Dart".to_string(),
3722                comment_prefix: Some("//".to_string()),
3723                auto_indent: true,
3724                auto_close: None,
3725                auto_surround: None,
3726                textmate_grammar: None,
3727                show_whitespace_tabs: true,
3728                line_wrap: None,
3729                wrap_column: None,
3730                page_view: None,
3731                page_width: None,
3732                use_tabs: None,
3733                tab_size: None,
3734                formatter: None,
3735                format_on_save: false,
3736                on_save: vec![],
3737                word_characters: None,
3738            },
3739        );
3740
3741        languages.insert(
3742            "elixir".to_string(),
3743            LanguageConfig {
3744                extensions: vec!["ex".to_string(), "exs".to_string()],
3745                filenames: vec![],
3746                grammar: "Elixir".to_string(),
3747                comment_prefix: Some("#".to_string()),
3748                auto_indent: true,
3749                auto_close: None,
3750                auto_surround: None,
3751                textmate_grammar: None,
3752                show_whitespace_tabs: true,
3753                line_wrap: None,
3754                wrap_column: None,
3755                page_view: None,
3756                page_width: None,
3757                use_tabs: None,
3758                tab_size: None,
3759                formatter: None,
3760                format_on_save: false,
3761                on_save: vec![],
3762                word_characters: None,
3763            },
3764        );
3765
3766        languages.insert(
3767            "erlang".to_string(),
3768            LanguageConfig {
3769                extensions: vec!["erl".to_string(), "hrl".to_string()],
3770                filenames: vec![],
3771                grammar: "Erlang".to_string(),
3772                comment_prefix: Some("%".to_string()),
3773                auto_indent: true,
3774                auto_close: None,
3775                auto_surround: None,
3776                textmate_grammar: None,
3777                show_whitespace_tabs: true,
3778                line_wrap: None,
3779                wrap_column: None,
3780                page_view: None,
3781                page_width: None,
3782                use_tabs: None,
3783                tab_size: None,
3784                formatter: None,
3785                format_on_save: false,
3786                on_save: vec![],
3787                word_characters: None,
3788            },
3789        );
3790
3791        languages.insert(
3792            "haskell".to_string(),
3793            LanguageConfig {
3794                extensions: vec!["hs".to_string(), "lhs".to_string()],
3795                filenames: vec![],
3796                grammar: "Haskell".to_string(),
3797                comment_prefix: Some("--".to_string()),
3798                auto_indent: true,
3799                auto_close: None,
3800                auto_surround: None,
3801                textmate_grammar: None,
3802                show_whitespace_tabs: true,
3803                line_wrap: None,
3804                wrap_column: None,
3805                page_view: None,
3806                page_width: None,
3807                use_tabs: None,
3808                tab_size: None,
3809                formatter: None,
3810                format_on_save: false,
3811                on_save: vec![],
3812                word_characters: None,
3813            },
3814        );
3815
3816        languages.insert(
3817            "ocaml".to_string(),
3818            LanguageConfig {
3819                extensions: vec!["ml".to_string(), "mli".to_string()],
3820                filenames: vec![],
3821                grammar: "OCaml".to_string(),
3822                comment_prefix: None,
3823                auto_indent: true,
3824                auto_close: None,
3825                auto_surround: None,
3826                textmate_grammar: None,
3827                show_whitespace_tabs: true,
3828                line_wrap: None,
3829                wrap_column: None,
3830                page_view: None,
3831                page_width: None,
3832                use_tabs: None,
3833                tab_size: None,
3834                formatter: None,
3835                format_on_save: false,
3836                on_save: vec![],
3837                word_characters: None,
3838            },
3839        );
3840
3841        languages.insert(
3842            "clojure".to_string(),
3843            LanguageConfig {
3844                extensions: vec![
3845                    "clj".to_string(),
3846                    "cljs".to_string(),
3847                    "cljc".to_string(),
3848                    "edn".to_string(),
3849                ],
3850                filenames: vec![],
3851                grammar: "Clojure".to_string(),
3852                comment_prefix: Some(";".to_string()),
3853                auto_indent: true,
3854                auto_close: None,
3855                auto_surround: None,
3856                textmate_grammar: None,
3857                show_whitespace_tabs: true,
3858                line_wrap: None,
3859                wrap_column: None,
3860                page_view: None,
3861                page_width: None,
3862                use_tabs: None,
3863                tab_size: None,
3864                formatter: None,
3865                format_on_save: false,
3866                on_save: vec![],
3867                word_characters: None,
3868            },
3869        );
3870
3871        languages.insert(
3872            "r".to_string(),
3873            LanguageConfig {
3874                extensions: vec!["r".to_string(), "R".to_string(), "rmd".to_string()],
3875                filenames: vec![],
3876                grammar: "R".to_string(),
3877                comment_prefix: Some("#".to_string()),
3878                auto_indent: true,
3879                auto_close: None,
3880                auto_surround: None,
3881                textmate_grammar: None,
3882                show_whitespace_tabs: true,
3883                line_wrap: None,
3884                wrap_column: None,
3885                page_view: None,
3886                page_width: None,
3887                use_tabs: None,
3888                tab_size: None,
3889                formatter: None,
3890                format_on_save: false,
3891                on_save: vec![],
3892                word_characters: None,
3893            },
3894        );
3895
3896        languages.insert(
3897            "julia".to_string(),
3898            LanguageConfig {
3899                extensions: vec!["jl".to_string()],
3900                filenames: vec![],
3901                grammar: "Julia".to_string(),
3902                comment_prefix: Some("#".to_string()),
3903                auto_indent: true,
3904                auto_close: None,
3905                auto_surround: None,
3906                textmate_grammar: None,
3907                show_whitespace_tabs: true,
3908                line_wrap: None,
3909                wrap_column: None,
3910                page_view: None,
3911                page_width: None,
3912                use_tabs: None,
3913                tab_size: None,
3914                formatter: None,
3915                format_on_save: false,
3916                on_save: vec![],
3917                word_characters: None,
3918            },
3919        );
3920
3921        languages.insert(
3922            "perl".to_string(),
3923            LanguageConfig {
3924                extensions: vec!["pl".to_string(), "pm".to_string(), "t".to_string()],
3925                filenames: vec![],
3926                grammar: "Perl".to_string(),
3927                comment_prefix: Some("#".to_string()),
3928                auto_indent: true,
3929                auto_close: None,
3930                auto_surround: None,
3931                textmate_grammar: None,
3932                show_whitespace_tabs: true,
3933                line_wrap: None,
3934                wrap_column: None,
3935                page_view: None,
3936                page_width: None,
3937                use_tabs: None,
3938                tab_size: None,
3939                formatter: None,
3940                format_on_save: false,
3941                on_save: vec![],
3942                word_characters: None,
3943            },
3944        );
3945
3946        languages.insert(
3947            "nim".to_string(),
3948            LanguageConfig {
3949                extensions: vec!["nim".to_string(), "nims".to_string(), "nimble".to_string()],
3950                filenames: vec![],
3951                grammar: "Nim".to_string(),
3952                comment_prefix: Some("#".to_string()),
3953                auto_indent: true,
3954                auto_close: None,
3955                auto_surround: None,
3956                textmate_grammar: None,
3957                show_whitespace_tabs: true,
3958                line_wrap: None,
3959                wrap_column: None,
3960                page_view: None,
3961                page_width: None,
3962                use_tabs: None,
3963                tab_size: None,
3964                formatter: None,
3965                format_on_save: false,
3966                on_save: vec![],
3967                word_characters: None,
3968            },
3969        );
3970
3971        languages.insert(
3972            "gleam".to_string(),
3973            LanguageConfig {
3974                extensions: vec!["gleam".to_string()],
3975                filenames: vec![],
3976                grammar: "Gleam".to_string(),
3977                comment_prefix: Some("//".to_string()),
3978                auto_indent: true,
3979                auto_close: None,
3980                auto_surround: None,
3981                textmate_grammar: None,
3982                show_whitespace_tabs: true,
3983                line_wrap: None,
3984                wrap_column: None,
3985                page_view: None,
3986                page_width: None,
3987                use_tabs: None,
3988                tab_size: None,
3989                formatter: None,
3990                format_on_save: false,
3991                on_save: vec![],
3992                word_characters: None,
3993            },
3994        );
3995
3996        languages.insert(
3997            "fsharp".to_string(),
3998            LanguageConfig {
3999                extensions: vec!["fs".to_string(), "fsi".to_string(), "fsx".to_string()],
4000                filenames: vec![],
4001                grammar: "FSharp".to_string(),
4002                comment_prefix: Some("//".to_string()),
4003                auto_indent: true,
4004                auto_close: None,
4005                auto_surround: None,
4006                textmate_grammar: None,
4007                show_whitespace_tabs: true,
4008                line_wrap: None,
4009                wrap_column: None,
4010                page_view: None,
4011                page_width: None,
4012                use_tabs: None,
4013                tab_size: None,
4014                formatter: None,
4015                format_on_save: false,
4016                on_save: vec![],
4017                word_characters: None,
4018            },
4019        );
4020
4021        languages.insert(
4022            "nix".to_string(),
4023            LanguageConfig {
4024                extensions: vec!["nix".to_string()],
4025                filenames: vec![],
4026                grammar: "Nix".to_string(),
4027                comment_prefix: Some("#".to_string()),
4028                auto_indent: true,
4029                auto_close: None,
4030                auto_surround: None,
4031                textmate_grammar: None,
4032                show_whitespace_tabs: true,
4033                line_wrap: None,
4034                wrap_column: None,
4035                page_view: None,
4036                page_width: None,
4037                use_tabs: None,
4038                tab_size: None,
4039                formatter: None,
4040                format_on_save: false,
4041                on_save: vec![],
4042                word_characters: None,
4043            },
4044        );
4045
4046        languages.insert(
4047            "nushell".to_string(),
4048            LanguageConfig {
4049                extensions: vec!["nu".to_string()],
4050                filenames: vec![],
4051                grammar: "Nushell".to_string(),
4052                comment_prefix: Some("#".to_string()),
4053                auto_indent: true,
4054                auto_close: None,
4055                auto_surround: None,
4056                textmate_grammar: None,
4057                show_whitespace_tabs: true,
4058                line_wrap: None,
4059                wrap_column: None,
4060                page_view: None,
4061                page_width: None,
4062                use_tabs: None,
4063                tab_size: None,
4064                formatter: None,
4065                format_on_save: false,
4066                on_save: vec![],
4067                word_characters: None,
4068            },
4069        );
4070
4071        languages.insert(
4072            "solidity".to_string(),
4073            LanguageConfig {
4074                extensions: vec!["sol".to_string()],
4075                filenames: vec![],
4076                grammar: "Solidity".to_string(),
4077                comment_prefix: Some("//".to_string()),
4078                auto_indent: true,
4079                auto_close: None,
4080                auto_surround: None,
4081                textmate_grammar: None,
4082                show_whitespace_tabs: true,
4083                line_wrap: None,
4084                wrap_column: None,
4085                page_view: None,
4086                page_width: None,
4087                use_tabs: None,
4088                tab_size: None,
4089                formatter: None,
4090                format_on_save: false,
4091                on_save: vec![],
4092                word_characters: None,
4093            },
4094        );
4095
4096        languages.insert(
4097            "ruby".to_string(),
4098            LanguageConfig {
4099                extensions: vec!["rb".to_string(), "rake".to_string(), "gemspec".to_string()],
4100                filenames: vec![
4101                    "Gemfile".to_string(),
4102                    "Rakefile".to_string(),
4103                    "Guardfile".to_string(),
4104                ],
4105                grammar: "Ruby".to_string(),
4106                comment_prefix: Some("#".to_string()),
4107                auto_indent: true,
4108                auto_close: None,
4109                auto_surround: None,
4110                textmate_grammar: None,
4111                show_whitespace_tabs: true,
4112                line_wrap: None,
4113                wrap_column: None,
4114                page_view: None,
4115                page_width: None,
4116                use_tabs: None,
4117                tab_size: None,
4118                formatter: None,
4119                format_on_save: false,
4120                on_save: vec![],
4121                word_characters: None,
4122            },
4123        );
4124
4125        languages.insert(
4126            "php".to_string(),
4127            LanguageConfig {
4128                extensions: vec!["php".to_string(), "phtml".to_string()],
4129                filenames: vec![],
4130                grammar: "PHP".to_string(),
4131                comment_prefix: Some("//".to_string()),
4132                auto_indent: true,
4133                auto_close: None,
4134                auto_surround: None,
4135                textmate_grammar: None,
4136                show_whitespace_tabs: true,
4137                line_wrap: None,
4138                wrap_column: None,
4139                page_view: None,
4140                page_width: None,
4141                use_tabs: None,
4142                tab_size: None,
4143                formatter: None,
4144                format_on_save: false,
4145                on_save: vec![],
4146                word_characters: None,
4147            },
4148        );
4149
4150        languages.insert(
4151            "lua".to_string(),
4152            LanguageConfig {
4153                extensions: vec!["lua".to_string()],
4154                filenames: vec![],
4155                grammar: "Lua".to_string(),
4156                comment_prefix: Some("--".to_string()),
4157                auto_indent: true,
4158                auto_close: None,
4159                auto_surround: None,
4160                textmate_grammar: None,
4161                show_whitespace_tabs: true,
4162                line_wrap: None,
4163                wrap_column: None,
4164                page_view: None,
4165                page_width: None,
4166                use_tabs: None,
4167                tab_size: None,
4168                formatter: None,
4169                format_on_save: false,
4170                on_save: vec![],
4171                word_characters: None,
4172            },
4173        );
4174
4175        languages.insert(
4176            "html".to_string(),
4177            LanguageConfig {
4178                extensions: vec!["html".to_string(), "htm".to_string()],
4179                filenames: vec![],
4180                grammar: "HTML".to_string(),
4181                comment_prefix: None,
4182                auto_indent: true,
4183                auto_close: None,
4184                auto_surround: None,
4185                textmate_grammar: None,
4186                show_whitespace_tabs: true,
4187                line_wrap: None,
4188                wrap_column: None,
4189                page_view: None,
4190                page_width: None,
4191                use_tabs: None,
4192                tab_size: None,
4193                formatter: None,
4194                format_on_save: false,
4195                on_save: vec![],
4196                word_characters: None,
4197            },
4198        );
4199
4200        languages.insert(
4201            "css".to_string(),
4202            LanguageConfig {
4203                extensions: vec!["css".to_string()],
4204                filenames: vec![],
4205                grammar: "CSS".to_string(),
4206                comment_prefix: None,
4207                auto_indent: true,
4208                auto_close: None,
4209                auto_surround: None,
4210                textmate_grammar: None,
4211                show_whitespace_tabs: true,
4212                line_wrap: None,
4213                wrap_column: None,
4214                page_view: None,
4215                page_width: None,
4216                use_tabs: None,
4217                tab_size: None,
4218                formatter: None,
4219                format_on_save: false,
4220                on_save: vec![],
4221                word_characters: None,
4222            },
4223        );
4224
4225        languages.insert(
4226            "sql".to_string(),
4227            LanguageConfig {
4228                extensions: vec!["sql".to_string()],
4229                filenames: vec![],
4230                grammar: "SQL".to_string(),
4231                comment_prefix: Some("--".to_string()),
4232                auto_indent: true,
4233                auto_close: None,
4234                auto_surround: None,
4235                textmate_grammar: None,
4236                show_whitespace_tabs: true,
4237                line_wrap: None,
4238                wrap_column: None,
4239                page_view: None,
4240                page_width: None,
4241                use_tabs: None,
4242                tab_size: None,
4243                formatter: None,
4244                format_on_save: false,
4245                on_save: vec![],
4246                word_characters: None,
4247            },
4248        );
4249
4250        languages.insert(
4251            "graphql".to_string(),
4252            LanguageConfig {
4253                extensions: vec!["graphql".to_string(), "gql".to_string()],
4254                filenames: vec![],
4255                grammar: "GraphQL".to_string(),
4256                comment_prefix: Some("#".to_string()),
4257                auto_indent: true,
4258                auto_close: None,
4259                auto_surround: None,
4260                textmate_grammar: None,
4261                show_whitespace_tabs: true,
4262                line_wrap: None,
4263                wrap_column: None,
4264                page_view: None,
4265                page_width: None,
4266                use_tabs: None,
4267                tab_size: None,
4268                formatter: None,
4269                format_on_save: false,
4270                on_save: vec![],
4271                word_characters: None,
4272            },
4273        );
4274
4275        languages.insert(
4276            "protobuf".to_string(),
4277            LanguageConfig {
4278                extensions: vec!["proto".to_string()],
4279                filenames: vec![],
4280                grammar: "Protocol Buffers".to_string(),
4281                comment_prefix: Some("//".to_string()),
4282                auto_indent: true,
4283                auto_close: None,
4284                auto_surround: None,
4285                textmate_grammar: None,
4286                show_whitespace_tabs: true,
4287                line_wrap: None,
4288                wrap_column: None,
4289                page_view: None,
4290                page_width: None,
4291                use_tabs: None,
4292                tab_size: None,
4293                formatter: None,
4294                format_on_save: false,
4295                on_save: vec![],
4296                word_characters: None,
4297            },
4298        );
4299
4300        languages.insert(
4301            "cmake".to_string(),
4302            LanguageConfig {
4303                extensions: vec!["cmake".to_string()],
4304                filenames: vec!["CMakeLists.txt".to_string()],
4305                grammar: "CMake".to_string(),
4306                comment_prefix: Some("#".to_string()),
4307                auto_indent: true,
4308                auto_close: None,
4309                auto_surround: None,
4310                textmate_grammar: None,
4311                show_whitespace_tabs: true,
4312                line_wrap: None,
4313                wrap_column: None,
4314                page_view: None,
4315                page_width: None,
4316                use_tabs: None,
4317                tab_size: None,
4318                formatter: None,
4319                format_on_save: false,
4320                on_save: vec![],
4321                word_characters: None,
4322            },
4323        );
4324
4325        languages.insert(
4326            "terraform".to_string(),
4327            LanguageConfig {
4328                extensions: vec!["tf".to_string(), "tfvars".to_string(), "hcl".to_string()],
4329                filenames: vec![],
4330                grammar: "HCL".to_string(),
4331                comment_prefix: Some("#".to_string()),
4332                auto_indent: true,
4333                auto_close: None,
4334                auto_surround: None,
4335                textmate_grammar: None,
4336                show_whitespace_tabs: true,
4337                line_wrap: None,
4338                wrap_column: None,
4339                page_view: None,
4340                page_width: None,
4341                use_tabs: None,
4342                tab_size: None,
4343                formatter: None,
4344                format_on_save: false,
4345                on_save: vec![],
4346                word_characters: None,
4347            },
4348        );
4349
4350        languages.insert(
4351            "vue".to_string(),
4352            LanguageConfig {
4353                extensions: vec!["vue".to_string()],
4354                filenames: vec![],
4355                grammar: "Vue".to_string(),
4356                comment_prefix: None,
4357                auto_indent: true,
4358                auto_close: None,
4359                auto_surround: None,
4360                textmate_grammar: None,
4361                show_whitespace_tabs: true,
4362                line_wrap: None,
4363                wrap_column: None,
4364                page_view: None,
4365                page_width: None,
4366                use_tabs: None,
4367                tab_size: None,
4368                formatter: None,
4369                format_on_save: false,
4370                on_save: vec![],
4371                word_characters: None,
4372            },
4373        );
4374
4375        languages.insert(
4376            "svelte".to_string(),
4377            LanguageConfig {
4378                extensions: vec!["svelte".to_string()],
4379                filenames: vec![],
4380                grammar: "Svelte".to_string(),
4381                comment_prefix: None,
4382                auto_indent: true,
4383                auto_close: None,
4384                auto_surround: None,
4385                textmate_grammar: None,
4386                show_whitespace_tabs: true,
4387                line_wrap: None,
4388                wrap_column: None,
4389                page_view: None,
4390                page_width: None,
4391                use_tabs: None,
4392                tab_size: None,
4393                formatter: None,
4394                format_on_save: false,
4395                on_save: vec![],
4396                word_characters: None,
4397            },
4398        );
4399
4400        languages.insert(
4401            "astro".to_string(),
4402            LanguageConfig {
4403                extensions: vec!["astro".to_string()],
4404                filenames: vec![],
4405                grammar: "Astro".to_string(),
4406                comment_prefix: None,
4407                auto_indent: true,
4408                auto_close: None,
4409                auto_surround: None,
4410                textmate_grammar: None,
4411                show_whitespace_tabs: true,
4412                line_wrap: None,
4413                wrap_column: None,
4414                page_view: None,
4415                page_width: None,
4416                use_tabs: None,
4417                tab_size: None,
4418                formatter: None,
4419                format_on_save: false,
4420                on_save: vec![],
4421                word_characters: None,
4422            },
4423        );
4424
4425        // --- Languages for embedded grammars (syntax highlighting only) ---
4426
4427        languages.insert(
4428            "scss".to_string(),
4429            LanguageConfig {
4430                extensions: vec!["scss".to_string()],
4431                filenames: vec![],
4432                grammar: "SCSS".to_string(),
4433                comment_prefix: Some("//".to_string()),
4434                auto_indent: true,
4435                auto_close: None,
4436                auto_surround: None,
4437                textmate_grammar: None,
4438                show_whitespace_tabs: true,
4439                line_wrap: None,
4440                wrap_column: None,
4441                page_view: None,
4442                page_width: None,
4443                use_tabs: None,
4444                tab_size: None,
4445                formatter: None,
4446                format_on_save: false,
4447                on_save: vec![],
4448                word_characters: None,
4449            },
4450        );
4451
4452        languages.insert(
4453            "less".to_string(),
4454            LanguageConfig {
4455                extensions: vec!["less".to_string()],
4456                filenames: vec![],
4457                grammar: "LESS".to_string(),
4458                comment_prefix: Some("//".to_string()),
4459                auto_indent: true,
4460                auto_close: None,
4461                auto_surround: None,
4462                textmate_grammar: None,
4463                show_whitespace_tabs: true,
4464                line_wrap: None,
4465                wrap_column: None,
4466                page_view: None,
4467                page_width: None,
4468                use_tabs: None,
4469                tab_size: None,
4470                formatter: None,
4471                format_on_save: false,
4472                on_save: vec![],
4473                word_characters: None,
4474            },
4475        );
4476
4477        languages.insert(
4478            "powershell".to_string(),
4479            LanguageConfig {
4480                extensions: vec!["ps1".to_string(), "psm1".to_string(), "psd1".to_string()],
4481                filenames: vec![],
4482                grammar: "PowerShell".to_string(),
4483                comment_prefix: Some("#".to_string()),
4484                auto_indent: true,
4485                auto_close: None,
4486                auto_surround: None,
4487                textmate_grammar: None,
4488                show_whitespace_tabs: true,
4489                line_wrap: None,
4490                wrap_column: None,
4491                page_view: None,
4492                page_width: None,
4493                use_tabs: None,
4494                tab_size: None,
4495                formatter: None,
4496                format_on_save: false,
4497                on_save: vec![],
4498                word_characters: None,
4499            },
4500        );
4501
4502        languages.insert(
4503            "kdl".to_string(),
4504            LanguageConfig {
4505                extensions: vec!["kdl".to_string()],
4506                filenames: vec![],
4507                grammar: "KDL".to_string(),
4508                comment_prefix: Some("//".to_string()),
4509                auto_indent: true,
4510                auto_close: None,
4511                auto_surround: None,
4512                textmate_grammar: None,
4513                show_whitespace_tabs: true,
4514                line_wrap: None,
4515                wrap_column: None,
4516                page_view: None,
4517                page_width: None,
4518                use_tabs: None,
4519                tab_size: None,
4520                formatter: None,
4521                format_on_save: false,
4522                on_save: vec![],
4523                word_characters: None,
4524            },
4525        );
4526
4527        languages.insert(
4528            "starlark".to_string(),
4529            LanguageConfig {
4530                extensions: vec!["bzl".to_string(), "star".to_string()],
4531                filenames: vec!["BUILD".to_string(), "WORKSPACE".to_string()],
4532                grammar: "Starlark".to_string(),
4533                comment_prefix: Some("#".to_string()),
4534                auto_indent: true,
4535                auto_close: None,
4536                auto_surround: None,
4537                textmate_grammar: None,
4538                show_whitespace_tabs: true,
4539                line_wrap: None,
4540                wrap_column: None,
4541                page_view: None,
4542                page_width: None,
4543                use_tabs: None,
4544                tab_size: None,
4545                formatter: None,
4546                format_on_save: false,
4547                on_save: vec![],
4548                word_characters: None,
4549            },
4550        );
4551
4552        languages.insert(
4553            "justfile".to_string(),
4554            LanguageConfig {
4555                extensions: vec![],
4556                filenames: vec![
4557                    "justfile".to_string(),
4558                    "Justfile".to_string(),
4559                    ".justfile".to_string(),
4560                ],
4561                grammar: "Justfile".to_string(),
4562                comment_prefix: Some("#".to_string()),
4563                auto_indent: true,
4564                auto_close: None,
4565                auto_surround: None,
4566                textmate_grammar: None,
4567                show_whitespace_tabs: true,
4568                line_wrap: None,
4569                wrap_column: None,
4570                page_view: None,
4571                page_width: None,
4572                use_tabs: Some(true),
4573                tab_size: None,
4574                formatter: None,
4575                format_on_save: false,
4576                on_save: vec![],
4577                word_characters: None,
4578            },
4579        );
4580
4581        languages.insert(
4582            "earthfile".to_string(),
4583            LanguageConfig {
4584                extensions: vec!["earth".to_string()],
4585                filenames: vec!["Earthfile".to_string()],
4586                grammar: "Earthfile".to_string(),
4587                comment_prefix: Some("#".to_string()),
4588                auto_indent: true,
4589                auto_close: None,
4590                auto_surround: None,
4591                textmate_grammar: None,
4592                show_whitespace_tabs: true,
4593                line_wrap: None,
4594                wrap_column: None,
4595                page_view: None,
4596                page_width: None,
4597                use_tabs: None,
4598                tab_size: None,
4599                formatter: None,
4600                format_on_save: false,
4601                on_save: vec![],
4602                word_characters: None,
4603            },
4604        );
4605
4606        languages.insert(
4607            "gomod".to_string(),
4608            LanguageConfig {
4609                extensions: vec![],
4610                filenames: vec!["go.mod".to_string(), "go.sum".to_string()],
4611                grammar: "Go Module".to_string(),
4612                comment_prefix: Some("//".to_string()),
4613                auto_indent: true,
4614                auto_close: None,
4615                auto_surround: None,
4616                textmate_grammar: None,
4617                show_whitespace_tabs: true,
4618                line_wrap: None,
4619                wrap_column: None,
4620                page_view: None,
4621                page_width: None,
4622                use_tabs: Some(true),
4623                tab_size: None,
4624                formatter: None,
4625                format_on_save: false,
4626                on_save: vec![],
4627                word_characters: None,
4628            },
4629        );
4630
4631        languages.insert(
4632            "vlang".to_string(),
4633            LanguageConfig {
4634                extensions: vec!["v".to_string(), "vv".to_string()],
4635                filenames: vec![],
4636                grammar: "V".to_string(),
4637                comment_prefix: Some("//".to_string()),
4638                auto_indent: true,
4639                auto_close: None,
4640                auto_surround: None,
4641                textmate_grammar: None,
4642                show_whitespace_tabs: true,
4643                line_wrap: None,
4644                wrap_column: None,
4645                page_view: None,
4646                page_width: None,
4647                use_tabs: None,
4648                tab_size: None,
4649                formatter: None,
4650                format_on_save: false,
4651                on_save: vec![],
4652                word_characters: None,
4653            },
4654        );
4655
4656        languages.insert(
4657            "ini".to_string(),
4658            LanguageConfig {
4659                extensions: vec!["ini".to_string(), "cfg".to_string()],
4660                filenames: vec![],
4661                grammar: "INI".to_string(),
4662                comment_prefix: Some(";".to_string()),
4663                auto_indent: false,
4664                auto_close: None,
4665                auto_surround: None,
4666                textmate_grammar: None,
4667                show_whitespace_tabs: true,
4668                line_wrap: None,
4669                wrap_column: None,
4670                page_view: None,
4671                page_width: None,
4672                use_tabs: None,
4673                tab_size: None,
4674                formatter: None,
4675                format_on_save: false,
4676                on_save: vec![],
4677                word_characters: None,
4678            },
4679        );
4680
4681        languages.insert(
4682            "hyprlang".to_string(),
4683            LanguageConfig {
4684                extensions: vec!["hl".to_string()],
4685                filenames: vec!["hyprland.conf".to_string()],
4686                grammar: "Hyprlang".to_string(),
4687                comment_prefix: Some("#".to_string()),
4688                auto_indent: true,
4689                auto_close: None,
4690                auto_surround: None,
4691                textmate_grammar: None,
4692                show_whitespace_tabs: true,
4693                line_wrap: None,
4694                wrap_column: None,
4695                page_view: None,
4696                page_width: None,
4697                use_tabs: None,
4698                tab_size: None,
4699                formatter: None,
4700                format_on_save: false,
4701                on_save: vec![],
4702                word_characters: None,
4703            },
4704        );
4705
4706        languages
4707    }
4708
4709    /// Create default LSP configurations
4710    #[cfg(feature = "runtime")]
4711    fn default_lsp_config() -> HashMap<String, LspLanguageConfig> {
4712        let mut lsp = HashMap::new();
4713
4714        // rust-analyzer (installed via rustup or package manager)
4715        // Enable logging to help debug LSP issues (stored in XDG state directory)
4716        let ra_log_path = crate::services::log_dirs::lsp_log_path("rust-analyzer")
4717            .to_string_lossy()
4718            .to_string();
4719
4720        Self::populate_lsp_config(&mut lsp, ra_log_path);
4721        lsp
4722    }
4723
4724    /// Create empty LSP configurations for WASM builds
4725    #[cfg(not(feature = "runtime"))]
4726    fn default_lsp_config() -> HashMap<String, LspLanguageConfig> {
4727        // LSP is not available in WASM builds
4728        HashMap::new()
4729    }
4730
4731    /// Create default universal LSP configurations (servers that apply to all languages)
4732    #[cfg(feature = "runtime")]
4733    fn default_universal_lsp_config() -> HashMap<String, LspLanguageConfig> {
4734        let mut universal = HashMap::new();
4735
4736        // quicklsp: our built-in universal LSP server.
4737        // Provides fast cross-language hover, signature help, go-to-definition,
4738        // completions, and workspace symbols with doc extraction and dependency
4739        // source indexing. Designed as a lightweight complement to heavyweight
4740        // language-specific servers.
4741        //
4742        // Disabled by default — enable via config or command palette after
4743        // installing: `cargo install --path crates/quicklsp`
4744        //
4745        // `only_features` is left unset so quicklsp can serve every feature it
4746        // advertises via its server capabilities (including go-to-definition).
4747        // Users who want to scope it down can set `only_features` explicitly.
4748        universal.insert(
4749            "quicklsp".to_string(),
4750            LspLanguageConfig::Multi(vec![LspServerConfig {
4751                command: "quicklsp".to_string(),
4752                args: vec![],
4753                enabled: false,
4754                auto_start: false,
4755                process_limits: ProcessLimits::default(),
4756                initialization_options: None,
4757                env: Default::default(),
4758                language_id_overrides: Default::default(),
4759                name: Some("QuickLSP".to_string()),
4760                only_features: None,
4761                except_features: None,
4762                root_markers: vec![
4763                    "Cargo.toml".to_string(),
4764                    "package.json".to_string(),
4765                    "go.mod".to_string(),
4766                    "pyproject.toml".to_string(),
4767                    "requirements.txt".to_string(),
4768                    ".git".to_string(),
4769                ],
4770            }]),
4771        );
4772
4773        universal
4774    }
4775
4776    /// Create empty universal LSP configurations for WASM builds
4777    #[cfg(not(feature = "runtime"))]
4778    fn default_universal_lsp_config() -> HashMap<String, LspLanguageConfig> {
4779        HashMap::new()
4780    }
4781
4782    #[cfg(feature = "runtime")]
4783    fn populate_lsp_config(lsp: &mut HashMap<String, LspLanguageConfig>, ra_log_path: String) {
4784        // rust-analyzer: full mode by default (no init param restrictions, no process limits).
4785        // Users can switch to reduced-memory mode via the "Rust LSP: Reduced Memory Mode"
4786        // command palette command (provided by the rust-lsp plugin).
4787        lsp.insert(
4788            "rust".to_string(),
4789            LspLanguageConfig::Multi(vec![LspServerConfig {
4790                command: "rust-analyzer".to_string(),
4791                args: vec!["--log-file".to_string(), ra_log_path],
4792                enabled: true,
4793                auto_start: false,
4794                process_limits: ProcessLimits::unlimited(),
4795                initialization_options: None,
4796                env: Default::default(),
4797                language_id_overrides: Default::default(),
4798                name: None,
4799                only_features: None,
4800                except_features: None,
4801                root_markers: vec![
4802                    "Cargo.toml".to_string(),
4803                    "rust-project.json".to_string(),
4804                    ".git".to_string(),
4805                ],
4806            }]),
4807        );
4808
4809        // pylsp (installed via pip)
4810        lsp.insert(
4811            "python".to_string(),
4812            LspLanguageConfig::Multi(vec![LspServerConfig {
4813                command: "pylsp".to_string(),
4814                args: vec![],
4815                enabled: true,
4816                auto_start: false,
4817                process_limits: ProcessLimits::default(),
4818                initialization_options: None,
4819                env: Default::default(),
4820                language_id_overrides: Default::default(),
4821                name: None,
4822                only_features: None,
4823                except_features: None,
4824                root_markers: vec![
4825                    "pyproject.toml".to_string(),
4826                    "setup.py".to_string(),
4827                    "setup.cfg".to_string(),
4828                    "pyrightconfig.json".to_string(),
4829                    ".git".to_string(),
4830                ],
4831            }]),
4832        );
4833
4834        // typescript-language-server (installed via npm)
4835        // Alternative: use "deno lsp" with initialization_options: {"enable": true}
4836        lsp.insert(
4837            "javascript".to_string(),
4838            LspLanguageConfig::Multi(vec![LspServerConfig {
4839                command: "typescript-language-server".to_string(),
4840                args: vec!["--stdio".to_string()],
4841                enabled: true,
4842                auto_start: false,
4843                process_limits: ProcessLimits::default(),
4844                initialization_options: None,
4845                env: Default::default(),
4846                language_id_overrides: HashMap::from([(
4847                    "jsx".to_string(),
4848                    "javascriptreact".to_string(),
4849                )]),
4850                name: None,
4851                only_features: None,
4852                except_features: None,
4853                root_markers: vec![
4854                    "tsconfig.json".to_string(),
4855                    "jsconfig.json".to_string(),
4856                    "package.json".to_string(),
4857                    ".git".to_string(),
4858                ],
4859            }]),
4860        );
4861        lsp.insert(
4862            "typescript".to_string(),
4863            LspLanguageConfig::Multi(vec![LspServerConfig {
4864                command: "typescript-language-server".to_string(),
4865                args: vec!["--stdio".to_string()],
4866                enabled: true,
4867                auto_start: false,
4868                process_limits: ProcessLimits::default(),
4869                initialization_options: None,
4870                env: Default::default(),
4871                language_id_overrides: HashMap::from([(
4872                    "tsx".to_string(),
4873                    "typescriptreact".to_string(),
4874                )]),
4875                name: None,
4876                only_features: None,
4877                except_features: None,
4878                root_markers: vec![
4879                    "tsconfig.json".to_string(),
4880                    "jsconfig.json".to_string(),
4881                    "package.json".to_string(),
4882                    ".git".to_string(),
4883                ],
4884            }]),
4885        );
4886
4887        // vscode-html-language-server (installed via npm install -g vscode-langservers-extracted)
4888        lsp.insert(
4889            "html".to_string(),
4890            LspLanguageConfig::Multi(vec![LspServerConfig {
4891                command: "vscode-html-language-server".to_string(),
4892                args: vec!["--stdio".to_string()],
4893                enabled: true,
4894                auto_start: false,
4895                process_limits: ProcessLimits::default(),
4896                initialization_options: None,
4897                env: Default::default(),
4898                language_id_overrides: Default::default(),
4899                name: None,
4900                only_features: None,
4901                except_features: None,
4902                root_markers: Default::default(),
4903            }]),
4904        );
4905
4906        // vscode-css-language-server (installed via npm install -g vscode-langservers-extracted)
4907        lsp.insert(
4908            "css".to_string(),
4909            LspLanguageConfig::Multi(vec![LspServerConfig {
4910                command: "vscode-css-language-server".to_string(),
4911                args: vec!["--stdio".to_string()],
4912                enabled: true,
4913                auto_start: false,
4914                process_limits: ProcessLimits::default(),
4915                initialization_options: None,
4916                env: Default::default(),
4917                language_id_overrides: Default::default(),
4918                name: None,
4919                only_features: None,
4920                except_features: None,
4921                root_markers: Default::default(),
4922            }]),
4923        );
4924
4925        // clangd (installed via package manager)
4926        lsp.insert(
4927            "c".to_string(),
4928            LspLanguageConfig::Multi(vec![LspServerConfig {
4929                command: "clangd".to_string(),
4930                args: vec![],
4931                enabled: true,
4932                auto_start: false,
4933                process_limits: ProcessLimits::default(),
4934                initialization_options: None,
4935                env: Default::default(),
4936                language_id_overrides: Default::default(),
4937                name: None,
4938                only_features: None,
4939                except_features: None,
4940                root_markers: vec![
4941                    "compile_commands.json".to_string(),
4942                    "CMakeLists.txt".to_string(),
4943                    "Makefile".to_string(),
4944                    ".git".to_string(),
4945                ],
4946            }]),
4947        );
4948        lsp.insert(
4949            "cpp".to_string(),
4950            LspLanguageConfig::Multi(vec![LspServerConfig {
4951                command: "clangd".to_string(),
4952                args: vec![],
4953                enabled: true,
4954                auto_start: false,
4955                process_limits: ProcessLimits::default(),
4956                initialization_options: None,
4957                env: Default::default(),
4958                language_id_overrides: Default::default(),
4959                name: None,
4960                only_features: None,
4961                except_features: None,
4962                root_markers: vec![
4963                    "compile_commands.json".to_string(),
4964                    "CMakeLists.txt".to_string(),
4965                    "Makefile".to_string(),
4966                    ".git".to_string(),
4967                ],
4968            }]),
4969        );
4970
4971        // gopls (installed via go install)
4972        lsp.insert(
4973            "go".to_string(),
4974            LspLanguageConfig::Multi(vec![LspServerConfig {
4975                command: "gopls".to_string(),
4976                args: vec![],
4977                enabled: true,
4978                auto_start: false,
4979                process_limits: ProcessLimits::default(),
4980                initialization_options: None,
4981                env: Default::default(),
4982                language_id_overrides: Default::default(),
4983                name: None,
4984                only_features: None,
4985                except_features: None,
4986                root_markers: vec![
4987                    "go.mod".to_string(),
4988                    "go.work".to_string(),
4989                    ".git".to_string(),
4990                ],
4991            }]),
4992        );
4993
4994        // vscode-json-language-server (installed via npm install -g vscode-langservers-extracted)
4995        lsp.insert(
4996            "json".to_string(),
4997            LspLanguageConfig::Multi(vec![LspServerConfig {
4998                command: "vscode-json-language-server".to_string(),
4999                args: vec!["--stdio".to_string()],
5000                enabled: true,
5001                auto_start: false,
5002                process_limits: ProcessLimits::default(),
5003                initialization_options: None,
5004                env: Default::default(),
5005                language_id_overrides: Default::default(),
5006                name: None,
5007                only_features: None,
5008                except_features: None,
5009                root_markers: Default::default(),
5010            }]),
5011        );
5012
5013        // csharp-language-server (installed via dotnet tool install -g csharp-ls)
5014        lsp.insert(
5015            "csharp".to_string(),
5016            LspLanguageConfig::Multi(vec![LspServerConfig {
5017                command: "csharp-ls".to_string(),
5018                args: vec![],
5019                enabled: true,
5020                auto_start: false,
5021                process_limits: ProcessLimits::default(),
5022                initialization_options: None,
5023                env: Default::default(),
5024                language_id_overrides: Default::default(),
5025                name: None,
5026                only_features: None,
5027                except_features: None,
5028                root_markers: vec![
5029                    "*.csproj".to_string(),
5030                    "*.sln".to_string(),
5031                    ".git".to_string(),
5032                ],
5033            }]),
5034        );
5035
5036        // ols - Odin Language Server (https://github.com/DanielGavin/ols)
5037        // Build from source: cd ols && ./build.sh (Linux/macOS) or ./build.bat (Windows)
5038        lsp.insert(
5039            "odin".to_string(),
5040            LspLanguageConfig::Multi(vec![LspServerConfig {
5041                command: "ols".to_string(),
5042                args: vec![],
5043                enabled: true,
5044                auto_start: false,
5045                process_limits: ProcessLimits::default(),
5046                initialization_options: None,
5047                env: Default::default(),
5048                language_id_overrides: Default::default(),
5049                name: None,
5050                only_features: None,
5051                except_features: None,
5052                root_markers: Default::default(),
5053            }]),
5054        );
5055
5056        // zls - Zig Language Server (https://github.com/zigtools/zls)
5057        // Install via package manager or download from releases
5058        lsp.insert(
5059            "zig".to_string(),
5060            LspLanguageConfig::Multi(vec![LspServerConfig {
5061                command: "zls".to_string(),
5062                args: vec![],
5063                enabled: true,
5064                auto_start: false,
5065                process_limits: ProcessLimits::default(),
5066                initialization_options: None,
5067                env: Default::default(),
5068                language_id_overrides: Default::default(),
5069                name: None,
5070                only_features: None,
5071                except_features: None,
5072                root_markers: Default::default(),
5073            }]),
5074        );
5075
5076        // jdtls - Eclipse JDT Language Server for Java
5077        // Install via package manager or download from Eclipse
5078        lsp.insert(
5079            "java".to_string(),
5080            LspLanguageConfig::Multi(vec![LspServerConfig {
5081                command: "jdtls".to_string(),
5082                args: vec![],
5083                enabled: true,
5084                auto_start: false,
5085                process_limits: ProcessLimits::default(),
5086                initialization_options: None,
5087                env: Default::default(),
5088                language_id_overrides: Default::default(),
5089                name: None,
5090                only_features: None,
5091                except_features: None,
5092                root_markers: vec![
5093                    "pom.xml".to_string(),
5094                    "build.gradle".to_string(),
5095                    "build.gradle.kts".to_string(),
5096                    ".git".to_string(),
5097                ],
5098            }]),
5099        );
5100
5101        // texlab - LaTeX Language Server (https://github.com/latex-lsp/texlab)
5102        // Install via cargo install texlab or package manager
5103        lsp.insert(
5104            "latex".to_string(),
5105            LspLanguageConfig::Multi(vec![LspServerConfig {
5106                command: "texlab".to_string(),
5107                args: vec![],
5108                enabled: true,
5109                auto_start: false,
5110                process_limits: ProcessLimits::default(),
5111                initialization_options: None,
5112                env: Default::default(),
5113                language_id_overrides: Default::default(),
5114                name: None,
5115                only_features: None,
5116                except_features: None,
5117                root_markers: Default::default(),
5118            }]),
5119        );
5120
5121        // marksman - Markdown Language Server (https://github.com/artempyanykh/marksman)
5122        // Install via package manager or download from releases
5123        lsp.insert(
5124            "markdown".to_string(),
5125            LspLanguageConfig::Multi(vec![LspServerConfig {
5126                command: "marksman".to_string(),
5127                args: vec!["server".to_string()],
5128                enabled: true,
5129                auto_start: false,
5130                process_limits: ProcessLimits::default(),
5131                initialization_options: None,
5132                env: Default::default(),
5133                language_id_overrides: Default::default(),
5134                name: None,
5135                only_features: None,
5136                except_features: None,
5137                root_markers: Default::default(),
5138            }]),
5139        );
5140
5141        // templ - Templ Language Server (https://templ.guide)
5142        // Install via go install github.com/a-h/templ/cmd/templ@latest
5143        lsp.insert(
5144            "templ".to_string(),
5145            LspLanguageConfig::Multi(vec![LspServerConfig {
5146                command: "templ".to_string(),
5147                args: vec!["lsp".to_string()],
5148                enabled: true,
5149                auto_start: false,
5150                process_limits: ProcessLimits::default(),
5151                initialization_options: None,
5152                env: Default::default(),
5153                language_id_overrides: Default::default(),
5154                name: None,
5155                only_features: None,
5156                except_features: None,
5157                root_markers: Default::default(),
5158            }]),
5159        );
5160
5161        // tinymist - Typst Language Server (https://github.com/Myriad-Dreamin/tinymist)
5162        // Install via cargo install tinymist or download from releases
5163        lsp.insert(
5164            "typst".to_string(),
5165            LspLanguageConfig::Multi(vec![LspServerConfig {
5166                command: "tinymist".to_string(),
5167                args: vec![],
5168                enabled: true,
5169                auto_start: false,
5170                process_limits: ProcessLimits::default(),
5171                initialization_options: None,
5172                env: Default::default(),
5173                language_id_overrides: Default::default(),
5174                name: None,
5175                only_features: None,
5176                except_features: None,
5177                root_markers: Default::default(),
5178            }]),
5179        );
5180
5181        // bash-language-server (installed via npm install -g bash-language-server)
5182        lsp.insert(
5183            "bash".to_string(),
5184            LspLanguageConfig::Multi(vec![LspServerConfig {
5185                command: "bash-language-server".to_string(),
5186                args: vec!["start".to_string()],
5187                enabled: true,
5188                auto_start: false,
5189                process_limits: ProcessLimits::default(),
5190                initialization_options: None,
5191                env: Default::default(),
5192                language_id_overrides: Default::default(),
5193                name: None,
5194                only_features: None,
5195                except_features: None,
5196                root_markers: Default::default(),
5197            }]),
5198        );
5199
5200        // lua-language-server (https://github.com/LuaLS/lua-language-server)
5201        // Install via package manager or download from releases
5202        lsp.insert(
5203            "lua".to_string(),
5204            LspLanguageConfig::Multi(vec![LspServerConfig {
5205                command: "lua-language-server".to_string(),
5206                args: vec![],
5207                enabled: true,
5208                auto_start: false,
5209                process_limits: ProcessLimits::default(),
5210                initialization_options: None,
5211                env: Default::default(),
5212                language_id_overrides: Default::default(),
5213                name: None,
5214                only_features: None,
5215                except_features: None,
5216                root_markers: vec![
5217                    ".luarc.json".to_string(),
5218                    ".luarc.jsonc".to_string(),
5219                    ".luacheckrc".to_string(),
5220                    ".stylua.toml".to_string(),
5221                    ".git".to_string(),
5222                ],
5223            }]),
5224        );
5225
5226        // solargraph - Ruby Language Server (installed via gem install solargraph)
5227        lsp.insert(
5228            "ruby".to_string(),
5229            LspLanguageConfig::Multi(vec![LspServerConfig {
5230                command: "solargraph".to_string(),
5231                args: vec!["stdio".to_string()],
5232                enabled: true,
5233                auto_start: false,
5234                process_limits: ProcessLimits::default(),
5235                initialization_options: None,
5236                env: Default::default(),
5237                language_id_overrides: Default::default(),
5238                name: None,
5239                only_features: None,
5240                except_features: None,
5241                root_markers: vec![
5242                    "Gemfile".to_string(),
5243                    ".ruby-version".to_string(),
5244                    ".git".to_string(),
5245                ],
5246            }]),
5247        );
5248
5249        // phpactor - PHP Language Server (https://phpactor.readthedocs.io)
5250        // Install via composer global require phpactor/phpactor
5251        lsp.insert(
5252            "php".to_string(),
5253            LspLanguageConfig::Multi(vec![LspServerConfig {
5254                command: "phpactor".to_string(),
5255                args: vec!["language-server".to_string()],
5256                enabled: true,
5257                auto_start: false,
5258                process_limits: ProcessLimits::default(),
5259                initialization_options: None,
5260                env: Default::default(),
5261                language_id_overrides: Default::default(),
5262                name: None,
5263                only_features: None,
5264                except_features: None,
5265                root_markers: vec!["composer.json".to_string(), ".git".to_string()],
5266            }]),
5267        );
5268
5269        // yaml-language-server (installed via npm install -g yaml-language-server)
5270        lsp.insert(
5271            "yaml".to_string(),
5272            LspLanguageConfig::Multi(vec![LspServerConfig {
5273                command: "yaml-language-server".to_string(),
5274                args: vec!["--stdio".to_string()],
5275                enabled: true,
5276                auto_start: false,
5277                process_limits: ProcessLimits::default(),
5278                initialization_options: None,
5279                env: Default::default(),
5280                language_id_overrides: Default::default(),
5281                name: None,
5282                only_features: None,
5283                except_features: None,
5284                root_markers: Default::default(),
5285            }]),
5286        );
5287
5288        // taplo - TOML Language Server (https://taplo.tamasfe.dev)
5289        // Install via cargo install taplo-cli or npm install -g @taplo/cli
5290        lsp.insert(
5291            "toml".to_string(),
5292            LspLanguageConfig::Multi(vec![LspServerConfig {
5293                command: "taplo".to_string(),
5294                args: vec!["lsp".to_string(), "stdio".to_string()],
5295                enabled: true,
5296                auto_start: false,
5297                process_limits: ProcessLimits::default(),
5298                initialization_options: None,
5299                env: Default::default(),
5300                language_id_overrides: Default::default(),
5301                name: None,
5302                only_features: None,
5303                except_features: None,
5304                root_markers: Default::default(),
5305            }]),
5306        );
5307
5308        // dart - Dart Language Server (#1252)
5309        // Included with the Dart SDK
5310        lsp.insert(
5311            "dart".to_string(),
5312            LspLanguageConfig::Multi(vec![LspServerConfig {
5313                command: "dart".to_string(),
5314                args: vec!["language-server".to_string(), "--protocol=lsp".to_string()],
5315                enabled: true,
5316                auto_start: false,
5317                process_limits: ProcessLimits::default(),
5318                initialization_options: None,
5319                env: Default::default(),
5320                language_id_overrides: Default::default(),
5321                name: None,
5322                only_features: None,
5323                except_features: None,
5324                root_markers: vec!["pubspec.yaml".to_string(), ".git".to_string()],
5325            }]),
5326        );
5327
5328        // nu - Nushell Language Server (#1031)
5329        // Built into the Nushell binary
5330        lsp.insert(
5331            "nushell".to_string(),
5332            LspLanguageConfig::Multi(vec![LspServerConfig {
5333                command: "nu".to_string(),
5334                args: vec!["--lsp".to_string()],
5335                enabled: true,
5336                auto_start: false,
5337                process_limits: ProcessLimits::default(),
5338                initialization_options: None,
5339                env: Default::default(),
5340                language_id_overrides: Default::default(),
5341                name: None,
5342                only_features: None,
5343                except_features: None,
5344                root_markers: Default::default(),
5345            }]),
5346        );
5347
5348        // solc - Solidity Language Server (#857)
5349        // Install via npm install -g @nomicfoundation/solidity-language-server
5350        lsp.insert(
5351            "solidity".to_string(),
5352            LspLanguageConfig::Multi(vec![LspServerConfig {
5353                command: "nomicfoundation-solidity-language-server".to_string(),
5354                args: vec!["--stdio".to_string()],
5355                enabled: true,
5356                auto_start: false,
5357                process_limits: ProcessLimits::default(),
5358                initialization_options: None,
5359                env: Default::default(),
5360                language_id_overrides: Default::default(),
5361                name: None,
5362                only_features: None,
5363                except_features: None,
5364                root_markers: Default::default(),
5365            }]),
5366        );
5367
5368        // --- DevOps / infrastructure LSP servers ---
5369
5370        // terraform-ls - Terraform Language Server (https://github.com/hashicorp/terraform-ls)
5371        // Install via package manager or download from releases
5372        lsp.insert(
5373            "terraform".to_string(),
5374            LspLanguageConfig::Multi(vec![LspServerConfig {
5375                command: "terraform-ls".to_string(),
5376                args: vec!["serve".to_string()],
5377                enabled: true,
5378                auto_start: false,
5379                process_limits: ProcessLimits::default(),
5380                initialization_options: None,
5381                env: Default::default(),
5382                language_id_overrides: Default::default(),
5383                name: None,
5384                only_features: None,
5385                except_features: None,
5386                root_markers: vec![
5387                    "*.tf".to_string(),
5388                    ".terraform".to_string(),
5389                    ".git".to_string(),
5390                ],
5391            }]),
5392        );
5393
5394        // cmake-language-server (https://github.com/regen100/cmake-language-server)
5395        // Install via pip: pip install cmake-language-server
5396        lsp.insert(
5397            "cmake".to_string(),
5398            LspLanguageConfig::Multi(vec![LspServerConfig {
5399                command: "cmake-language-server".to_string(),
5400                args: vec![],
5401                enabled: true,
5402                auto_start: false,
5403                process_limits: ProcessLimits::default(),
5404                initialization_options: None,
5405                env: Default::default(),
5406                language_id_overrides: Default::default(),
5407                name: None,
5408                only_features: None,
5409                except_features: None,
5410                root_markers: vec!["CMakeLists.txt".to_string(), ".git".to_string()],
5411            }]),
5412        );
5413
5414        // buf - Protobuf Language Server (https://buf.build)
5415        // Install via package manager or curl
5416        lsp.insert(
5417            "protobuf".to_string(),
5418            LspLanguageConfig::Multi(vec![LspServerConfig {
5419                command: "buf".to_string(),
5420                args: vec!["beta".to_string(), "lsp".to_string()],
5421                enabled: true,
5422                auto_start: false,
5423                process_limits: ProcessLimits::default(),
5424                initialization_options: None,
5425                env: Default::default(),
5426                language_id_overrides: Default::default(),
5427                name: None,
5428                only_features: None,
5429                except_features: None,
5430                root_markers: Default::default(),
5431            }]),
5432        );
5433
5434        // graphql-lsp (https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service-cli)
5435        // Install via npm: npm install -g graphql-language-service-cli
5436        lsp.insert(
5437            "graphql".to_string(),
5438            LspLanguageConfig::Multi(vec![LspServerConfig {
5439                command: "graphql-lsp".to_string(),
5440                args: vec!["server".to_string(), "-m".to_string(), "stream".to_string()],
5441                enabled: true,
5442                auto_start: false,
5443                process_limits: ProcessLimits::default(),
5444                initialization_options: None,
5445                env: Default::default(),
5446                language_id_overrides: Default::default(),
5447                name: None,
5448                only_features: None,
5449                except_features: None,
5450                root_markers: Default::default(),
5451            }]),
5452        );
5453
5454        // sqls - SQL Language Server (https://github.com/sqls-server/sqls)
5455        // Install via go: go install github.com/sqls-server/sqls@latest
5456        lsp.insert(
5457            "sql".to_string(),
5458            LspLanguageConfig::Multi(vec![LspServerConfig {
5459                command: "sqls".to_string(),
5460                args: vec![],
5461                enabled: true,
5462                auto_start: false,
5463                process_limits: ProcessLimits::default(),
5464                initialization_options: None,
5465                env: Default::default(),
5466                language_id_overrides: Default::default(),
5467                name: None,
5468                only_features: None,
5469                except_features: None,
5470                root_markers: Default::default(),
5471            }]),
5472        );
5473
5474        // --- Web framework LSP servers ---
5475
5476        // vue-language-server (installed via npm install -g @vue/language-server)
5477        lsp.insert(
5478            "vue".to_string(),
5479            LspLanguageConfig::Multi(vec![LspServerConfig {
5480                command: "vue-language-server".to_string(),
5481                args: vec!["--stdio".to_string()],
5482                enabled: true,
5483                auto_start: false,
5484                process_limits: ProcessLimits::default(),
5485                initialization_options: None,
5486                env: Default::default(),
5487                language_id_overrides: Default::default(),
5488                name: None,
5489                only_features: None,
5490                except_features: None,
5491                root_markers: Default::default(),
5492            }]),
5493        );
5494
5495        // svelte-language-server (installed via npm install -g svelte-language-server)
5496        lsp.insert(
5497            "svelte".to_string(),
5498            LspLanguageConfig::Multi(vec![LspServerConfig {
5499                command: "svelteserver".to_string(),
5500                args: vec!["--stdio".to_string()],
5501                enabled: true,
5502                auto_start: false,
5503                process_limits: ProcessLimits::default(),
5504                initialization_options: None,
5505                env: Default::default(),
5506                language_id_overrides: Default::default(),
5507                name: None,
5508                only_features: None,
5509                except_features: None,
5510                root_markers: Default::default(),
5511            }]),
5512        );
5513
5514        // astro-ls - Astro Language Server (installed via npm install -g @astrojs/language-server)
5515        lsp.insert(
5516            "astro".to_string(),
5517            LspLanguageConfig::Multi(vec![LspServerConfig {
5518                command: "astro-ls".to_string(),
5519                args: vec!["--stdio".to_string()],
5520                enabled: true,
5521                auto_start: false,
5522                process_limits: ProcessLimits::default(),
5523                initialization_options: None,
5524                env: Default::default(),
5525                language_id_overrides: Default::default(),
5526                name: None,
5527                only_features: None,
5528                except_features: None,
5529                root_markers: Default::default(),
5530            }]),
5531        );
5532
5533        // tailwindcss-language-server (installed via npm install -g @tailwindcss/language-server)
5534        lsp.insert(
5535            "tailwindcss".to_string(),
5536            LspLanguageConfig::Multi(vec![LspServerConfig {
5537                command: "tailwindcss-language-server".to_string(),
5538                args: vec!["--stdio".to_string()],
5539                enabled: true,
5540                auto_start: false,
5541                process_limits: ProcessLimits::default(),
5542                initialization_options: None,
5543                env: Default::default(),
5544                language_id_overrides: Default::default(),
5545                name: None,
5546                only_features: None,
5547                except_features: None,
5548                root_markers: Default::default(),
5549            }]),
5550        );
5551
5552        // --- Programming language LSP servers ---
5553
5554        // nil - Nix Language Server (https://github.com/oxalica/nil)
5555        // Install via nix profile install github:oxalica/nil
5556        lsp.insert(
5557            "nix".to_string(),
5558            LspLanguageConfig::Multi(vec![LspServerConfig {
5559                command: "nil".to_string(),
5560                args: vec![],
5561                enabled: true,
5562                auto_start: false,
5563                process_limits: ProcessLimits::default(),
5564                initialization_options: None,
5565                env: Default::default(),
5566                language_id_overrides: Default::default(),
5567                name: None,
5568                only_features: None,
5569                except_features: None,
5570                root_markers: Default::default(),
5571            }]),
5572        );
5573
5574        // kotlin-language-server (https://github.com/fwcd/kotlin-language-server)
5575        // Install via package manager or build from source
5576        lsp.insert(
5577            "kotlin".to_string(),
5578            LspLanguageConfig::Multi(vec![LspServerConfig {
5579                command: "kotlin-language-server".to_string(),
5580                args: vec![],
5581                enabled: true,
5582                auto_start: false,
5583                process_limits: ProcessLimits::default(),
5584                initialization_options: None,
5585                env: Default::default(),
5586                language_id_overrides: Default::default(),
5587                name: None,
5588                only_features: None,
5589                except_features: None,
5590                root_markers: Default::default(),
5591            }]),
5592        );
5593
5594        // sourcekit-lsp - Swift Language Server (included with Swift toolchain)
5595        lsp.insert(
5596            "swift".to_string(),
5597            LspLanguageConfig::Multi(vec![LspServerConfig {
5598                command: "sourcekit-lsp".to_string(),
5599                args: vec![],
5600                enabled: true,
5601                auto_start: false,
5602                process_limits: ProcessLimits::default(),
5603                initialization_options: None,
5604                env: Default::default(),
5605                language_id_overrides: Default::default(),
5606                name: None,
5607                only_features: None,
5608                except_features: None,
5609                root_markers: Default::default(),
5610            }]),
5611        );
5612
5613        // metals - Scala Language Server (https://scalameta.org/metals/)
5614        // Install via coursier: cs install metals
5615        lsp.insert(
5616            "scala".to_string(),
5617            LspLanguageConfig::Multi(vec![LspServerConfig {
5618                command: "metals".to_string(),
5619                args: vec![],
5620                enabled: true,
5621                auto_start: false,
5622                process_limits: ProcessLimits::default(),
5623                initialization_options: None,
5624                env: Default::default(),
5625                language_id_overrides: Default::default(),
5626                name: None,
5627                only_features: None,
5628                except_features: None,
5629                root_markers: Default::default(),
5630            }]),
5631        );
5632
5633        // elixir-ls - Elixir Language Server (https://github.com/elixir-lsp/elixir-ls)
5634        // Install via mix: mix escript.install hex elixir_ls
5635        lsp.insert(
5636            "elixir".to_string(),
5637            LspLanguageConfig::Multi(vec![LspServerConfig {
5638                command: "elixir-ls".to_string(),
5639                args: vec![],
5640                enabled: true,
5641                auto_start: false,
5642                process_limits: ProcessLimits::default(),
5643                initialization_options: None,
5644                env: Default::default(),
5645                language_id_overrides: Default::default(),
5646                name: None,
5647                only_features: None,
5648                except_features: None,
5649                root_markers: Default::default(),
5650            }]),
5651        );
5652
5653        // erlang_ls - Erlang Language Server (https://github.com/erlang-ls/erlang_ls)
5654        lsp.insert(
5655            "erlang".to_string(),
5656            LspLanguageConfig::Multi(vec![LspServerConfig {
5657                command: "erlang_ls".to_string(),
5658                args: vec![],
5659                enabled: true,
5660                auto_start: false,
5661                process_limits: ProcessLimits::default(),
5662                initialization_options: None,
5663                env: Default::default(),
5664                language_id_overrides: Default::default(),
5665                name: None,
5666                only_features: None,
5667                except_features: None,
5668                root_markers: Default::default(),
5669            }]),
5670        );
5671
5672        // haskell-language-server (https://github.com/haskell/haskell-language-server)
5673        // Install via ghcup: ghcup install hls
5674        lsp.insert(
5675            "haskell".to_string(),
5676            LspLanguageConfig::Multi(vec![LspServerConfig {
5677                command: "haskell-language-server-wrapper".to_string(),
5678                args: vec!["--lsp".to_string()],
5679                enabled: true,
5680                auto_start: false,
5681                process_limits: ProcessLimits::default(),
5682                initialization_options: None,
5683                env: Default::default(),
5684                language_id_overrides: Default::default(),
5685                name: None,
5686                only_features: None,
5687                except_features: None,
5688                root_markers: Default::default(),
5689            }]),
5690        );
5691
5692        // ocamllsp - OCaml Language Server (https://github.com/ocaml/ocaml-lsp)
5693        // Install via opam: opam install ocaml-lsp-server
5694        lsp.insert(
5695            "ocaml".to_string(),
5696            LspLanguageConfig::Multi(vec![LspServerConfig {
5697                command: "ocamllsp".to_string(),
5698                args: vec![],
5699                enabled: true,
5700                auto_start: false,
5701                process_limits: ProcessLimits::default(),
5702                initialization_options: None,
5703                env: Default::default(),
5704                language_id_overrides: Default::default(),
5705                name: None,
5706                only_features: None,
5707                except_features: None,
5708                root_markers: Default::default(),
5709            }]),
5710        );
5711
5712        // clojure-lsp (https://github.com/clojure-lsp/clojure-lsp)
5713        // Install via package manager or download from releases
5714        lsp.insert(
5715            "clojure".to_string(),
5716            LspLanguageConfig::Multi(vec![LspServerConfig {
5717                command: "clojure-lsp".to_string(),
5718                args: vec![],
5719                enabled: true,
5720                auto_start: false,
5721                process_limits: ProcessLimits::default(),
5722                initialization_options: None,
5723                env: Default::default(),
5724                language_id_overrides: Default::default(),
5725                name: None,
5726                only_features: None,
5727                except_features: None,
5728                root_markers: Default::default(),
5729            }]),
5730        );
5731
5732        // r-languageserver (https://github.com/REditorSupport/languageserver)
5733        // Install via R: install.packages("languageserver")
5734        lsp.insert(
5735            "r".to_string(),
5736            LspLanguageConfig::Multi(vec![LspServerConfig {
5737                command: "R".to_string(),
5738                args: vec![
5739                    "--vanilla".to_string(),
5740                    "-e".to_string(),
5741                    "languageserver::run()".to_string(),
5742                ],
5743                enabled: true,
5744                auto_start: false,
5745                process_limits: ProcessLimits::default(),
5746                initialization_options: None,
5747                env: Default::default(),
5748                language_id_overrides: Default::default(),
5749                name: None,
5750                only_features: None,
5751                except_features: None,
5752                root_markers: Default::default(),
5753            }]),
5754        );
5755
5756        // julia LanguageServer.jl (https://github.com/julia-vscode/LanguageServer.jl)
5757        // Install via Julia: using Pkg; Pkg.add("LanguageServer")
5758        lsp.insert(
5759            "julia".to_string(),
5760            LspLanguageConfig::Multi(vec![LspServerConfig {
5761                command: "julia".to_string(),
5762                args: vec![
5763                    "--startup-file=no".to_string(),
5764                    "--history-file=no".to_string(),
5765                    "-e".to_string(),
5766                    "using LanguageServer; runserver()".to_string(),
5767                ],
5768                enabled: true,
5769                auto_start: false,
5770                process_limits: ProcessLimits::default(),
5771                initialization_options: None,
5772                env: Default::default(),
5773                language_id_overrides: Default::default(),
5774                name: None,
5775                only_features: None,
5776                except_features: None,
5777                root_markers: Default::default(),
5778            }]),
5779        );
5780
5781        // PerlNavigator (https://github.com/bscan/PerlNavigator)
5782        // Install via npm: npm install -g perlnavigator-server
5783        lsp.insert(
5784            "perl".to_string(),
5785            LspLanguageConfig::Multi(vec![LspServerConfig {
5786                command: "perlnavigator".to_string(),
5787                args: vec!["--stdio".to_string()],
5788                enabled: true,
5789                auto_start: false,
5790                process_limits: ProcessLimits::default(),
5791                initialization_options: None,
5792                env: Default::default(),
5793                language_id_overrides: Default::default(),
5794                name: None,
5795                only_features: None,
5796                except_features: None,
5797                root_markers: Default::default(),
5798            }]),
5799        );
5800
5801        // nimlangserver - Nim Language Server (https://github.com/nim-lang/langserver)
5802        // Install via nimble: nimble install nimlangserver
5803        lsp.insert(
5804            "nim".to_string(),
5805            LspLanguageConfig::Multi(vec![LspServerConfig {
5806                command: "nimlangserver".to_string(),
5807                args: vec![],
5808                enabled: true,
5809                auto_start: false,
5810                process_limits: ProcessLimits::default(),
5811                initialization_options: None,
5812                env: Default::default(),
5813                language_id_overrides: Default::default(),
5814                name: None,
5815                only_features: None,
5816                except_features: None,
5817                root_markers: Default::default(),
5818            }]),
5819        );
5820
5821        // gleam lsp - Gleam Language Server (built into the gleam binary)
5822        lsp.insert(
5823            "gleam".to_string(),
5824            LspLanguageConfig::Multi(vec![LspServerConfig {
5825                command: "gleam".to_string(),
5826                args: vec!["lsp".to_string()],
5827                enabled: true,
5828                auto_start: false,
5829                process_limits: ProcessLimits::default(),
5830                initialization_options: None,
5831                env: Default::default(),
5832                language_id_overrides: Default::default(),
5833                name: None,
5834                only_features: None,
5835                except_features: None,
5836                root_markers: Default::default(),
5837            }]),
5838        );
5839
5840        // fsharp - F# Language Server (https://github.com/fsharp/FsAutoComplete)
5841        // Install via dotnet: dotnet tool install -g fsautocomplete
5842        lsp.insert(
5843            "fsharp".to_string(),
5844            LspLanguageConfig::Multi(vec![LspServerConfig {
5845                command: "fsautocomplete".to_string(),
5846                args: vec!["--adaptive-lsp-server-enabled".to_string()],
5847                enabled: true,
5848                auto_start: false,
5849                process_limits: ProcessLimits::default(),
5850                initialization_options: None,
5851                env: Default::default(),
5852                language_id_overrides: Default::default(),
5853                name: None,
5854                only_features: None,
5855                except_features: None,
5856                root_markers: Default::default(),
5857            }]),
5858        );
5859    }
5860    pub fn validate(&self) -> Result<(), ConfigError> {
5861        // Validate tab size
5862        if self.editor.tab_size == 0 {
5863            return Err(ConfigError::ValidationError(
5864                "tab_size must be greater than 0".to_string(),
5865            ));
5866        }
5867
5868        // Validate scroll offset
5869        if self.editor.scroll_offset > 100 {
5870            return Err(ConfigError::ValidationError(
5871                "scroll_offset must be <= 100".to_string(),
5872            ));
5873        }
5874
5875        // Validate keybindings
5876        for binding in &self.keybindings {
5877            if binding.key.is_empty() {
5878                return Err(ConfigError::ValidationError(
5879                    "keybinding key cannot be empty".to_string(),
5880                ));
5881            }
5882            if binding.action.is_empty() {
5883                return Err(ConfigError::ValidationError(
5884                    "keybinding action cannot be empty".to_string(),
5885                ));
5886            }
5887        }
5888
5889        Ok(())
5890    }
5891}
5892
5893/// Configuration error types
5894#[derive(Debug)]
5895pub enum ConfigError {
5896    IoError(String),
5897    ParseError(String),
5898    SerializeError(String),
5899    ValidationError(String),
5900}
5901
5902impl std::fmt::Display for ConfigError {
5903    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5904        match self {
5905            Self::IoError(msg) => write!(f, "IO error: {msg}"),
5906            Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
5907            Self::SerializeError(msg) => write!(f, "Serialize error: {msg}"),
5908            Self::ValidationError(msg) => write!(f, "Validation error: {msg}"),
5909        }
5910    }
5911}
5912
5913impl std::error::Error for ConfigError {}
5914
5915#[cfg(test)]
5916mod tests {
5917    use super::*;
5918
5919    #[test]
5920    fn test_default_config() {
5921        let config = Config::default();
5922        assert_eq!(config.editor.tab_size, 4);
5923        assert!(config.editor.line_numbers);
5924        assert!(config.editor.syntax_highlighting);
5925        // keybindings is empty by design - it's for user customizations only
5926        // The actual keybindings come from resolve_keymap(active_keybinding_map)
5927        assert!(config.keybindings.is_empty());
5928        // But the resolved keymap should have bindings
5929        let resolved = config.resolve_keymap(&config.active_keybinding_map);
5930        assert!(!resolved.is_empty());
5931    }
5932
5933    #[test]
5934    fn test_all_builtin_keymaps_loadable() {
5935        for name in KeybindingMapName::BUILTIN_OPTIONS {
5936            let keymap = Config::load_builtin_keymap(name);
5937            assert!(keymap.is_some(), "Failed to load builtin keymap '{}'", name);
5938        }
5939    }
5940
5941    #[test]
5942    fn test_config_validation() {
5943        let mut config = Config::default();
5944        assert!(config.validate().is_ok());
5945
5946        config.editor.tab_size = 0;
5947        assert!(config.validate().is_err());
5948    }
5949
5950    #[test]
5951    fn test_macos_keymap_inherits_enter_bindings() {
5952        let config = Config::default();
5953        let bindings = config.resolve_keymap("macos");
5954
5955        let enter_bindings: Vec<_> = bindings.iter().filter(|b| b.key == "Enter").collect();
5956        assert!(
5957            !enter_bindings.is_empty(),
5958            "macos keymap should inherit Enter bindings from default, got {} Enter bindings",
5959            enter_bindings.len()
5960        );
5961        // Should have at least insert_newline for normal mode
5962        let has_insert_newline = enter_bindings.iter().any(|b| b.action == "insert_newline");
5963        assert!(
5964            has_insert_newline,
5965            "macos keymap should have insert_newline action for Enter key"
5966        );
5967    }
5968
5969    #[test]
5970    fn test_config_serialize_deserialize() {
5971        // Test that Config can be serialized and deserialized correctly
5972        let config = Config::default();
5973
5974        // Serialize to JSON
5975        let json = serde_json::to_string_pretty(&config).unwrap();
5976
5977        // Deserialize back
5978        let loaded: Config = serde_json::from_str(&json).unwrap();
5979
5980        assert_eq!(config.editor.tab_size, loaded.editor.tab_size);
5981        assert_eq!(config.theme, loaded.theme);
5982    }
5983
5984    #[test]
5985    fn test_config_with_custom_keybinding() {
5986        let json = r#"{
5987            "editor": {
5988                "tab_size": 2
5989            },
5990            "keybindings": [
5991                {
5992                    "key": "x",
5993                    "modifiers": ["ctrl", "shift"],
5994                    "action": "custom_action",
5995                    "args": {},
5996                    "when": null
5997                }
5998            ]
5999        }"#;
6000
6001        let config: Config = serde_json::from_str(json).unwrap();
6002        assert_eq!(config.editor.tab_size, 2);
6003        assert_eq!(config.keybindings.len(), 1);
6004        assert_eq!(config.keybindings[0].key, "x");
6005        assert_eq!(config.keybindings[0].modifiers.len(), 2);
6006    }
6007
6008    #[test]
6009    fn test_sparse_config_merges_with_defaults() {
6010        // User config that only specifies one LSP server
6011        let temp_dir = tempfile::tempdir().unwrap();
6012        let config_path = temp_dir.path().join("config.json");
6013
6014        // Write a sparse config - only overriding rust LSP
6015        let sparse_config = r#"{
6016            "lsp": {
6017                "rust": {
6018                    "command": "custom-rust-analyzer",
6019                    "args": ["--custom-arg"]
6020                }
6021            }
6022        }"#;
6023        std::fs::write(&config_path, sparse_config).unwrap();
6024
6025        // Load the config - should merge with defaults
6026        let loaded = Config::load_from_file(&config_path).unwrap();
6027
6028        // User's rust override should be present
6029        assert!(loaded.lsp.contains_key("rust"));
6030        assert_eq!(
6031            loaded.lsp["rust"].as_slice()[0].command,
6032            "custom-rust-analyzer".to_string()
6033        );
6034
6035        // Default LSP servers should also be present (merged from defaults)
6036        assert!(
6037            loaded.lsp.contains_key("python"),
6038            "python LSP should be merged from defaults"
6039        );
6040        assert!(
6041            loaded.lsp.contains_key("typescript"),
6042            "typescript LSP should be merged from defaults"
6043        );
6044        assert!(
6045            loaded.lsp.contains_key("javascript"),
6046            "javascript LSP should be merged from defaults"
6047        );
6048
6049        // Default language configs should also be present
6050        assert!(loaded.languages.contains_key("rust"));
6051        assert!(loaded.languages.contains_key("python"));
6052        assert!(loaded.languages.contains_key("typescript"));
6053    }
6054
6055    #[test]
6056    fn test_empty_config_gets_all_defaults() {
6057        let temp_dir = tempfile::tempdir().unwrap();
6058        let config_path = temp_dir.path().join("config.json");
6059
6060        // Write an empty config
6061        std::fs::write(&config_path, "{}").unwrap();
6062
6063        let loaded = Config::load_from_file(&config_path).unwrap();
6064        let defaults = Config::default();
6065
6066        // Should have all default LSP servers
6067        assert_eq!(loaded.lsp.len(), defaults.lsp.len());
6068
6069        // Should have all default languages
6070        assert_eq!(loaded.languages.len(), defaults.languages.len());
6071    }
6072
6073    #[test]
6074    fn test_dynamic_submenu_expansion() {
6075        // Test that DynamicSubmenu expands to Submenu with generated items
6076        let temp_dir = tempfile::tempdir().unwrap();
6077        let themes_dir = temp_dir.path().to_path_buf();
6078
6079        let dynamic = MenuItem::DynamicSubmenu {
6080            label: "Test".to_string(),
6081            source: "copy_with_theme".to_string(),
6082        };
6083
6084        let expanded = dynamic.expand_dynamic(&themes_dir);
6085
6086        // Should expand to a Submenu
6087        match expanded {
6088            MenuItem::Submenu { label, items } => {
6089                assert_eq!(label, "Test");
6090                // Should have items for each available theme (embedded themes only, no user themes in temp dir)
6091                let loader = crate::view::theme::ThemeLoader::embedded_only();
6092                let registry = loader.load_all(&[]);
6093                assert_eq!(items.len(), registry.len());
6094
6095                // Each item should be an Action with copy_with_theme
6096                for (item, theme_info) in items.iter().zip(registry.list().iter()) {
6097                    match item {
6098                        MenuItem::Action {
6099                            label,
6100                            action,
6101                            args,
6102                            ..
6103                        } => {
6104                            assert_eq!(label, &theme_info.name);
6105                            assert_eq!(action, "copy_with_theme");
6106                            assert_eq!(
6107                                args.get("theme").and_then(|v| v.as_str()),
6108                                Some(theme_info.name.as_str())
6109                            );
6110                        }
6111                        _ => panic!("Expected Action item"),
6112                    }
6113                }
6114            }
6115            _ => panic!("Expected Submenu after expansion"),
6116        }
6117    }
6118
6119    #[test]
6120    fn test_non_dynamic_item_unchanged() {
6121        // Non-DynamicSubmenu items should be unchanged by expand_dynamic
6122        let temp_dir = tempfile::tempdir().unwrap();
6123        let themes_dir = temp_dir.path();
6124
6125        let action = MenuItem::Action {
6126            label: "Test".to_string(),
6127            action: "test".to_string(),
6128            args: HashMap::new(),
6129            when: None,
6130            checkbox: None,
6131        };
6132
6133        let expanded = action.expand_dynamic(themes_dir);
6134        match expanded {
6135            MenuItem::Action { label, action, .. } => {
6136                assert_eq!(label, "Test");
6137                assert_eq!(action, "test");
6138            }
6139            _ => panic!("Action should remain Action after expand_dynamic"),
6140        }
6141    }
6142
6143    #[test]
6144    fn test_buffer_config_uses_global_defaults() {
6145        let config = Config::default();
6146        let buffer_config = BufferConfig::resolve(&config, None);
6147
6148        assert_eq!(buffer_config.tab_size, config.editor.tab_size);
6149        assert_eq!(buffer_config.auto_indent, config.editor.auto_indent);
6150        assert!(!buffer_config.use_tabs); // Default is spaces
6151        assert!(buffer_config.whitespace.any_tabs()); // Tabs visible by default
6152        assert!(buffer_config.formatter.is_none());
6153        assert!(!buffer_config.format_on_save);
6154    }
6155
6156    #[test]
6157    fn test_buffer_config_applies_language_overrides() {
6158        let mut config = Config::default();
6159
6160        // Add a language config with custom settings
6161        config.languages.insert(
6162            "go".to_string(),
6163            LanguageConfig {
6164                extensions: vec!["go".to_string()],
6165                filenames: vec![],
6166                grammar: "go".to_string(),
6167                comment_prefix: Some("//".to_string()),
6168                auto_indent: true,
6169                auto_close: None,
6170                auto_surround: None,
6171                textmate_grammar: None,
6172                show_whitespace_tabs: false, // Go hides tab indicators
6173                line_wrap: None,
6174                wrap_column: None,
6175                page_view: None,
6176                page_width: None,
6177                use_tabs: Some(true), // Go uses tabs
6178                tab_size: Some(8),    // Go uses 8-space tabs
6179                formatter: Some(FormatterConfig {
6180                    command: "gofmt".to_string(),
6181                    args: vec![],
6182                    stdin: true,
6183                    timeout_ms: 10000,
6184                }),
6185                format_on_save: true,
6186                on_save: vec![],
6187                word_characters: None,
6188            },
6189        );
6190
6191        let buffer_config = BufferConfig::resolve(&config, Some("go"));
6192
6193        assert_eq!(buffer_config.tab_size, 8);
6194        assert!(buffer_config.use_tabs);
6195        assert!(!buffer_config.whitespace.any_tabs()); // Go disables tab indicators
6196        assert!(buffer_config.format_on_save);
6197        assert!(buffer_config.formatter.is_some());
6198        assert_eq!(buffer_config.formatter.as_ref().unwrap().command, "gofmt");
6199    }
6200
6201    #[test]
6202    fn test_buffer_config_unknown_language_uses_global() {
6203        let config = Config::default();
6204        let buffer_config = BufferConfig::resolve(&config, Some("unknown_lang"));
6205
6206        // Should fall back to global settings
6207        assert_eq!(buffer_config.tab_size, config.editor.tab_size);
6208        assert!(!buffer_config.use_tabs);
6209    }
6210
6211    #[test]
6212    fn test_buffer_config_per_language_line_wrap() {
6213        let mut config = Config::default();
6214        config.editor.line_wrap = false;
6215
6216        // Add markdown with line_wrap override
6217        config.languages.insert(
6218            "markdown".to_string(),
6219            LanguageConfig {
6220                extensions: vec!["md".to_string()],
6221                line_wrap: Some(true),
6222                ..Default::default()
6223            },
6224        );
6225
6226        // Markdown should override global line_wrap=false
6227        let md_config = BufferConfig::resolve(&config, Some("markdown"));
6228        assert!(md_config.line_wrap, "Markdown should have line_wrap=true");
6229
6230        // Other languages should use global default (false)
6231        let other_config = BufferConfig::resolve(&config, Some("rust"));
6232        assert!(
6233            !other_config.line_wrap,
6234            "Non-configured languages should use global line_wrap=false"
6235        );
6236
6237        // No language should use global default
6238        let no_lang_config = BufferConfig::resolve(&config, None);
6239        assert!(
6240            !no_lang_config.line_wrap,
6241            "No language should use global line_wrap=false"
6242        );
6243    }
6244
6245    #[test]
6246    fn test_buffer_config_per_language_wrap_column() {
6247        let mut config = Config::default();
6248        config.editor.wrap_column = Some(120);
6249
6250        // Add markdown with wrap_column override
6251        config.languages.insert(
6252            "markdown".to_string(),
6253            LanguageConfig {
6254                extensions: vec!["md".to_string()],
6255                wrap_column: Some(80),
6256                ..Default::default()
6257            },
6258        );
6259
6260        // Markdown should use its own wrap_column
6261        let md_config = BufferConfig::resolve(&config, Some("markdown"));
6262        assert_eq!(md_config.wrap_column, Some(80));
6263
6264        // Other languages should use global wrap_column
6265        let other_config = BufferConfig::resolve(&config, Some("rust"));
6266        assert_eq!(other_config.wrap_column, Some(120));
6267
6268        // No language should use global wrap_column
6269        let no_lang_config = BufferConfig::resolve(&config, None);
6270        assert_eq!(no_lang_config.wrap_column, Some(120));
6271    }
6272
6273    #[test]
6274    fn test_buffer_config_indent_string() {
6275        let config = Config::default();
6276
6277        // Spaces indent
6278        let spaces_config = BufferConfig::resolve(&config, None);
6279        assert_eq!(spaces_config.indent_string(), "    "); // 4 spaces
6280
6281        // Tabs indent - create a language that uses tabs
6282        let mut config_with_tabs = Config::default();
6283        config_with_tabs.languages.insert(
6284            "makefile".to_string(),
6285            LanguageConfig {
6286                use_tabs: Some(true),
6287                tab_size: Some(8),
6288                ..Default::default()
6289            },
6290        );
6291        let tabs_config = BufferConfig::resolve(&config_with_tabs, Some("makefile"));
6292        assert_eq!(tabs_config.indent_string(), "\t");
6293    }
6294
6295    #[test]
6296    fn test_buffer_config_global_use_tabs_inherited() {
6297        // When editor.use_tabs is true, buffers without a language-specific
6298        // override should inherit the global setting.
6299        let mut config = Config::default();
6300        config.editor.use_tabs = true;
6301
6302        // Unknown language inherits global
6303        let buffer_config = BufferConfig::resolve(&config, Some("unknown_lang"));
6304        assert!(buffer_config.use_tabs);
6305
6306        // No language inherits global
6307        let buffer_config = BufferConfig::resolve(&config, None);
6308        assert!(buffer_config.use_tabs);
6309
6310        // Language with explicit use_tabs: Some(false) overrides global
6311        config.languages.insert(
6312            "python".to_string(),
6313            LanguageConfig {
6314                use_tabs: Some(false),
6315                ..Default::default()
6316            },
6317        );
6318        let buffer_config = BufferConfig::resolve(&config, Some("python"));
6319        assert!(!buffer_config.use_tabs);
6320
6321        // Language with use_tabs: None inherits global true
6322        config.languages.insert(
6323            "rust".to_string(),
6324            LanguageConfig {
6325                use_tabs: None,
6326                ..Default::default()
6327            },
6328        );
6329        let buffer_config = BufferConfig::resolve(&config, Some("rust"));
6330        assert!(buffer_config.use_tabs);
6331    }
6332
6333    /// Verify that every LSP config key has a matching entry in default_languages().
6334    /// Without this, detect_language() won't map file extensions to the language name,
6335    /// causing "No LSP server configured for this file type" even though the LSP config
6336    /// exists. The only exception is "tailwindcss" which attaches to CSS/HTML/JS files
6337    /// rather than having its own file type.
6338    #[test]
6339    #[cfg(feature = "runtime")]
6340    fn test_lsp_languages_have_language_config() {
6341        let config = Config::default();
6342        let exceptions = ["tailwindcss"];
6343        for lsp_key in config.lsp.keys() {
6344            if exceptions.contains(&lsp_key.as_str()) {
6345                continue;
6346            }
6347            assert!(
6348                config.languages.contains_key(lsp_key),
6349                "LSP config key '{}' has no matching entry in default_languages(). \
6350                 Add a LanguageConfig with the correct file extensions so detect_language() \
6351                 can map files to this language.",
6352                lsp_key
6353            );
6354        }
6355    }
6356
6357    #[test]
6358    #[cfg(feature = "runtime")]
6359    fn test_default_config_has_quicklsp_in_universal_lsp() {
6360        let config = Config::default();
6361        assert!(
6362            config.universal_lsp.contains_key("quicklsp"),
6363            "Default config should contain quicklsp in universal_lsp"
6364        );
6365        let quicklsp = &config.universal_lsp["quicklsp"];
6366        let server = &quicklsp.as_slice()[0];
6367        assert_eq!(server.command, "quicklsp");
6368        assert!(!server.enabled, "quicklsp should be disabled by default");
6369        assert_eq!(server.name.as_deref(), Some("QuickLSP"));
6370        // only_features must stay unset so quicklsp can serve every capability
6371        // its server advertises — including go-to-definition. A restrictive
6372        // whitelist here silently breaks F12 for users on a vanilla install.
6373        assert!(
6374            server.only_features.is_none(),
6375            "quicklsp must not default to a feature whitelist"
6376        );
6377        assert!(server.except_features.is_none());
6378    }
6379
6380    #[test]
6381    fn test_empty_config_preserves_universal_lsp_defaults() {
6382        let temp_dir = tempfile::tempdir().unwrap();
6383        let config_path = temp_dir.path().join("config.json");
6384
6385        // Write an empty config
6386        std::fs::write(&config_path, "{}").unwrap();
6387
6388        let loaded = Config::load_from_file(&config_path).unwrap();
6389        let defaults = Config::default();
6390
6391        // Should have all default universal LSP servers
6392        assert_eq!(
6393            loaded.universal_lsp.len(),
6394            defaults.universal_lsp.len(),
6395            "Empty config should preserve all default universal_lsp entries"
6396        );
6397    }
6398
6399    #[test]
6400    fn test_universal_lsp_config_merges_with_defaults() {
6401        let temp_dir = tempfile::tempdir().unwrap();
6402        let config_path = temp_dir.path().join("config.json");
6403
6404        // Write a config that enables quicklsp
6405        let config_json = r#"{
6406            "universal_lsp": {
6407                "quicklsp": {
6408                    "enabled": true
6409                }
6410            }
6411        }"#;
6412        std::fs::write(&config_path, config_json).unwrap();
6413
6414        let loaded = Config::load_from_file(&config_path).unwrap();
6415
6416        // quicklsp should be enabled (user override)
6417        assert!(loaded.universal_lsp.contains_key("quicklsp"));
6418        let server = &loaded.universal_lsp["quicklsp"].as_slice()[0];
6419        assert!(server.enabled, "User override should enable quicklsp");
6420        // Command should be merged from defaults
6421        assert_eq!(
6422            server.command, "quicklsp",
6423            "Default command should be merged when not specified by user"
6424        );
6425    }
6426
6427    #[test]
6428    fn test_universal_lsp_custom_server_added() {
6429        let temp_dir = tempfile::tempdir().unwrap();
6430        let config_path = temp_dir.path().join("config.json");
6431
6432        // Write a config that adds a custom universal server
6433        let config_json = r#"{
6434            "universal_lsp": {
6435                "my-custom-server": {
6436                    "command": "my-server",
6437                    "enabled": true,
6438                    "auto_start": true
6439                }
6440            }
6441        }"#;
6442        std::fs::write(&config_path, config_json).unwrap();
6443
6444        let loaded = Config::load_from_file(&config_path).unwrap();
6445
6446        // Custom server should be present
6447        assert!(
6448            loaded.universal_lsp.contains_key("my-custom-server"),
6449            "Custom universal server should be loaded"
6450        );
6451        let server = &loaded.universal_lsp["my-custom-server"].as_slice()[0];
6452        assert_eq!(server.command, "my-server");
6453        assert!(server.enabled);
6454        assert!(server.auto_start);
6455
6456        // Default quicklsp should also still be present
6457        assert!(
6458            loaded.universal_lsp.contains_key("quicklsp"),
6459            "Default quicklsp should be merged from defaults"
6460        );
6461    }
6462}