Skip to main content

fresh/
config.rs

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