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