1use crate::config::{
7 AcceptSuggestionOnEnter, ClipboardConfig, CursorStyle, FileBrowserConfig, FileExplorerConfig,
8 FormatterConfig, HighlighterPreference, Keybinding, KeybindingMapName, KeymapConfig,
9 LanguageConfig, LineEndingOption, OnSaveAction, PluginConfig, TerminalConfig, ThemeName,
10 WarningsConfig,
11};
12use crate::types::LspServerConfig;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16pub trait Merge {
19 fn merge_from(&mut self, other: &Self);
22}
23
24impl<T: Clone> Merge for Option<T> {
25 fn merge_from(&mut self, other: &Self) {
26 if self.is_none() {
27 *self = other.clone();
28 }
29 }
30}
31
32fn merge_hashmap<K: Clone + Eq + std::hash::Hash, V: Clone>(
35 target: &mut Option<HashMap<K, V>>,
36 other: &Option<HashMap<K, V>>,
37) {
38 match (target, other) {
39 (Some(t), Some(o)) => {
40 for (key, value) in o {
41 t.entry(key.clone()).or_insert_with(|| value.clone());
42 }
43 }
44 (t @ None, Some(o)) => {
45 *t = Some(o.clone());
46 }
47 _ => {}
48 }
49}
50
51fn merge_hashmap_recursive<K, V>(target: &mut Option<HashMap<K, V>>, other: &Option<HashMap<K, V>>)
53where
54 K: Clone + Eq + std::hash::Hash,
55 V: Clone + Merge + Default,
56{
57 match (target, other) {
58 (Some(t), Some(o)) => {
59 for (key, value) in o {
60 t.entry(key.clone())
61 .and_modify(|existing| existing.merge_from(value))
62 .or_insert_with(|| value.clone());
63 }
64 }
65 (t @ None, Some(o)) => {
66 *t = Some(o.clone());
67 }
68 _ => {}
69 }
70}
71
72#[derive(Debug, Clone, Default, Deserialize, Serialize)]
75#[serde(default)]
76pub struct PartialConfig {
77 pub version: Option<u32>,
78 pub theme: Option<ThemeName>,
79 pub locale: Option<String>,
80 pub check_for_updates: Option<bool>,
81 pub editor: Option<PartialEditorConfig>,
82 pub file_explorer: Option<PartialFileExplorerConfig>,
83 pub file_browser: Option<PartialFileBrowserConfig>,
84 pub clipboard: Option<PartialClipboardConfig>,
85 pub terminal: Option<PartialTerminalConfig>,
86 pub keybindings: Option<Vec<Keybinding>>,
87 pub keybinding_maps: Option<HashMap<String, KeymapConfig>>,
88 pub active_keybinding_map: Option<KeybindingMapName>,
89 pub languages: Option<HashMap<String, PartialLanguageConfig>>,
90 pub lsp: Option<HashMap<String, LspServerConfig>>,
91 pub warnings: Option<PartialWarningsConfig>,
92 pub plugins: Option<HashMap<String, PartialPluginConfig>>,
93 pub packages: Option<PartialPackagesConfig>,
94}
95
96impl Merge for PartialConfig {
97 fn merge_from(&mut self, other: &Self) {
98 self.version.merge_from(&other.version);
99 self.theme.merge_from(&other.theme);
100 self.locale.merge_from(&other.locale);
101 self.check_for_updates.merge_from(&other.check_for_updates);
102
103 merge_partial(&mut self.editor, &other.editor);
105 merge_partial(&mut self.file_explorer, &other.file_explorer);
106 merge_partial(&mut self.file_browser, &other.file_browser);
107 merge_partial(&mut self.clipboard, &other.clipboard);
108 merge_partial(&mut self.terminal, &other.terminal);
109 merge_partial(&mut self.warnings, &other.warnings);
110 merge_partial(&mut self.packages, &other.packages);
111
112 self.keybindings.merge_from(&other.keybindings);
114
115 merge_hashmap(&mut self.keybinding_maps, &other.keybinding_maps);
117 merge_hashmap_recursive(&mut self.languages, &other.languages);
118 merge_hashmap_recursive(&mut self.lsp, &other.lsp);
119 merge_hashmap_recursive(&mut self.plugins, &other.plugins);
120
121 self.active_keybinding_map
122 .merge_from(&other.active_keybinding_map);
123 }
124}
125
126fn merge_partial<T: Merge + Clone>(target: &mut Option<T>, other: &Option<T>) {
128 match (target, other) {
129 (Some(t), Some(o)) => t.merge_from(o),
130 (t @ None, Some(o)) => *t = Some(o.clone()),
131 _ => {}
132 }
133}
134
135#[derive(Debug, Clone, Default, Deserialize, Serialize)]
137#[serde(default)]
138pub struct PartialEditorConfig {
139 pub tab_size: Option<usize>,
140 pub auto_indent: Option<bool>,
141 pub auto_close: Option<bool>,
142 pub auto_surround: Option<bool>,
143 pub line_numbers: Option<bool>,
144 pub relative_line_numbers: Option<bool>,
145 pub scroll_offset: Option<usize>,
146 pub syntax_highlighting: Option<bool>,
147 pub line_wrap: Option<bool>,
148 pub wrap_indent: Option<bool>,
149 pub highlight_timeout_ms: Option<u64>,
150 pub snapshot_interval: Option<usize>,
151 pub large_file_threshold_bytes: Option<u64>,
152 pub estimated_line_length: Option<usize>,
153 pub enable_inlay_hints: Option<bool>,
154 pub enable_semantic_tokens_full: Option<bool>,
155 pub diagnostics_inline_text: Option<bool>,
156 pub recovery_enabled: Option<bool>,
157 pub auto_recovery_save_interval_secs: Option<u32>,
158 pub auto_save_enabled: Option<bool>,
159 pub auto_save_interval_secs: Option<u32>,
160 pub highlight_context_bytes: Option<usize>,
161 pub mouse_hover_enabled: Option<bool>,
162 pub mouse_hover_delay_ms: Option<u64>,
163 pub double_click_time_ms: Option<u64>,
164 pub auto_revert_poll_interval_ms: Option<u64>,
165 pub read_concurrency: Option<usize>,
166 pub file_tree_poll_interval_ms: Option<u64>,
167 pub default_line_ending: Option<LineEndingOption>,
168 pub trim_trailing_whitespace_on_save: Option<bool>,
169 pub ensure_final_newline_on_save: Option<bool>,
170 pub highlight_matching_brackets: Option<bool>,
171 pub rainbow_brackets: Option<bool>,
172 pub cursor_style: Option<CursorStyle>,
173 pub keyboard_disambiguate_escape_codes: Option<bool>,
174 pub keyboard_report_event_types: Option<bool>,
175 pub keyboard_report_alternate_keys: Option<bool>,
176 pub keyboard_report_all_keys_as_escape_codes: Option<bool>,
177 pub quick_suggestions: Option<bool>,
178 pub quick_suggestions_delay_ms: Option<u64>,
179 pub suggest_on_trigger_characters: Option<bool>,
180 pub accept_suggestion_on_enter: Option<AcceptSuggestionOnEnter>,
181 pub show_menu_bar: Option<bool>,
182 pub show_tab_bar: Option<bool>,
183 pub show_status_bar: Option<bool>,
184 pub show_vertical_scrollbar: Option<bool>,
185 pub show_horizontal_scrollbar: Option<bool>,
186 pub use_terminal_bg: Option<bool>,
187 pub rulers: Option<Vec<usize>>,
188 pub whitespace_show: Option<bool>,
189 pub whitespace_spaces_leading: Option<bool>,
190 pub whitespace_spaces_inner: Option<bool>,
191 pub whitespace_spaces_trailing: Option<bool>,
192 pub whitespace_tabs_leading: Option<bool>,
193 pub whitespace_tabs_inner: Option<bool>,
194 pub whitespace_tabs_trailing: Option<bool>,
195}
196
197impl Merge for PartialEditorConfig {
198 fn merge_from(&mut self, other: &Self) {
199 self.tab_size.merge_from(&other.tab_size);
200 self.auto_indent.merge_from(&other.auto_indent);
201 self.auto_close.merge_from(&other.auto_close);
202 self.auto_surround.merge_from(&other.auto_surround);
203 self.line_numbers.merge_from(&other.line_numbers);
204 self.relative_line_numbers
205 .merge_from(&other.relative_line_numbers);
206 self.scroll_offset.merge_from(&other.scroll_offset);
207 self.syntax_highlighting
208 .merge_from(&other.syntax_highlighting);
209 self.line_wrap.merge_from(&other.line_wrap);
210 self.wrap_indent.merge_from(&other.wrap_indent);
211 self.highlight_timeout_ms
212 .merge_from(&other.highlight_timeout_ms);
213 self.snapshot_interval.merge_from(&other.snapshot_interval);
214 self.large_file_threshold_bytes
215 .merge_from(&other.large_file_threshold_bytes);
216 self.estimated_line_length
217 .merge_from(&other.estimated_line_length);
218 self.enable_inlay_hints
219 .merge_from(&other.enable_inlay_hints);
220 self.enable_semantic_tokens_full
221 .merge_from(&other.enable_semantic_tokens_full);
222 self.diagnostics_inline_text
223 .merge_from(&other.diagnostics_inline_text);
224 self.recovery_enabled.merge_from(&other.recovery_enabled);
225 self.auto_recovery_save_interval_secs
226 .merge_from(&other.auto_recovery_save_interval_secs);
227 self.auto_save_enabled.merge_from(&other.auto_save_enabled);
228 self.auto_save_interval_secs
229 .merge_from(&other.auto_save_interval_secs);
230 self.highlight_context_bytes
231 .merge_from(&other.highlight_context_bytes);
232 self.mouse_hover_enabled
233 .merge_from(&other.mouse_hover_enabled);
234 self.mouse_hover_delay_ms
235 .merge_from(&other.mouse_hover_delay_ms);
236 self.double_click_time_ms
237 .merge_from(&other.double_click_time_ms);
238 self.auto_revert_poll_interval_ms
239 .merge_from(&other.auto_revert_poll_interval_ms);
240 self.read_concurrency.merge_from(&other.read_concurrency);
241 self.file_tree_poll_interval_ms
242 .merge_from(&other.file_tree_poll_interval_ms);
243 self.default_line_ending
244 .merge_from(&other.default_line_ending);
245 self.trim_trailing_whitespace_on_save
246 .merge_from(&other.trim_trailing_whitespace_on_save);
247 self.ensure_final_newline_on_save
248 .merge_from(&other.ensure_final_newline_on_save);
249 self.highlight_matching_brackets
250 .merge_from(&other.highlight_matching_brackets);
251 self.rainbow_brackets.merge_from(&other.rainbow_brackets);
252 self.cursor_style.merge_from(&other.cursor_style);
253 self.keyboard_disambiguate_escape_codes
254 .merge_from(&other.keyboard_disambiguate_escape_codes);
255 self.keyboard_report_event_types
256 .merge_from(&other.keyboard_report_event_types);
257 self.keyboard_report_alternate_keys
258 .merge_from(&other.keyboard_report_alternate_keys);
259 self.keyboard_report_all_keys_as_escape_codes
260 .merge_from(&other.keyboard_report_all_keys_as_escape_codes);
261 self.quick_suggestions.merge_from(&other.quick_suggestions);
262 self.quick_suggestions_delay_ms
263 .merge_from(&other.quick_suggestions_delay_ms);
264 self.suggest_on_trigger_characters
265 .merge_from(&other.suggest_on_trigger_characters);
266 self.accept_suggestion_on_enter
267 .merge_from(&other.accept_suggestion_on_enter);
268 self.show_menu_bar.merge_from(&other.show_menu_bar);
269 self.show_tab_bar.merge_from(&other.show_tab_bar);
270 self.show_status_bar.merge_from(&other.show_status_bar);
271 self.show_vertical_scrollbar
272 .merge_from(&other.show_vertical_scrollbar);
273 self.show_horizontal_scrollbar
274 .merge_from(&other.show_horizontal_scrollbar);
275 self.use_terminal_bg.merge_from(&other.use_terminal_bg);
276 self.rulers.merge_from(&other.rulers);
277 self.whitespace_show.merge_from(&other.whitespace_show);
278 self.whitespace_spaces_leading
279 .merge_from(&other.whitespace_spaces_leading);
280 self.whitespace_spaces_inner
281 .merge_from(&other.whitespace_spaces_inner);
282 self.whitespace_spaces_trailing
283 .merge_from(&other.whitespace_spaces_trailing);
284 self.whitespace_tabs_leading
285 .merge_from(&other.whitespace_tabs_leading);
286 self.whitespace_tabs_inner
287 .merge_from(&other.whitespace_tabs_inner);
288 self.whitespace_tabs_trailing
289 .merge_from(&other.whitespace_tabs_trailing);
290 }
291}
292
293#[derive(Debug, Clone, Default, Deserialize, Serialize)]
295#[serde(default)]
296pub struct PartialFileExplorerConfig {
297 pub respect_gitignore: Option<bool>,
298 pub show_hidden: Option<bool>,
299 pub show_gitignored: Option<bool>,
300 pub custom_ignore_patterns: Option<Vec<String>>,
301 pub width: Option<f32>,
302}
303
304impl Merge for PartialFileExplorerConfig {
305 fn merge_from(&mut self, other: &Self) {
306 self.respect_gitignore.merge_from(&other.respect_gitignore);
307 self.show_hidden.merge_from(&other.show_hidden);
308 self.show_gitignored.merge_from(&other.show_gitignored);
309 self.custom_ignore_patterns
310 .merge_from(&other.custom_ignore_patterns);
311 self.width.merge_from(&other.width);
312 }
313}
314
315#[derive(Debug, Clone, Default, Deserialize, Serialize)]
317#[serde(default)]
318pub struct PartialFileBrowserConfig {
319 pub show_hidden: Option<bool>,
320}
321
322impl Merge for PartialFileBrowserConfig {
323 fn merge_from(&mut self, other: &Self) {
324 self.show_hidden.merge_from(&other.show_hidden);
325 }
326}
327
328#[derive(Debug, Clone, Default, Deserialize, Serialize)]
330#[serde(default)]
331pub struct PartialClipboardConfig {
332 pub use_osc52: Option<bool>,
333 pub use_system_clipboard: Option<bool>,
334}
335
336impl Merge for PartialClipboardConfig {
337 fn merge_from(&mut self, other: &Self) {
338 self.use_osc52.merge_from(&other.use_osc52);
339 self.use_system_clipboard
340 .merge_from(&other.use_system_clipboard);
341 }
342}
343
344#[derive(Debug, Clone, Default, Deserialize, Serialize)]
346#[serde(default)]
347pub struct PartialTerminalConfig {
348 pub jump_to_end_on_output: Option<bool>,
349}
350
351impl Merge for PartialTerminalConfig {
352 fn merge_from(&mut self, other: &Self) {
353 self.jump_to_end_on_output
354 .merge_from(&other.jump_to_end_on_output);
355 }
356}
357
358#[derive(Debug, Clone, Default, Deserialize, Serialize)]
360#[serde(default)]
361pub struct PartialWarningsConfig {
362 pub show_status_indicator: Option<bool>,
363}
364
365impl Merge for PartialWarningsConfig {
366 fn merge_from(&mut self, other: &Self) {
367 self.show_status_indicator
368 .merge_from(&other.show_status_indicator);
369 }
370}
371
372#[derive(Debug, Clone, Default, Deserialize, Serialize)]
374#[serde(default)]
375pub struct PartialPackagesConfig {
376 pub sources: Option<Vec<String>>,
377}
378
379impl Merge for PartialPackagesConfig {
380 fn merge_from(&mut self, other: &Self) {
381 self.sources.merge_from(&other.sources);
382 }
383}
384
385#[derive(Debug, Clone, Default, Deserialize, Serialize)]
387#[serde(default)]
388pub struct PartialPluginConfig {
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub enabled: Option<bool>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub path: Option<std::path::PathBuf>,
393}
394
395impl Merge for PartialPluginConfig {
396 fn merge_from(&mut self, other: &Self) {
397 self.enabled.merge_from(&other.enabled);
398 self.path.merge_from(&other.path);
399 }
400}
401
402#[derive(Debug, Clone, Default, Deserialize, Serialize)]
404#[serde(default)]
405pub struct PartialLanguageConfig {
406 pub extensions: Option<Vec<String>>,
407 pub filenames: Option<Vec<String>>,
408 pub grammar: Option<String>,
409 pub comment_prefix: Option<String>,
410 pub auto_indent: Option<bool>,
411 pub auto_close: Option<bool>,
412 pub auto_surround: Option<bool>,
413 pub highlighter: Option<HighlighterPreference>,
414 pub textmate_grammar: Option<std::path::PathBuf>,
415 pub show_whitespace_tabs: Option<bool>,
416 pub use_tabs: Option<bool>,
417 pub tab_size: Option<usize>,
418 pub formatter: Option<FormatterConfig>,
419 pub format_on_save: Option<bool>,
420 pub on_save: Option<Vec<OnSaveAction>>,
421}
422
423impl Merge for PartialLanguageConfig {
424 fn merge_from(&mut self, other: &Self) {
425 self.extensions.merge_from(&other.extensions);
426 self.filenames.merge_from(&other.filenames);
427 self.grammar.merge_from(&other.grammar);
428 self.comment_prefix.merge_from(&other.comment_prefix);
429 self.auto_indent.merge_from(&other.auto_indent);
430 self.auto_close.merge_from(&other.auto_close);
431 self.auto_surround.merge_from(&other.auto_surround);
432 self.highlighter.merge_from(&other.highlighter);
433 self.textmate_grammar.merge_from(&other.textmate_grammar);
434 self.show_whitespace_tabs
435 .merge_from(&other.show_whitespace_tabs);
436 self.use_tabs.merge_from(&other.use_tabs);
437 self.tab_size.merge_from(&other.tab_size);
438 self.formatter.merge_from(&other.formatter);
439 self.format_on_save.merge_from(&other.format_on_save);
440 self.on_save.merge_from(&other.on_save);
441 }
442}
443
444impl Merge for LspServerConfig {
445 fn merge_from(&mut self, other: &Self) {
446 if self.command.is_empty() {
448 self.command = other.command.clone();
449 }
450 if self.args.is_empty() {
452 self.args = other.args.clone();
453 }
454 if self.initialization_options.is_none() {
458 self.initialization_options = other.initialization_options.clone();
459 }
460 if self.env.is_empty() {
462 self.env = other.env.clone();
463 } else if !other.env.is_empty() {
464 let mut merged = other.env.clone();
465 merged.extend(self.env.drain());
466 self.env = merged;
467 }
468 if self.language_id_overrides.is_empty() {
470 self.language_id_overrides = other.language_id_overrides.clone();
471 } else if !other.language_id_overrides.is_empty() {
472 let mut merged = other.language_id_overrides.clone();
473 merged.extend(self.language_id_overrides.drain());
474 self.language_id_overrides = merged;
475 }
476 }
477}
478
479impl From<&crate::config::EditorConfig> for PartialEditorConfig {
482 fn from(cfg: &crate::config::EditorConfig) -> Self {
483 Self {
484 tab_size: Some(cfg.tab_size),
485 auto_indent: Some(cfg.auto_indent),
486 auto_close: Some(cfg.auto_close),
487 auto_surround: Some(cfg.auto_surround),
488 line_numbers: Some(cfg.line_numbers),
489 relative_line_numbers: Some(cfg.relative_line_numbers),
490 scroll_offset: Some(cfg.scroll_offset),
491 syntax_highlighting: Some(cfg.syntax_highlighting),
492 line_wrap: Some(cfg.line_wrap),
493 wrap_indent: Some(cfg.wrap_indent),
494 highlight_timeout_ms: Some(cfg.highlight_timeout_ms),
495 snapshot_interval: Some(cfg.snapshot_interval),
496 large_file_threshold_bytes: Some(cfg.large_file_threshold_bytes),
497 estimated_line_length: Some(cfg.estimated_line_length),
498 enable_inlay_hints: Some(cfg.enable_inlay_hints),
499 enable_semantic_tokens_full: Some(cfg.enable_semantic_tokens_full),
500 diagnostics_inline_text: Some(cfg.diagnostics_inline_text),
501 recovery_enabled: Some(cfg.recovery_enabled),
502 auto_recovery_save_interval_secs: Some(cfg.auto_recovery_save_interval_secs),
503 auto_save_enabled: Some(cfg.auto_save_enabled),
504 auto_save_interval_secs: Some(cfg.auto_save_interval_secs),
505 highlight_context_bytes: Some(cfg.highlight_context_bytes),
506 mouse_hover_enabled: Some(cfg.mouse_hover_enabled),
507 mouse_hover_delay_ms: Some(cfg.mouse_hover_delay_ms),
508 double_click_time_ms: Some(cfg.double_click_time_ms),
509 auto_revert_poll_interval_ms: Some(cfg.auto_revert_poll_interval_ms),
510 read_concurrency: Some(cfg.read_concurrency),
511 file_tree_poll_interval_ms: Some(cfg.file_tree_poll_interval_ms),
512 default_line_ending: Some(cfg.default_line_ending.clone()),
513 trim_trailing_whitespace_on_save: Some(cfg.trim_trailing_whitespace_on_save),
514 ensure_final_newline_on_save: Some(cfg.ensure_final_newline_on_save),
515 highlight_matching_brackets: Some(cfg.highlight_matching_brackets),
516 rainbow_brackets: Some(cfg.rainbow_brackets),
517 cursor_style: Some(cfg.cursor_style),
518 keyboard_disambiguate_escape_codes: Some(cfg.keyboard_disambiguate_escape_codes),
519 keyboard_report_event_types: Some(cfg.keyboard_report_event_types),
520 keyboard_report_alternate_keys: Some(cfg.keyboard_report_alternate_keys),
521 keyboard_report_all_keys_as_escape_codes: Some(
522 cfg.keyboard_report_all_keys_as_escape_codes,
523 ),
524 quick_suggestions: Some(cfg.quick_suggestions),
525 quick_suggestions_delay_ms: Some(cfg.quick_suggestions_delay_ms),
526 suggest_on_trigger_characters: Some(cfg.suggest_on_trigger_characters),
527 accept_suggestion_on_enter: Some(cfg.accept_suggestion_on_enter),
528 show_menu_bar: Some(cfg.show_menu_bar),
529 show_tab_bar: Some(cfg.show_tab_bar),
530 show_status_bar: Some(cfg.show_status_bar),
531 show_vertical_scrollbar: Some(cfg.show_vertical_scrollbar),
532 show_horizontal_scrollbar: Some(cfg.show_horizontal_scrollbar),
533 use_terminal_bg: Some(cfg.use_terminal_bg),
534 rulers: Some(cfg.rulers.clone()),
535 whitespace_show: Some(cfg.whitespace_show),
536 whitespace_spaces_leading: Some(cfg.whitespace_spaces_leading),
537 whitespace_spaces_inner: Some(cfg.whitespace_spaces_inner),
538 whitespace_spaces_trailing: Some(cfg.whitespace_spaces_trailing),
539 whitespace_tabs_leading: Some(cfg.whitespace_tabs_leading),
540 whitespace_tabs_inner: Some(cfg.whitespace_tabs_inner),
541 whitespace_tabs_trailing: Some(cfg.whitespace_tabs_trailing),
542 }
543 }
544}
545
546impl PartialEditorConfig {
547 pub fn resolve(self, defaults: &crate::config::EditorConfig) -> crate::config::EditorConfig {
549 crate::config::EditorConfig {
550 tab_size: self.tab_size.unwrap_or(defaults.tab_size),
551 auto_indent: self.auto_indent.unwrap_or(defaults.auto_indent),
552 auto_close: self.auto_close.unwrap_or(defaults.auto_close),
553 auto_surround: self.auto_surround.unwrap_or(defaults.auto_surround),
554 line_numbers: self.line_numbers.unwrap_or(defaults.line_numbers),
555 relative_line_numbers: self
556 .relative_line_numbers
557 .unwrap_or(defaults.relative_line_numbers),
558 scroll_offset: self.scroll_offset.unwrap_or(defaults.scroll_offset),
559 syntax_highlighting: self
560 .syntax_highlighting
561 .unwrap_or(defaults.syntax_highlighting),
562 line_wrap: self.line_wrap.unwrap_or(defaults.line_wrap),
563 wrap_indent: self.wrap_indent.unwrap_or(defaults.wrap_indent),
564 highlight_timeout_ms: self
565 .highlight_timeout_ms
566 .unwrap_or(defaults.highlight_timeout_ms),
567 snapshot_interval: self.snapshot_interval.unwrap_or(defaults.snapshot_interval),
568 large_file_threshold_bytes: self
569 .large_file_threshold_bytes
570 .unwrap_or(defaults.large_file_threshold_bytes),
571 estimated_line_length: self
572 .estimated_line_length
573 .unwrap_or(defaults.estimated_line_length),
574 enable_inlay_hints: self
575 .enable_inlay_hints
576 .unwrap_or(defaults.enable_inlay_hints),
577 enable_semantic_tokens_full: self
578 .enable_semantic_tokens_full
579 .unwrap_or(defaults.enable_semantic_tokens_full),
580 diagnostics_inline_text: self
581 .diagnostics_inline_text
582 .unwrap_or(defaults.diagnostics_inline_text),
583 recovery_enabled: self.recovery_enabled.unwrap_or(defaults.recovery_enabled),
584 auto_recovery_save_interval_secs: self
585 .auto_recovery_save_interval_secs
586 .unwrap_or(defaults.auto_recovery_save_interval_secs),
587 auto_save_enabled: self.auto_save_enabled.unwrap_or(defaults.auto_save_enabled),
588 auto_save_interval_secs: self
589 .auto_save_interval_secs
590 .unwrap_or(defaults.auto_save_interval_secs),
591 highlight_context_bytes: self
592 .highlight_context_bytes
593 .unwrap_or(defaults.highlight_context_bytes),
594 mouse_hover_enabled: self
595 .mouse_hover_enabled
596 .unwrap_or(defaults.mouse_hover_enabled),
597 mouse_hover_delay_ms: self
598 .mouse_hover_delay_ms
599 .unwrap_or(defaults.mouse_hover_delay_ms),
600 double_click_time_ms: self
601 .double_click_time_ms
602 .unwrap_or(defaults.double_click_time_ms),
603 auto_revert_poll_interval_ms: self
604 .auto_revert_poll_interval_ms
605 .unwrap_or(defaults.auto_revert_poll_interval_ms),
606 read_concurrency: self.read_concurrency.unwrap_or(defaults.read_concurrency),
607 file_tree_poll_interval_ms: self
608 .file_tree_poll_interval_ms
609 .unwrap_or(defaults.file_tree_poll_interval_ms),
610 default_line_ending: self
611 .default_line_ending
612 .unwrap_or(defaults.default_line_ending.clone()),
613 trim_trailing_whitespace_on_save: self
614 .trim_trailing_whitespace_on_save
615 .unwrap_or(defaults.trim_trailing_whitespace_on_save),
616 ensure_final_newline_on_save: self
617 .ensure_final_newline_on_save
618 .unwrap_or(defaults.ensure_final_newline_on_save),
619 highlight_matching_brackets: self
620 .highlight_matching_brackets
621 .unwrap_or(defaults.highlight_matching_brackets),
622 rainbow_brackets: self.rainbow_brackets.unwrap_or(defaults.rainbow_brackets),
623 cursor_style: self.cursor_style.unwrap_or(defaults.cursor_style),
624 keyboard_disambiguate_escape_codes: self
625 .keyboard_disambiguate_escape_codes
626 .unwrap_or(defaults.keyboard_disambiguate_escape_codes),
627 keyboard_report_event_types: self
628 .keyboard_report_event_types
629 .unwrap_or(defaults.keyboard_report_event_types),
630 keyboard_report_alternate_keys: self
631 .keyboard_report_alternate_keys
632 .unwrap_or(defaults.keyboard_report_alternate_keys),
633 keyboard_report_all_keys_as_escape_codes: self
634 .keyboard_report_all_keys_as_escape_codes
635 .unwrap_or(defaults.keyboard_report_all_keys_as_escape_codes),
636 quick_suggestions: self.quick_suggestions.unwrap_or(defaults.quick_suggestions),
637 quick_suggestions_delay_ms: self
638 .quick_suggestions_delay_ms
639 .unwrap_or(defaults.quick_suggestions_delay_ms),
640 suggest_on_trigger_characters: self
641 .suggest_on_trigger_characters
642 .unwrap_or(defaults.suggest_on_trigger_characters),
643 accept_suggestion_on_enter: self
644 .accept_suggestion_on_enter
645 .unwrap_or(defaults.accept_suggestion_on_enter),
646 show_menu_bar: self.show_menu_bar.unwrap_or(defaults.show_menu_bar),
647 show_tab_bar: self.show_tab_bar.unwrap_or(defaults.show_tab_bar),
648 show_status_bar: self.show_status_bar.unwrap_or(defaults.show_status_bar),
649 show_vertical_scrollbar: self
650 .show_vertical_scrollbar
651 .unwrap_or(defaults.show_vertical_scrollbar),
652 show_horizontal_scrollbar: self
653 .show_horizontal_scrollbar
654 .unwrap_or(defaults.show_horizontal_scrollbar),
655 use_terminal_bg: self.use_terminal_bg.unwrap_or(defaults.use_terminal_bg),
656 rulers: self.rulers.unwrap_or_else(|| defaults.rulers.clone()),
657 whitespace_show: self.whitespace_show.unwrap_or(defaults.whitespace_show),
658 whitespace_spaces_leading: self
659 .whitespace_spaces_leading
660 .unwrap_or(defaults.whitespace_spaces_leading),
661 whitespace_spaces_inner: self
662 .whitespace_spaces_inner
663 .unwrap_or(defaults.whitespace_spaces_inner),
664 whitespace_spaces_trailing: self
665 .whitespace_spaces_trailing
666 .unwrap_or(defaults.whitespace_spaces_trailing),
667 whitespace_tabs_leading: self
668 .whitespace_tabs_leading
669 .unwrap_or(defaults.whitespace_tabs_leading),
670 whitespace_tabs_inner: self
671 .whitespace_tabs_inner
672 .unwrap_or(defaults.whitespace_tabs_inner),
673 whitespace_tabs_trailing: self
674 .whitespace_tabs_trailing
675 .unwrap_or(defaults.whitespace_tabs_trailing),
676 }
677 }
678}
679
680impl From<&FileExplorerConfig> for PartialFileExplorerConfig {
681 fn from(cfg: &FileExplorerConfig) -> Self {
682 Self {
683 respect_gitignore: Some(cfg.respect_gitignore),
684 show_hidden: Some(cfg.show_hidden),
685 show_gitignored: Some(cfg.show_gitignored),
686 custom_ignore_patterns: Some(cfg.custom_ignore_patterns.clone()),
687 width: Some(cfg.width),
688 }
689 }
690}
691
692impl PartialFileExplorerConfig {
693 pub fn resolve(self, defaults: &FileExplorerConfig) -> FileExplorerConfig {
694 FileExplorerConfig {
695 respect_gitignore: self.respect_gitignore.unwrap_or(defaults.respect_gitignore),
696 show_hidden: self.show_hidden.unwrap_or(defaults.show_hidden),
697 show_gitignored: self.show_gitignored.unwrap_or(defaults.show_gitignored),
698 custom_ignore_patterns: self
699 .custom_ignore_patterns
700 .unwrap_or_else(|| defaults.custom_ignore_patterns.clone()),
701 width: self.width.unwrap_or(defaults.width),
702 }
703 }
704}
705
706impl From<&FileBrowserConfig> for PartialFileBrowserConfig {
707 fn from(cfg: &FileBrowserConfig) -> Self {
708 Self {
709 show_hidden: Some(cfg.show_hidden),
710 }
711 }
712}
713
714impl PartialFileBrowserConfig {
715 pub fn resolve(self, defaults: &FileBrowserConfig) -> FileBrowserConfig {
716 FileBrowserConfig {
717 show_hidden: self.show_hidden.unwrap_or(defaults.show_hidden),
718 }
719 }
720}
721
722impl From<&ClipboardConfig> for PartialClipboardConfig {
723 fn from(cfg: &ClipboardConfig) -> Self {
724 Self {
725 use_osc52: Some(cfg.use_osc52),
726 use_system_clipboard: Some(cfg.use_system_clipboard),
727 }
728 }
729}
730
731impl PartialClipboardConfig {
732 pub fn resolve(self, defaults: &ClipboardConfig) -> ClipboardConfig {
733 ClipboardConfig {
734 use_osc52: self.use_osc52.unwrap_or(defaults.use_osc52),
735 use_system_clipboard: self
736 .use_system_clipboard
737 .unwrap_or(defaults.use_system_clipboard),
738 }
739 }
740}
741
742impl From<&TerminalConfig> for PartialTerminalConfig {
743 fn from(cfg: &TerminalConfig) -> Self {
744 Self {
745 jump_to_end_on_output: Some(cfg.jump_to_end_on_output),
746 }
747 }
748}
749
750impl PartialTerminalConfig {
751 pub fn resolve(self, defaults: &TerminalConfig) -> TerminalConfig {
752 TerminalConfig {
753 jump_to_end_on_output: self
754 .jump_to_end_on_output
755 .unwrap_or(defaults.jump_to_end_on_output),
756 }
757 }
758}
759
760impl From<&WarningsConfig> for PartialWarningsConfig {
761 fn from(cfg: &WarningsConfig) -> Self {
762 Self {
763 show_status_indicator: Some(cfg.show_status_indicator),
764 }
765 }
766}
767
768impl PartialWarningsConfig {
769 pub fn resolve(self, defaults: &WarningsConfig) -> WarningsConfig {
770 WarningsConfig {
771 show_status_indicator: self
772 .show_status_indicator
773 .unwrap_or(defaults.show_status_indicator),
774 }
775 }
776}
777
778impl From<&crate::config::PackagesConfig> for PartialPackagesConfig {
779 fn from(cfg: &crate::config::PackagesConfig) -> Self {
780 Self {
781 sources: Some(cfg.sources.clone()),
782 }
783 }
784}
785
786impl PartialPackagesConfig {
787 pub fn resolve(
788 self,
789 defaults: &crate::config::PackagesConfig,
790 ) -> crate::config::PackagesConfig {
791 crate::config::PackagesConfig {
792 sources: self.sources.unwrap_or_else(|| defaults.sources.clone()),
793 }
794 }
795}
796
797impl From<&PluginConfig> for PartialPluginConfig {
798 fn from(cfg: &PluginConfig) -> Self {
799 Self {
800 enabled: Some(cfg.enabled),
801 path: cfg.path.clone(),
802 }
803 }
804}
805
806impl PartialPluginConfig {
807 pub fn resolve(self, defaults: &PluginConfig) -> PluginConfig {
808 PluginConfig {
809 enabled: self.enabled.unwrap_or(defaults.enabled),
810 path: self.path.or_else(|| defaults.path.clone()),
811 }
812 }
813}
814
815impl From<&LanguageConfig> for PartialLanguageConfig {
816 fn from(cfg: &LanguageConfig) -> Self {
817 Self {
818 extensions: Some(cfg.extensions.clone()),
819 filenames: Some(cfg.filenames.clone()),
820 grammar: Some(cfg.grammar.clone()),
821 comment_prefix: cfg.comment_prefix.clone(),
822 auto_indent: Some(cfg.auto_indent),
823 auto_close: cfg.auto_close,
824 auto_surround: cfg.auto_surround,
825 highlighter: Some(cfg.highlighter),
826 textmate_grammar: cfg.textmate_grammar.clone(),
827 show_whitespace_tabs: Some(cfg.show_whitespace_tabs),
828 use_tabs: Some(cfg.use_tabs),
829 tab_size: cfg.tab_size,
830 formatter: cfg.formatter.clone(),
831 format_on_save: Some(cfg.format_on_save),
832 on_save: Some(cfg.on_save.clone()),
833 }
834 }
835}
836
837impl PartialLanguageConfig {
838 pub fn resolve(self, defaults: &LanguageConfig) -> LanguageConfig {
839 LanguageConfig {
840 extensions: self
841 .extensions
842 .unwrap_or_else(|| defaults.extensions.clone()),
843 filenames: self.filenames.unwrap_or_else(|| defaults.filenames.clone()),
844 grammar: self.grammar.unwrap_or_else(|| defaults.grammar.clone()),
845 comment_prefix: self
846 .comment_prefix
847 .or_else(|| defaults.comment_prefix.clone()),
848 auto_indent: self.auto_indent.unwrap_or(defaults.auto_indent),
849 auto_close: self.auto_close.or(defaults.auto_close),
850 auto_surround: self.auto_surround.or(defaults.auto_surround),
851 highlighter: self.highlighter.unwrap_or(defaults.highlighter),
852 textmate_grammar: self
853 .textmate_grammar
854 .or_else(|| defaults.textmate_grammar.clone()),
855 show_whitespace_tabs: self
856 .show_whitespace_tabs
857 .unwrap_or(defaults.show_whitespace_tabs),
858 use_tabs: self.use_tabs.unwrap_or(defaults.use_tabs),
859 tab_size: self.tab_size.or(defaults.tab_size),
860 formatter: self.formatter.or_else(|| defaults.formatter.clone()),
861 format_on_save: self.format_on_save.unwrap_or(defaults.format_on_save),
862 on_save: self.on_save.unwrap_or_else(|| defaults.on_save.clone()),
863 }
864 }
865}
866
867impl From<&crate::config::Config> for PartialConfig {
868 fn from(cfg: &crate::config::Config) -> Self {
869 Self {
870 version: Some(cfg.version),
871 theme: Some(cfg.theme.clone()),
872 locale: cfg.locale.0.clone(),
873 check_for_updates: Some(cfg.check_for_updates),
874 editor: Some(PartialEditorConfig::from(&cfg.editor)),
875 file_explorer: Some(PartialFileExplorerConfig::from(&cfg.file_explorer)),
876 file_browser: Some(PartialFileBrowserConfig::from(&cfg.file_browser)),
877 clipboard: Some(PartialClipboardConfig::from(&cfg.clipboard)),
878 terminal: Some(PartialTerminalConfig::from(&cfg.terminal)),
879 keybindings: Some(cfg.keybindings.clone()),
880 keybinding_maps: Some(cfg.keybinding_maps.clone()),
881 active_keybinding_map: Some(cfg.active_keybinding_map.clone()),
882 languages: Some(
883 cfg.languages
884 .iter()
885 .map(|(k, v)| (k.clone(), PartialLanguageConfig::from(v)))
886 .collect(),
887 ),
888 lsp: Some(cfg.lsp.clone()),
889 warnings: Some(PartialWarningsConfig::from(&cfg.warnings)),
890 plugins: {
893 let default_plugin = crate::config::PluginConfig::default();
894 let non_default_plugins: HashMap<String, PartialPluginConfig> = cfg
895 .plugins
896 .iter()
897 .filter(|(_, v)| v.enabled != default_plugin.enabled)
898 .map(|(k, v)| {
899 (
900 k.clone(),
901 PartialPluginConfig {
902 enabled: Some(v.enabled),
903 path: None, },
905 )
906 })
907 .collect();
908 if non_default_plugins.is_empty() {
909 None
910 } else {
911 Some(non_default_plugins)
912 }
913 },
914 packages: Some(PartialPackagesConfig::from(&cfg.packages)),
915 }
916 }
917}
918
919impl PartialConfig {
920 pub fn resolve(self) -> crate::config::Config {
922 let defaults = crate::config::Config::default();
923 self.resolve_with_defaults(&defaults)
924 }
925
926 pub fn resolve_with_defaults(self, defaults: &crate::config::Config) -> crate::config::Config {
928 let languages = {
930 let mut result = defaults.languages.clone();
931 if let Some(partial_langs) = self.languages {
932 for (key, partial_lang) in partial_langs {
933 let default_lang = result.get(&key).cloned().unwrap_or_default();
934 result.insert(key, partial_lang.resolve(&default_lang));
935 }
936 }
937 result
938 };
939
940 let lsp = {
942 let mut result = defaults.lsp.clone();
943 if let Some(partial_lsp) = self.lsp {
944 for (key, partial_config) in partial_lsp {
945 if let Some(default_config) = result.get(&key) {
946 result.insert(key, partial_config.merge_with_defaults(default_config));
947 } else {
948 result.insert(key, partial_config);
950 }
951 }
952 }
953 result
954 };
955
956 let keybinding_maps = {
958 let mut result = defaults.keybinding_maps.clone();
959 if let Some(partial_maps) = self.keybinding_maps {
960 for (key, config) in partial_maps {
961 result.insert(key, config);
962 }
963 }
964 result
965 };
966
967 let plugins = {
969 let mut result = defaults.plugins.clone();
970 if let Some(partial_plugins) = self.plugins {
971 for (key, partial_plugin) in partial_plugins {
972 let default_plugin = result.get(&key).cloned().unwrap_or_default();
973 result.insert(key, partial_plugin.resolve(&default_plugin));
974 }
975 }
976 result
977 };
978
979 crate::config::Config {
980 version: self.version.unwrap_or(defaults.version),
981 theme: self.theme.unwrap_or_else(|| defaults.theme.clone()),
982 locale: crate::config::LocaleName::from(
983 self.locale.or_else(|| defaults.locale.0.clone()),
984 ),
985 check_for_updates: self.check_for_updates.unwrap_or(defaults.check_for_updates),
986 editor: self
987 .editor
988 .map(|e| e.resolve(&defaults.editor))
989 .unwrap_or_else(|| defaults.editor.clone()),
990 file_explorer: self
991 .file_explorer
992 .map(|e| e.resolve(&defaults.file_explorer))
993 .unwrap_or_else(|| defaults.file_explorer.clone()),
994 file_browser: self
995 .file_browser
996 .map(|e| e.resolve(&defaults.file_browser))
997 .unwrap_or_else(|| defaults.file_browser.clone()),
998 clipboard: self
999 .clipboard
1000 .map(|e| e.resolve(&defaults.clipboard))
1001 .unwrap_or_else(|| defaults.clipboard.clone()),
1002 terminal: self
1003 .terminal
1004 .map(|e| e.resolve(&defaults.terminal))
1005 .unwrap_or_else(|| defaults.terminal.clone()),
1006 keybindings: self
1007 .keybindings
1008 .unwrap_or_else(|| defaults.keybindings.clone()),
1009 keybinding_maps,
1010 active_keybinding_map: self
1011 .active_keybinding_map
1012 .unwrap_or_else(|| defaults.active_keybinding_map.clone()),
1013 languages,
1014 lsp,
1015 warnings: self
1016 .warnings
1017 .map(|e| e.resolve(&defaults.warnings))
1018 .unwrap_or_else(|| defaults.warnings.clone()),
1019 plugins,
1020 packages: self
1021 .packages
1022 .map(|e| e.resolve(&defaults.packages))
1023 .unwrap_or_else(|| defaults.packages.clone()),
1024 }
1025 }
1026}
1027
1028impl Default for LanguageConfig {
1030 fn default() -> Self {
1031 Self {
1032 extensions: Vec::new(),
1033 filenames: Vec::new(),
1034 grammar: String::new(),
1035 comment_prefix: None,
1036 auto_indent: true,
1037 auto_close: None,
1038 auto_surround: None,
1039 highlighter: HighlighterPreference::default(),
1040 textmate_grammar: None,
1041 show_whitespace_tabs: true,
1042 use_tabs: false,
1043 tab_size: None,
1044 formatter: None,
1045 format_on_save: false,
1046 on_save: Vec::new(),
1047 }
1048 }
1049}
1050
1051#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1059#[serde(default)]
1060pub struct SessionConfig {
1061 pub theme: Option<ThemeName>,
1063
1064 pub editor: Option<PartialEditorConfig>,
1066
1067 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1070 pub buffer_overrides: HashMap<std::path::PathBuf, PartialEditorConfig>,
1071}
1072
1073impl SessionConfig {
1074 pub fn new() -> Self {
1076 Self::default()
1077 }
1078
1079 pub fn set_theme(&mut self, theme: ThemeName) {
1081 self.theme = Some(theme);
1082 }
1083
1084 pub fn clear_theme(&mut self) {
1086 self.theme = None;
1087 }
1088
1089 pub fn set_editor_option<F>(&mut self, setter: F)
1091 where
1092 F: FnOnce(&mut PartialEditorConfig),
1093 {
1094 let editor = self.editor.get_or_insert_with(Default::default);
1095 setter(editor);
1096 }
1097
1098 pub fn set_buffer_override(&mut self, path: std::path::PathBuf, config: PartialEditorConfig) {
1100 self.buffer_overrides.insert(path, config);
1101 }
1102
1103 pub fn clear_buffer_override(&mut self, path: &std::path::Path) {
1105 self.buffer_overrides.remove(path);
1106 }
1107
1108 pub fn get_buffer_override(&self, path: &std::path::Path) -> Option<&PartialEditorConfig> {
1110 self.buffer_overrides.get(path)
1111 }
1112
1113 pub fn to_partial_config(&self) -> PartialConfig {
1115 PartialConfig {
1116 theme: self.theme.clone(),
1117 editor: self.editor.clone(),
1118 ..Default::default()
1119 }
1120 }
1121
1122 pub fn is_empty(&self) -> bool {
1124 self.theme.is_none() && self.editor.is_none() && self.buffer_overrides.is_empty()
1125 }
1126}
1127
1128impl From<PartialConfig> for SessionConfig {
1129 fn from(partial: PartialConfig) -> Self {
1130 Self {
1131 theme: partial.theme,
1132 editor: partial.editor,
1133 buffer_overrides: HashMap::new(),
1134 }
1135 }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use super::*;
1141
1142 #[test]
1143 fn merge_option_higher_precedence_wins() {
1144 let mut higher: Option<i32> = Some(10);
1145 let lower: Option<i32> = Some(5);
1146 higher.merge_from(&lower);
1147 assert_eq!(higher, Some(10));
1148 }
1149
1150 #[test]
1151 fn merge_option_fills_from_lower_when_none() {
1152 let mut higher: Option<i32> = None;
1153 let lower: Option<i32> = Some(5);
1154 higher.merge_from(&lower);
1155 assert_eq!(higher, Some(5));
1156 }
1157
1158 #[test]
1159 fn merge_editor_config_recursive() {
1160 let mut higher = PartialEditorConfig {
1161 tab_size: Some(2),
1162 ..Default::default()
1163 };
1164 let lower = PartialEditorConfig {
1165 tab_size: Some(4),
1166 line_numbers: Some(true),
1167 ..Default::default()
1168 };
1169
1170 higher.merge_from(&lower);
1171
1172 assert_eq!(higher.tab_size, Some(2)); assert_eq!(higher.line_numbers, Some(true)); }
1175
1176 #[test]
1177 fn merge_partial_config_combines_languages() {
1178 let mut higher = PartialConfig {
1179 languages: Some(HashMap::from([(
1180 "rust".to_string(),
1181 PartialLanguageConfig {
1182 tab_size: Some(4),
1183 ..Default::default()
1184 },
1185 )])),
1186 ..Default::default()
1187 };
1188 let lower = PartialConfig {
1189 languages: Some(HashMap::from([(
1190 "python".to_string(),
1191 PartialLanguageConfig {
1192 tab_size: Some(4),
1193 ..Default::default()
1194 },
1195 )])),
1196 ..Default::default()
1197 };
1198
1199 higher.merge_from(&lower);
1200
1201 let langs = higher.languages.unwrap();
1202 assert!(langs.contains_key("rust"));
1203 assert!(langs.contains_key("python"));
1204 }
1205
1206 #[test]
1207 fn merge_languages_same_key_higher_wins() {
1208 let mut higher = PartialConfig {
1209 languages: Some(HashMap::from([(
1210 "rust".to_string(),
1211 PartialLanguageConfig {
1212 tab_size: Some(2),
1213 use_tabs: Some(true),
1214 ..Default::default()
1215 },
1216 )])),
1217 ..Default::default()
1218 };
1219 let lower = PartialConfig {
1220 languages: Some(HashMap::from([(
1221 "rust".to_string(),
1222 PartialLanguageConfig {
1223 tab_size: Some(4),
1224 auto_indent: Some(false),
1225 ..Default::default()
1226 },
1227 )])),
1228 ..Default::default()
1229 };
1230
1231 higher.merge_from(&lower);
1232
1233 let langs = higher.languages.unwrap();
1234 let rust = langs.get("rust").unwrap();
1235 assert_eq!(rust.tab_size, Some(2)); assert_eq!(rust.use_tabs, Some(true)); assert_eq!(rust.auto_indent, Some(false)); }
1239
1240 #[test]
1241 fn resolve_fills_defaults() {
1242 let partial = PartialConfig {
1243 theme: Some(ThemeName::from("dark")),
1244 ..Default::default()
1245 };
1246
1247 let resolved = partial.resolve();
1248
1249 assert_eq!(resolved.theme.0, "dark");
1250 assert_eq!(resolved.editor.tab_size, 4); assert!(resolved.editor.line_numbers); }
1253
1254 #[test]
1255 fn resolve_preserves_set_values() {
1256 let partial = PartialConfig {
1257 editor: Some(PartialEditorConfig {
1258 tab_size: Some(2),
1259 line_numbers: Some(false),
1260 ..Default::default()
1261 }),
1262 ..Default::default()
1263 };
1264
1265 let resolved = partial.resolve();
1266
1267 assert_eq!(resolved.editor.tab_size, 2);
1268 assert!(!resolved.editor.line_numbers);
1269 }
1270
1271 #[test]
1272 fn roundtrip_config_to_partial_and_back() {
1273 let original = crate::config::Config::default();
1274 let partial = PartialConfig::from(&original);
1275 let resolved = partial.resolve();
1276
1277 assert_eq!(original.theme, resolved.theme);
1278 assert_eq!(original.editor.tab_size, resolved.editor.tab_size);
1279 assert_eq!(original.check_for_updates, resolved.check_for_updates);
1280 }
1281
1282 #[test]
1283 fn session_config_new_is_empty() {
1284 let session = SessionConfig::new();
1285 assert!(session.is_empty());
1286 }
1287
1288 #[test]
1289 fn session_config_set_theme() {
1290 let mut session = SessionConfig::new();
1291 session.set_theme(ThemeName::from("dark"));
1292 assert_eq!(session.theme, Some(ThemeName::from("dark")));
1293 assert!(!session.is_empty());
1294 }
1295
1296 #[test]
1297 fn session_config_clear_theme() {
1298 let mut session = SessionConfig::new();
1299 session.set_theme(ThemeName::from("dark"));
1300 session.clear_theme();
1301 assert!(session.theme.is_none());
1302 }
1303
1304 #[test]
1305 fn session_config_set_editor_option() {
1306 let mut session = SessionConfig::new();
1307 session.set_editor_option(|e| e.tab_size = Some(2));
1308 assert_eq!(session.editor.as_ref().unwrap().tab_size, Some(2));
1309 }
1310
1311 #[test]
1312 fn session_config_buffer_overrides() {
1313 let mut session = SessionConfig::new();
1314 let path = std::path::PathBuf::from("/test/file.rs");
1315 let config = PartialEditorConfig {
1316 tab_size: Some(8),
1317 ..Default::default()
1318 };
1319
1320 session.set_buffer_override(path.clone(), config);
1321 assert!(session.get_buffer_override(&path).is_some());
1322 assert_eq!(
1323 session.get_buffer_override(&path).unwrap().tab_size,
1324 Some(8)
1325 );
1326
1327 session.clear_buffer_override(&path);
1328 assert!(session.get_buffer_override(&path).is_none());
1329 }
1330
1331 #[test]
1332 fn session_config_to_partial_config() {
1333 let mut session = SessionConfig::new();
1334 session.set_theme(ThemeName::from("dark"));
1335 session.set_editor_option(|e| e.tab_size = Some(2));
1336
1337 let partial = session.to_partial_config();
1338 assert_eq!(partial.theme, Some(ThemeName::from("dark")));
1339 assert_eq!(partial.editor.as_ref().unwrap().tab_size, Some(2));
1340 }
1341
1342 #[test]
1345 fn plugins_with_default_enabled_not_serialized() {
1346 let mut config = crate::config::Config::default();
1348 config.plugins.insert(
1349 "test_plugin".to_string(),
1350 PluginConfig {
1351 enabled: true, path: Some(std::path::PathBuf::from("/path/to/plugin.ts")),
1353 },
1354 );
1355
1356 let partial = PartialConfig::from(&config);
1357
1358 assert!(
1360 partial.plugins.is_none(),
1361 "Plugins with default enabled=true should not be serialized"
1362 );
1363 }
1364
1365 #[test]
1366 fn plugins_with_disabled_are_serialized() {
1367 let mut config = crate::config::Config::default();
1369 config.plugins.insert(
1370 "enabled_plugin".to_string(),
1371 PluginConfig {
1372 enabled: true,
1373 path: Some(std::path::PathBuf::from("/path/to/enabled.ts")),
1374 },
1375 );
1376 config.plugins.insert(
1377 "disabled_plugin".to_string(),
1378 PluginConfig {
1379 enabled: false, path: Some(std::path::PathBuf::from("/path/to/disabled.ts")),
1381 },
1382 );
1383
1384 let partial = PartialConfig::from(&config);
1385
1386 assert!(partial.plugins.is_some());
1388 let plugins = partial.plugins.unwrap();
1389 assert_eq!(
1390 plugins.len(),
1391 1,
1392 "Only disabled plugins should be serialized"
1393 );
1394 assert!(plugins.contains_key("disabled_plugin"));
1395 assert!(!plugins.contains_key("enabled_plugin"));
1396
1397 let disabled = plugins.get("disabled_plugin").unwrap();
1399 assert_eq!(disabled.enabled, Some(false));
1400 assert!(disabled.path.is_none(), "Path should not be serialized");
1402 }
1403
1404 #[test]
1405 fn plugin_path_never_serialized() {
1406 let mut config = crate::config::Config::default();
1408 config.plugins.insert(
1409 "my_plugin".to_string(),
1410 PluginConfig {
1411 enabled: false,
1412 path: Some(std::path::PathBuf::from("/some/path/plugin.ts")),
1413 },
1414 );
1415
1416 let partial = PartialConfig::from(&config);
1417 let plugins = partial.plugins.unwrap();
1418 let plugin = plugins.get("my_plugin").unwrap();
1419
1420 assert!(
1421 plugin.path.is_none(),
1422 "Path is runtime-discovered and should never be serialized"
1423 );
1424 }
1425
1426 #[test]
1427 fn resolving_partial_with_disabled_plugin_preserves_state() {
1428 let partial = PartialConfig {
1430 plugins: Some(HashMap::from([(
1431 "my_plugin".to_string(),
1432 PartialPluginConfig {
1433 enabled: Some(false),
1434 path: None,
1435 },
1436 )])),
1437 ..Default::default()
1438 };
1439
1440 let resolved = partial.resolve();
1441
1442 let plugin = resolved.plugins.get("my_plugin");
1444 assert!(
1445 plugin.is_some(),
1446 "Disabled plugin should be in resolved config"
1447 );
1448 assert!(
1449 !plugin.unwrap().enabled,
1450 "Plugin should remain disabled after resolve"
1451 );
1452 }
1453
1454 #[test]
1455 fn merge_plugins_preserves_higher_precedence_disabled_state() {
1456 let mut higher = PartialConfig {
1458 plugins: Some(HashMap::from([(
1459 "my_plugin".to_string(),
1460 PartialPluginConfig {
1461 enabled: Some(false), path: None,
1463 },
1464 )])),
1465 ..Default::default()
1466 };
1467
1468 let lower = PartialConfig {
1469 plugins: Some(HashMap::from([(
1470 "my_plugin".to_string(),
1471 PartialPluginConfig {
1472 enabled: Some(true), path: None,
1474 },
1475 )])),
1476 ..Default::default()
1477 };
1478
1479 higher.merge_from(&lower);
1480
1481 let plugins = higher.plugins.unwrap();
1482 let plugin = plugins.get("my_plugin").unwrap();
1483 assert_eq!(
1484 plugin.enabled,
1485 Some(false),
1486 "Higher precedence disabled state should win"
1487 );
1488 }
1489
1490 #[test]
1491 fn roundtrip_disabled_plugin_only_saves_delta() {
1492 let mut config = crate::config::Config::default();
1495 config.plugins.insert(
1496 "plugin_a".to_string(),
1497 PluginConfig {
1498 enabled: true,
1499 path: Some(std::path::PathBuf::from("/a.ts")),
1500 },
1501 );
1502 config.plugins.insert(
1503 "plugin_b".to_string(),
1504 PluginConfig {
1505 enabled: false,
1506 path: Some(std::path::PathBuf::from("/b.ts")),
1507 },
1508 );
1509 config.plugins.insert(
1510 "plugin_c".to_string(),
1511 PluginConfig {
1512 enabled: true,
1513 path: Some(std::path::PathBuf::from("/c.ts")),
1514 },
1515 );
1516
1517 let partial = PartialConfig::from(&config);
1519
1520 let json = serde_json::to_string(&partial).unwrap();
1522
1523 assert!(
1525 json.contains("plugin_b"),
1526 "Disabled plugin should be in serialized JSON"
1527 );
1528 assert!(
1529 !json.contains("plugin_a"),
1530 "Enabled plugin_a should not be in serialized JSON"
1531 );
1532 assert!(
1533 !json.contains("plugin_c"),
1534 "Enabled plugin_c should not be in serialized JSON"
1535 );
1536
1537 let deserialized: PartialConfig = serde_json::from_str(&json).unwrap();
1539
1540 let plugins = deserialized.plugins.unwrap();
1542 assert_eq!(plugins.len(), 1);
1543 assert!(plugins.contains_key("plugin_b"));
1544 assert_eq!(plugins.get("plugin_b").unwrap().enabled, Some(false));
1545 }
1546}