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