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