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