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