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