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