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