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