Skip to main content

fresh/
config.rs

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