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