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