Skip to main content

fresh/
partial_config.rs

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