1use crate::config::{
7 ClipboardConfig, CursorStyle, FileBrowserConfig, FileExplorerConfig, FormatterConfig,
8 IndentationGuideMode, Keybinding, KeybindingMapName, KeymapConfig, LanguageConfig,
9 LineEndingOption, OnSaveAction, 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_enabled: Option<bool>,
91 pub lsp: Option<HashMap<String, LspLanguageConfig>>,
92 pub universal_lsp: Option<HashMap<String, LspLanguageConfig>>,
93 pub warnings: Option<PartialWarningsConfig>,
94 pub plugins: Option<HashMap<String, PartialPluginConfig>>,
95 pub packages: Option<PartialPackagesConfig>,
96 pub env: Option<crate::config::EnvConfig>,
99}
100
101impl Merge for PartialConfig {
102 fn merge_from(&mut self, other: &Self) {
103 self.version.merge_from(&other.version);
104 self.theme.merge_from(&other.theme);
105 self.locale.merge_from(&other.locale);
106 self.check_for_updates.merge_from(&other.check_for_updates);
107
108 merge_partial(&mut self.editor, &other.editor);
110 merge_partial(&mut self.file_explorer, &other.file_explorer);
111 merge_partial(&mut self.file_browser, &other.file_browser);
112 merge_partial(&mut self.clipboard, &other.clipboard);
113 merge_partial(&mut self.terminal, &other.terminal);
114 merge_partial(&mut self.warnings, &other.warnings);
115 merge_partial(&mut self.packages, &other.packages);
116 self.env.merge_from(&other.env);
118
119 self.keybindings.merge_from(&other.keybindings);
121
122 merge_hashmap(&mut self.keybinding_maps, &other.keybinding_maps);
124 merge_hashmap_recursive(&mut self.languages, &other.languages);
125 self.default_language.merge_from(&other.default_language);
126 self.lsp_enabled.merge_from(&other.lsp_enabled);
127 merge_hashmap(&mut self.lsp, &other.lsp);
128 merge_hashmap(&mut self.universal_lsp, &other.universal_lsp);
129 merge_hashmap_recursive(&mut self.plugins, &other.plugins);
130
131 self.active_keybinding_map
132 .merge_from(&other.active_keybinding_map);
133 }
134}
135
136fn merge_partial<T: Merge + Clone>(target: &mut Option<T>, other: &Option<T>) {
138 match (target, other) {
139 (Some(t), Some(o)) => t.merge_from(o),
140 (t @ None, Some(o)) => *t = Some(o.clone()),
141 _ => {}
142 }
143}
144
145#[derive(Debug, Clone, Default, Deserialize, Serialize)]
147#[serde(default)]
148pub struct PartialEditorConfig {
149 pub use_tabs: Option<bool>,
150 pub tab_size: Option<usize>,
151 pub auto_indent: Option<bool>,
152 pub auto_close: Option<bool>,
153 pub auto_surround: Option<bool>,
154 pub animations: Option<bool>,
155 pub cursor_jump_animation: Option<bool>,
156 pub line_numbers: Option<bool>,
157 pub relative_line_numbers: Option<bool>,
158 pub scroll_offset: Option<usize>,
159 pub syntax_highlighting: Option<bool>,
160 pub highlight_current_line: Option<bool>,
161 pub highlight_occurrences: Option<bool>,
162 pub hide_current_line_on_selection: Option<bool>,
163 pub highlight_current_column: Option<bool>,
164 pub line_wrap: Option<bool>,
165 pub wrap_indent: Option<bool>,
166 pub wrap_column: Option<Option<usize>>,
167 pub page_width: Option<Option<usize>>,
168 pub highlight_timeout_ms: Option<u64>,
169 pub snapshot_interval: Option<usize>,
170 pub large_file_threshold_bytes: Option<u64>,
171 pub estimated_line_length: Option<usize>,
172 pub enable_inlay_hints: Option<bool>,
173 pub enable_semantic_tokens_full: Option<bool>,
174 pub diagnostics_inline_text: Option<bool>,
175 pub recovery_enabled: Option<bool>,
176 pub auto_recovery_save_interval_secs: Option<u32>,
177 pub auto_save_enabled: Option<bool>,
178 pub auto_save_interval_secs: Option<u32>,
179 pub hot_exit: Option<bool>,
180 pub confirm_quit: Option<bool>,
181 pub restore_previous_session: Option<bool>,
182 pub skip_session_restore_when_files_passed: Option<bool>,
183 pub auto_create_empty_buffer_on_last_buffer_close: Option<bool>,
184 pub highlight_context_bytes: Option<usize>,
185 pub mouse_hover_enabled: Option<bool>,
186 pub mouse_hover_delay_ms: Option<u64>,
187 pub double_click_time_ms: Option<u64>,
188 pub auto_revert_poll_interval_ms: Option<u64>,
189 pub read_concurrency: Option<usize>,
190 pub file_tree_poll_interval_ms: Option<u64>,
191 pub default_line_ending: Option<LineEndingOption>,
192 pub trim_trailing_whitespace_on_save: Option<bool>,
193 pub ensure_final_newline_on_save: Option<bool>,
194 pub auto_read_only: Option<bool>,
195 pub highlight_matching_brackets: Option<bool>,
196 pub rainbow_brackets: Option<bool>,
197 pub cursor_style: Option<CursorStyle>,
198 pub keyboard_disambiguate_escape_codes: Option<bool>,
199 pub keyboard_report_event_types: Option<bool>,
200 pub keyboard_report_alternate_keys: Option<bool>,
201 pub keyboard_report_all_keys_as_escape_codes: Option<bool>,
202 pub completion_popup_auto_show: Option<bool>,
203 pub quick_suggestions: Option<bool>,
204 pub quick_suggestions_delay_ms: Option<u64>,
205 pub suggest_on_trigger_characters: Option<bool>,
206 pub show_menu_bar: Option<bool>,
207 pub screensaver_enabled: Option<bool>,
208 pub screensaver_idle_minutes: Option<u32>,
209 pub menu_bar_mnemonics: Option<bool>,
210 pub show_tab_bar: Option<bool>,
211 pub show_status_bar: Option<bool>,
212 pub status_bar: Option<crate::config::StatusBarConfig>,
213 pub show_prompt_line: Option<bool>,
214 pub show_vertical_scrollbar: Option<bool>,
215 pub show_horizontal_scrollbar: Option<bool>,
216 pub show_tilde: Option<bool>,
217 pub use_terminal_bg: Option<bool>,
218 pub set_window_title: Option<bool>,
219 pub terminal_auto_title: Option<bool>,
220 pub rulers: Option<Vec<usize>>,
221 pub indentation_guide: Option<IndentationGuideMode>,
222 pub indentation_guide_glyph: Option<String>,
223 pub whitespace_show: Option<bool>,
224 pub whitespace_spaces_leading: Option<bool>,
225 pub whitespace_spaces_inner: Option<bool>,
226 pub whitespace_spaces_trailing: Option<bool>,
227 pub whitespace_tabs_leading: Option<bool>,
228 pub whitespace_tabs_inner: Option<bool>,
229 pub whitespace_tabs_trailing: Option<bool>,
230}
231
232impl Merge for PartialEditorConfig {
233 fn merge_from(&mut self, other: &Self) {
234 self.use_tabs.merge_from(&other.use_tabs);
235 self.tab_size.merge_from(&other.tab_size);
236 self.auto_indent.merge_from(&other.auto_indent);
237 self.auto_close.merge_from(&other.auto_close);
238 self.auto_surround.merge_from(&other.auto_surround);
239 self.animations.merge_from(&other.animations);
240 self.cursor_jump_animation
241 .merge_from(&other.cursor_jump_animation);
242 self.line_numbers.merge_from(&other.line_numbers);
243 self.relative_line_numbers
244 .merge_from(&other.relative_line_numbers);
245 self.scroll_offset.merge_from(&other.scroll_offset);
246 self.syntax_highlighting
247 .merge_from(&other.syntax_highlighting);
248 self.line_wrap.merge_from(&other.line_wrap);
249 self.wrap_indent.merge_from(&other.wrap_indent);
250 self.wrap_column.merge_from(&other.wrap_column);
251 self.page_width.merge_from(&other.page_width);
252 self.highlight_timeout_ms
253 .merge_from(&other.highlight_timeout_ms);
254 self.snapshot_interval.merge_from(&other.snapshot_interval);
255 self.large_file_threshold_bytes
256 .merge_from(&other.large_file_threshold_bytes);
257 self.estimated_line_length
258 .merge_from(&other.estimated_line_length);
259 self.enable_inlay_hints
260 .merge_from(&other.enable_inlay_hints);
261 self.enable_semantic_tokens_full
262 .merge_from(&other.enable_semantic_tokens_full);
263 self.diagnostics_inline_text
264 .merge_from(&other.diagnostics_inline_text);
265 self.recovery_enabled.merge_from(&other.recovery_enabled);
266 self.auto_recovery_save_interval_secs
267 .merge_from(&other.auto_recovery_save_interval_secs);
268 self.auto_save_enabled.merge_from(&other.auto_save_enabled);
269 self.auto_save_interval_secs
270 .merge_from(&other.auto_save_interval_secs);
271 self.hot_exit.merge_from(&other.hot_exit);
272 self.confirm_quit.merge_from(&other.confirm_quit);
273 self.restore_previous_session
274 .merge_from(&other.restore_previous_session);
275 self.skip_session_restore_when_files_passed
276 .merge_from(&other.skip_session_restore_when_files_passed);
277 self.auto_create_empty_buffer_on_last_buffer_close
278 .merge_from(&other.auto_create_empty_buffer_on_last_buffer_close);
279 self.highlight_context_bytes
280 .merge_from(&other.highlight_context_bytes);
281 self.mouse_hover_enabled
282 .merge_from(&other.mouse_hover_enabled);
283 self.mouse_hover_delay_ms
284 .merge_from(&other.mouse_hover_delay_ms);
285 self.double_click_time_ms
286 .merge_from(&other.double_click_time_ms);
287 self.auto_revert_poll_interval_ms
288 .merge_from(&other.auto_revert_poll_interval_ms);
289 self.read_concurrency.merge_from(&other.read_concurrency);
290 self.file_tree_poll_interval_ms
291 .merge_from(&other.file_tree_poll_interval_ms);
292 self.default_line_ending
293 .merge_from(&other.default_line_ending);
294 self.trim_trailing_whitespace_on_save
295 .merge_from(&other.trim_trailing_whitespace_on_save);
296 self.ensure_final_newline_on_save
297 .merge_from(&other.ensure_final_newline_on_save);
298 self.auto_read_only.merge_from(&other.auto_read_only);
299 self.highlight_matching_brackets
300 .merge_from(&other.highlight_matching_brackets);
301 self.rainbow_brackets.merge_from(&other.rainbow_brackets);
302 self.cursor_style.merge_from(&other.cursor_style);
303 self.keyboard_disambiguate_escape_codes
304 .merge_from(&other.keyboard_disambiguate_escape_codes);
305 self.keyboard_report_event_types
306 .merge_from(&other.keyboard_report_event_types);
307 self.keyboard_report_alternate_keys
308 .merge_from(&other.keyboard_report_alternate_keys);
309 self.keyboard_report_all_keys_as_escape_codes
310 .merge_from(&other.keyboard_report_all_keys_as_escape_codes);
311 self.completion_popup_auto_show
312 .merge_from(&other.completion_popup_auto_show);
313 self.quick_suggestions.merge_from(&other.quick_suggestions);
314 self.quick_suggestions_delay_ms
315 .merge_from(&other.quick_suggestions_delay_ms);
316 self.suggest_on_trigger_characters
317 .merge_from(&other.suggest_on_trigger_characters);
318 self.show_menu_bar.merge_from(&other.show_menu_bar);
319 self.screensaver_enabled
320 .merge_from(&other.screensaver_enabled);
321 self.screensaver_idle_minutes
322 .merge_from(&other.screensaver_idle_minutes);
323 self.menu_bar_mnemonics
324 .merge_from(&other.menu_bar_mnemonics);
325 self.show_tab_bar.merge_from(&other.show_tab_bar);
326 self.show_status_bar.merge_from(&other.show_status_bar);
327 if other.status_bar.is_some() {
328 self.status_bar = other.status_bar.clone();
329 }
330 self.show_prompt_line.merge_from(&other.show_prompt_line);
331 self.show_vertical_scrollbar
332 .merge_from(&other.show_vertical_scrollbar);
333 self.show_horizontal_scrollbar
334 .merge_from(&other.show_horizontal_scrollbar);
335 self.show_tilde.merge_from(&other.show_tilde);
336 self.use_terminal_bg.merge_from(&other.use_terminal_bg);
337 self.set_window_title.merge_from(&other.set_window_title);
338 self.terminal_auto_title
339 .merge_from(&other.terminal_auto_title);
340 self.rulers.merge_from(&other.rulers);
341 self.indentation_guide.merge_from(&other.indentation_guide);
342 self.indentation_guide_glyph
343 .merge_from(&other.indentation_guide_glyph);
344 self.whitespace_show.merge_from(&other.whitespace_show);
345 self.whitespace_spaces_leading
346 .merge_from(&other.whitespace_spaces_leading);
347 self.whitespace_spaces_inner
348 .merge_from(&other.whitespace_spaces_inner);
349 self.whitespace_spaces_trailing
350 .merge_from(&other.whitespace_spaces_trailing);
351 self.whitespace_tabs_leading
352 .merge_from(&other.whitespace_tabs_leading);
353 self.whitespace_tabs_inner
354 .merge_from(&other.whitespace_tabs_inner);
355 self.whitespace_tabs_trailing
356 .merge_from(&other.whitespace_tabs_trailing);
357 }
358}
359
360#[derive(Debug, Clone, Default, Deserialize, Serialize)]
362#[serde(default)]
363pub struct PartialFileExplorerConfig {
364 pub respect_gitignore: Option<bool>,
365 pub show_hidden: Option<bool>,
366 pub show_gitignored: Option<bool>,
367 pub custom_ignore_patterns: Option<Vec<String>>,
368 #[serde(
369 default,
370 deserialize_with = "crate::config::explorer_width::deserialize_optional"
371 )]
372 pub width: Option<crate::config::ExplorerWidth>,
373 pub preview_tabs: Option<bool>,
374 pub side: Option<crate::config::FileExplorerSide>,
375 pub auto_open_on_last_buffer_close: Option<bool>,
376 pub follow_active_buffer: Option<bool>,
377 pub compact_directories: Option<bool>,
378 pub tree_indicator_collapsed: Option<String>,
379 pub tree_indicator_expanded: Option<String>,
380}
381
382impl Merge for PartialFileExplorerConfig {
383 fn merge_from(&mut self, other: &Self) {
384 self.respect_gitignore.merge_from(&other.respect_gitignore);
385 self.show_hidden.merge_from(&other.show_hidden);
386 self.show_gitignored.merge_from(&other.show_gitignored);
387 self.custom_ignore_patterns
388 .merge_from(&other.custom_ignore_patterns);
389 self.width.merge_from(&other.width);
390 self.preview_tabs.merge_from(&other.preview_tabs);
391 self.side.merge_from(&other.side);
392 self.auto_open_on_last_buffer_close
393 .merge_from(&other.auto_open_on_last_buffer_close);
394 self.follow_active_buffer
395 .merge_from(&other.follow_active_buffer);
396 self.compact_directories
397 .merge_from(&other.compact_directories);
398 self.tree_indicator_collapsed
399 .merge_from(&other.tree_indicator_collapsed);
400 self.tree_indicator_expanded
401 .merge_from(&other.tree_indicator_expanded);
402 }
403}
404
405#[derive(Debug, Clone, Default, Deserialize, Serialize)]
407#[serde(default)]
408pub struct PartialFileBrowserConfig {
409 pub show_hidden: Option<bool>,
410}
411
412impl Merge for PartialFileBrowserConfig {
413 fn merge_from(&mut self, other: &Self) {
414 self.show_hidden.merge_from(&other.show_hidden);
415 }
416}
417
418#[derive(Debug, Clone, Default, Deserialize, Serialize)]
420#[serde(default)]
421pub struct PartialClipboardConfig {
422 pub use_osc52: Option<bool>,
423 pub use_system_clipboard: Option<bool>,
424}
425
426impl Merge for PartialClipboardConfig {
427 fn merge_from(&mut self, other: &Self) {
428 self.use_osc52.merge_from(&other.use_osc52);
429 self.use_system_clipboard
430 .merge_from(&other.use_system_clipboard);
431 }
432}
433
434#[derive(Debug, Clone, Default, Deserialize, Serialize)]
436#[serde(default)]
437pub struct PartialTerminalConfig {
438 pub jump_to_end_on_output: Option<bool>,
439 pub shell: Option<crate::config::TerminalShellConfig>,
440 pub skip_app_execution_alias: Option<bool>,
441 pub resume_agents: Option<bool>,
442}
443
444impl Merge for PartialTerminalConfig {
445 fn merge_from(&mut self, other: &Self) {
446 self.jump_to_end_on_output
447 .merge_from(&other.jump_to_end_on_output);
448 self.shell.merge_from(&other.shell);
449 self.skip_app_execution_alias
450 .merge_from(&other.skip_app_execution_alias);
451 self.resume_agents.merge_from(&other.resume_agents);
452 }
453}
454
455#[derive(Debug, Clone, Default, Deserialize, Serialize)]
457#[serde(default)]
458pub struct PartialWarningsConfig {
459 pub show_status_indicator: Option<bool>,
460}
461
462impl Merge for PartialWarningsConfig {
463 fn merge_from(&mut self, other: &Self) {
464 self.show_status_indicator
465 .merge_from(&other.show_status_indicator);
466 }
467}
468
469#[derive(Debug, Clone, Default, Deserialize, Serialize)]
471#[serde(default)]
472pub struct PartialPackagesConfig {
473 pub sources: Option<Vec<String>>,
474}
475
476impl Merge for PartialPackagesConfig {
477 fn merge_from(&mut self, other: &Self) {
478 self.sources.merge_from(&other.sources);
479 }
480}
481
482#[derive(Debug, Clone, Default, Deserialize, Serialize)]
484#[serde(default)]
485pub struct PartialPluginConfig {
486 #[serde(skip_serializing_if = "Option::is_none")]
487 pub enabled: Option<bool>,
488 #[serde(skip_serializing_if = "Option::is_none")]
489 pub path: Option<std::path::PathBuf>,
490 #[serde(skip_serializing_if = "serde_json::Value::is_null", default)]
495 pub settings: serde_json::Value,
496}
497
498fn merge_json_values(target: &mut serde_json::Value, other: &serde_json::Value) {
499 match (target, other) {
500 (serde_json::Value::Object(t_map), serde_json::Value::Object(o_map)) => {
501 for (k, v) in o_map {
502 match t_map.get_mut(k) {
503 Some(existing) => merge_json_values(existing, v),
504 None => {
505 t_map.insert(k.clone(), v.clone());
506 }
507 }
508 }
509 }
510 (t @ serde_json::Value::Null, o) if !o.is_null() => *t = o.clone(),
511 _ => {}
512 }
513}
514
515impl Merge for PartialPluginConfig {
516 fn merge_from(&mut self, other: &Self) {
517 self.enabled.merge_from(&other.enabled);
518 self.path.merge_from(&other.path);
519 merge_json_values(&mut self.settings, &other.settings);
520 }
521}
522
523#[derive(Debug, Clone, Default, Deserialize, Serialize)]
525#[serde(default)]
526pub struct PartialLanguageConfig {
527 pub extensions: Option<Vec<String>>,
528 pub filenames: Option<Vec<String>>,
529 pub grammar: Option<String>,
530 pub comment_prefix: Option<String>,
531 pub auto_indent: Option<bool>,
532 pub auto_close: Option<bool>,
533 pub auto_surround: Option<bool>,
534 pub textmate_grammar: Option<std::path::PathBuf>,
535 pub show_whitespace_tabs: Option<bool>,
536 pub line_wrap: Option<bool>,
537 pub wrap_column: Option<Option<usize>>,
538 pub page_view: Option<bool>,
539 pub page_width: Option<Option<usize>>,
540 pub use_tabs: Option<bool>,
541 pub tab_size: Option<usize>,
542 pub formatter: Option<FormatterConfig>,
543 pub format_on_save: Option<bool>,
544 pub on_save: Option<Vec<OnSaveAction>>,
545 pub word_characters: Option<Option<String>>,
546 pub indent: Option<crate::config::IndentRulesConfig>,
547}
548
549impl Merge for PartialLanguageConfig {
550 fn merge_from(&mut self, other: &Self) {
551 self.extensions.merge_from(&other.extensions);
552 self.filenames.merge_from(&other.filenames);
553 self.grammar.merge_from(&other.grammar);
554 self.comment_prefix.merge_from(&other.comment_prefix);
555 self.auto_indent.merge_from(&other.auto_indent);
556 self.auto_close.merge_from(&other.auto_close);
557 self.auto_surround.merge_from(&other.auto_surround);
558 self.textmate_grammar.merge_from(&other.textmate_grammar);
559 self.show_whitespace_tabs
560 .merge_from(&other.show_whitespace_tabs);
561 self.line_wrap.merge_from(&other.line_wrap);
562 self.wrap_column.merge_from(&other.wrap_column);
563 self.page_view.merge_from(&other.page_view);
564 self.page_width.merge_from(&other.page_width);
565 self.use_tabs.merge_from(&other.use_tabs);
566 self.tab_size.merge_from(&other.tab_size);
567 self.formatter.merge_from(&other.formatter);
568 self.format_on_save.merge_from(&other.format_on_save);
569 self.on_save.merge_from(&other.on_save);
570 self.word_characters.merge_from(&other.word_characters);
571 self.indent.merge_from(&other.indent);
572 }
573}
574
575impl From<&crate::config::EditorConfig> for PartialEditorConfig {
578 fn from(cfg: &crate::config::EditorConfig) -> Self {
579 Self {
580 use_tabs: Some(cfg.use_tabs),
581 tab_size: Some(cfg.tab_size),
582 auto_indent: Some(cfg.auto_indent),
583 auto_close: Some(cfg.auto_close),
584 auto_surround: Some(cfg.auto_surround),
585 animations: Some(cfg.animations),
586 cursor_jump_animation: Some(cfg.cursor_jump_animation),
587 line_numbers: Some(cfg.line_numbers),
588 relative_line_numbers: Some(cfg.relative_line_numbers),
589 scroll_offset: Some(cfg.scroll_offset),
590 syntax_highlighting: Some(cfg.syntax_highlighting),
591 highlight_current_line: Some(cfg.highlight_current_line),
592 highlight_occurrences: Some(cfg.highlight_occurrences),
593 hide_current_line_on_selection: Some(cfg.hide_current_line_on_selection),
594 highlight_current_column: Some(cfg.highlight_current_column),
595 line_wrap: Some(cfg.line_wrap),
596 wrap_indent: Some(cfg.wrap_indent),
597 wrap_column: Some(cfg.wrap_column),
598 page_width: Some(cfg.page_width),
599 highlight_timeout_ms: Some(cfg.highlight_timeout_ms),
600 snapshot_interval: Some(cfg.snapshot_interval),
601 large_file_threshold_bytes: Some(cfg.large_file_threshold_bytes),
602 estimated_line_length: Some(cfg.estimated_line_length),
603 enable_inlay_hints: Some(cfg.enable_inlay_hints),
604 enable_semantic_tokens_full: Some(cfg.enable_semantic_tokens_full),
605 diagnostics_inline_text: Some(cfg.diagnostics_inline_text),
606 recovery_enabled: Some(cfg.recovery_enabled),
607 auto_recovery_save_interval_secs: Some(cfg.auto_recovery_save_interval_secs),
608 auto_save_enabled: Some(cfg.auto_save_enabled),
609 auto_save_interval_secs: Some(cfg.auto_save_interval_secs),
610 hot_exit: Some(cfg.hot_exit),
611 confirm_quit: Some(cfg.confirm_quit),
612 restore_previous_session: Some(cfg.restore_previous_session),
613 skip_session_restore_when_files_passed: Some(
614 cfg.skip_session_restore_when_files_passed,
615 ),
616 auto_create_empty_buffer_on_last_buffer_close: Some(
617 cfg.auto_create_empty_buffer_on_last_buffer_close,
618 ),
619 highlight_context_bytes: Some(cfg.highlight_context_bytes),
620 mouse_hover_enabled: Some(cfg.mouse_hover_enabled),
621 mouse_hover_delay_ms: Some(cfg.mouse_hover_delay_ms),
622 double_click_time_ms: Some(cfg.double_click_time_ms),
623 auto_revert_poll_interval_ms: Some(cfg.auto_revert_poll_interval_ms),
624 read_concurrency: Some(cfg.read_concurrency),
625 file_tree_poll_interval_ms: Some(cfg.file_tree_poll_interval_ms),
626 default_line_ending: Some(cfg.default_line_ending.clone()),
627 trim_trailing_whitespace_on_save: Some(cfg.trim_trailing_whitespace_on_save),
628 ensure_final_newline_on_save: Some(cfg.ensure_final_newline_on_save),
629 auto_read_only: Some(cfg.auto_read_only),
630 highlight_matching_brackets: Some(cfg.highlight_matching_brackets),
631 rainbow_brackets: Some(cfg.rainbow_brackets),
632 cursor_style: Some(cfg.cursor_style),
633 keyboard_disambiguate_escape_codes: Some(cfg.keyboard_disambiguate_escape_codes),
634 keyboard_report_event_types: Some(cfg.keyboard_report_event_types),
635 keyboard_report_alternate_keys: Some(cfg.keyboard_report_alternate_keys),
636 keyboard_report_all_keys_as_escape_codes: Some(
637 cfg.keyboard_report_all_keys_as_escape_codes,
638 ),
639 completion_popup_auto_show: Some(cfg.completion_popup_auto_show),
640 quick_suggestions: Some(cfg.quick_suggestions),
641 quick_suggestions_delay_ms: Some(cfg.quick_suggestions_delay_ms),
642 suggest_on_trigger_characters: Some(cfg.suggest_on_trigger_characters),
643 show_menu_bar: Some(cfg.show_menu_bar),
644 screensaver_enabled: Some(cfg.screensaver_enabled),
645 screensaver_idle_minutes: Some(cfg.screensaver_idle_minutes),
646 menu_bar_mnemonics: Some(cfg.menu_bar_mnemonics),
647 show_tab_bar: Some(cfg.show_tab_bar),
648 show_status_bar: Some(cfg.show_status_bar),
649 status_bar: Some(cfg.status_bar.clone()),
650 show_prompt_line: Some(cfg.show_prompt_line),
651 show_vertical_scrollbar: Some(cfg.show_vertical_scrollbar),
652 show_horizontal_scrollbar: Some(cfg.show_horizontal_scrollbar),
653 show_tilde: Some(cfg.show_tilde),
654 use_terminal_bg: Some(cfg.use_terminal_bg),
655 set_window_title: Some(cfg.set_window_title),
656 terminal_auto_title: Some(cfg.terminal_auto_title),
657 rulers: Some(cfg.rulers.clone()),
658 indentation_guide: Some(cfg.indentation_guide),
659 indentation_guide_glyph: Some(cfg.indentation_guide_glyph.clone()),
660 whitespace_show: Some(cfg.whitespace_show),
661 whitespace_spaces_leading: Some(cfg.whitespace_spaces_leading),
662 whitespace_spaces_inner: Some(cfg.whitespace_spaces_inner),
663 whitespace_spaces_trailing: Some(cfg.whitespace_spaces_trailing),
664 whitespace_tabs_leading: Some(cfg.whitespace_tabs_leading),
665 whitespace_tabs_inner: Some(cfg.whitespace_tabs_inner),
666 whitespace_tabs_trailing: Some(cfg.whitespace_tabs_trailing),
667 }
668 }
669}
670
671impl PartialEditorConfig {
672 pub fn resolve(self, defaults: &crate::config::EditorConfig) -> crate::config::EditorConfig {
674 crate::config::EditorConfig {
675 use_tabs: self.use_tabs.unwrap_or(defaults.use_tabs),
676 tab_size: self.tab_size.unwrap_or(defaults.tab_size),
677 auto_indent: self.auto_indent.unwrap_or(defaults.auto_indent),
678 auto_close: self.auto_close.unwrap_or(defaults.auto_close),
679 auto_surround: self.auto_surround.unwrap_or(defaults.auto_surround),
680 animations: self.animations.unwrap_or(defaults.animations),
681 cursor_jump_animation: self
682 .cursor_jump_animation
683 .unwrap_or(defaults.cursor_jump_animation),
684 line_numbers: self.line_numbers.unwrap_or(defaults.line_numbers),
685 relative_line_numbers: self
686 .relative_line_numbers
687 .unwrap_or(defaults.relative_line_numbers),
688 scroll_offset: self.scroll_offset.unwrap_or(defaults.scroll_offset),
689 syntax_highlighting: self
690 .syntax_highlighting
691 .unwrap_or(defaults.syntax_highlighting),
692 highlight_current_line: self
693 .highlight_current_line
694 .unwrap_or(defaults.highlight_current_line),
695 highlight_occurrences: self
696 .highlight_occurrences
697 .unwrap_or(defaults.highlight_occurrences),
698 hide_current_line_on_selection: self
699 .hide_current_line_on_selection
700 .unwrap_or(defaults.hide_current_line_on_selection),
701 highlight_current_column: self
702 .highlight_current_column
703 .unwrap_or(defaults.highlight_current_column),
704 line_wrap: self.line_wrap.unwrap_or(defaults.line_wrap),
705 wrap_indent: self.wrap_indent.unwrap_or(defaults.wrap_indent),
706 wrap_column: self.wrap_column.unwrap_or(defaults.wrap_column),
707 page_width: self.page_width.unwrap_or(defaults.page_width),
708 highlight_timeout_ms: self
709 .highlight_timeout_ms
710 .unwrap_or(defaults.highlight_timeout_ms),
711 snapshot_interval: self.snapshot_interval.unwrap_or(defaults.snapshot_interval),
712 large_file_threshold_bytes: self
713 .large_file_threshold_bytes
714 .unwrap_or(defaults.large_file_threshold_bytes),
715 estimated_line_length: self
716 .estimated_line_length
717 .unwrap_or(defaults.estimated_line_length),
718 enable_inlay_hints: self
719 .enable_inlay_hints
720 .unwrap_or(defaults.enable_inlay_hints),
721 enable_semantic_tokens_full: self
722 .enable_semantic_tokens_full
723 .unwrap_or(defaults.enable_semantic_tokens_full),
724 diagnostics_inline_text: self
725 .diagnostics_inline_text
726 .unwrap_or(defaults.diagnostics_inline_text),
727 recovery_enabled: self.recovery_enabled.unwrap_or(defaults.recovery_enabled),
728 auto_recovery_save_interval_secs: self
729 .auto_recovery_save_interval_secs
730 .unwrap_or(defaults.auto_recovery_save_interval_secs),
731 auto_save_enabled: self.auto_save_enabled.unwrap_or(defaults.auto_save_enabled),
732 auto_save_interval_secs: self
733 .auto_save_interval_secs
734 .unwrap_or(defaults.auto_save_interval_secs),
735 hot_exit: self.hot_exit.unwrap_or(defaults.hot_exit),
736 confirm_quit: self.confirm_quit.unwrap_or(defaults.confirm_quit),
737 restore_previous_session: self
738 .restore_previous_session
739 .unwrap_or(defaults.restore_previous_session),
740 skip_session_restore_when_files_passed: self
741 .skip_session_restore_when_files_passed
742 .unwrap_or(defaults.skip_session_restore_when_files_passed),
743 auto_create_empty_buffer_on_last_buffer_close: self
744 .auto_create_empty_buffer_on_last_buffer_close
745 .unwrap_or(defaults.auto_create_empty_buffer_on_last_buffer_close),
746 highlight_context_bytes: self
747 .highlight_context_bytes
748 .unwrap_or(defaults.highlight_context_bytes),
749 mouse_hover_enabled: self
750 .mouse_hover_enabled
751 .unwrap_or(defaults.mouse_hover_enabled),
752 mouse_hover_delay_ms: self
753 .mouse_hover_delay_ms
754 .unwrap_or(defaults.mouse_hover_delay_ms),
755 double_click_time_ms: self
756 .double_click_time_ms
757 .unwrap_or(defaults.double_click_time_ms),
758 auto_revert_poll_interval_ms: self
759 .auto_revert_poll_interval_ms
760 .unwrap_or(defaults.auto_revert_poll_interval_ms),
761 read_concurrency: self.read_concurrency.unwrap_or(defaults.read_concurrency),
762 file_tree_poll_interval_ms: self
763 .file_tree_poll_interval_ms
764 .unwrap_or(defaults.file_tree_poll_interval_ms),
765 default_line_ending: self
766 .default_line_ending
767 .unwrap_or(defaults.default_line_ending.clone()),
768 trim_trailing_whitespace_on_save: self
769 .trim_trailing_whitespace_on_save
770 .unwrap_or(defaults.trim_trailing_whitespace_on_save),
771 ensure_final_newline_on_save: self
772 .ensure_final_newline_on_save
773 .unwrap_or(defaults.ensure_final_newline_on_save),
774 auto_read_only: self.auto_read_only.unwrap_or(defaults.auto_read_only),
775 highlight_matching_brackets: self
776 .highlight_matching_brackets
777 .unwrap_or(defaults.highlight_matching_brackets),
778 rainbow_brackets: self.rainbow_brackets.unwrap_or(defaults.rainbow_brackets),
779 cursor_style: self.cursor_style.unwrap_or(defaults.cursor_style),
780 keyboard_disambiguate_escape_codes: self
781 .keyboard_disambiguate_escape_codes
782 .unwrap_or(defaults.keyboard_disambiguate_escape_codes),
783 keyboard_report_event_types: self
784 .keyboard_report_event_types
785 .unwrap_or(defaults.keyboard_report_event_types),
786 keyboard_report_alternate_keys: self
787 .keyboard_report_alternate_keys
788 .unwrap_or(defaults.keyboard_report_alternate_keys),
789 keyboard_report_all_keys_as_escape_codes: self
790 .keyboard_report_all_keys_as_escape_codes
791 .unwrap_or(defaults.keyboard_report_all_keys_as_escape_codes),
792 completion_popup_auto_show: self
793 .completion_popup_auto_show
794 .unwrap_or(defaults.completion_popup_auto_show),
795 quick_suggestions: self.quick_suggestions.unwrap_or(defaults.quick_suggestions),
796 quick_suggestions_delay_ms: self
797 .quick_suggestions_delay_ms
798 .unwrap_or(defaults.quick_suggestions_delay_ms),
799 suggest_on_trigger_characters: self
800 .suggest_on_trigger_characters
801 .unwrap_or(defaults.suggest_on_trigger_characters),
802 show_menu_bar: self.show_menu_bar.unwrap_or(defaults.show_menu_bar),
803 screensaver_enabled: self
804 .screensaver_enabled
805 .unwrap_or(defaults.screensaver_enabled),
806 screensaver_idle_minutes: self
807 .screensaver_idle_minutes
808 .unwrap_or(defaults.screensaver_idle_minutes),
809 menu_bar_mnemonics: self
810 .menu_bar_mnemonics
811 .unwrap_or(defaults.menu_bar_mnemonics),
812 show_tab_bar: self.show_tab_bar.unwrap_or(defaults.show_tab_bar),
813 show_status_bar: self.show_status_bar.unwrap_or(defaults.show_status_bar),
814 status_bar: self
815 .status_bar
816 .unwrap_or_else(|| defaults.status_bar.clone()),
817 show_prompt_line: self.show_prompt_line.unwrap_or(defaults.show_prompt_line),
818 show_vertical_scrollbar: self
819 .show_vertical_scrollbar
820 .unwrap_or(defaults.show_vertical_scrollbar),
821 show_horizontal_scrollbar: self
822 .show_horizontal_scrollbar
823 .unwrap_or(defaults.show_horizontal_scrollbar),
824 show_tilde: self.show_tilde.unwrap_or(defaults.show_tilde),
825 use_terminal_bg: self.use_terminal_bg.unwrap_or(defaults.use_terminal_bg),
826 set_window_title: self.set_window_title.unwrap_or(defaults.set_window_title),
827 terminal_auto_title: self
828 .terminal_auto_title
829 .unwrap_or(defaults.terminal_auto_title),
830 rulers: self.rulers.unwrap_or_else(|| defaults.rulers.clone()),
831 indentation_guide: self.indentation_guide.unwrap_or(defaults.indentation_guide),
832 indentation_guide_glyph: self
833 .indentation_guide_glyph
834 .map(|glyph| crate::config::normalize_indentation_guide_glyph(&glyph))
835 .unwrap_or_else(|| defaults.indentation_guide_glyph.clone()),
836 whitespace_show: self.whitespace_show.unwrap_or(defaults.whitespace_show),
837 whitespace_spaces_leading: self
838 .whitespace_spaces_leading
839 .unwrap_or(defaults.whitespace_spaces_leading),
840 whitespace_spaces_inner: self
841 .whitespace_spaces_inner
842 .unwrap_or(defaults.whitespace_spaces_inner),
843 whitespace_spaces_trailing: self
844 .whitespace_spaces_trailing
845 .unwrap_or(defaults.whitespace_spaces_trailing),
846 whitespace_tabs_leading: self
847 .whitespace_tabs_leading
848 .unwrap_or(defaults.whitespace_tabs_leading),
849 whitespace_tabs_inner: self
850 .whitespace_tabs_inner
851 .unwrap_or(defaults.whitespace_tabs_inner),
852 whitespace_tabs_trailing: self
853 .whitespace_tabs_trailing
854 .unwrap_or(defaults.whitespace_tabs_trailing),
855 }
856 }
857}
858
859impl From<&FileExplorerConfig> for PartialFileExplorerConfig {
860 fn from(cfg: &FileExplorerConfig) -> Self {
861 Self {
862 respect_gitignore: Some(cfg.respect_gitignore),
863 show_hidden: Some(cfg.show_hidden),
864 show_gitignored: Some(cfg.show_gitignored),
865 custom_ignore_patterns: Some(cfg.custom_ignore_patterns.clone()),
866 width: Some(cfg.width),
867 preview_tabs: Some(cfg.preview_tabs),
868 side: Some(cfg.side),
869 auto_open_on_last_buffer_close: Some(cfg.auto_open_on_last_buffer_close),
870 follow_active_buffer: Some(cfg.follow_active_buffer),
871 compact_directories: Some(cfg.compact_directories),
872 tree_indicator_collapsed: Some(cfg.tree_indicator_collapsed.clone()),
873 tree_indicator_expanded: Some(cfg.tree_indicator_expanded.clone()),
874 }
875 }
876}
877
878impl PartialFileExplorerConfig {
879 pub fn resolve(self, defaults: &FileExplorerConfig) -> FileExplorerConfig {
880 FileExplorerConfig {
881 respect_gitignore: self.respect_gitignore.unwrap_or(defaults.respect_gitignore),
882 show_hidden: self.show_hidden.unwrap_or(defaults.show_hidden),
883 show_gitignored: self.show_gitignored.unwrap_or(defaults.show_gitignored),
884 custom_ignore_patterns: self
885 .custom_ignore_patterns
886 .unwrap_or_else(|| defaults.custom_ignore_patterns.clone()),
887 width: self.width.unwrap_or(defaults.width),
888 preview_tabs: self.preview_tabs.unwrap_or(defaults.preview_tabs),
889 side: self.side.unwrap_or(defaults.side),
890 auto_open_on_last_buffer_close: self
891 .auto_open_on_last_buffer_close
892 .unwrap_or(defaults.auto_open_on_last_buffer_close),
893 follow_active_buffer: self
894 .follow_active_buffer
895 .unwrap_or(defaults.follow_active_buffer),
896 compact_directories: self
897 .compact_directories
898 .unwrap_or(defaults.compact_directories),
899 tree_indicator_collapsed: self
900 .tree_indicator_collapsed
901 .unwrap_or_else(|| defaults.tree_indicator_collapsed.clone()),
902 tree_indicator_expanded: self
903 .tree_indicator_expanded
904 .unwrap_or_else(|| defaults.tree_indicator_expanded.clone()),
905 }
906 }
907}
908
909impl From<&FileBrowserConfig> for PartialFileBrowserConfig {
910 fn from(cfg: &FileBrowserConfig) -> Self {
911 Self {
912 show_hidden: Some(cfg.show_hidden),
913 }
914 }
915}
916
917impl PartialFileBrowserConfig {
918 pub fn resolve(self, defaults: &FileBrowserConfig) -> FileBrowserConfig {
919 FileBrowserConfig {
920 show_hidden: self.show_hidden.unwrap_or(defaults.show_hidden),
921 }
922 }
923}
924
925impl From<&ClipboardConfig> for PartialClipboardConfig {
926 fn from(cfg: &ClipboardConfig) -> Self {
927 Self {
928 use_osc52: Some(cfg.use_osc52),
929 use_system_clipboard: Some(cfg.use_system_clipboard),
930 }
931 }
932}
933
934impl PartialClipboardConfig {
935 pub fn resolve(self, defaults: &ClipboardConfig) -> ClipboardConfig {
936 ClipboardConfig {
937 use_osc52: self.use_osc52.unwrap_or(defaults.use_osc52),
938 use_system_clipboard: self
939 .use_system_clipboard
940 .unwrap_or(defaults.use_system_clipboard),
941 }
942 }
943}
944
945impl From<&TerminalConfig> for PartialTerminalConfig {
946 fn from(cfg: &TerminalConfig) -> Self {
947 Self {
948 jump_to_end_on_output: Some(cfg.jump_to_end_on_output),
949 shell: cfg.shell.clone(),
950 skip_app_execution_alias: Some(cfg.skip_app_execution_alias),
951 resume_agents: Some(cfg.resume_agents),
952 }
953 }
954}
955
956impl PartialTerminalConfig {
957 pub fn resolve(self, defaults: &TerminalConfig) -> TerminalConfig {
958 TerminalConfig {
959 jump_to_end_on_output: self
960 .jump_to_end_on_output
961 .unwrap_or(defaults.jump_to_end_on_output),
962 shell: self.shell.or_else(|| defaults.shell.clone()),
963 skip_app_execution_alias: self
964 .skip_app_execution_alias
965 .unwrap_or(defaults.skip_app_execution_alias),
966 resume_agents: self.resume_agents.unwrap_or(defaults.resume_agents),
967 }
968 }
969}
970
971impl From<&WarningsConfig> for PartialWarningsConfig {
972 fn from(cfg: &WarningsConfig) -> Self {
973 Self {
974 show_status_indicator: Some(cfg.show_status_indicator),
975 }
976 }
977}
978
979impl PartialWarningsConfig {
980 pub fn resolve(self, defaults: &WarningsConfig) -> WarningsConfig {
981 WarningsConfig {
982 show_status_indicator: self
983 .show_status_indicator
984 .unwrap_or(defaults.show_status_indicator),
985 }
986 }
987}
988
989impl From<&crate::config::PackagesConfig> for PartialPackagesConfig {
990 fn from(cfg: &crate::config::PackagesConfig) -> Self {
991 Self {
992 sources: Some(cfg.sources.clone()),
993 }
994 }
995}
996
997impl PartialPackagesConfig {
998 pub fn resolve(
999 self,
1000 defaults: &crate::config::PackagesConfig,
1001 ) -> crate::config::PackagesConfig {
1002 crate::config::PackagesConfig {
1003 sources: self.sources.unwrap_or_else(|| defaults.sources.clone()),
1004 }
1005 }
1006}
1007
1008impl From<&PluginConfig> for PartialPluginConfig {
1009 fn from(cfg: &PluginConfig) -> Self {
1010 Self {
1011 enabled: Some(cfg.enabled),
1012 path: cfg.path.clone(),
1013 settings: cfg.settings.clone(),
1014 }
1015 }
1016}
1017
1018impl PartialPluginConfig {
1019 pub fn resolve(self, defaults: &PluginConfig) -> PluginConfig {
1020 let mut settings = self.settings;
1021 if settings.is_null() {
1022 settings = defaults.settings.clone();
1023 }
1024 PluginConfig {
1025 enabled: self.enabled.unwrap_or(defaults.enabled),
1026 path: self.path.or_else(|| defaults.path.clone()),
1027 settings,
1028 }
1029 }
1030}
1031
1032impl From<&LanguageConfig> for PartialLanguageConfig {
1033 fn from(cfg: &LanguageConfig) -> Self {
1034 Self {
1035 extensions: Some(cfg.extensions.clone()),
1036 filenames: Some(cfg.filenames.clone()),
1037 grammar: Some(cfg.grammar.clone()),
1038 comment_prefix: cfg.comment_prefix.clone(),
1039 auto_indent: Some(cfg.auto_indent),
1040 auto_close: cfg.auto_close,
1041 auto_surround: cfg.auto_surround,
1042 textmate_grammar: cfg.textmate_grammar.clone(),
1043 show_whitespace_tabs: Some(cfg.show_whitespace_tabs),
1044 line_wrap: cfg.line_wrap,
1045 wrap_column: Some(cfg.wrap_column),
1046 page_view: cfg.page_view,
1047 page_width: Some(cfg.page_width),
1048 use_tabs: cfg.use_tabs,
1049 tab_size: cfg.tab_size,
1050 formatter: cfg.formatter.clone(),
1051 format_on_save: Some(cfg.format_on_save),
1052 on_save: Some(cfg.on_save.clone()),
1053 word_characters: Some(cfg.word_characters.clone()),
1054 indent: cfg.indent.clone(),
1055 }
1056 }
1057}
1058
1059impl PartialLanguageConfig {
1060 pub fn resolve(self, defaults: &LanguageConfig) -> LanguageConfig {
1061 LanguageConfig {
1062 extensions: self
1063 .extensions
1064 .unwrap_or_else(|| defaults.extensions.clone()),
1065 filenames: self.filenames.unwrap_or_else(|| defaults.filenames.clone()),
1066 grammar: self.grammar.unwrap_or_else(|| defaults.grammar.clone()),
1067 comment_prefix: self
1068 .comment_prefix
1069 .or_else(|| defaults.comment_prefix.clone()),
1070 auto_indent: self.auto_indent.unwrap_or(defaults.auto_indent),
1071 auto_close: self.auto_close.or(defaults.auto_close),
1072 auto_surround: self.auto_surround.or(defaults.auto_surround),
1073 textmate_grammar: self
1074 .textmate_grammar
1075 .or_else(|| defaults.textmate_grammar.clone()),
1076 show_whitespace_tabs: self
1077 .show_whitespace_tabs
1078 .unwrap_or(defaults.show_whitespace_tabs),
1079 line_wrap: self.line_wrap.or(defaults.line_wrap),
1080 wrap_column: self.wrap_column.unwrap_or(defaults.wrap_column),
1081 page_view: self.page_view.or(defaults.page_view),
1082 page_width: self.page_width.unwrap_or(defaults.page_width),
1083 use_tabs: self.use_tabs.or(defaults.use_tabs),
1084 tab_size: self.tab_size.or(defaults.tab_size),
1085 formatter: self.formatter.or_else(|| defaults.formatter.clone()),
1086 format_on_save: self.format_on_save.unwrap_or(defaults.format_on_save),
1087 on_save: self.on_save.unwrap_or_else(|| defaults.on_save.clone()),
1088 word_characters: self
1089 .word_characters
1090 .unwrap_or_else(|| defaults.word_characters.clone()),
1091 indent: self.indent.or_else(|| defaults.indent.clone()),
1092 }
1093 }
1094}
1095
1096impl From<&crate::config::Config> for PartialConfig {
1097 fn from(cfg: &crate::config::Config) -> Self {
1098 Self {
1099 version: Some(cfg.version),
1100 theme: Some(cfg.theme.clone()),
1101 locale: cfg.locale.0.clone(),
1102 check_for_updates: Some(cfg.check_for_updates),
1103 editor: Some(PartialEditorConfig::from(&cfg.editor)),
1104 file_explorer: Some(PartialFileExplorerConfig::from(&cfg.file_explorer)),
1105 file_browser: Some(PartialFileBrowserConfig::from(&cfg.file_browser)),
1106 clipboard: Some(PartialClipboardConfig::from(&cfg.clipboard)),
1107 terminal: Some(PartialTerminalConfig::from(&cfg.terminal)),
1108 keybindings: Some(cfg.keybindings.clone()),
1109 keybinding_maps: Some(cfg.keybinding_maps.clone()),
1110 active_keybinding_map: Some(cfg.active_keybinding_map.clone()),
1111 languages: Some(
1112 cfg.languages
1113 .iter()
1114 .map(|(k, v)| (k.clone(), PartialLanguageConfig::from(v)))
1115 .collect(),
1116 ),
1117 default_language: cfg.default_language.clone(),
1118 lsp_enabled: Some(cfg.lsp_enabled),
1119 lsp: Some(
1120 cfg.lsp
1121 .iter()
1122 .map(|(k, v)| {
1123 let lang_config = match v {
1127 LspLanguageConfig::Multi(vec) if vec.len() == 1 => {
1128 LspLanguageConfig::Single(Box::new(vec[0].clone()))
1129 }
1130 other => other.clone(),
1131 };
1132 (k.clone(), lang_config)
1133 })
1134 .collect(),
1135 ),
1136 universal_lsp: Some(
1137 cfg.universal_lsp
1138 .iter()
1139 .map(|(k, v)| {
1140 let lang_config = match v {
1141 LspLanguageConfig::Multi(vec) if vec.len() == 1 => {
1142 LspLanguageConfig::Single(Box::new(vec[0].clone()))
1143 }
1144 other => other.clone(),
1145 };
1146 (k.clone(), lang_config)
1147 })
1148 .collect(),
1149 ),
1150 warnings: Some(PartialWarningsConfig::from(&cfg.warnings)),
1151 plugins: {
1154 let default_plugin = crate::config::PluginConfig::default();
1155 let non_default_plugins: HashMap<String, PartialPluginConfig> = cfg
1156 .plugins
1157 .iter()
1158 .filter(|(_, v)| {
1159 let settings_changed = match &v.settings {
1160 serde_json::Value::Null => false,
1161 serde_json::Value::Object(o) => !o.is_empty(),
1162 _ => true,
1163 };
1164 v.enabled != default_plugin.enabled || settings_changed
1165 })
1166 .map(|(k, v)| {
1167 (
1168 k.clone(),
1169 PartialPluginConfig {
1170 enabled: Some(v.enabled),
1171 path: None, settings: v.settings.clone(),
1173 },
1174 )
1175 })
1176 .collect();
1177 if non_default_plugins.is_empty() {
1178 None
1179 } else {
1180 Some(non_default_plugins)
1181 }
1182 },
1183 packages: Some(PartialPackagesConfig::from(&cfg.packages)),
1184 env: Some(cfg.env.clone()),
1185 }
1186 }
1187}
1188
1189impl PartialConfig {
1190 pub fn resolve(self) -> crate::config::Config {
1192 let defaults = crate::config::Config::default();
1193 self.resolve_with_defaults(&defaults)
1194 }
1195
1196 pub fn resolve_with_defaults(self, defaults: &crate::config::Config) -> crate::config::Config {
1198 let languages = {
1200 let mut result = defaults.languages.clone();
1201 if let Some(partial_langs) = self.languages {
1202 for (key, partial_lang) in partial_langs {
1203 let default_lang = result.get(&key).cloned().unwrap_or_default();
1204 result.insert(key, partial_lang.resolve(&default_lang));
1205 }
1206 }
1207 result
1208 };
1209
1210 let lsp = {
1214 let mut result = defaults.lsp.clone();
1215 if let Some(partial_lsp) = self.lsp {
1216 for (key, lang_config) in partial_lsp {
1217 let user_configs = lang_config.into_vec();
1218 if let Some(default_configs) = result.get(&key) {
1219 let default_slice = default_configs.as_slice();
1220 if user_configs.len() == 1 && default_slice.len() == 1 {
1224 let merged = user_configs
1225 .into_iter()
1226 .next()
1227 .unwrap()
1228 .merge_with_defaults(&default_slice[0]);
1229 result.insert(key, LspLanguageConfig::Multi(vec![merged]));
1230 } else {
1231 result.insert(key, LspLanguageConfig::Multi(user_configs));
1232 }
1233 } else {
1234 result.insert(key, LspLanguageConfig::Multi(user_configs));
1236 }
1237 }
1238 }
1239 result
1240 };
1241
1242 let universal_lsp = {
1244 let mut result = defaults.universal_lsp.clone();
1245 if let Some(partial_universal_lsp) = self.universal_lsp {
1246 for (key, lang_config) in partial_universal_lsp {
1247 let user_configs = lang_config.into_vec();
1248 if let Some(default_configs) = result.get(&key) {
1249 let default_slice = default_configs.as_slice();
1250 if user_configs.len() == 1 && default_slice.len() == 1 {
1251 let merged = user_configs
1252 .into_iter()
1253 .next()
1254 .unwrap()
1255 .merge_with_defaults(&default_slice[0]);
1256 result.insert(key, LspLanguageConfig::Multi(vec![merged]));
1257 } else {
1258 result.insert(key, LspLanguageConfig::Multi(user_configs));
1259 }
1260 } else {
1261 result.insert(key, LspLanguageConfig::Multi(user_configs));
1262 }
1263 }
1264 }
1265 result
1266 };
1267
1268 let keybinding_maps = {
1270 let mut result = defaults.keybinding_maps.clone();
1271 if let Some(partial_maps) = self.keybinding_maps {
1272 for (key, config) in partial_maps {
1273 result.insert(key, config);
1274 }
1275 }
1276 result
1277 };
1278
1279 let plugins = {
1281 let mut result = defaults.plugins.clone();
1282 if let Some(partial_plugins) = self.plugins {
1283 for (key, partial_plugin) in partial_plugins {
1284 let default_plugin = result.get(&key).cloned().unwrap_or_default();
1285 result.insert(key, partial_plugin.resolve(&default_plugin));
1286 }
1287 }
1288 result
1289 };
1290
1291 let mut config = crate::config::Config {
1292 version: self.version.unwrap_or(defaults.version),
1293 theme: self.theme.unwrap_or_else(|| defaults.theme.clone()),
1294 locale: crate::config::LocaleName::from(
1295 self.locale.or_else(|| defaults.locale.0.clone()),
1296 ),
1297 check_for_updates: self.check_for_updates.unwrap_or(defaults.check_for_updates),
1298 editor: self
1299 .editor
1300 .map(|e| e.resolve(&defaults.editor))
1301 .unwrap_or_else(|| defaults.editor.clone()),
1302 file_explorer: self
1303 .file_explorer
1304 .map(|e| e.resolve(&defaults.file_explorer))
1305 .unwrap_or_else(|| defaults.file_explorer.clone()),
1306 file_browser: self
1307 .file_browser
1308 .map(|e| e.resolve(&defaults.file_browser))
1309 .unwrap_or_else(|| defaults.file_browser.clone()),
1310 clipboard: self
1311 .clipboard
1312 .map(|e| e.resolve(&defaults.clipboard))
1313 .unwrap_or_else(|| defaults.clipboard.clone()),
1314 terminal: self
1315 .terminal
1316 .map(|e| e.resolve(&defaults.terminal))
1317 .unwrap_or_else(|| defaults.terminal.clone()),
1318 keybindings: self
1319 .keybindings
1320 .unwrap_or_else(|| defaults.keybindings.clone()),
1321 keybinding_maps,
1322 active_keybinding_map: self
1323 .active_keybinding_map
1324 .unwrap_or_else(|| defaults.active_keybinding_map.clone()),
1325 languages,
1326 default_language: self
1327 .default_language
1328 .or_else(|| defaults.default_language.clone()),
1329 lsp_enabled: self.lsp_enabled.unwrap_or(defaults.lsp_enabled),
1330 lsp,
1331 universal_lsp,
1332 warnings: self
1333 .warnings
1334 .map(|e| e.resolve(&defaults.warnings))
1335 .unwrap_or_else(|| defaults.warnings.clone()),
1336 plugins,
1337 packages: self
1338 .packages
1339 .map(|e| e.resolve(&defaults.packages))
1340 .unwrap_or_else(|| defaults.packages.clone()),
1341 env: self.env.unwrap_or_else(|| defaults.env.clone()),
1342 };
1343 config.normalize_zero_sentinels();
1346 config
1347 }
1348}
1349
1350impl Default for LanguageConfig {
1352 fn default() -> Self {
1353 Self {
1354 extensions: Vec::new(),
1355 filenames: Vec::new(),
1356 grammar: String::new(),
1357 comment_prefix: None,
1358 auto_indent: true,
1359 auto_close: None,
1360 auto_surround: None,
1361 textmate_grammar: None,
1362 show_whitespace_tabs: true,
1363 line_wrap: None,
1364 wrap_column: None,
1365 page_view: None,
1366 page_width: None,
1367 use_tabs: None,
1368 tab_size: None,
1369 formatter: None,
1370 format_on_save: false,
1371 on_save: Vec::new(),
1372 word_characters: None,
1373 indent: None,
1374 }
1375 }
1376}
1377
1378#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1386#[serde(default)]
1387pub struct SessionConfig {
1388 pub theme: Option<ThemeName>,
1390
1391 pub editor: Option<PartialEditorConfig>,
1393
1394 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1397 pub buffer_overrides: HashMap<std::path::PathBuf, PartialEditorConfig>,
1398}
1399
1400impl SessionConfig {
1401 pub fn new() -> Self {
1403 Self::default()
1404 }
1405
1406 pub fn set_theme(&mut self, theme: ThemeName) {
1408 self.theme = Some(theme);
1409 }
1410
1411 pub fn clear_theme(&mut self) {
1413 self.theme = None;
1414 }
1415
1416 pub fn set_editor_option<F>(&mut self, setter: F)
1418 where
1419 F: FnOnce(&mut PartialEditorConfig),
1420 {
1421 let editor = self.editor.get_or_insert_with(Default::default);
1422 setter(editor);
1423 }
1424
1425 pub fn set_buffer_override(&mut self, path: std::path::PathBuf, config: PartialEditorConfig) {
1427 self.buffer_overrides.insert(path, config);
1428 }
1429
1430 pub fn clear_buffer_override(&mut self, path: &std::path::Path) {
1432 self.buffer_overrides.remove(path);
1433 }
1434
1435 pub fn get_buffer_override(&self, path: &std::path::Path) -> Option<&PartialEditorConfig> {
1437 self.buffer_overrides.get(path)
1438 }
1439
1440 pub fn to_partial_config(&self) -> PartialConfig {
1442 PartialConfig {
1443 theme: self.theme.clone(),
1444 editor: self.editor.clone(),
1445 ..Default::default()
1446 }
1447 }
1448
1449 pub fn is_empty(&self) -> bool {
1451 self.theme.is_none() && self.editor.is_none() && self.buffer_overrides.is_empty()
1452 }
1453}
1454
1455impl From<PartialConfig> for SessionConfig {
1456 fn from(partial: PartialConfig) -> Self {
1457 Self {
1458 theme: partial.theme,
1459 editor: partial.editor,
1460 buffer_overrides: HashMap::new(),
1461 }
1462 }
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use super::*;
1468
1469 #[test]
1470 fn merge_option_higher_precedence_wins() {
1471 let mut higher: Option<i32> = Some(10);
1472 let lower: Option<i32> = Some(5);
1473 higher.merge_from(&lower);
1474 assert_eq!(higher, Some(10));
1475 }
1476
1477 #[test]
1478 fn merge_option_fills_from_lower_when_none() {
1479 let mut higher: Option<i32> = None;
1480 let lower: Option<i32> = Some(5);
1481 higher.merge_from(&lower);
1482 assert_eq!(higher, Some(5));
1483 }
1484
1485 #[test]
1486 fn merge_editor_config_recursive() {
1487 let mut higher = PartialEditorConfig {
1488 tab_size: Some(2),
1489 ..Default::default()
1490 };
1491 let lower = PartialEditorConfig {
1492 tab_size: Some(4),
1493 line_numbers: Some(true),
1494 ..Default::default()
1495 };
1496
1497 higher.merge_from(&lower);
1498
1499 assert_eq!(higher.tab_size, Some(2)); assert_eq!(higher.line_numbers, Some(true)); }
1502
1503 #[test]
1504 fn merge_partial_config_combines_languages() {
1505 let mut higher = PartialConfig {
1506 languages: Some(HashMap::from([(
1507 "rust".to_string(),
1508 PartialLanguageConfig {
1509 tab_size: Some(4),
1510 ..Default::default()
1511 },
1512 )])),
1513 ..Default::default()
1514 };
1515 let lower = PartialConfig {
1516 languages: Some(HashMap::from([(
1517 "python".to_string(),
1518 PartialLanguageConfig {
1519 tab_size: Some(4),
1520 ..Default::default()
1521 },
1522 )])),
1523 ..Default::default()
1524 };
1525
1526 higher.merge_from(&lower);
1527
1528 let langs = higher.languages.unwrap();
1529 assert!(langs.contains_key("rust"));
1530 assert!(langs.contains_key("python"));
1531 }
1532
1533 #[test]
1534 fn merge_languages_same_key_higher_wins() {
1535 let mut higher = PartialConfig {
1536 languages: Some(HashMap::from([(
1537 "rust".to_string(),
1538 PartialLanguageConfig {
1539 tab_size: Some(2),
1540 use_tabs: Some(true),
1541 ..Default::default()
1542 },
1543 )])),
1544 ..Default::default()
1545 };
1546 let lower = PartialConfig {
1547 languages: Some(HashMap::from([(
1548 "rust".to_string(),
1549 PartialLanguageConfig {
1550 tab_size: Some(4),
1551 auto_indent: Some(false),
1552 ..Default::default()
1553 },
1554 )])),
1555 ..Default::default()
1556 };
1557
1558 higher.merge_from(&lower);
1559
1560 let langs = higher.languages.unwrap();
1561 let rust = langs.get("rust").unwrap();
1562 assert_eq!(rust.tab_size, Some(2)); assert_eq!(rust.use_tabs, Some(true)); assert_eq!(rust.auto_indent, Some(false)); }
1566
1567 #[test]
1568 fn resolve_fills_defaults() {
1569 let partial = PartialConfig {
1570 theme: Some(ThemeName::from("dark")),
1571 ..Default::default()
1572 };
1573
1574 let resolved = partial.resolve();
1575
1576 assert_eq!(resolved.theme.0, "dark");
1577 assert_eq!(resolved.editor.tab_size, 4); assert!(resolved.editor.line_numbers); }
1580
1581 #[test]
1582 fn resolve_preserves_set_values() {
1583 let partial = PartialConfig {
1584 editor: Some(PartialEditorConfig {
1585 tab_size: Some(2),
1586 line_numbers: Some(false),
1587 ..Default::default()
1588 }),
1589 ..Default::default()
1590 };
1591
1592 let resolved = partial.resolve();
1593
1594 assert_eq!(resolved.editor.tab_size, 2);
1595 assert!(!resolved.editor.line_numbers);
1596 }
1597
1598 #[test]
1599 fn resolve_normalizes_indentation_guide_glyph() {
1600 let partial = PartialConfig {
1601 editor: Some(PartialEditorConfig {
1602 indentation_guide_glyph: Some(" ┊ ".to_string()),
1603 ..Default::default()
1604 }),
1605 ..Default::default()
1606 };
1607
1608 let resolved = partial.resolve();
1609 assert_eq!(resolved.editor.indentation_guide_glyph, "┊");
1610
1611 let partial = PartialConfig {
1612 editor: Some(PartialEditorConfig {
1613 indentation_guide_glyph: Some(" ".to_string()),
1614 ..Default::default()
1615 }),
1616 ..Default::default()
1617 };
1618
1619 let resolved = partial.resolve();
1620 assert_eq!(resolved.editor.indentation_guide_glyph, "▏");
1621 }
1622
1623 #[test]
1624 fn roundtrip_config_to_partial_and_back() {
1625 let original = crate::config::Config::default();
1626 let partial = PartialConfig::from(&original);
1627 let resolved = partial.resolve();
1628
1629 assert_eq!(original.theme, resolved.theme);
1630 assert_eq!(original.editor.tab_size, resolved.editor.tab_size);
1631 assert_eq!(original.check_for_updates, resolved.check_for_updates);
1632 }
1633
1634 #[test]
1635 fn session_config_new_is_empty() {
1636 let session = SessionConfig::new();
1637 assert!(session.is_empty());
1638 }
1639
1640 #[test]
1641 fn session_config_set_theme() {
1642 let mut session = SessionConfig::new();
1643 session.set_theme(ThemeName::from("dark"));
1644 assert_eq!(session.theme, Some(ThemeName::from("dark")));
1645 assert!(!session.is_empty());
1646 }
1647
1648 #[test]
1649 fn session_config_clear_theme() {
1650 let mut session = SessionConfig::new();
1651 session.set_theme(ThemeName::from("dark"));
1652 session.clear_theme();
1653 assert!(session.theme.is_none());
1654 }
1655
1656 #[test]
1657 fn session_config_set_editor_option() {
1658 let mut session = SessionConfig::new();
1659 session.set_editor_option(|e| e.tab_size = Some(2));
1660 assert_eq!(session.editor.as_ref().unwrap().tab_size, Some(2));
1661 }
1662
1663 #[test]
1664 fn session_config_buffer_overrides() {
1665 let mut session = SessionConfig::new();
1666 let path = std::path::PathBuf::from("/test/file.rs");
1667 let config = PartialEditorConfig {
1668 tab_size: Some(8),
1669 ..Default::default()
1670 };
1671
1672 session.set_buffer_override(path.clone(), config);
1673 assert!(session.get_buffer_override(&path).is_some());
1674 assert_eq!(
1675 session.get_buffer_override(&path).unwrap().tab_size,
1676 Some(8)
1677 );
1678
1679 session.clear_buffer_override(&path);
1680 assert!(session.get_buffer_override(&path).is_none());
1681 }
1682
1683 #[test]
1684 fn session_config_to_partial_config() {
1685 let mut session = SessionConfig::new();
1686 session.set_theme(ThemeName::from("dark"));
1687 session.set_editor_option(|e| e.tab_size = Some(2));
1688
1689 let partial = session.to_partial_config();
1690 assert_eq!(partial.theme, Some(ThemeName::from("dark")));
1691 assert_eq!(partial.editor.as_ref().unwrap().tab_size, Some(2));
1692 }
1693
1694 #[test]
1697 fn plugins_with_default_enabled_not_serialized() {
1698 let mut config = crate::config::Config::default();
1700 config.plugins.insert(
1701 "test_plugin".to_string(),
1702 PluginConfig {
1703 enabled: true, path: Some(std::path::PathBuf::from("/path/to/plugin.ts")),
1705 settings: serde_json::Value::Null,
1706 },
1707 );
1708
1709 let partial = PartialConfig::from(&config);
1710
1711 assert!(
1713 partial.plugins.is_none(),
1714 "Plugins with default enabled=true should not be serialized"
1715 );
1716 }
1717
1718 #[test]
1719 fn plugins_with_disabled_are_serialized() {
1720 let mut config = crate::config::Config::default();
1722 config.plugins.insert(
1723 "enabled_plugin".to_string(),
1724 PluginConfig {
1725 enabled: true,
1726 path: Some(std::path::PathBuf::from("/path/to/enabled.ts")),
1727 settings: serde_json::Value::Null,
1728 },
1729 );
1730 config.plugins.insert(
1731 "disabled_plugin".to_string(),
1732 PluginConfig {
1733 enabled: false, path: Some(std::path::PathBuf::from("/path/to/disabled.ts")),
1735 settings: serde_json::Value::Null,
1736 },
1737 );
1738
1739 let partial = PartialConfig::from(&config);
1740
1741 assert!(partial.plugins.is_some());
1743 let plugins = partial.plugins.unwrap();
1744 assert_eq!(
1745 plugins.len(),
1746 1,
1747 "Only disabled plugins should be serialized"
1748 );
1749 assert!(plugins.contains_key("disabled_plugin"));
1750 assert!(!plugins.contains_key("enabled_plugin"));
1751
1752 let disabled = plugins.get("disabled_plugin").unwrap();
1754 assert_eq!(disabled.enabled, Some(false));
1755 assert!(disabled.path.is_none(), "Path should not be serialized");
1757 }
1758
1759 #[test]
1760 fn plugin_path_never_serialized() {
1761 let mut config = crate::config::Config::default();
1763 config.plugins.insert(
1764 "my_plugin".to_string(),
1765 PluginConfig {
1766 enabled: false,
1767 path: Some(std::path::PathBuf::from("/some/path/plugin.ts")),
1768 settings: serde_json::Value::Null,
1769 },
1770 );
1771
1772 let partial = PartialConfig::from(&config);
1773 let plugins = partial.plugins.unwrap();
1774 let plugin = plugins.get("my_plugin").unwrap();
1775
1776 assert!(
1777 plugin.path.is_none(),
1778 "Path is runtime-discovered and should never be serialized"
1779 );
1780 }
1781
1782 #[test]
1783 fn resolving_partial_with_disabled_plugin_preserves_state() {
1784 let partial = PartialConfig {
1786 plugins: Some(HashMap::from([(
1787 "my_plugin".to_string(),
1788 PartialPluginConfig {
1789 enabled: Some(false),
1790 path: None,
1791 settings: serde_json::Value::Null,
1792 },
1793 )])),
1794 ..Default::default()
1795 };
1796
1797 let resolved = partial.resolve();
1798
1799 let plugin = resolved.plugins.get("my_plugin");
1801 assert!(
1802 plugin.is_some(),
1803 "Disabled plugin should be in resolved config"
1804 );
1805 assert!(
1806 !plugin.unwrap().enabled,
1807 "Plugin should remain disabled after resolve"
1808 );
1809 }
1810
1811 #[test]
1812 fn merge_plugins_preserves_higher_precedence_disabled_state() {
1813 let mut higher = PartialConfig {
1815 plugins: Some(HashMap::from([(
1816 "my_plugin".to_string(),
1817 PartialPluginConfig {
1818 enabled: Some(false), path: None,
1820 settings: serde_json::Value::Null,
1821 },
1822 )])),
1823 ..Default::default()
1824 };
1825
1826 let lower = PartialConfig {
1827 plugins: Some(HashMap::from([(
1828 "my_plugin".to_string(),
1829 PartialPluginConfig {
1830 enabled: Some(true), path: None,
1832 settings: serde_json::Value::Null,
1833 },
1834 )])),
1835 ..Default::default()
1836 };
1837
1838 higher.merge_from(&lower);
1839
1840 let plugins = higher.plugins.unwrap();
1841 let plugin = plugins.get("my_plugin").unwrap();
1842 assert_eq!(
1843 plugin.enabled,
1844 Some(false),
1845 "Higher precedence disabled state should win"
1846 );
1847 }
1848
1849 #[test]
1850 fn roundtrip_disabled_plugin_only_saves_delta() {
1851 let mut config = crate::config::Config::default();
1854 config.plugins.insert(
1855 "plugin_a".to_string(),
1856 PluginConfig {
1857 enabled: true,
1858 path: Some(std::path::PathBuf::from("/a.ts")),
1859 settings: serde_json::Value::Null,
1860 },
1861 );
1862 config.plugins.insert(
1863 "plugin_b".to_string(),
1864 PluginConfig {
1865 enabled: false,
1866 path: Some(std::path::PathBuf::from("/b.ts")),
1867 settings: serde_json::Value::Null,
1868 },
1869 );
1870 config.plugins.insert(
1871 "plugin_c".to_string(),
1872 PluginConfig {
1873 enabled: true,
1874 path: Some(std::path::PathBuf::from("/c.ts")),
1875 settings: serde_json::Value::Null,
1876 },
1877 );
1878
1879 let partial = PartialConfig::from(&config);
1881
1882 let json = serde_json::to_string(&partial).unwrap();
1884
1885 assert!(
1887 json.contains("plugin_b"),
1888 "Disabled plugin should be in serialized JSON"
1889 );
1890 assert!(
1891 !json.contains("plugin_a"),
1892 "Enabled plugin_a should not be in serialized JSON"
1893 );
1894 assert!(
1895 !json.contains("plugin_c"),
1896 "Enabled plugin_c should not be in serialized JSON"
1897 );
1898
1899 let deserialized: PartialConfig = serde_json::from_str(&json).unwrap();
1901
1902 let plugins = deserialized.plugins.unwrap();
1904 assert_eq!(plugins.len(), 1);
1905 assert!(plugins.contains_key("plugin_b"));
1906 assert_eq!(plugins.get("plugin_b").unwrap().enabled, Some(false));
1907 }
1908}