Skip to main content

fresh/
partial_config.rs

1//! Partial configuration types for layered config merging.
2//!
3//! This module provides `Option`-wrapped versions of all config structs,
4//! enabling a 4-level overlay architecture (System → User → Project → Session).
5
6use crate::config::{
7    ClipboardConfig, CursorStyle, FileBrowserConfig, FileExplorerConfig, FormatterConfig,
8    Keybinding, KeybindingMapName, KeymapConfig, LanguageConfig, LineEndingOption, OnSaveAction,
9    PluginConfig, TerminalConfig, ThemeName, WarningsConfig,
10};
11use crate::types::LspLanguageConfig;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15/// Trait for merging configuration layers.
16/// Higher precedence values (self) override lower precedence (other).
17pub trait Merge {
18    /// Merge values from a lower-precedence layer into this layer.
19    /// Values already set in self take precedence over values in other.
20    fn merge_from(&mut self, other: &Self);
21}
22
23impl<T: Clone> Merge for Option<T> {
24    fn merge_from(&mut self, other: &Self) {
25        if self.is_none() {
26            *self = other.clone();
27        }
28    }
29}
30
31/// Merge two HashMaps where self's entries take precedence.
32/// Entries from other are added if not present in self.
33fn merge_hashmap<K: Clone + Eq + std::hash::Hash, V: Clone>(
34    target: &mut Option<HashMap<K, V>>,
35    other: &Option<HashMap<K, V>>,
36) {
37    match (target, other) {
38        (Some(t), Some(o)) => {
39            for (key, value) in o {
40                t.entry(key.clone()).or_insert_with(|| value.clone());
41            }
42        }
43        (t @ None, Some(o)) => {
44            *t = Some(o.clone());
45        }
46        _ => {}
47    }
48}
49
50/// Merge two HashMaps where values implement Merge (for recursive merging).
51fn merge_hashmap_recursive<K, V>(target: &mut Option<HashMap<K, V>>, other: &Option<HashMap<K, V>>)
52where
53    K: Clone + Eq + std::hash::Hash,
54    V: Clone + Merge + Default,
55{
56    match (target, other) {
57        (Some(t), Some(o)) => {
58            for (key, value) in o {
59                t.entry(key.clone())
60                    .and_modify(|existing| existing.merge_from(value))
61                    .or_insert_with(|| value.clone());
62            }
63        }
64        (t @ None, Some(o)) => {
65            *t = Some(o.clone());
66        }
67        _ => {}
68    }
69}
70
71/// Partial configuration where all fields are optional.
72/// Represents a single configuration layer (User, Project, or Session).
73#[derive(Debug, Clone, Default, Deserialize, Serialize)]
74#[serde(default)]
75pub struct PartialConfig {
76    pub version: Option<u32>,
77    pub theme: Option<ThemeName>,
78    pub locale: Option<String>,
79    pub check_for_updates: Option<bool>,
80    pub editor: Option<PartialEditorConfig>,
81    pub file_explorer: Option<PartialFileExplorerConfig>,
82    pub file_browser: Option<PartialFileBrowserConfig>,
83    pub clipboard: Option<PartialClipboardConfig>,
84    pub terminal: Option<PartialTerminalConfig>,
85    pub keybindings: Option<Vec<Keybinding>>,
86    pub keybinding_maps: Option<HashMap<String, KeymapConfig>>,
87    pub active_keybinding_map: Option<KeybindingMapName>,
88    pub languages: Option<HashMap<String, PartialLanguageConfig>>,
89    pub default_language: Option<String>,
90    pub lsp: Option<HashMap<String, LspLanguageConfig>>,
91    pub universal_lsp: Option<HashMap<String, LspLanguageConfig>>,
92    pub warnings: Option<PartialWarningsConfig>,
93    pub plugins: Option<HashMap<String, PartialPluginConfig>>,
94    pub packages: Option<PartialPackagesConfig>,
95}
96
97impl Merge for PartialConfig {
98    fn merge_from(&mut self, other: &Self) {
99        self.version.merge_from(&other.version);
100        self.theme.merge_from(&other.theme);
101        self.locale.merge_from(&other.locale);
102        self.check_for_updates.merge_from(&other.check_for_updates);
103
104        // Nested structs: merge recursively
105        merge_partial(&mut self.editor, &other.editor);
106        merge_partial(&mut self.file_explorer, &other.file_explorer);
107        merge_partial(&mut self.file_browser, &other.file_browser);
108        merge_partial(&mut self.clipboard, &other.clipboard);
109        merge_partial(&mut self.terminal, &other.terminal);
110        merge_partial(&mut self.warnings, &other.warnings);
111        merge_partial(&mut self.packages, &other.packages);
112
113        // Lists: higher precedence replaces (per design doc)
114        self.keybindings.merge_from(&other.keybindings);
115
116        // HashMaps: merge entries, higher precedence wins on key collision
117        merge_hashmap(&mut self.keybinding_maps, &other.keybinding_maps);
118        merge_hashmap_recursive(&mut self.languages, &other.languages);
119        self.default_language.merge_from(&other.default_language);
120        merge_hashmap(&mut self.lsp, &other.lsp);
121        merge_hashmap(&mut self.universal_lsp, &other.universal_lsp);
122        merge_hashmap_recursive(&mut self.plugins, &other.plugins);
123
124        self.active_keybinding_map
125            .merge_from(&other.active_keybinding_map);
126    }
127}
128
129/// Helper to merge nested partial structs.
130fn merge_partial<T: Merge + Clone>(target: &mut Option<T>, other: &Option<T>) {
131    match (target, other) {
132        (Some(t), Some(o)) => t.merge_from(o),
133        (t @ None, Some(o)) => *t = Some(o.clone()),
134        _ => {}
135    }
136}
137
138/// Partial editor configuration.
139#[derive(Debug, Clone, Default, Deserialize, Serialize)]
140#[serde(default)]
141pub struct PartialEditorConfig {
142    pub use_tabs: Option<bool>,
143    pub tab_size: Option<usize>,
144    pub auto_indent: Option<bool>,
145    pub auto_close: Option<bool>,
146    pub auto_surround: Option<bool>,
147    pub line_numbers: Option<bool>,
148    pub relative_line_numbers: Option<bool>,
149    pub scroll_offset: Option<usize>,
150    pub syntax_highlighting: Option<bool>,
151    pub highlight_current_line: Option<bool>,
152    pub line_wrap: Option<bool>,
153    pub wrap_indent: Option<bool>,
154    pub wrap_column: Option<Option<usize>>,
155    pub page_width: Option<Option<usize>>,
156    pub highlight_timeout_ms: Option<u64>,
157    pub snapshot_interval: Option<usize>,
158    pub large_file_threshold_bytes: Option<u64>,
159    pub estimated_line_length: Option<usize>,
160    pub enable_inlay_hints: Option<bool>,
161    pub enable_semantic_tokens_full: Option<bool>,
162    pub diagnostics_inline_text: Option<bool>,
163    pub recovery_enabled: Option<bool>,
164    pub auto_recovery_save_interval_secs: Option<u32>,
165    pub auto_save_enabled: Option<bool>,
166    pub auto_save_interval_secs: Option<u32>,
167    pub hot_exit: Option<bool>,
168    pub highlight_context_bytes: Option<usize>,
169    pub mouse_hover_enabled: Option<bool>,
170    pub mouse_hover_delay_ms: Option<u64>,
171    pub double_click_time_ms: Option<u64>,
172    pub auto_revert_poll_interval_ms: Option<u64>,
173    pub read_concurrency: Option<usize>,
174    pub file_tree_poll_interval_ms: Option<u64>,
175    pub default_line_ending: Option<LineEndingOption>,
176    pub trim_trailing_whitespace_on_save: Option<bool>,
177    pub ensure_final_newline_on_save: Option<bool>,
178    pub highlight_matching_brackets: Option<bool>,
179    pub rainbow_brackets: Option<bool>,
180    pub cursor_style: Option<CursorStyle>,
181    pub keyboard_disambiguate_escape_codes: Option<bool>,
182    pub keyboard_report_event_types: Option<bool>,
183    pub keyboard_report_alternate_keys: Option<bool>,
184    pub keyboard_report_all_keys_as_escape_codes: Option<bool>,
185    pub completion_popup_auto_show: Option<bool>,
186    pub quick_suggestions: Option<bool>,
187    pub quick_suggestions_delay_ms: Option<u64>,
188    pub suggest_on_trigger_characters: Option<bool>,
189    pub show_menu_bar: Option<bool>,
190    pub menu_bar_mnemonics: Option<bool>,
191    pub show_tab_bar: Option<bool>,
192    pub show_status_bar: Option<bool>,
193    pub show_prompt_line: Option<bool>,
194    pub show_vertical_scrollbar: Option<bool>,
195    pub show_horizontal_scrollbar: Option<bool>,
196    pub show_tilde: Option<bool>,
197    pub use_terminal_bg: Option<bool>,
198    pub rulers: Option<Vec<usize>>,
199    pub whitespace_show: Option<bool>,
200    pub whitespace_spaces_leading: Option<bool>,
201    pub whitespace_spaces_inner: Option<bool>,
202    pub whitespace_spaces_trailing: Option<bool>,
203    pub whitespace_tabs_leading: Option<bool>,
204    pub whitespace_tabs_inner: Option<bool>,
205    pub whitespace_tabs_trailing: Option<bool>,
206}
207
208impl Merge for PartialEditorConfig {
209    fn merge_from(&mut self, other: &Self) {
210        self.use_tabs.merge_from(&other.use_tabs);
211        self.tab_size.merge_from(&other.tab_size);
212        self.auto_indent.merge_from(&other.auto_indent);
213        self.auto_close.merge_from(&other.auto_close);
214        self.auto_surround.merge_from(&other.auto_surround);
215        self.line_numbers.merge_from(&other.line_numbers);
216        self.relative_line_numbers
217            .merge_from(&other.relative_line_numbers);
218        self.scroll_offset.merge_from(&other.scroll_offset);
219        self.syntax_highlighting
220            .merge_from(&other.syntax_highlighting);
221        self.line_wrap.merge_from(&other.line_wrap);
222        self.wrap_indent.merge_from(&other.wrap_indent);
223        self.wrap_column.merge_from(&other.wrap_column);
224        self.page_width.merge_from(&other.page_width);
225        self.highlight_timeout_ms
226            .merge_from(&other.highlight_timeout_ms);
227        self.snapshot_interval.merge_from(&other.snapshot_interval);
228        self.large_file_threshold_bytes
229            .merge_from(&other.large_file_threshold_bytes);
230        self.estimated_line_length
231            .merge_from(&other.estimated_line_length);
232        self.enable_inlay_hints
233            .merge_from(&other.enable_inlay_hints);
234        self.enable_semantic_tokens_full
235            .merge_from(&other.enable_semantic_tokens_full);
236        self.diagnostics_inline_text
237            .merge_from(&other.diagnostics_inline_text);
238        self.recovery_enabled.merge_from(&other.recovery_enabled);
239        self.auto_recovery_save_interval_secs
240            .merge_from(&other.auto_recovery_save_interval_secs);
241        self.auto_save_enabled.merge_from(&other.auto_save_enabled);
242        self.auto_save_interval_secs
243            .merge_from(&other.auto_save_interval_secs);
244        self.hot_exit.merge_from(&other.hot_exit);
245        self.highlight_context_bytes
246            .merge_from(&other.highlight_context_bytes);
247        self.mouse_hover_enabled
248            .merge_from(&other.mouse_hover_enabled);
249        self.mouse_hover_delay_ms
250            .merge_from(&other.mouse_hover_delay_ms);
251        self.double_click_time_ms
252            .merge_from(&other.double_click_time_ms);
253        self.auto_revert_poll_interval_ms
254            .merge_from(&other.auto_revert_poll_interval_ms);
255        self.read_concurrency.merge_from(&other.read_concurrency);
256        self.file_tree_poll_interval_ms
257            .merge_from(&other.file_tree_poll_interval_ms);
258        self.default_line_ending
259            .merge_from(&other.default_line_ending);
260        self.trim_trailing_whitespace_on_save
261            .merge_from(&other.trim_trailing_whitespace_on_save);
262        self.ensure_final_newline_on_save
263            .merge_from(&other.ensure_final_newline_on_save);
264        self.highlight_matching_brackets
265            .merge_from(&other.highlight_matching_brackets);
266        self.rainbow_brackets.merge_from(&other.rainbow_brackets);
267        self.cursor_style.merge_from(&other.cursor_style);
268        self.keyboard_disambiguate_escape_codes
269            .merge_from(&other.keyboard_disambiguate_escape_codes);
270        self.keyboard_report_event_types
271            .merge_from(&other.keyboard_report_event_types);
272        self.keyboard_report_alternate_keys
273            .merge_from(&other.keyboard_report_alternate_keys);
274        self.keyboard_report_all_keys_as_escape_codes
275            .merge_from(&other.keyboard_report_all_keys_as_escape_codes);
276        self.completion_popup_auto_show
277            .merge_from(&other.completion_popup_auto_show);
278        self.quick_suggestions.merge_from(&other.quick_suggestions);
279        self.quick_suggestions_delay_ms
280            .merge_from(&other.quick_suggestions_delay_ms);
281        self.suggest_on_trigger_characters
282            .merge_from(&other.suggest_on_trigger_characters);
283        self.show_menu_bar.merge_from(&other.show_menu_bar);
284        self.menu_bar_mnemonics
285            .merge_from(&other.menu_bar_mnemonics);
286        self.show_tab_bar.merge_from(&other.show_tab_bar);
287        self.show_status_bar.merge_from(&other.show_status_bar);
288        self.show_prompt_line.merge_from(&other.show_prompt_line);
289        self.show_vertical_scrollbar
290            .merge_from(&other.show_vertical_scrollbar);
291        self.show_horizontal_scrollbar
292            .merge_from(&other.show_horizontal_scrollbar);
293        self.show_tilde.merge_from(&other.show_tilde);
294        self.use_terminal_bg.merge_from(&other.use_terminal_bg);
295        self.rulers.merge_from(&other.rulers);
296        self.whitespace_show.merge_from(&other.whitespace_show);
297        self.whitespace_spaces_leading
298            .merge_from(&other.whitespace_spaces_leading);
299        self.whitespace_spaces_inner
300            .merge_from(&other.whitespace_spaces_inner);
301        self.whitespace_spaces_trailing
302            .merge_from(&other.whitespace_spaces_trailing);
303        self.whitespace_tabs_leading
304            .merge_from(&other.whitespace_tabs_leading);
305        self.whitespace_tabs_inner
306            .merge_from(&other.whitespace_tabs_inner);
307        self.whitespace_tabs_trailing
308            .merge_from(&other.whitespace_tabs_trailing);
309    }
310}
311
312/// Partial file explorer configuration.
313#[derive(Debug, Clone, Default, Deserialize, Serialize)]
314#[serde(default)]
315pub struct PartialFileExplorerConfig {
316    pub respect_gitignore: Option<bool>,
317    pub show_hidden: Option<bool>,
318    pub show_gitignored: Option<bool>,
319    pub custom_ignore_patterns: Option<Vec<String>>,
320    pub width: Option<f32>,
321}
322
323impl Merge for PartialFileExplorerConfig {
324    fn merge_from(&mut self, other: &Self) {
325        self.respect_gitignore.merge_from(&other.respect_gitignore);
326        self.show_hidden.merge_from(&other.show_hidden);
327        self.show_gitignored.merge_from(&other.show_gitignored);
328        self.custom_ignore_patterns
329            .merge_from(&other.custom_ignore_patterns);
330        self.width.merge_from(&other.width);
331    }
332}
333
334/// Partial file browser configuration.
335#[derive(Debug, Clone, Default, Deserialize, Serialize)]
336#[serde(default)]
337pub struct PartialFileBrowserConfig {
338    pub show_hidden: Option<bool>,
339}
340
341impl Merge for PartialFileBrowserConfig {
342    fn merge_from(&mut self, other: &Self) {
343        self.show_hidden.merge_from(&other.show_hidden);
344    }
345}
346
347/// Partial clipboard configuration.
348#[derive(Debug, Clone, Default, Deserialize, Serialize)]
349#[serde(default)]
350pub struct PartialClipboardConfig {
351    pub use_osc52: Option<bool>,
352    pub use_system_clipboard: Option<bool>,
353}
354
355impl Merge for PartialClipboardConfig {
356    fn merge_from(&mut self, other: &Self) {
357        self.use_osc52.merge_from(&other.use_osc52);
358        self.use_system_clipboard
359            .merge_from(&other.use_system_clipboard);
360    }
361}
362
363/// Partial terminal configuration.
364#[derive(Debug, Clone, Default, Deserialize, Serialize)]
365#[serde(default)]
366pub struct PartialTerminalConfig {
367    pub jump_to_end_on_output: Option<bool>,
368}
369
370impl Merge for PartialTerminalConfig {
371    fn merge_from(&mut self, other: &Self) {
372        self.jump_to_end_on_output
373            .merge_from(&other.jump_to_end_on_output);
374    }
375}
376
377/// Partial warnings configuration.
378#[derive(Debug, Clone, Default, Deserialize, Serialize)]
379#[serde(default)]
380pub struct PartialWarningsConfig {
381    pub show_status_indicator: Option<bool>,
382}
383
384impl Merge for PartialWarningsConfig {
385    fn merge_from(&mut self, other: &Self) {
386        self.show_status_indicator
387            .merge_from(&other.show_status_indicator);
388    }
389}
390
391/// Partial packages configuration for plugin/theme package management.
392#[derive(Debug, Clone, Default, Deserialize, Serialize)]
393#[serde(default)]
394pub struct PartialPackagesConfig {
395    pub sources: Option<Vec<String>>,
396}
397
398impl Merge for PartialPackagesConfig {
399    fn merge_from(&mut self, other: &Self) {
400        self.sources.merge_from(&other.sources);
401    }
402}
403
404/// Partial plugin configuration.
405#[derive(Debug, Clone, Default, Deserialize, Serialize)]
406#[serde(default)]
407pub struct PartialPluginConfig {
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub enabled: Option<bool>,
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub path: Option<std::path::PathBuf>,
412}
413
414impl Merge for PartialPluginConfig {
415    fn merge_from(&mut self, other: &Self) {
416        self.enabled.merge_from(&other.enabled);
417        self.path.merge_from(&other.path);
418    }
419}
420
421/// Partial language configuration.
422#[derive(Debug, Clone, Default, Deserialize, Serialize)]
423#[serde(default)]
424pub struct PartialLanguageConfig {
425    pub extensions: Option<Vec<String>>,
426    pub filenames: Option<Vec<String>>,
427    pub grammar: Option<String>,
428    pub comment_prefix: Option<String>,
429    pub auto_indent: Option<bool>,
430    pub auto_close: Option<bool>,
431    pub auto_surround: Option<bool>,
432    pub textmate_grammar: Option<std::path::PathBuf>,
433    pub show_whitespace_tabs: Option<bool>,
434    pub line_wrap: Option<bool>,
435    pub wrap_column: Option<Option<usize>>,
436    pub page_view: Option<bool>,
437    pub page_width: Option<Option<usize>>,
438    pub use_tabs: Option<bool>,
439    pub tab_size: Option<usize>,
440    pub formatter: Option<FormatterConfig>,
441    pub format_on_save: Option<bool>,
442    pub on_save: Option<Vec<OnSaveAction>>,
443    pub word_characters: Option<Option<String>>,
444}
445
446impl Merge for PartialLanguageConfig {
447    fn merge_from(&mut self, other: &Self) {
448        self.extensions.merge_from(&other.extensions);
449        self.filenames.merge_from(&other.filenames);
450        self.grammar.merge_from(&other.grammar);
451        self.comment_prefix.merge_from(&other.comment_prefix);
452        self.auto_indent.merge_from(&other.auto_indent);
453        self.auto_close.merge_from(&other.auto_close);
454        self.auto_surround.merge_from(&other.auto_surround);
455        self.textmate_grammar.merge_from(&other.textmate_grammar);
456        self.show_whitespace_tabs
457            .merge_from(&other.show_whitespace_tabs);
458        self.line_wrap.merge_from(&other.line_wrap);
459        self.wrap_column.merge_from(&other.wrap_column);
460        self.page_view.merge_from(&other.page_view);
461        self.page_width.merge_from(&other.page_width);
462        self.use_tabs.merge_from(&other.use_tabs);
463        self.tab_size.merge_from(&other.tab_size);
464        self.formatter.merge_from(&other.formatter);
465        self.format_on_save.merge_from(&other.format_on_save);
466        self.on_save.merge_from(&other.on_save);
467        self.word_characters.merge_from(&other.word_characters);
468    }
469}
470
471// Conversion traits for resolving partial configs to concrete configs
472
473impl From<&crate::config::EditorConfig> for PartialEditorConfig {
474    fn from(cfg: &crate::config::EditorConfig) -> Self {
475        Self {
476            use_tabs: Some(cfg.use_tabs),
477            tab_size: Some(cfg.tab_size),
478            auto_indent: Some(cfg.auto_indent),
479            auto_close: Some(cfg.auto_close),
480            auto_surround: Some(cfg.auto_surround),
481            line_numbers: Some(cfg.line_numbers),
482            relative_line_numbers: Some(cfg.relative_line_numbers),
483            scroll_offset: Some(cfg.scroll_offset),
484            syntax_highlighting: Some(cfg.syntax_highlighting),
485            highlight_current_line: Some(cfg.highlight_current_line),
486            line_wrap: Some(cfg.line_wrap),
487            wrap_indent: Some(cfg.wrap_indent),
488            wrap_column: Some(cfg.wrap_column),
489            page_width: Some(cfg.page_width),
490            highlight_timeout_ms: Some(cfg.highlight_timeout_ms),
491            snapshot_interval: Some(cfg.snapshot_interval),
492            large_file_threshold_bytes: Some(cfg.large_file_threshold_bytes),
493            estimated_line_length: Some(cfg.estimated_line_length),
494            enable_inlay_hints: Some(cfg.enable_inlay_hints),
495            enable_semantic_tokens_full: Some(cfg.enable_semantic_tokens_full),
496            diagnostics_inline_text: Some(cfg.diagnostics_inline_text),
497            recovery_enabled: Some(cfg.recovery_enabled),
498            auto_recovery_save_interval_secs: Some(cfg.auto_recovery_save_interval_secs),
499            auto_save_enabled: Some(cfg.auto_save_enabled),
500            auto_save_interval_secs: Some(cfg.auto_save_interval_secs),
501            hot_exit: Some(cfg.hot_exit),
502            highlight_context_bytes: Some(cfg.highlight_context_bytes),
503            mouse_hover_enabled: Some(cfg.mouse_hover_enabled),
504            mouse_hover_delay_ms: Some(cfg.mouse_hover_delay_ms),
505            double_click_time_ms: Some(cfg.double_click_time_ms),
506            auto_revert_poll_interval_ms: Some(cfg.auto_revert_poll_interval_ms),
507            read_concurrency: Some(cfg.read_concurrency),
508            file_tree_poll_interval_ms: Some(cfg.file_tree_poll_interval_ms),
509            default_line_ending: Some(cfg.default_line_ending.clone()),
510            trim_trailing_whitespace_on_save: Some(cfg.trim_trailing_whitespace_on_save),
511            ensure_final_newline_on_save: Some(cfg.ensure_final_newline_on_save),
512            highlight_matching_brackets: Some(cfg.highlight_matching_brackets),
513            rainbow_brackets: Some(cfg.rainbow_brackets),
514            cursor_style: Some(cfg.cursor_style),
515            keyboard_disambiguate_escape_codes: Some(cfg.keyboard_disambiguate_escape_codes),
516            keyboard_report_event_types: Some(cfg.keyboard_report_event_types),
517            keyboard_report_alternate_keys: Some(cfg.keyboard_report_alternate_keys),
518            keyboard_report_all_keys_as_escape_codes: Some(
519                cfg.keyboard_report_all_keys_as_escape_codes,
520            ),
521            completion_popup_auto_show: Some(cfg.completion_popup_auto_show),
522            quick_suggestions: Some(cfg.quick_suggestions),
523            quick_suggestions_delay_ms: Some(cfg.quick_suggestions_delay_ms),
524            suggest_on_trigger_characters: Some(cfg.suggest_on_trigger_characters),
525            show_menu_bar: Some(cfg.show_menu_bar),
526            menu_bar_mnemonics: Some(cfg.menu_bar_mnemonics),
527            show_tab_bar: Some(cfg.show_tab_bar),
528            show_status_bar: Some(cfg.show_status_bar),
529            show_prompt_line: Some(cfg.show_prompt_line),
530            show_vertical_scrollbar: Some(cfg.show_vertical_scrollbar),
531            show_horizontal_scrollbar: Some(cfg.show_horizontal_scrollbar),
532            show_tilde: Some(cfg.show_tilde),
533            use_terminal_bg: Some(cfg.use_terminal_bg),
534            rulers: Some(cfg.rulers.clone()),
535            whitespace_show: Some(cfg.whitespace_show),
536            whitespace_spaces_leading: Some(cfg.whitespace_spaces_leading),
537            whitespace_spaces_inner: Some(cfg.whitespace_spaces_inner),
538            whitespace_spaces_trailing: Some(cfg.whitespace_spaces_trailing),
539            whitespace_tabs_leading: Some(cfg.whitespace_tabs_leading),
540            whitespace_tabs_inner: Some(cfg.whitespace_tabs_inner),
541            whitespace_tabs_trailing: Some(cfg.whitespace_tabs_trailing),
542        }
543    }
544}
545
546impl PartialEditorConfig {
547    /// Resolve this partial config to a concrete EditorConfig using defaults.
548    pub fn resolve(self, defaults: &crate::config::EditorConfig) -> crate::config::EditorConfig {
549        crate::config::EditorConfig {
550            use_tabs: self.use_tabs.unwrap_or(defaults.use_tabs),
551            tab_size: self.tab_size.unwrap_or(defaults.tab_size),
552            auto_indent: self.auto_indent.unwrap_or(defaults.auto_indent),
553            auto_close: self.auto_close.unwrap_or(defaults.auto_close),
554            auto_surround: self.auto_surround.unwrap_or(defaults.auto_surround),
555            line_numbers: self.line_numbers.unwrap_or(defaults.line_numbers),
556            relative_line_numbers: self
557                .relative_line_numbers
558                .unwrap_or(defaults.relative_line_numbers),
559            scroll_offset: self.scroll_offset.unwrap_or(defaults.scroll_offset),
560            syntax_highlighting: self
561                .syntax_highlighting
562                .unwrap_or(defaults.syntax_highlighting),
563            highlight_current_line: self
564                .highlight_current_line
565                .unwrap_or(defaults.highlight_current_line),
566            line_wrap: self.line_wrap.unwrap_or(defaults.line_wrap),
567            wrap_indent: self.wrap_indent.unwrap_or(defaults.wrap_indent),
568            wrap_column: self.wrap_column.unwrap_or(defaults.wrap_column),
569            page_width: self.page_width.unwrap_or(defaults.page_width),
570            highlight_timeout_ms: self
571                .highlight_timeout_ms
572                .unwrap_or(defaults.highlight_timeout_ms),
573            snapshot_interval: self.snapshot_interval.unwrap_or(defaults.snapshot_interval),
574            large_file_threshold_bytes: self
575                .large_file_threshold_bytes
576                .unwrap_or(defaults.large_file_threshold_bytes),
577            estimated_line_length: self
578                .estimated_line_length
579                .unwrap_or(defaults.estimated_line_length),
580            enable_inlay_hints: self
581                .enable_inlay_hints
582                .unwrap_or(defaults.enable_inlay_hints),
583            enable_semantic_tokens_full: self
584                .enable_semantic_tokens_full
585                .unwrap_or(defaults.enable_semantic_tokens_full),
586            diagnostics_inline_text: self
587                .diagnostics_inline_text
588                .unwrap_or(defaults.diagnostics_inline_text),
589            recovery_enabled: self.recovery_enabled.unwrap_or(defaults.recovery_enabled),
590            auto_recovery_save_interval_secs: self
591                .auto_recovery_save_interval_secs
592                .unwrap_or(defaults.auto_recovery_save_interval_secs),
593            auto_save_enabled: self.auto_save_enabled.unwrap_or(defaults.auto_save_enabled),
594            auto_save_interval_secs: self
595                .auto_save_interval_secs
596                .unwrap_or(defaults.auto_save_interval_secs),
597            hot_exit: self.hot_exit.unwrap_or(defaults.hot_exit),
598            highlight_context_bytes: self
599                .highlight_context_bytes
600                .unwrap_or(defaults.highlight_context_bytes),
601            mouse_hover_enabled: self
602                .mouse_hover_enabled
603                .unwrap_or(defaults.mouse_hover_enabled),
604            mouse_hover_delay_ms: self
605                .mouse_hover_delay_ms
606                .unwrap_or(defaults.mouse_hover_delay_ms),
607            double_click_time_ms: self
608                .double_click_time_ms
609                .unwrap_or(defaults.double_click_time_ms),
610            auto_revert_poll_interval_ms: self
611                .auto_revert_poll_interval_ms
612                .unwrap_or(defaults.auto_revert_poll_interval_ms),
613            read_concurrency: self.read_concurrency.unwrap_or(defaults.read_concurrency),
614            file_tree_poll_interval_ms: self
615                .file_tree_poll_interval_ms
616                .unwrap_or(defaults.file_tree_poll_interval_ms),
617            default_line_ending: self
618                .default_line_ending
619                .unwrap_or(defaults.default_line_ending.clone()),
620            trim_trailing_whitespace_on_save: self
621                .trim_trailing_whitespace_on_save
622                .unwrap_or(defaults.trim_trailing_whitespace_on_save),
623            ensure_final_newline_on_save: self
624                .ensure_final_newline_on_save
625                .unwrap_or(defaults.ensure_final_newline_on_save),
626            highlight_matching_brackets: self
627                .highlight_matching_brackets
628                .unwrap_or(defaults.highlight_matching_brackets),
629            rainbow_brackets: self.rainbow_brackets.unwrap_or(defaults.rainbow_brackets),
630            cursor_style: self.cursor_style.unwrap_or(defaults.cursor_style),
631            keyboard_disambiguate_escape_codes: self
632                .keyboard_disambiguate_escape_codes
633                .unwrap_or(defaults.keyboard_disambiguate_escape_codes),
634            keyboard_report_event_types: self
635                .keyboard_report_event_types
636                .unwrap_or(defaults.keyboard_report_event_types),
637            keyboard_report_alternate_keys: self
638                .keyboard_report_alternate_keys
639                .unwrap_or(defaults.keyboard_report_alternate_keys),
640            keyboard_report_all_keys_as_escape_codes: self
641                .keyboard_report_all_keys_as_escape_codes
642                .unwrap_or(defaults.keyboard_report_all_keys_as_escape_codes),
643            completion_popup_auto_show: self
644                .completion_popup_auto_show
645                .unwrap_or(defaults.completion_popup_auto_show),
646            quick_suggestions: self.quick_suggestions.unwrap_or(defaults.quick_suggestions),
647            quick_suggestions_delay_ms: self
648                .quick_suggestions_delay_ms
649                .unwrap_or(defaults.quick_suggestions_delay_ms),
650            suggest_on_trigger_characters: self
651                .suggest_on_trigger_characters
652                .unwrap_or(defaults.suggest_on_trigger_characters),
653            show_menu_bar: self.show_menu_bar.unwrap_or(defaults.show_menu_bar),
654            menu_bar_mnemonics: self
655                .menu_bar_mnemonics
656                .unwrap_or(defaults.menu_bar_mnemonics),
657            show_tab_bar: self.show_tab_bar.unwrap_or(defaults.show_tab_bar),
658            show_status_bar: self.show_status_bar.unwrap_or(defaults.show_status_bar),
659            show_prompt_line: self.show_prompt_line.unwrap_or(defaults.show_prompt_line),
660            show_vertical_scrollbar: self
661                .show_vertical_scrollbar
662                .unwrap_or(defaults.show_vertical_scrollbar),
663            show_horizontal_scrollbar: self
664                .show_horizontal_scrollbar
665                .unwrap_or(defaults.show_horizontal_scrollbar),
666            show_tilde: self.show_tilde.unwrap_or(defaults.show_tilde),
667            use_terminal_bg: self.use_terminal_bg.unwrap_or(defaults.use_terminal_bg),
668            rulers: self.rulers.unwrap_or_else(|| defaults.rulers.clone()),
669            whitespace_show: self.whitespace_show.unwrap_or(defaults.whitespace_show),
670            whitespace_spaces_leading: self
671                .whitespace_spaces_leading
672                .unwrap_or(defaults.whitespace_spaces_leading),
673            whitespace_spaces_inner: self
674                .whitespace_spaces_inner
675                .unwrap_or(defaults.whitespace_spaces_inner),
676            whitespace_spaces_trailing: self
677                .whitespace_spaces_trailing
678                .unwrap_or(defaults.whitespace_spaces_trailing),
679            whitespace_tabs_leading: self
680                .whitespace_tabs_leading
681                .unwrap_or(defaults.whitespace_tabs_leading),
682            whitespace_tabs_inner: self
683                .whitespace_tabs_inner
684                .unwrap_or(defaults.whitespace_tabs_inner),
685            whitespace_tabs_trailing: self
686                .whitespace_tabs_trailing
687                .unwrap_or(defaults.whitespace_tabs_trailing),
688        }
689    }
690}
691
692impl From<&FileExplorerConfig> for PartialFileExplorerConfig {
693    fn from(cfg: &FileExplorerConfig) -> Self {
694        Self {
695            respect_gitignore: Some(cfg.respect_gitignore),
696            show_hidden: Some(cfg.show_hidden),
697            show_gitignored: Some(cfg.show_gitignored),
698            custom_ignore_patterns: Some(cfg.custom_ignore_patterns.clone()),
699            width: Some(cfg.width),
700        }
701    }
702}
703
704impl PartialFileExplorerConfig {
705    pub fn resolve(self, defaults: &FileExplorerConfig) -> FileExplorerConfig {
706        FileExplorerConfig {
707            respect_gitignore: self.respect_gitignore.unwrap_or(defaults.respect_gitignore),
708            show_hidden: self.show_hidden.unwrap_or(defaults.show_hidden),
709            show_gitignored: self.show_gitignored.unwrap_or(defaults.show_gitignored),
710            custom_ignore_patterns: self
711                .custom_ignore_patterns
712                .unwrap_or_else(|| defaults.custom_ignore_patterns.clone()),
713            width: self.width.unwrap_or(defaults.width),
714        }
715    }
716}
717
718impl From<&FileBrowserConfig> for PartialFileBrowserConfig {
719    fn from(cfg: &FileBrowserConfig) -> Self {
720        Self {
721            show_hidden: Some(cfg.show_hidden),
722        }
723    }
724}
725
726impl PartialFileBrowserConfig {
727    pub fn resolve(self, defaults: &FileBrowserConfig) -> FileBrowserConfig {
728        FileBrowserConfig {
729            show_hidden: self.show_hidden.unwrap_or(defaults.show_hidden),
730        }
731    }
732}
733
734impl From<&ClipboardConfig> for PartialClipboardConfig {
735    fn from(cfg: &ClipboardConfig) -> Self {
736        Self {
737            use_osc52: Some(cfg.use_osc52),
738            use_system_clipboard: Some(cfg.use_system_clipboard),
739        }
740    }
741}
742
743impl PartialClipboardConfig {
744    pub fn resolve(self, defaults: &ClipboardConfig) -> ClipboardConfig {
745        ClipboardConfig {
746            use_osc52: self.use_osc52.unwrap_or(defaults.use_osc52),
747            use_system_clipboard: self
748                .use_system_clipboard
749                .unwrap_or(defaults.use_system_clipboard),
750        }
751    }
752}
753
754impl From<&TerminalConfig> for PartialTerminalConfig {
755    fn from(cfg: &TerminalConfig) -> Self {
756        Self {
757            jump_to_end_on_output: Some(cfg.jump_to_end_on_output),
758        }
759    }
760}
761
762impl PartialTerminalConfig {
763    pub fn resolve(self, defaults: &TerminalConfig) -> TerminalConfig {
764        TerminalConfig {
765            jump_to_end_on_output: self
766                .jump_to_end_on_output
767                .unwrap_or(defaults.jump_to_end_on_output),
768        }
769    }
770}
771
772impl From<&WarningsConfig> for PartialWarningsConfig {
773    fn from(cfg: &WarningsConfig) -> Self {
774        Self {
775            show_status_indicator: Some(cfg.show_status_indicator),
776        }
777    }
778}
779
780impl PartialWarningsConfig {
781    pub fn resolve(self, defaults: &WarningsConfig) -> WarningsConfig {
782        WarningsConfig {
783            show_status_indicator: self
784                .show_status_indicator
785                .unwrap_or(defaults.show_status_indicator),
786        }
787    }
788}
789
790impl From<&crate::config::PackagesConfig> for PartialPackagesConfig {
791    fn from(cfg: &crate::config::PackagesConfig) -> Self {
792        Self {
793            sources: Some(cfg.sources.clone()),
794        }
795    }
796}
797
798impl PartialPackagesConfig {
799    pub fn resolve(
800        self,
801        defaults: &crate::config::PackagesConfig,
802    ) -> crate::config::PackagesConfig {
803        crate::config::PackagesConfig {
804            sources: self.sources.unwrap_or_else(|| defaults.sources.clone()),
805        }
806    }
807}
808
809impl From<&PluginConfig> for PartialPluginConfig {
810    fn from(cfg: &PluginConfig) -> Self {
811        Self {
812            enabled: Some(cfg.enabled),
813            path: cfg.path.clone(),
814        }
815    }
816}
817
818impl PartialPluginConfig {
819    pub fn resolve(self, defaults: &PluginConfig) -> PluginConfig {
820        PluginConfig {
821            enabled: self.enabled.unwrap_or(defaults.enabled),
822            path: self.path.or_else(|| defaults.path.clone()),
823        }
824    }
825}
826
827impl From<&LanguageConfig> for PartialLanguageConfig {
828    fn from(cfg: &LanguageConfig) -> Self {
829        Self {
830            extensions: Some(cfg.extensions.clone()),
831            filenames: Some(cfg.filenames.clone()),
832            grammar: Some(cfg.grammar.clone()),
833            comment_prefix: cfg.comment_prefix.clone(),
834            auto_indent: Some(cfg.auto_indent),
835            auto_close: cfg.auto_close,
836            auto_surround: cfg.auto_surround,
837            textmate_grammar: cfg.textmate_grammar.clone(),
838            show_whitespace_tabs: Some(cfg.show_whitespace_tabs),
839            line_wrap: cfg.line_wrap,
840            wrap_column: Some(cfg.wrap_column),
841            page_view: cfg.page_view,
842            page_width: Some(cfg.page_width),
843            use_tabs: cfg.use_tabs,
844            tab_size: cfg.tab_size,
845            formatter: cfg.formatter.clone(),
846            format_on_save: Some(cfg.format_on_save),
847            on_save: Some(cfg.on_save.clone()),
848            word_characters: Some(cfg.word_characters.clone()),
849        }
850    }
851}
852
853impl PartialLanguageConfig {
854    pub fn resolve(self, defaults: &LanguageConfig) -> LanguageConfig {
855        LanguageConfig {
856            extensions: self
857                .extensions
858                .unwrap_or_else(|| defaults.extensions.clone()),
859            filenames: self.filenames.unwrap_or_else(|| defaults.filenames.clone()),
860            grammar: self.grammar.unwrap_or_else(|| defaults.grammar.clone()),
861            comment_prefix: self
862                .comment_prefix
863                .or_else(|| defaults.comment_prefix.clone()),
864            auto_indent: self.auto_indent.unwrap_or(defaults.auto_indent),
865            auto_close: self.auto_close.or(defaults.auto_close),
866            auto_surround: self.auto_surround.or(defaults.auto_surround),
867            textmate_grammar: self
868                .textmate_grammar
869                .or_else(|| defaults.textmate_grammar.clone()),
870            show_whitespace_tabs: self
871                .show_whitespace_tabs
872                .unwrap_or(defaults.show_whitespace_tabs),
873            line_wrap: self.line_wrap.or(defaults.line_wrap),
874            wrap_column: self.wrap_column.unwrap_or(defaults.wrap_column),
875            page_view: self.page_view.or(defaults.page_view),
876            page_width: self.page_width.unwrap_or(defaults.page_width),
877            use_tabs: self.use_tabs.or(defaults.use_tabs),
878            tab_size: self.tab_size.or(defaults.tab_size),
879            formatter: self.formatter.or_else(|| defaults.formatter.clone()),
880            format_on_save: self.format_on_save.unwrap_or(defaults.format_on_save),
881            on_save: self.on_save.unwrap_or_else(|| defaults.on_save.clone()),
882            word_characters: self
883                .word_characters
884                .unwrap_or_else(|| defaults.word_characters.clone()),
885        }
886    }
887}
888
889impl From<&crate::config::Config> for PartialConfig {
890    fn from(cfg: &crate::config::Config) -> Self {
891        Self {
892            version: Some(cfg.version),
893            theme: Some(cfg.theme.clone()),
894            locale: cfg.locale.0.clone(),
895            check_for_updates: Some(cfg.check_for_updates),
896            editor: Some(PartialEditorConfig::from(&cfg.editor)),
897            file_explorer: Some(PartialFileExplorerConfig::from(&cfg.file_explorer)),
898            file_browser: Some(PartialFileBrowserConfig::from(&cfg.file_browser)),
899            clipboard: Some(PartialClipboardConfig::from(&cfg.clipboard)),
900            terminal: Some(PartialTerminalConfig::from(&cfg.terminal)),
901            keybindings: Some(cfg.keybindings.clone()),
902            keybinding_maps: Some(cfg.keybinding_maps.clone()),
903            active_keybinding_map: Some(cfg.active_keybinding_map.clone()),
904            languages: Some(
905                cfg.languages
906                    .iter()
907                    .map(|(k, v)| (k.clone(), PartialLanguageConfig::from(v)))
908                    .collect(),
909            ),
910            default_language: cfg.default_language.clone(),
911            lsp: Some(
912                cfg.lsp
913                    .iter()
914                    .map(|(k, v)| {
915                        // Normalize to Single for 1-element arrays so that
916                        // json_diff can compare object fields recursively
917                        // (arrays are compared wholesale, not element-wise).
918                        let lang_config = match v {
919                            LspLanguageConfig::Multi(vec) if vec.len() == 1 => {
920                                LspLanguageConfig::Single(Box::new(vec[0].clone()))
921                            }
922                            other => other.clone(),
923                        };
924                        (k.clone(), lang_config)
925                    })
926                    .collect(),
927            ),
928            universal_lsp: Some(
929                cfg.universal_lsp
930                    .iter()
931                    .map(|(k, v)| {
932                        let lang_config = match v {
933                            LspLanguageConfig::Multi(vec) if vec.len() == 1 => {
934                                LspLanguageConfig::Single(Box::new(vec[0].clone()))
935                            }
936                            other => other.clone(),
937                        };
938                        (k.clone(), lang_config)
939                    })
940                    .collect(),
941            ),
942            warnings: Some(PartialWarningsConfig::from(&cfg.warnings)),
943            // Only include plugins that differ from defaults
944            // Path is auto-discovered at runtime and should never be saved
945            plugins: {
946                let default_plugin = crate::config::PluginConfig::default();
947                let non_default_plugins: HashMap<String, PartialPluginConfig> = cfg
948                    .plugins
949                    .iter()
950                    .filter(|(_, v)| v.enabled != default_plugin.enabled)
951                    .map(|(k, v)| {
952                        (
953                            k.clone(),
954                            PartialPluginConfig {
955                                enabled: Some(v.enabled),
956                                path: None, // Don't save path - it's auto-discovered
957                            },
958                        )
959                    })
960                    .collect();
961                if non_default_plugins.is_empty() {
962                    None
963                } else {
964                    Some(non_default_plugins)
965                }
966            },
967            packages: Some(PartialPackagesConfig::from(&cfg.packages)),
968        }
969    }
970}
971
972impl PartialConfig {
973    /// Resolve this partial config to a concrete Config using system defaults.
974    pub fn resolve(self) -> crate::config::Config {
975        let defaults = crate::config::Config::default();
976        self.resolve_with_defaults(&defaults)
977    }
978
979    /// Resolve this partial config to a concrete Config using provided defaults.
980    pub fn resolve_with_defaults(self, defaults: &crate::config::Config) -> crate::config::Config {
981        // Resolve languages HashMap - merge with defaults
982        let languages = {
983            let mut result = defaults.languages.clone();
984            if let Some(partial_langs) = self.languages {
985                for (key, partial_lang) in partial_langs {
986                    let default_lang = result.get(&key).cloned().unwrap_or_default();
987                    result.insert(key, partial_lang.resolve(&default_lang));
988                }
989            }
990            result
991        };
992
993        // Resolve lsp HashMap - merge with defaults
994        // Each language can have one or more server configs.
995        // User config (LspLanguageConfig) can be a single object or an array.
996        let lsp = {
997            let mut result = defaults.lsp.clone();
998            if let Some(partial_lsp) = self.lsp {
999                for (key, lang_config) in partial_lsp {
1000                    let user_configs = lang_config.into_vec();
1001                    if let Some(default_configs) = result.get(&key) {
1002                        let default_slice = default_configs.as_slice();
1003                        // For single-server user config, merge with the first default.
1004                        // For multi-server user config, replace entirely (user is
1005                        // explicitly configuring the full server list).
1006                        if user_configs.len() == 1 && default_slice.len() == 1 {
1007                            let merged = user_configs
1008                                .into_iter()
1009                                .next()
1010                                .unwrap()
1011                                .merge_with_defaults(&default_slice[0]);
1012                            result.insert(key, LspLanguageConfig::Multi(vec![merged]));
1013                        } else {
1014                            result.insert(key, LspLanguageConfig::Multi(user_configs));
1015                        }
1016                    } else {
1017                        // New language not in defaults - use as-is
1018                        result.insert(key, LspLanguageConfig::Multi(user_configs));
1019                    }
1020                }
1021            }
1022            result
1023        };
1024
1025        // Resolve universal_lsp HashMap - same merge strategy as lsp
1026        let universal_lsp = {
1027            let mut result = defaults.universal_lsp.clone();
1028            if let Some(partial_universal_lsp) = self.universal_lsp {
1029                for (key, lang_config) in partial_universal_lsp {
1030                    let user_configs = lang_config.into_vec();
1031                    if let Some(default_configs) = result.get(&key) {
1032                        let default_slice = default_configs.as_slice();
1033                        if user_configs.len() == 1 && default_slice.len() == 1 {
1034                            let merged = user_configs
1035                                .into_iter()
1036                                .next()
1037                                .unwrap()
1038                                .merge_with_defaults(&default_slice[0]);
1039                            result.insert(key, LspLanguageConfig::Multi(vec![merged]));
1040                        } else {
1041                            result.insert(key, LspLanguageConfig::Multi(user_configs));
1042                        }
1043                    } else {
1044                        result.insert(key, LspLanguageConfig::Multi(user_configs));
1045                    }
1046                }
1047            }
1048            result
1049        };
1050
1051        // Resolve keybinding_maps HashMap - merge with defaults
1052        let keybinding_maps = {
1053            let mut result = defaults.keybinding_maps.clone();
1054            if let Some(partial_maps) = self.keybinding_maps {
1055                for (key, config) in partial_maps {
1056                    result.insert(key, config);
1057                }
1058            }
1059            result
1060        };
1061
1062        // Resolve plugins HashMap - merge with defaults
1063        let plugins = {
1064            let mut result = defaults.plugins.clone();
1065            if let Some(partial_plugins) = self.plugins {
1066                for (key, partial_plugin) in partial_plugins {
1067                    let default_plugin = result.get(&key).cloned().unwrap_or_default();
1068                    result.insert(key, partial_plugin.resolve(&default_plugin));
1069                }
1070            }
1071            result
1072        };
1073
1074        crate::config::Config {
1075            version: self.version.unwrap_or(defaults.version),
1076            theme: self.theme.unwrap_or_else(|| defaults.theme.clone()),
1077            locale: crate::config::LocaleName::from(
1078                self.locale.or_else(|| defaults.locale.0.clone()),
1079            ),
1080            check_for_updates: self.check_for_updates.unwrap_or(defaults.check_for_updates),
1081            editor: self
1082                .editor
1083                .map(|e| e.resolve(&defaults.editor))
1084                .unwrap_or_else(|| defaults.editor.clone()),
1085            file_explorer: self
1086                .file_explorer
1087                .map(|e| e.resolve(&defaults.file_explorer))
1088                .unwrap_or_else(|| defaults.file_explorer.clone()),
1089            file_browser: self
1090                .file_browser
1091                .map(|e| e.resolve(&defaults.file_browser))
1092                .unwrap_or_else(|| defaults.file_browser.clone()),
1093            clipboard: self
1094                .clipboard
1095                .map(|e| e.resolve(&defaults.clipboard))
1096                .unwrap_or_else(|| defaults.clipboard.clone()),
1097            terminal: self
1098                .terminal
1099                .map(|e| e.resolve(&defaults.terminal))
1100                .unwrap_or_else(|| defaults.terminal.clone()),
1101            keybindings: self
1102                .keybindings
1103                .unwrap_or_else(|| defaults.keybindings.clone()),
1104            keybinding_maps,
1105            active_keybinding_map: self
1106                .active_keybinding_map
1107                .unwrap_or_else(|| defaults.active_keybinding_map.clone()),
1108            languages,
1109            default_language: self
1110                .default_language
1111                .or_else(|| defaults.default_language.clone()),
1112            lsp,
1113            universal_lsp,
1114            warnings: self
1115                .warnings
1116                .map(|e| e.resolve(&defaults.warnings))
1117                .unwrap_or_else(|| defaults.warnings.clone()),
1118            plugins,
1119            packages: self
1120                .packages
1121                .map(|e| e.resolve(&defaults.packages))
1122                .unwrap_or_else(|| defaults.packages.clone()),
1123        }
1124    }
1125}
1126
1127// Default implementation for LanguageConfig to support merge_hashmap_recursive
1128impl Default for LanguageConfig {
1129    fn default() -> Self {
1130        Self {
1131            extensions: Vec::new(),
1132            filenames: Vec::new(),
1133            grammar: String::new(),
1134            comment_prefix: None,
1135            auto_indent: true,
1136            auto_close: None,
1137            auto_surround: None,
1138            textmate_grammar: None,
1139            show_whitespace_tabs: true,
1140            line_wrap: None,
1141            wrap_column: None,
1142            page_view: None,
1143            page_width: None,
1144            use_tabs: None,
1145            tab_size: None,
1146            formatter: None,
1147            format_on_save: false,
1148            on_save: Vec::new(),
1149            word_characters: None,
1150        }
1151    }
1152}
1153
1154/// Session-specific configuration for runtime/volatile overrides.
1155///
1156/// This struct represents the session layer of the config hierarchy - settings
1157/// that are temporary and may not persist across editor restarts.
1158///
1159/// Unlike PartialConfig, SessionConfig provides a focused API for common
1160/// runtime modifications like temporary theme switching.
1161#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1162#[serde(default)]
1163pub struct SessionConfig {
1164    /// Temporarily override the theme (e.g., for preview)
1165    pub theme: Option<ThemeName>,
1166
1167    /// Temporary editor overrides (e.g., changing tab_size for current session)
1168    pub editor: Option<PartialEditorConfig>,
1169
1170    /// Buffer-specific overrides keyed by absolute file path.
1171    /// These allow per-file settings that persist only during the session.
1172    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1173    pub buffer_overrides: HashMap<std::path::PathBuf, PartialEditorConfig>,
1174}
1175
1176impl SessionConfig {
1177    /// Create a new empty session config.
1178    pub fn new() -> Self {
1179        Self::default()
1180    }
1181
1182    /// Set a temporary theme override.
1183    pub fn set_theme(&mut self, theme: ThemeName) {
1184        self.theme = Some(theme);
1185    }
1186
1187    /// Clear the theme override, reverting to lower layers.
1188    pub fn clear_theme(&mut self) {
1189        self.theme = None;
1190    }
1191
1192    /// Set an editor setting for the current session.
1193    pub fn set_editor_option<F>(&mut self, setter: F)
1194    where
1195        F: FnOnce(&mut PartialEditorConfig),
1196    {
1197        let editor = self.editor.get_or_insert_with(Default::default);
1198        setter(editor);
1199    }
1200
1201    /// Set a buffer-specific editor override.
1202    pub fn set_buffer_override(&mut self, path: std::path::PathBuf, config: PartialEditorConfig) {
1203        self.buffer_overrides.insert(path, config);
1204    }
1205
1206    /// Clear buffer-specific overrides for a path.
1207    pub fn clear_buffer_override(&mut self, path: &std::path::Path) {
1208        self.buffer_overrides.remove(path);
1209    }
1210
1211    /// Get buffer-specific editor config if set.
1212    pub fn get_buffer_override(&self, path: &std::path::Path) -> Option<&PartialEditorConfig> {
1213        self.buffer_overrides.get(path)
1214    }
1215
1216    /// Convert to a PartialConfig for merging with other layers.
1217    pub fn to_partial_config(&self) -> PartialConfig {
1218        PartialConfig {
1219            theme: self.theme.clone(),
1220            editor: self.editor.clone(),
1221            ..Default::default()
1222        }
1223    }
1224
1225    /// Check if this session config has any values set.
1226    pub fn is_empty(&self) -> bool {
1227        self.theme.is_none() && self.editor.is_none() && self.buffer_overrides.is_empty()
1228    }
1229}
1230
1231impl From<PartialConfig> for SessionConfig {
1232    fn from(partial: PartialConfig) -> Self {
1233        Self {
1234            theme: partial.theme,
1235            editor: partial.editor,
1236            buffer_overrides: HashMap::new(),
1237        }
1238    }
1239}
1240
1241#[cfg(test)]
1242mod tests {
1243    use super::*;
1244
1245    #[test]
1246    fn merge_option_higher_precedence_wins() {
1247        let mut higher: Option<i32> = Some(10);
1248        let lower: Option<i32> = Some(5);
1249        higher.merge_from(&lower);
1250        assert_eq!(higher, Some(10));
1251    }
1252
1253    #[test]
1254    fn merge_option_fills_from_lower_when_none() {
1255        let mut higher: Option<i32> = None;
1256        let lower: Option<i32> = Some(5);
1257        higher.merge_from(&lower);
1258        assert_eq!(higher, Some(5));
1259    }
1260
1261    #[test]
1262    fn merge_editor_config_recursive() {
1263        let mut higher = PartialEditorConfig {
1264            tab_size: Some(2),
1265            ..Default::default()
1266        };
1267        let lower = PartialEditorConfig {
1268            tab_size: Some(4),
1269            line_numbers: Some(true),
1270            ..Default::default()
1271        };
1272
1273        higher.merge_from(&lower);
1274
1275        assert_eq!(higher.tab_size, Some(2)); // Higher wins
1276        assert_eq!(higher.line_numbers, Some(true)); // Filled from lower
1277    }
1278
1279    #[test]
1280    fn merge_partial_config_combines_languages() {
1281        let mut higher = PartialConfig {
1282            languages: Some(HashMap::from([(
1283                "rust".to_string(),
1284                PartialLanguageConfig {
1285                    tab_size: Some(4),
1286                    ..Default::default()
1287                },
1288            )])),
1289            ..Default::default()
1290        };
1291        let lower = PartialConfig {
1292            languages: Some(HashMap::from([(
1293                "python".to_string(),
1294                PartialLanguageConfig {
1295                    tab_size: Some(4),
1296                    ..Default::default()
1297                },
1298            )])),
1299            ..Default::default()
1300        };
1301
1302        higher.merge_from(&lower);
1303
1304        let langs = higher.languages.unwrap();
1305        assert!(langs.contains_key("rust"));
1306        assert!(langs.contains_key("python"));
1307    }
1308
1309    #[test]
1310    fn merge_languages_same_key_higher_wins() {
1311        let mut higher = PartialConfig {
1312            languages: Some(HashMap::from([(
1313                "rust".to_string(),
1314                PartialLanguageConfig {
1315                    tab_size: Some(2),
1316                    use_tabs: Some(true),
1317                    ..Default::default()
1318                },
1319            )])),
1320            ..Default::default()
1321        };
1322        let lower = PartialConfig {
1323            languages: Some(HashMap::from([(
1324                "rust".to_string(),
1325                PartialLanguageConfig {
1326                    tab_size: Some(4),
1327                    auto_indent: Some(false),
1328                    ..Default::default()
1329                },
1330            )])),
1331            ..Default::default()
1332        };
1333
1334        higher.merge_from(&lower);
1335
1336        let langs = higher.languages.unwrap();
1337        let rust = langs.get("rust").unwrap();
1338        assert_eq!(rust.tab_size, Some(2)); // Higher wins
1339        assert_eq!(rust.use_tabs, Some(true)); // From higher
1340        assert_eq!(rust.auto_indent, Some(false)); // Filled from lower
1341    }
1342
1343    #[test]
1344    fn resolve_fills_defaults() {
1345        let partial = PartialConfig {
1346            theme: Some(ThemeName::from("dark")),
1347            ..Default::default()
1348        };
1349
1350        let resolved = partial.resolve();
1351
1352        assert_eq!(resolved.theme.0, "dark");
1353        assert_eq!(resolved.editor.tab_size, 4); // Default
1354        assert!(resolved.editor.line_numbers); // Default true
1355    }
1356
1357    #[test]
1358    fn resolve_preserves_set_values() {
1359        let partial = PartialConfig {
1360            editor: Some(PartialEditorConfig {
1361                tab_size: Some(2),
1362                line_numbers: Some(false),
1363                ..Default::default()
1364            }),
1365            ..Default::default()
1366        };
1367
1368        let resolved = partial.resolve();
1369
1370        assert_eq!(resolved.editor.tab_size, 2);
1371        assert!(!resolved.editor.line_numbers);
1372    }
1373
1374    #[test]
1375    fn roundtrip_config_to_partial_and_back() {
1376        let original = crate::config::Config::default();
1377        let partial = PartialConfig::from(&original);
1378        let resolved = partial.resolve();
1379
1380        assert_eq!(original.theme, resolved.theme);
1381        assert_eq!(original.editor.tab_size, resolved.editor.tab_size);
1382        assert_eq!(original.check_for_updates, resolved.check_for_updates);
1383    }
1384
1385    #[test]
1386    fn session_config_new_is_empty() {
1387        let session = SessionConfig::new();
1388        assert!(session.is_empty());
1389    }
1390
1391    #[test]
1392    fn session_config_set_theme() {
1393        let mut session = SessionConfig::new();
1394        session.set_theme(ThemeName::from("dark"));
1395        assert_eq!(session.theme, Some(ThemeName::from("dark")));
1396        assert!(!session.is_empty());
1397    }
1398
1399    #[test]
1400    fn session_config_clear_theme() {
1401        let mut session = SessionConfig::new();
1402        session.set_theme(ThemeName::from("dark"));
1403        session.clear_theme();
1404        assert!(session.theme.is_none());
1405    }
1406
1407    #[test]
1408    fn session_config_set_editor_option() {
1409        let mut session = SessionConfig::new();
1410        session.set_editor_option(|e| e.tab_size = Some(2));
1411        assert_eq!(session.editor.as_ref().unwrap().tab_size, Some(2));
1412    }
1413
1414    #[test]
1415    fn session_config_buffer_overrides() {
1416        let mut session = SessionConfig::new();
1417        let path = std::path::PathBuf::from("/test/file.rs");
1418        let config = PartialEditorConfig {
1419            tab_size: Some(8),
1420            ..Default::default()
1421        };
1422
1423        session.set_buffer_override(path.clone(), config);
1424        assert!(session.get_buffer_override(&path).is_some());
1425        assert_eq!(
1426            session.get_buffer_override(&path).unwrap().tab_size,
1427            Some(8)
1428        );
1429
1430        session.clear_buffer_override(&path);
1431        assert!(session.get_buffer_override(&path).is_none());
1432    }
1433
1434    #[test]
1435    fn session_config_to_partial_config() {
1436        let mut session = SessionConfig::new();
1437        session.set_theme(ThemeName::from("dark"));
1438        session.set_editor_option(|e| e.tab_size = Some(2));
1439
1440        let partial = session.to_partial_config();
1441        assert_eq!(partial.theme, Some(ThemeName::from("dark")));
1442        assert_eq!(partial.editor.as_ref().unwrap().tab_size, Some(2));
1443    }
1444
1445    // ============= Plugin Config Delta Saving Tests =============
1446
1447    #[test]
1448    fn plugins_with_default_enabled_not_serialized() {
1449        // When all plugins have enabled=true (the default), plugins should be None
1450        let mut config = crate::config::Config::default();
1451        config.plugins.insert(
1452            "test_plugin".to_string(),
1453            PluginConfig {
1454                enabled: true, // Default value
1455                path: Some(std::path::PathBuf::from("/path/to/plugin.ts")),
1456            },
1457        );
1458
1459        let partial = PartialConfig::from(&config);
1460
1461        // plugins should be None since all have default values
1462        assert!(
1463            partial.plugins.is_none(),
1464            "Plugins with default enabled=true should not be serialized"
1465        );
1466    }
1467
1468    #[test]
1469    fn plugins_with_disabled_are_serialized() {
1470        // When a plugin is disabled, it should be included in the partial config
1471        let mut config = crate::config::Config::default();
1472        config.plugins.insert(
1473            "enabled_plugin".to_string(),
1474            PluginConfig {
1475                enabled: true,
1476                path: Some(std::path::PathBuf::from("/path/to/enabled.ts")),
1477            },
1478        );
1479        config.plugins.insert(
1480            "disabled_plugin".to_string(),
1481            PluginConfig {
1482                enabled: false, // Not default!
1483                path: Some(std::path::PathBuf::from("/path/to/disabled.ts")),
1484            },
1485        );
1486
1487        let partial = PartialConfig::from(&config);
1488
1489        // plugins should contain only the disabled plugin
1490        assert!(partial.plugins.is_some());
1491        let plugins = partial.plugins.unwrap();
1492        assert_eq!(
1493            plugins.len(),
1494            1,
1495            "Only disabled plugins should be serialized"
1496        );
1497        assert!(plugins.contains_key("disabled_plugin"));
1498        assert!(!plugins.contains_key("enabled_plugin"));
1499
1500        // Check the disabled plugin has correct values
1501        let disabled = plugins.get("disabled_plugin").unwrap();
1502        assert_eq!(disabled.enabled, Some(false));
1503        // Path should be None - it's auto-discovered and shouldn't be saved
1504        assert!(disabled.path.is_none(), "Path should not be serialized");
1505    }
1506
1507    #[test]
1508    fn plugin_path_never_serialized() {
1509        // Even for disabled plugins, path should never be serialized
1510        let mut config = crate::config::Config::default();
1511        config.plugins.insert(
1512            "my_plugin".to_string(),
1513            PluginConfig {
1514                enabled: false,
1515                path: Some(std::path::PathBuf::from("/some/path/plugin.ts")),
1516            },
1517        );
1518
1519        let partial = PartialConfig::from(&config);
1520        let plugins = partial.plugins.unwrap();
1521        let plugin = plugins.get("my_plugin").unwrap();
1522
1523        assert!(
1524            plugin.path.is_none(),
1525            "Path is runtime-discovered and should never be serialized"
1526        );
1527    }
1528
1529    #[test]
1530    fn resolving_partial_with_disabled_plugin_preserves_state() {
1531        // Loading a config with a disabled plugin should preserve disabled state
1532        let partial = PartialConfig {
1533            plugins: Some(HashMap::from([(
1534                "my_plugin".to_string(),
1535                PartialPluginConfig {
1536                    enabled: Some(false),
1537                    path: None,
1538                },
1539            )])),
1540            ..Default::default()
1541        };
1542
1543        let resolved = partial.resolve();
1544
1545        // Plugin should exist and be disabled
1546        let plugin = resolved.plugins.get("my_plugin");
1547        assert!(
1548            plugin.is_some(),
1549            "Disabled plugin should be in resolved config"
1550        );
1551        assert!(
1552            !plugin.unwrap().enabled,
1553            "Plugin should remain disabled after resolve"
1554        );
1555    }
1556
1557    #[test]
1558    fn merge_plugins_preserves_higher_precedence_disabled_state() {
1559        // When merging, higher precedence disabled state should win
1560        let mut higher = PartialConfig {
1561            plugins: Some(HashMap::from([(
1562                "my_plugin".to_string(),
1563                PartialPluginConfig {
1564                    enabled: Some(false), // User disabled
1565                    path: None,
1566                },
1567            )])),
1568            ..Default::default()
1569        };
1570
1571        let lower = PartialConfig {
1572            plugins: Some(HashMap::from([(
1573                "my_plugin".to_string(),
1574                PartialPluginConfig {
1575                    enabled: Some(true), // Lower layer has it enabled
1576                    path: None,
1577                },
1578            )])),
1579            ..Default::default()
1580        };
1581
1582        higher.merge_from(&lower);
1583
1584        let plugins = higher.plugins.unwrap();
1585        let plugin = plugins.get("my_plugin").unwrap();
1586        assert_eq!(
1587            plugin.enabled,
1588            Some(false),
1589            "Higher precedence disabled state should win"
1590        );
1591    }
1592
1593    #[test]
1594    fn roundtrip_disabled_plugin_only_saves_delta() {
1595        // Roundtrip test: create config with mix of enabled/disabled plugins,
1596        // convert to partial, serialize to JSON, deserialize, and verify
1597        let mut config = crate::config::Config::default();
1598        config.plugins.insert(
1599            "plugin_a".to_string(),
1600            PluginConfig {
1601                enabled: true,
1602                path: Some(std::path::PathBuf::from("/a.ts")),
1603            },
1604        );
1605        config.plugins.insert(
1606            "plugin_b".to_string(),
1607            PluginConfig {
1608                enabled: false,
1609                path: Some(std::path::PathBuf::from("/b.ts")),
1610            },
1611        );
1612        config.plugins.insert(
1613            "plugin_c".to_string(),
1614            PluginConfig {
1615                enabled: true,
1616                path: Some(std::path::PathBuf::from("/c.ts")),
1617            },
1618        );
1619
1620        // Convert to partial (delta)
1621        let partial = PartialConfig::from(&config);
1622
1623        // Serialize to JSON
1624        let json = serde_json::to_string(&partial).unwrap();
1625
1626        // Verify only plugin_b is in the JSON
1627        assert!(
1628            json.contains("plugin_b"),
1629            "Disabled plugin should be in serialized JSON"
1630        );
1631        assert!(
1632            !json.contains("plugin_a"),
1633            "Enabled plugin_a should not be in serialized JSON"
1634        );
1635        assert!(
1636            !json.contains("plugin_c"),
1637            "Enabled plugin_c should not be in serialized JSON"
1638        );
1639
1640        // Deserialize back
1641        let deserialized: PartialConfig = serde_json::from_str(&json).unwrap();
1642
1643        // Verify plugins section only contains the disabled one
1644        let plugins = deserialized.plugins.unwrap();
1645        assert_eq!(plugins.len(), 1);
1646        assert!(plugins.contains_key("plugin_b"));
1647        assert_eq!(plugins.get("plugin_b").unwrap().enabled, Some(false));
1648    }
1649}