1use crate::config::{
7 AcceptSuggestionOnEnter, CursorStyle, FileBrowserConfig, FileExplorerConfig, FormatterConfig,
8 HighlighterPreference, Keybinding, KeybindingMapName, KeymapConfig, LanguageConfig,
9 LineEndingOption, OnSaveAction, PluginConfig, TerminalConfig, ThemeName, WarningsConfig,
10};
11use crate::types::LspServerConfig;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15pub trait Merge {
18 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
31fn 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
50fn 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#[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 terminal: Option<PartialTerminalConfig>,
84 pub keybindings: Option<Vec<Keybinding>>,
85 pub keybinding_maps: Option<HashMap<String, KeymapConfig>>,
86 pub active_keybinding_map: Option<KeybindingMapName>,
87 pub languages: Option<HashMap<String, PartialLanguageConfig>>,
88 pub lsp: Option<HashMap<String, LspServerConfig>>,
89 pub warnings: Option<PartialWarningsConfig>,
90 pub plugins: Option<HashMap<String, PartialPluginConfig>>,
91 pub packages: Option<PartialPackagesConfig>,
92}
93
94impl Merge for PartialConfig {
95 fn merge_from(&mut self, other: &Self) {
96 self.version.merge_from(&other.version);
97 self.theme.merge_from(&other.theme);
98 self.locale.merge_from(&other.locale);
99 self.check_for_updates.merge_from(&other.check_for_updates);
100
101 merge_partial(&mut self.editor, &other.editor);
103 merge_partial(&mut self.file_explorer, &other.file_explorer);
104 merge_partial(&mut self.file_browser, &other.file_browser);
105 merge_partial(&mut self.terminal, &other.terminal);
106 merge_partial(&mut self.warnings, &other.warnings);
107 merge_partial(&mut self.packages, &other.packages);
108
109 self.keybindings.merge_from(&other.keybindings);
111
112 merge_hashmap(&mut self.keybinding_maps, &other.keybinding_maps);
114 merge_hashmap_recursive(&mut self.languages, &other.languages);
115 merge_hashmap_recursive(&mut self.lsp, &other.lsp);
116 merge_hashmap_recursive(&mut self.plugins, &other.plugins);
117
118 self.active_keybinding_map
119 .merge_from(&other.active_keybinding_map);
120 }
121}
122
123fn merge_partial<T: Merge + Clone>(target: &mut Option<T>, other: &Option<T>) {
125 match (target, other) {
126 (Some(t), Some(o)) => t.merge_from(o),
127 (t @ None, Some(o)) => *t = Some(o.clone()),
128 _ => {}
129 }
130}
131
132#[derive(Debug, Clone, Default, Deserialize, Serialize)]
134#[serde(default)]
135pub struct PartialEditorConfig {
136 pub tab_size: Option<usize>,
137 pub auto_indent: Option<bool>,
138 pub line_numbers: Option<bool>,
139 pub relative_line_numbers: Option<bool>,
140 pub scroll_offset: Option<usize>,
141 pub syntax_highlighting: Option<bool>,
142 pub line_wrap: Option<bool>,
143 pub highlight_timeout_ms: Option<u64>,
144 pub snapshot_interval: Option<usize>,
145 pub large_file_threshold_bytes: Option<u64>,
146 pub estimated_line_length: Option<usize>,
147 pub enable_inlay_hints: Option<bool>,
148 pub enable_semantic_tokens_full: Option<bool>,
149 pub recovery_enabled: Option<bool>,
150 pub auto_save_interval_secs: Option<u32>,
151 pub highlight_context_bytes: Option<usize>,
152 pub mouse_hover_enabled: Option<bool>,
153 pub mouse_hover_delay_ms: Option<u64>,
154 pub double_click_time_ms: Option<u64>,
155 pub auto_revert_poll_interval_ms: Option<u64>,
156 pub file_tree_poll_interval_ms: Option<u64>,
157 pub default_line_ending: Option<LineEndingOption>,
158 pub trim_trailing_whitespace_on_save: Option<bool>,
159 pub ensure_final_newline_on_save: Option<bool>,
160 pub highlight_matching_brackets: Option<bool>,
161 pub rainbow_brackets: Option<bool>,
162 pub cursor_style: Option<CursorStyle>,
163 pub keyboard_disambiguate_escape_codes: Option<bool>,
164 pub keyboard_report_event_types: Option<bool>,
165 pub keyboard_report_alternate_keys: Option<bool>,
166 pub keyboard_report_all_keys_as_escape_codes: Option<bool>,
167 pub quick_suggestions: Option<bool>,
168 pub quick_suggestions_delay_ms: Option<u64>,
169 pub suggest_on_trigger_characters: Option<bool>,
170 pub accept_suggestion_on_enter: Option<AcceptSuggestionOnEnter>,
171 pub show_menu_bar: Option<bool>,
172 pub show_tab_bar: Option<bool>,
173 pub use_terminal_bg: Option<bool>,
174}
175
176impl Merge for PartialEditorConfig {
177 fn merge_from(&mut self, other: &Self) {
178 self.tab_size.merge_from(&other.tab_size);
179 self.auto_indent.merge_from(&other.auto_indent);
180 self.line_numbers.merge_from(&other.line_numbers);
181 self.relative_line_numbers
182 .merge_from(&other.relative_line_numbers);
183 self.scroll_offset.merge_from(&other.scroll_offset);
184 self.syntax_highlighting
185 .merge_from(&other.syntax_highlighting);
186 self.line_wrap.merge_from(&other.line_wrap);
187 self.highlight_timeout_ms
188 .merge_from(&other.highlight_timeout_ms);
189 self.snapshot_interval.merge_from(&other.snapshot_interval);
190 self.large_file_threshold_bytes
191 .merge_from(&other.large_file_threshold_bytes);
192 self.estimated_line_length
193 .merge_from(&other.estimated_line_length);
194 self.enable_inlay_hints
195 .merge_from(&other.enable_inlay_hints);
196 self.enable_semantic_tokens_full
197 .merge_from(&other.enable_semantic_tokens_full);
198 self.recovery_enabled.merge_from(&other.recovery_enabled);
199 self.auto_save_interval_secs
200 .merge_from(&other.auto_save_interval_secs);
201 self.highlight_context_bytes
202 .merge_from(&other.highlight_context_bytes);
203 self.mouse_hover_enabled
204 .merge_from(&other.mouse_hover_enabled);
205 self.mouse_hover_delay_ms
206 .merge_from(&other.mouse_hover_delay_ms);
207 self.double_click_time_ms
208 .merge_from(&other.double_click_time_ms);
209 self.auto_revert_poll_interval_ms
210 .merge_from(&other.auto_revert_poll_interval_ms);
211 self.file_tree_poll_interval_ms
212 .merge_from(&other.file_tree_poll_interval_ms);
213 self.default_line_ending
214 .merge_from(&other.default_line_ending);
215 self.trim_trailing_whitespace_on_save
216 .merge_from(&other.trim_trailing_whitespace_on_save);
217 self.ensure_final_newline_on_save
218 .merge_from(&other.ensure_final_newline_on_save);
219 self.highlight_matching_brackets
220 .merge_from(&other.highlight_matching_brackets);
221 self.rainbow_brackets.merge_from(&other.rainbow_brackets);
222 self.cursor_style.merge_from(&other.cursor_style);
223 self.keyboard_disambiguate_escape_codes
224 .merge_from(&other.keyboard_disambiguate_escape_codes);
225 self.keyboard_report_event_types
226 .merge_from(&other.keyboard_report_event_types);
227 self.keyboard_report_alternate_keys
228 .merge_from(&other.keyboard_report_alternate_keys);
229 self.keyboard_report_all_keys_as_escape_codes
230 .merge_from(&other.keyboard_report_all_keys_as_escape_codes);
231 self.quick_suggestions.merge_from(&other.quick_suggestions);
232 self.quick_suggestions_delay_ms
233 .merge_from(&other.quick_suggestions_delay_ms);
234 self.suggest_on_trigger_characters
235 .merge_from(&other.suggest_on_trigger_characters);
236 self.accept_suggestion_on_enter
237 .merge_from(&other.accept_suggestion_on_enter);
238 self.show_menu_bar.merge_from(&other.show_menu_bar);
239 self.show_tab_bar.merge_from(&other.show_tab_bar);
240 self.use_terminal_bg.merge_from(&other.use_terminal_bg);
241 }
242}
243
244#[derive(Debug, Clone, Default, Deserialize, Serialize)]
246#[serde(default)]
247pub struct PartialFileExplorerConfig {
248 pub respect_gitignore: Option<bool>,
249 pub show_hidden: Option<bool>,
250 pub show_gitignored: Option<bool>,
251 pub custom_ignore_patterns: Option<Vec<String>>,
252 pub width: Option<f32>,
253}
254
255impl Merge for PartialFileExplorerConfig {
256 fn merge_from(&mut self, other: &Self) {
257 self.respect_gitignore.merge_from(&other.respect_gitignore);
258 self.show_hidden.merge_from(&other.show_hidden);
259 self.show_gitignored.merge_from(&other.show_gitignored);
260 self.custom_ignore_patterns
261 .merge_from(&other.custom_ignore_patterns);
262 self.width.merge_from(&other.width);
263 }
264}
265
266#[derive(Debug, Clone, Default, Deserialize, Serialize)]
268#[serde(default)]
269pub struct PartialFileBrowserConfig {
270 pub show_hidden: Option<bool>,
271}
272
273impl Merge for PartialFileBrowserConfig {
274 fn merge_from(&mut self, other: &Self) {
275 self.show_hidden.merge_from(&other.show_hidden);
276 }
277}
278
279#[derive(Debug, Clone, Default, Deserialize, Serialize)]
281#[serde(default)]
282pub struct PartialTerminalConfig {
283 pub jump_to_end_on_output: Option<bool>,
284}
285
286impl Merge for PartialTerminalConfig {
287 fn merge_from(&mut self, other: &Self) {
288 self.jump_to_end_on_output
289 .merge_from(&other.jump_to_end_on_output);
290 }
291}
292
293#[derive(Debug, Clone, Default, Deserialize, Serialize)]
295#[serde(default)]
296pub struct PartialWarningsConfig {
297 pub show_status_indicator: Option<bool>,
298}
299
300impl Merge for PartialWarningsConfig {
301 fn merge_from(&mut self, other: &Self) {
302 self.show_status_indicator
303 .merge_from(&other.show_status_indicator);
304 }
305}
306
307#[derive(Debug, Clone, Default, Deserialize, Serialize)]
309#[serde(default)]
310pub struct PartialPackagesConfig {
311 pub sources: Option<Vec<String>>,
312}
313
314impl Merge for PartialPackagesConfig {
315 fn merge_from(&mut self, other: &Self) {
316 self.sources.merge_from(&other.sources);
317 }
318}
319
320#[derive(Debug, Clone, Default, Deserialize, Serialize)]
322#[serde(default)]
323pub struct PartialPluginConfig {
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub enabled: Option<bool>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub path: Option<std::path::PathBuf>,
328}
329
330impl Merge for PartialPluginConfig {
331 fn merge_from(&mut self, other: &Self) {
332 self.enabled.merge_from(&other.enabled);
333 self.path.merge_from(&other.path);
334 }
335}
336
337#[derive(Debug, Clone, Default, Deserialize, Serialize)]
339#[serde(default)]
340pub struct PartialLanguageConfig {
341 pub extensions: Option<Vec<String>>,
342 pub filenames: Option<Vec<String>>,
343 pub grammar: Option<String>,
344 pub comment_prefix: Option<String>,
345 pub auto_indent: Option<bool>,
346 pub highlighter: Option<HighlighterPreference>,
347 pub textmate_grammar: Option<std::path::PathBuf>,
348 pub show_whitespace_tabs: Option<bool>,
349 pub use_tabs: Option<bool>,
350 pub tab_size: Option<usize>,
351 pub formatter: Option<FormatterConfig>,
352 pub format_on_save: Option<bool>,
353 pub on_save: Option<Vec<OnSaveAction>>,
354}
355
356impl Merge for PartialLanguageConfig {
357 fn merge_from(&mut self, other: &Self) {
358 self.extensions.merge_from(&other.extensions);
359 self.filenames.merge_from(&other.filenames);
360 self.grammar.merge_from(&other.grammar);
361 self.comment_prefix.merge_from(&other.comment_prefix);
362 self.auto_indent.merge_from(&other.auto_indent);
363 self.highlighter.merge_from(&other.highlighter);
364 self.textmate_grammar.merge_from(&other.textmate_grammar);
365 self.show_whitespace_tabs
366 .merge_from(&other.show_whitespace_tabs);
367 self.use_tabs.merge_from(&other.use_tabs);
368 self.tab_size.merge_from(&other.tab_size);
369 self.formatter.merge_from(&other.formatter);
370 self.format_on_save.merge_from(&other.format_on_save);
371 self.on_save.merge_from(&other.on_save);
372 }
373}
374
375impl Merge for LspServerConfig {
376 fn merge_from(&mut self, other: &Self) {
377 if self.command.is_empty() {
379 self.command = other.command.clone();
380 }
381 if self.args.is_empty() {
383 self.args = other.args.clone();
384 }
385 if self.initialization_options.is_none() {
389 self.initialization_options = other.initialization_options.clone();
390 }
391 }
392}
393
394impl From<&crate::config::EditorConfig> for PartialEditorConfig {
397 fn from(cfg: &crate::config::EditorConfig) -> Self {
398 Self {
399 tab_size: Some(cfg.tab_size),
400 auto_indent: Some(cfg.auto_indent),
401 line_numbers: Some(cfg.line_numbers),
402 relative_line_numbers: Some(cfg.relative_line_numbers),
403 scroll_offset: Some(cfg.scroll_offset),
404 syntax_highlighting: Some(cfg.syntax_highlighting),
405 line_wrap: Some(cfg.line_wrap),
406 highlight_timeout_ms: Some(cfg.highlight_timeout_ms),
407 snapshot_interval: Some(cfg.snapshot_interval),
408 large_file_threshold_bytes: Some(cfg.large_file_threshold_bytes),
409 estimated_line_length: Some(cfg.estimated_line_length),
410 enable_inlay_hints: Some(cfg.enable_inlay_hints),
411 enable_semantic_tokens_full: Some(cfg.enable_semantic_tokens_full),
412 recovery_enabled: Some(cfg.recovery_enabled),
413 auto_save_interval_secs: Some(cfg.auto_save_interval_secs),
414 highlight_context_bytes: Some(cfg.highlight_context_bytes),
415 mouse_hover_enabled: Some(cfg.mouse_hover_enabled),
416 mouse_hover_delay_ms: Some(cfg.mouse_hover_delay_ms),
417 double_click_time_ms: Some(cfg.double_click_time_ms),
418 auto_revert_poll_interval_ms: Some(cfg.auto_revert_poll_interval_ms),
419 file_tree_poll_interval_ms: Some(cfg.file_tree_poll_interval_ms),
420 default_line_ending: Some(cfg.default_line_ending.clone()),
421 trim_trailing_whitespace_on_save: Some(cfg.trim_trailing_whitespace_on_save),
422 ensure_final_newline_on_save: Some(cfg.ensure_final_newline_on_save),
423 highlight_matching_brackets: Some(cfg.highlight_matching_brackets),
424 rainbow_brackets: Some(cfg.rainbow_brackets),
425 cursor_style: Some(cfg.cursor_style),
426 keyboard_disambiguate_escape_codes: Some(cfg.keyboard_disambiguate_escape_codes),
427 keyboard_report_event_types: Some(cfg.keyboard_report_event_types),
428 keyboard_report_alternate_keys: Some(cfg.keyboard_report_alternate_keys),
429 keyboard_report_all_keys_as_escape_codes: Some(
430 cfg.keyboard_report_all_keys_as_escape_codes,
431 ),
432 quick_suggestions: Some(cfg.quick_suggestions),
433 quick_suggestions_delay_ms: Some(cfg.quick_suggestions_delay_ms),
434 suggest_on_trigger_characters: Some(cfg.suggest_on_trigger_characters),
435 accept_suggestion_on_enter: Some(cfg.accept_suggestion_on_enter),
436 show_menu_bar: Some(cfg.show_menu_bar),
437 show_tab_bar: Some(cfg.show_tab_bar),
438 use_terminal_bg: Some(cfg.use_terminal_bg),
439 }
440 }
441}
442
443impl PartialEditorConfig {
444 pub fn resolve(self, defaults: &crate::config::EditorConfig) -> crate::config::EditorConfig {
446 crate::config::EditorConfig {
447 tab_size: self.tab_size.unwrap_or(defaults.tab_size),
448 auto_indent: self.auto_indent.unwrap_or(defaults.auto_indent),
449 line_numbers: self.line_numbers.unwrap_or(defaults.line_numbers),
450 relative_line_numbers: self
451 .relative_line_numbers
452 .unwrap_or(defaults.relative_line_numbers),
453 scroll_offset: self.scroll_offset.unwrap_or(defaults.scroll_offset),
454 syntax_highlighting: self
455 .syntax_highlighting
456 .unwrap_or(defaults.syntax_highlighting),
457 line_wrap: self.line_wrap.unwrap_or(defaults.line_wrap),
458 highlight_timeout_ms: self
459 .highlight_timeout_ms
460 .unwrap_or(defaults.highlight_timeout_ms),
461 snapshot_interval: self.snapshot_interval.unwrap_or(defaults.snapshot_interval),
462 large_file_threshold_bytes: self
463 .large_file_threshold_bytes
464 .unwrap_or(defaults.large_file_threshold_bytes),
465 estimated_line_length: self
466 .estimated_line_length
467 .unwrap_or(defaults.estimated_line_length),
468 enable_inlay_hints: self
469 .enable_inlay_hints
470 .unwrap_or(defaults.enable_inlay_hints),
471 enable_semantic_tokens_full: self
472 .enable_semantic_tokens_full
473 .unwrap_or(defaults.enable_semantic_tokens_full),
474 recovery_enabled: self.recovery_enabled.unwrap_or(defaults.recovery_enabled),
475 auto_save_interval_secs: self
476 .auto_save_interval_secs
477 .unwrap_or(defaults.auto_save_interval_secs),
478 highlight_context_bytes: self
479 .highlight_context_bytes
480 .unwrap_or(defaults.highlight_context_bytes),
481 mouse_hover_enabled: self
482 .mouse_hover_enabled
483 .unwrap_or(defaults.mouse_hover_enabled),
484 mouse_hover_delay_ms: self
485 .mouse_hover_delay_ms
486 .unwrap_or(defaults.mouse_hover_delay_ms),
487 double_click_time_ms: self
488 .double_click_time_ms
489 .unwrap_or(defaults.double_click_time_ms),
490 auto_revert_poll_interval_ms: self
491 .auto_revert_poll_interval_ms
492 .unwrap_or(defaults.auto_revert_poll_interval_ms),
493 file_tree_poll_interval_ms: self
494 .file_tree_poll_interval_ms
495 .unwrap_or(defaults.file_tree_poll_interval_ms),
496 default_line_ending: self
497 .default_line_ending
498 .unwrap_or(defaults.default_line_ending.clone()),
499 trim_trailing_whitespace_on_save: self
500 .trim_trailing_whitespace_on_save
501 .unwrap_or(defaults.trim_trailing_whitespace_on_save),
502 ensure_final_newline_on_save: self
503 .ensure_final_newline_on_save
504 .unwrap_or(defaults.ensure_final_newline_on_save),
505 highlight_matching_brackets: self
506 .highlight_matching_brackets
507 .unwrap_or(defaults.highlight_matching_brackets),
508 rainbow_brackets: self.rainbow_brackets.unwrap_or(defaults.rainbow_brackets),
509 cursor_style: self.cursor_style.unwrap_or(defaults.cursor_style),
510 keyboard_disambiguate_escape_codes: self
511 .keyboard_disambiguate_escape_codes
512 .unwrap_or(defaults.keyboard_disambiguate_escape_codes),
513 keyboard_report_event_types: self
514 .keyboard_report_event_types
515 .unwrap_or(defaults.keyboard_report_event_types),
516 keyboard_report_alternate_keys: self
517 .keyboard_report_alternate_keys
518 .unwrap_or(defaults.keyboard_report_alternate_keys),
519 keyboard_report_all_keys_as_escape_codes: self
520 .keyboard_report_all_keys_as_escape_codes
521 .unwrap_or(defaults.keyboard_report_all_keys_as_escape_codes),
522 quick_suggestions: self.quick_suggestions.unwrap_or(defaults.quick_suggestions),
523 quick_suggestions_delay_ms: self
524 .quick_suggestions_delay_ms
525 .unwrap_or(defaults.quick_suggestions_delay_ms),
526 suggest_on_trigger_characters: self
527 .suggest_on_trigger_characters
528 .unwrap_or(defaults.suggest_on_trigger_characters),
529 accept_suggestion_on_enter: self
530 .accept_suggestion_on_enter
531 .unwrap_or(defaults.accept_suggestion_on_enter),
532 show_menu_bar: self.show_menu_bar.unwrap_or(defaults.show_menu_bar),
533 show_tab_bar: self.show_tab_bar.unwrap_or(defaults.show_tab_bar),
534 use_terminal_bg: self.use_terminal_bg.unwrap_or(defaults.use_terminal_bg),
535 }
536 }
537}
538
539impl From<&FileExplorerConfig> for PartialFileExplorerConfig {
540 fn from(cfg: &FileExplorerConfig) -> Self {
541 Self {
542 respect_gitignore: Some(cfg.respect_gitignore),
543 show_hidden: Some(cfg.show_hidden),
544 show_gitignored: Some(cfg.show_gitignored),
545 custom_ignore_patterns: Some(cfg.custom_ignore_patterns.clone()),
546 width: Some(cfg.width),
547 }
548 }
549}
550
551impl PartialFileExplorerConfig {
552 pub fn resolve(self, defaults: &FileExplorerConfig) -> FileExplorerConfig {
553 FileExplorerConfig {
554 respect_gitignore: self.respect_gitignore.unwrap_or(defaults.respect_gitignore),
555 show_hidden: self.show_hidden.unwrap_or(defaults.show_hidden),
556 show_gitignored: self.show_gitignored.unwrap_or(defaults.show_gitignored),
557 custom_ignore_patterns: self
558 .custom_ignore_patterns
559 .unwrap_or_else(|| defaults.custom_ignore_patterns.clone()),
560 width: self.width.unwrap_or(defaults.width),
561 }
562 }
563}
564
565impl From<&FileBrowserConfig> for PartialFileBrowserConfig {
566 fn from(cfg: &FileBrowserConfig) -> Self {
567 Self {
568 show_hidden: Some(cfg.show_hidden),
569 }
570 }
571}
572
573impl PartialFileBrowserConfig {
574 pub fn resolve(self, defaults: &FileBrowserConfig) -> FileBrowserConfig {
575 FileBrowserConfig {
576 show_hidden: self.show_hidden.unwrap_or(defaults.show_hidden),
577 }
578 }
579}
580
581impl From<&TerminalConfig> for PartialTerminalConfig {
582 fn from(cfg: &TerminalConfig) -> Self {
583 Self {
584 jump_to_end_on_output: Some(cfg.jump_to_end_on_output),
585 }
586 }
587}
588
589impl PartialTerminalConfig {
590 pub fn resolve(self, defaults: &TerminalConfig) -> TerminalConfig {
591 TerminalConfig {
592 jump_to_end_on_output: self
593 .jump_to_end_on_output
594 .unwrap_or(defaults.jump_to_end_on_output),
595 }
596 }
597}
598
599impl From<&WarningsConfig> for PartialWarningsConfig {
600 fn from(cfg: &WarningsConfig) -> Self {
601 Self {
602 show_status_indicator: Some(cfg.show_status_indicator),
603 }
604 }
605}
606
607impl PartialWarningsConfig {
608 pub fn resolve(self, defaults: &WarningsConfig) -> WarningsConfig {
609 WarningsConfig {
610 show_status_indicator: self
611 .show_status_indicator
612 .unwrap_or(defaults.show_status_indicator),
613 }
614 }
615}
616
617impl From<&crate::config::PackagesConfig> for PartialPackagesConfig {
618 fn from(cfg: &crate::config::PackagesConfig) -> Self {
619 Self {
620 sources: Some(cfg.sources.clone()),
621 }
622 }
623}
624
625impl PartialPackagesConfig {
626 pub fn resolve(
627 self,
628 defaults: &crate::config::PackagesConfig,
629 ) -> crate::config::PackagesConfig {
630 crate::config::PackagesConfig {
631 sources: self.sources.unwrap_or_else(|| defaults.sources.clone()),
632 }
633 }
634}
635
636impl From<&PluginConfig> for PartialPluginConfig {
637 fn from(cfg: &PluginConfig) -> Self {
638 Self {
639 enabled: Some(cfg.enabled),
640 path: cfg.path.clone(),
641 }
642 }
643}
644
645impl PartialPluginConfig {
646 pub fn resolve(self, defaults: &PluginConfig) -> PluginConfig {
647 PluginConfig {
648 enabled: self.enabled.unwrap_or(defaults.enabled),
649 path: self.path.or_else(|| defaults.path.clone()),
650 }
651 }
652}
653
654impl From<&LanguageConfig> for PartialLanguageConfig {
655 fn from(cfg: &LanguageConfig) -> Self {
656 Self {
657 extensions: Some(cfg.extensions.clone()),
658 filenames: Some(cfg.filenames.clone()),
659 grammar: Some(cfg.grammar.clone()),
660 comment_prefix: cfg.comment_prefix.clone(),
661 auto_indent: Some(cfg.auto_indent),
662 highlighter: Some(cfg.highlighter),
663 textmate_grammar: cfg.textmate_grammar.clone(),
664 show_whitespace_tabs: Some(cfg.show_whitespace_tabs),
665 use_tabs: Some(cfg.use_tabs),
666 tab_size: cfg.tab_size,
667 formatter: cfg.formatter.clone(),
668 format_on_save: Some(cfg.format_on_save),
669 on_save: Some(cfg.on_save.clone()),
670 }
671 }
672}
673
674impl PartialLanguageConfig {
675 pub fn resolve(self, defaults: &LanguageConfig) -> LanguageConfig {
676 LanguageConfig {
677 extensions: self
678 .extensions
679 .unwrap_or_else(|| defaults.extensions.clone()),
680 filenames: self.filenames.unwrap_or_else(|| defaults.filenames.clone()),
681 grammar: self.grammar.unwrap_or_else(|| defaults.grammar.clone()),
682 comment_prefix: self
683 .comment_prefix
684 .or_else(|| defaults.comment_prefix.clone()),
685 auto_indent: self.auto_indent.unwrap_or(defaults.auto_indent),
686 highlighter: self.highlighter.unwrap_or(defaults.highlighter),
687 textmate_grammar: self
688 .textmate_grammar
689 .or_else(|| defaults.textmate_grammar.clone()),
690 show_whitespace_tabs: self
691 .show_whitespace_tabs
692 .unwrap_or(defaults.show_whitespace_tabs),
693 use_tabs: self.use_tabs.unwrap_or(defaults.use_tabs),
694 tab_size: self.tab_size.or(defaults.tab_size),
695 formatter: self.formatter.or_else(|| defaults.formatter.clone()),
696 format_on_save: self.format_on_save.unwrap_or(defaults.format_on_save),
697 on_save: self.on_save.unwrap_or_else(|| defaults.on_save.clone()),
698 }
699 }
700}
701
702impl From<&crate::config::Config> for PartialConfig {
703 fn from(cfg: &crate::config::Config) -> Self {
704 Self {
705 version: Some(cfg.version),
706 theme: Some(cfg.theme.clone()),
707 locale: cfg.locale.0.clone(),
708 check_for_updates: Some(cfg.check_for_updates),
709 editor: Some(PartialEditorConfig::from(&cfg.editor)),
710 file_explorer: Some(PartialFileExplorerConfig::from(&cfg.file_explorer)),
711 file_browser: Some(PartialFileBrowserConfig::from(&cfg.file_browser)),
712 terminal: Some(PartialTerminalConfig::from(&cfg.terminal)),
713 keybindings: Some(cfg.keybindings.clone()),
714 keybinding_maps: Some(cfg.keybinding_maps.clone()),
715 active_keybinding_map: Some(cfg.active_keybinding_map.clone()),
716 languages: Some(
717 cfg.languages
718 .iter()
719 .map(|(k, v)| (k.clone(), PartialLanguageConfig::from(v)))
720 .collect(),
721 ),
722 lsp: Some(cfg.lsp.clone()),
723 warnings: Some(PartialWarningsConfig::from(&cfg.warnings)),
724 plugins: {
727 let default_plugin = crate::config::PluginConfig::default();
728 let non_default_plugins: HashMap<String, PartialPluginConfig> = cfg
729 .plugins
730 .iter()
731 .filter(|(_, v)| v.enabled != default_plugin.enabled)
732 .map(|(k, v)| {
733 (
734 k.clone(),
735 PartialPluginConfig {
736 enabled: Some(v.enabled),
737 path: None, },
739 )
740 })
741 .collect();
742 if non_default_plugins.is_empty() {
743 None
744 } else {
745 Some(non_default_plugins)
746 }
747 },
748 packages: Some(PartialPackagesConfig::from(&cfg.packages)),
749 }
750 }
751}
752
753impl PartialConfig {
754 pub fn resolve(self) -> crate::config::Config {
756 let defaults = crate::config::Config::default();
757 self.resolve_with_defaults(&defaults)
758 }
759
760 pub fn resolve_with_defaults(self, defaults: &crate::config::Config) -> crate::config::Config {
762 let languages = {
764 let mut result = defaults.languages.clone();
765 if let Some(partial_langs) = self.languages {
766 for (key, partial_lang) in partial_langs {
767 let default_lang = result.get(&key).cloned().unwrap_or_default();
768 result.insert(key, partial_lang.resolve(&default_lang));
769 }
770 }
771 result
772 };
773
774 let lsp = {
776 let mut result = defaults.lsp.clone();
777 if let Some(partial_lsp) = self.lsp {
778 for (key, partial_config) in partial_lsp {
779 if let Some(default_config) = result.get(&key) {
780 result.insert(key, partial_config.merge_with_defaults(default_config));
781 } else {
782 result.insert(key, partial_config);
784 }
785 }
786 }
787 result
788 };
789
790 let keybinding_maps = {
792 let mut result = defaults.keybinding_maps.clone();
793 if let Some(partial_maps) = self.keybinding_maps {
794 for (key, config) in partial_maps {
795 result.insert(key, config);
796 }
797 }
798 result
799 };
800
801 let plugins = {
803 let mut result = defaults.plugins.clone();
804 if let Some(partial_plugins) = self.plugins {
805 for (key, partial_plugin) in partial_plugins {
806 let default_plugin = result.get(&key).cloned().unwrap_or_default();
807 result.insert(key, partial_plugin.resolve(&default_plugin));
808 }
809 }
810 result
811 };
812
813 crate::config::Config {
814 version: self.version.unwrap_or(defaults.version),
815 theme: self.theme.unwrap_or_else(|| defaults.theme.clone()),
816 locale: crate::config::LocaleName::from(
817 self.locale.or_else(|| defaults.locale.0.clone()),
818 ),
819 check_for_updates: self.check_for_updates.unwrap_or(defaults.check_for_updates),
820 editor: self
821 .editor
822 .map(|e| e.resolve(&defaults.editor))
823 .unwrap_or_else(|| defaults.editor.clone()),
824 file_explorer: self
825 .file_explorer
826 .map(|e| e.resolve(&defaults.file_explorer))
827 .unwrap_or_else(|| defaults.file_explorer.clone()),
828 file_browser: self
829 .file_browser
830 .map(|e| e.resolve(&defaults.file_browser))
831 .unwrap_or_else(|| defaults.file_browser.clone()),
832 terminal: self
833 .terminal
834 .map(|e| e.resolve(&defaults.terminal))
835 .unwrap_or_else(|| defaults.terminal.clone()),
836 keybindings: self
837 .keybindings
838 .unwrap_or_else(|| defaults.keybindings.clone()),
839 keybinding_maps,
840 active_keybinding_map: self
841 .active_keybinding_map
842 .unwrap_or_else(|| defaults.active_keybinding_map.clone()),
843 languages,
844 lsp,
845 warnings: self
846 .warnings
847 .map(|e| e.resolve(&defaults.warnings))
848 .unwrap_or_else(|| defaults.warnings.clone()),
849 plugins,
850 packages: self
851 .packages
852 .map(|e| e.resolve(&defaults.packages))
853 .unwrap_or_else(|| defaults.packages.clone()),
854 }
855 }
856}
857
858impl Default for LanguageConfig {
860 fn default() -> Self {
861 Self {
862 extensions: Vec::new(),
863 filenames: Vec::new(),
864 grammar: String::new(),
865 comment_prefix: None,
866 auto_indent: true,
867 highlighter: HighlighterPreference::default(),
868 textmate_grammar: None,
869 show_whitespace_tabs: true,
870 use_tabs: false,
871 tab_size: None,
872 formatter: None,
873 format_on_save: false,
874 on_save: Vec::new(),
875 }
876 }
877}
878
879#[derive(Debug, Clone, Default, Deserialize, Serialize)]
887#[serde(default)]
888pub struct SessionConfig {
889 pub theme: Option<ThemeName>,
891
892 pub editor: Option<PartialEditorConfig>,
894
895 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
898 pub buffer_overrides: HashMap<std::path::PathBuf, PartialEditorConfig>,
899}
900
901impl SessionConfig {
902 pub fn new() -> Self {
904 Self::default()
905 }
906
907 pub fn set_theme(&mut self, theme: ThemeName) {
909 self.theme = Some(theme);
910 }
911
912 pub fn clear_theme(&mut self) {
914 self.theme = None;
915 }
916
917 pub fn set_editor_option<F>(&mut self, setter: F)
919 where
920 F: FnOnce(&mut PartialEditorConfig),
921 {
922 let editor = self.editor.get_or_insert_with(Default::default);
923 setter(editor);
924 }
925
926 pub fn set_buffer_override(&mut self, path: std::path::PathBuf, config: PartialEditorConfig) {
928 self.buffer_overrides.insert(path, config);
929 }
930
931 pub fn clear_buffer_override(&mut self, path: &std::path::Path) {
933 self.buffer_overrides.remove(path);
934 }
935
936 pub fn get_buffer_override(&self, path: &std::path::Path) -> Option<&PartialEditorConfig> {
938 self.buffer_overrides.get(path)
939 }
940
941 pub fn to_partial_config(&self) -> PartialConfig {
943 PartialConfig {
944 theme: self.theme.clone(),
945 editor: self.editor.clone(),
946 ..Default::default()
947 }
948 }
949
950 pub fn is_empty(&self) -> bool {
952 self.theme.is_none() && self.editor.is_none() && self.buffer_overrides.is_empty()
953 }
954}
955
956impl From<PartialConfig> for SessionConfig {
957 fn from(partial: PartialConfig) -> Self {
958 Self {
959 theme: partial.theme,
960 editor: partial.editor,
961 buffer_overrides: HashMap::new(),
962 }
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn merge_option_higher_precedence_wins() {
972 let mut higher: Option<i32> = Some(10);
973 let lower: Option<i32> = Some(5);
974 higher.merge_from(&lower);
975 assert_eq!(higher, Some(10));
976 }
977
978 #[test]
979 fn merge_option_fills_from_lower_when_none() {
980 let mut higher: Option<i32> = None;
981 let lower: Option<i32> = Some(5);
982 higher.merge_from(&lower);
983 assert_eq!(higher, Some(5));
984 }
985
986 #[test]
987 fn merge_editor_config_recursive() {
988 let mut higher = PartialEditorConfig {
989 tab_size: Some(2),
990 ..Default::default()
991 };
992 let lower = PartialEditorConfig {
993 tab_size: Some(4),
994 line_numbers: Some(true),
995 ..Default::default()
996 };
997
998 higher.merge_from(&lower);
999
1000 assert_eq!(higher.tab_size, Some(2)); assert_eq!(higher.line_numbers, Some(true)); }
1003
1004 #[test]
1005 fn merge_partial_config_combines_languages() {
1006 let mut higher = PartialConfig {
1007 languages: Some(HashMap::from([(
1008 "rust".to_string(),
1009 PartialLanguageConfig {
1010 tab_size: Some(4),
1011 ..Default::default()
1012 },
1013 )])),
1014 ..Default::default()
1015 };
1016 let lower = PartialConfig {
1017 languages: Some(HashMap::from([(
1018 "python".to_string(),
1019 PartialLanguageConfig {
1020 tab_size: Some(4),
1021 ..Default::default()
1022 },
1023 )])),
1024 ..Default::default()
1025 };
1026
1027 higher.merge_from(&lower);
1028
1029 let langs = higher.languages.unwrap();
1030 assert!(langs.contains_key("rust"));
1031 assert!(langs.contains_key("python"));
1032 }
1033
1034 #[test]
1035 fn merge_languages_same_key_higher_wins() {
1036 let mut higher = PartialConfig {
1037 languages: Some(HashMap::from([(
1038 "rust".to_string(),
1039 PartialLanguageConfig {
1040 tab_size: Some(2),
1041 use_tabs: Some(true),
1042 ..Default::default()
1043 },
1044 )])),
1045 ..Default::default()
1046 };
1047 let lower = PartialConfig {
1048 languages: Some(HashMap::from([(
1049 "rust".to_string(),
1050 PartialLanguageConfig {
1051 tab_size: Some(4),
1052 auto_indent: Some(false),
1053 ..Default::default()
1054 },
1055 )])),
1056 ..Default::default()
1057 };
1058
1059 higher.merge_from(&lower);
1060
1061 let langs = higher.languages.unwrap();
1062 let rust = langs.get("rust").unwrap();
1063 assert_eq!(rust.tab_size, Some(2)); assert_eq!(rust.use_tabs, Some(true)); assert_eq!(rust.auto_indent, Some(false)); }
1067
1068 #[test]
1069 fn resolve_fills_defaults() {
1070 let partial = PartialConfig {
1071 theme: Some(ThemeName::from("dark")),
1072 ..Default::default()
1073 };
1074
1075 let resolved = partial.resolve();
1076
1077 assert_eq!(resolved.theme.0, "dark");
1078 assert_eq!(resolved.editor.tab_size, 4); assert!(resolved.editor.line_numbers); }
1081
1082 #[test]
1083 fn resolve_preserves_set_values() {
1084 let partial = PartialConfig {
1085 editor: Some(PartialEditorConfig {
1086 tab_size: Some(2),
1087 line_numbers: Some(false),
1088 ..Default::default()
1089 }),
1090 ..Default::default()
1091 };
1092
1093 let resolved = partial.resolve();
1094
1095 assert_eq!(resolved.editor.tab_size, 2);
1096 assert!(!resolved.editor.line_numbers);
1097 }
1098
1099 #[test]
1100 fn roundtrip_config_to_partial_and_back() {
1101 let original = crate::config::Config::default();
1102 let partial = PartialConfig::from(&original);
1103 let resolved = partial.resolve();
1104
1105 assert_eq!(original.theme, resolved.theme);
1106 assert_eq!(original.editor.tab_size, resolved.editor.tab_size);
1107 assert_eq!(original.check_for_updates, resolved.check_for_updates);
1108 }
1109
1110 #[test]
1111 fn session_config_new_is_empty() {
1112 let session = SessionConfig::new();
1113 assert!(session.is_empty());
1114 }
1115
1116 #[test]
1117 fn session_config_set_theme() {
1118 let mut session = SessionConfig::new();
1119 session.set_theme(ThemeName::from("dark"));
1120 assert_eq!(session.theme, Some(ThemeName::from("dark")));
1121 assert!(!session.is_empty());
1122 }
1123
1124 #[test]
1125 fn session_config_clear_theme() {
1126 let mut session = SessionConfig::new();
1127 session.set_theme(ThemeName::from("dark"));
1128 session.clear_theme();
1129 assert!(session.theme.is_none());
1130 }
1131
1132 #[test]
1133 fn session_config_set_editor_option() {
1134 let mut session = SessionConfig::new();
1135 session.set_editor_option(|e| e.tab_size = Some(2));
1136 assert_eq!(session.editor.as_ref().unwrap().tab_size, Some(2));
1137 }
1138
1139 #[test]
1140 fn session_config_buffer_overrides() {
1141 let mut session = SessionConfig::new();
1142 let path = std::path::PathBuf::from("/test/file.rs");
1143 let config = PartialEditorConfig {
1144 tab_size: Some(8),
1145 ..Default::default()
1146 };
1147
1148 session.set_buffer_override(path.clone(), config);
1149 assert!(session.get_buffer_override(&path).is_some());
1150 assert_eq!(
1151 session.get_buffer_override(&path).unwrap().tab_size,
1152 Some(8)
1153 );
1154
1155 session.clear_buffer_override(&path);
1156 assert!(session.get_buffer_override(&path).is_none());
1157 }
1158
1159 #[test]
1160 fn session_config_to_partial_config() {
1161 let mut session = SessionConfig::new();
1162 session.set_theme(ThemeName::from("dark"));
1163 session.set_editor_option(|e| e.tab_size = Some(2));
1164
1165 let partial = session.to_partial_config();
1166 assert_eq!(partial.theme, Some(ThemeName::from("dark")));
1167 assert_eq!(partial.editor.as_ref().unwrap().tab_size, Some(2));
1168 }
1169
1170 #[test]
1173 fn plugins_with_default_enabled_not_serialized() {
1174 let mut config = crate::config::Config::default();
1176 config.plugins.insert(
1177 "test_plugin".to_string(),
1178 PluginConfig {
1179 enabled: true, path: Some(std::path::PathBuf::from("/path/to/plugin.ts")),
1181 },
1182 );
1183
1184 let partial = PartialConfig::from(&config);
1185
1186 assert!(
1188 partial.plugins.is_none(),
1189 "Plugins with default enabled=true should not be serialized"
1190 );
1191 }
1192
1193 #[test]
1194 fn plugins_with_disabled_are_serialized() {
1195 let mut config = crate::config::Config::default();
1197 config.plugins.insert(
1198 "enabled_plugin".to_string(),
1199 PluginConfig {
1200 enabled: true,
1201 path: Some(std::path::PathBuf::from("/path/to/enabled.ts")),
1202 },
1203 );
1204 config.plugins.insert(
1205 "disabled_plugin".to_string(),
1206 PluginConfig {
1207 enabled: false, path: Some(std::path::PathBuf::from("/path/to/disabled.ts")),
1209 },
1210 );
1211
1212 let partial = PartialConfig::from(&config);
1213
1214 assert!(partial.plugins.is_some());
1216 let plugins = partial.plugins.unwrap();
1217 assert_eq!(
1218 plugins.len(),
1219 1,
1220 "Only disabled plugins should be serialized"
1221 );
1222 assert!(plugins.contains_key("disabled_plugin"));
1223 assert!(!plugins.contains_key("enabled_plugin"));
1224
1225 let disabled = plugins.get("disabled_plugin").unwrap();
1227 assert_eq!(disabled.enabled, Some(false));
1228 assert!(disabled.path.is_none(), "Path should not be serialized");
1230 }
1231
1232 #[test]
1233 fn plugin_path_never_serialized() {
1234 let mut config = crate::config::Config::default();
1236 config.plugins.insert(
1237 "my_plugin".to_string(),
1238 PluginConfig {
1239 enabled: false,
1240 path: Some(std::path::PathBuf::from("/some/path/plugin.ts")),
1241 },
1242 );
1243
1244 let partial = PartialConfig::from(&config);
1245 let plugins = partial.plugins.unwrap();
1246 let plugin = plugins.get("my_plugin").unwrap();
1247
1248 assert!(
1249 plugin.path.is_none(),
1250 "Path is runtime-discovered and should never be serialized"
1251 );
1252 }
1253
1254 #[test]
1255 fn resolving_partial_with_disabled_plugin_preserves_state() {
1256 let partial = PartialConfig {
1258 plugins: Some(HashMap::from([(
1259 "my_plugin".to_string(),
1260 PartialPluginConfig {
1261 enabled: Some(false),
1262 path: None,
1263 },
1264 )])),
1265 ..Default::default()
1266 };
1267
1268 let resolved = partial.resolve();
1269
1270 let plugin = resolved.plugins.get("my_plugin");
1272 assert!(
1273 plugin.is_some(),
1274 "Disabled plugin should be in resolved config"
1275 );
1276 assert!(
1277 !plugin.unwrap().enabled,
1278 "Plugin should remain disabled after resolve"
1279 );
1280 }
1281
1282 #[test]
1283 fn merge_plugins_preserves_higher_precedence_disabled_state() {
1284 let mut higher = PartialConfig {
1286 plugins: Some(HashMap::from([(
1287 "my_plugin".to_string(),
1288 PartialPluginConfig {
1289 enabled: Some(false), path: None,
1291 },
1292 )])),
1293 ..Default::default()
1294 };
1295
1296 let lower = PartialConfig {
1297 plugins: Some(HashMap::from([(
1298 "my_plugin".to_string(),
1299 PartialPluginConfig {
1300 enabled: Some(true), path: None,
1302 },
1303 )])),
1304 ..Default::default()
1305 };
1306
1307 higher.merge_from(&lower);
1308
1309 let plugins = higher.plugins.unwrap();
1310 let plugin = plugins.get("my_plugin").unwrap();
1311 assert_eq!(
1312 plugin.enabled,
1313 Some(false),
1314 "Higher precedence disabled state should win"
1315 );
1316 }
1317
1318 #[test]
1319 fn roundtrip_disabled_plugin_only_saves_delta() {
1320 let mut config = crate::config::Config::default();
1323 config.plugins.insert(
1324 "plugin_a".to_string(),
1325 PluginConfig {
1326 enabled: true,
1327 path: Some(std::path::PathBuf::from("/a.ts")),
1328 },
1329 );
1330 config.plugins.insert(
1331 "plugin_b".to_string(),
1332 PluginConfig {
1333 enabled: false,
1334 path: Some(std::path::PathBuf::from("/b.ts")),
1335 },
1336 );
1337 config.plugins.insert(
1338 "plugin_c".to_string(),
1339 PluginConfig {
1340 enabled: true,
1341 path: Some(std::path::PathBuf::from("/c.ts")),
1342 },
1343 );
1344
1345 let partial = PartialConfig::from(&config);
1347
1348 let json = serde_json::to_string(&partial).unwrap();
1350
1351 assert!(
1353 json.contains("plugin_b"),
1354 "Disabled plugin should be in serialized JSON"
1355 );
1356 assert!(
1357 !json.contains("plugin_a"),
1358 "Enabled plugin_a should not be in serialized JSON"
1359 );
1360 assert!(
1361 !json.contains("plugin_c"),
1362 "Enabled plugin_c should not be in serialized JSON"
1363 );
1364
1365 let deserialized: PartialConfig = serde_json::from_str(&json).unwrap();
1367
1368 let plugins = deserialized.plugins.unwrap();
1370 assert_eq!(plugins.len(), 1);
1371 assert!(plugins.contains_key("plugin_b"));
1372 assert_eq!(plugins.get("plugin_b").unwrap().enabled, Some(false));
1373 }
1374}