Skip to main content

fresh/
config.rs

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