1//! ## When to create a migration and why?
2//! A migration is necessary when keymap actions or settings are renamed or transformed (e.g., from an array to a string, a string to an array, a boolean to an enum, etc.).
3//!
4//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally.
5//! It also provides a quick way to migrate their existing settings to the latest state using button in UI.
6//!
7//! ## How to create a migration?
8//! Migrations use Tree-sitter to query commonly used patterns, such as actions with a string or actions with an array where the second argument is an object, etc.
9//! Once queried, *you can filter out the modified items* and write the replacement logic.
10//!
11//! You *must not* modify previous migrations; always create new ones instead.
12//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state.
13//! Modifying existing migrations means they will only work for users upgrading from version x-1 to x, but not from x-2 to x, and so on, where x is the latest version.
14//!
15//! You only need to write replacement logic for x-1 to x because you can be certain that, internally, every user will be at x-1, regardless of their on disk state.
16
17use anyhow::{Context as _, Result};
18use std::{cmp::Reverse, ops::Range, sync::LazyLock};
19use streaming_iterator::StreamingIterator;
20use tree_sitter::{Query, QueryMatch};
21
22use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
23
24mod migrations;
25mod patterns;
26
27fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
28 let mut parser = tree_sitter::Parser::new();
29 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
30 let syntax_tree = parser
31 .parse(text, None)
32 .context("failed to parse settings")?;
33
34 let mut cursor = tree_sitter::QueryCursor::new();
35 let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
36
37 let mut edits = vec![];
38 while let Some(mat) = matches.next() {
39 if let Some((_, callback)) = patterns.get(mat.pattern_index) {
40 edits.extend(callback(text, mat, query));
41 }
42 }
43
44 edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
45 edits.dedup_by(|(range_b, _), (range_a, _)| {
46 range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
47 });
48
49 if edits.is_empty() {
50 Ok(None)
51 } else {
52 let mut new_text = text.to_string();
53 for (range, replacement) in edits.iter().rev() {
54 new_text.replace_range(range.clone(), replacement);
55 }
56 if new_text == text {
57 log::error!(
58 "Edits computed for configuration migration do not cause a change: {:?}",
59 edits
60 );
61 Ok(None)
62 } else {
63 Ok(Some(new_text))
64 }
65 }
66}
67
68/// Runs the provided migrations on the given text.
69/// Will automatically return `Ok(None)` if there's no content to migrate.
70fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<String>> {
71 if text.is_empty() {
72 return Ok(None);
73 }
74
75 let mut current_text = text.to_string();
76 let mut result: Option<String> = None;
77 for migration in migrations.iter() {
78 let migrated_text = match migration {
79 MigrationType::TreeSitter(patterns, query) => migrate(¤t_text, patterns, query)?,
80 MigrationType::Json(callback) => {
81 if current_text.trim().is_empty() {
82 return Ok(None);
83 }
84 let old_content: serde_json_lenient::Value =
85 settings::parse_json_with_comments(¤t_text)?;
86 let old_value = serde_json::to_value(&old_content).unwrap();
87 let mut new_value = old_value.clone();
88 callback(&mut new_value)?;
89 if new_value != old_value {
90 let mut current = current_text.clone();
91 let mut edits = vec![];
92 settings::update_value_in_json_text(
93 &mut current,
94 &mut vec![],
95 2,
96 &old_value,
97 &new_value,
98 &mut edits,
99 );
100 let mut migrated_text = current_text.clone();
101 for (range, replacement) in edits.into_iter() {
102 migrated_text.replace_range(range, &replacement);
103 }
104 Some(migrated_text)
105 } else {
106 None
107 }
108 }
109 };
110 if let Some(migrated_text) = migrated_text {
111 current_text = migrated_text.clone();
112 result = Some(migrated_text);
113 }
114 }
115 Ok(result.filter(|new_text| text != new_text))
116}
117
118pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
119 let migrations: &[MigrationType] = &[
120 MigrationType::TreeSitter(
121 migrations::m_2025_01_29::KEYMAP_PATTERNS,
122 &KEYMAP_QUERY_2025_01_29,
123 ),
124 MigrationType::TreeSitter(
125 migrations::m_2025_01_30::KEYMAP_PATTERNS,
126 &KEYMAP_QUERY_2025_01_30,
127 ),
128 MigrationType::TreeSitter(
129 migrations::m_2025_03_03::KEYMAP_PATTERNS,
130 &KEYMAP_QUERY_2025_03_03,
131 ),
132 MigrationType::TreeSitter(
133 migrations::m_2025_03_06::KEYMAP_PATTERNS,
134 &KEYMAP_QUERY_2025_03_06,
135 ),
136 MigrationType::TreeSitter(
137 migrations::m_2025_04_15::KEYMAP_PATTERNS,
138 &KEYMAP_QUERY_2025_04_15,
139 ),
140 ];
141 run_migrations(text, migrations)
142}
143
144enum MigrationType<'a> {
145 TreeSitter(MigrationPatterns, &'a Query),
146 Json(fn(&mut serde_json::Value) -> Result<()>),
147}
148
149pub fn migrate_settings(text: &str) -> Result<Option<String>> {
150 let migrations: &[MigrationType] = &[
151 MigrationType::TreeSitter(
152 migrations::m_2025_01_02::SETTINGS_PATTERNS,
153 &SETTINGS_QUERY_2025_01_02,
154 ),
155 MigrationType::TreeSitter(
156 migrations::m_2025_01_29::SETTINGS_PATTERNS,
157 &SETTINGS_QUERY_2025_01_29,
158 ),
159 MigrationType::TreeSitter(
160 migrations::m_2025_01_30::SETTINGS_PATTERNS,
161 &SETTINGS_QUERY_2025_01_30,
162 ),
163 MigrationType::TreeSitter(
164 migrations::m_2025_03_29::SETTINGS_PATTERNS,
165 &SETTINGS_QUERY_2025_03_29,
166 ),
167 MigrationType::TreeSitter(
168 migrations::m_2025_04_15::SETTINGS_PATTERNS,
169 &SETTINGS_QUERY_2025_04_15,
170 ),
171 MigrationType::TreeSitter(
172 migrations::m_2025_04_21::SETTINGS_PATTERNS,
173 &SETTINGS_QUERY_2025_04_21,
174 ),
175 MigrationType::TreeSitter(
176 migrations::m_2025_04_23::SETTINGS_PATTERNS,
177 &SETTINGS_QUERY_2025_04_23,
178 ),
179 MigrationType::TreeSitter(
180 migrations::m_2025_05_05::SETTINGS_PATTERNS,
181 &SETTINGS_QUERY_2025_05_05,
182 ),
183 MigrationType::TreeSitter(
184 migrations::m_2025_05_08::SETTINGS_PATTERNS,
185 &SETTINGS_QUERY_2025_05_08,
186 ),
187 MigrationType::TreeSitter(
188 migrations::m_2025_05_29::SETTINGS_PATTERNS,
189 &SETTINGS_QUERY_2025_05_29,
190 ),
191 MigrationType::TreeSitter(
192 migrations::m_2025_06_16::SETTINGS_PATTERNS,
193 &SETTINGS_QUERY_2025_06_16,
194 ),
195 MigrationType::TreeSitter(
196 migrations::m_2025_06_25::SETTINGS_PATTERNS,
197 &SETTINGS_QUERY_2025_06_25,
198 ),
199 MigrationType::TreeSitter(
200 migrations::m_2025_06_27::SETTINGS_PATTERNS,
201 &SETTINGS_QUERY_2025_06_27,
202 ),
203 MigrationType::TreeSitter(
204 migrations::m_2025_07_08::SETTINGS_PATTERNS,
205 &SETTINGS_QUERY_2025_07_08,
206 ),
207 MigrationType::TreeSitter(
208 migrations::m_2025_10_01::SETTINGS_PATTERNS,
209 &SETTINGS_QUERY_2025_10_01,
210 ),
211 MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save),
212 MigrationType::TreeSitter(
213 migrations::m_2025_10_03::SETTINGS_PATTERNS,
214 &SETTINGS_QUERY_2025_10_03,
215 ),
216 MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
217 ];
218 run_migrations(text, migrations)
219}
220
221pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
222 migrate(
223 text,
224 &[(
225 SETTINGS_NESTED_KEY_VALUE_PATTERN,
226 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
227 )],
228 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
229 )
230}
231
232pub type MigrationPatterns = &'static [(
233 &'static str,
234 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
235)];
236
237macro_rules! define_query {
238 ($var_name:ident, $patterns_path:path) => {
239 static $var_name: LazyLock<Query> = LazyLock::new(|| {
240 Query::new(
241 &tree_sitter_json::LANGUAGE.into(),
242 &$patterns_path
243 .iter()
244 .map(|pattern| pattern.0)
245 .collect::<String>(),
246 )
247 .unwrap()
248 });
249 };
250}
251
252// keymap
253define_query!(
254 KEYMAP_QUERY_2025_01_29,
255 migrations::m_2025_01_29::KEYMAP_PATTERNS
256);
257define_query!(
258 KEYMAP_QUERY_2025_01_30,
259 migrations::m_2025_01_30::KEYMAP_PATTERNS
260);
261define_query!(
262 KEYMAP_QUERY_2025_03_03,
263 migrations::m_2025_03_03::KEYMAP_PATTERNS
264);
265define_query!(
266 KEYMAP_QUERY_2025_03_06,
267 migrations::m_2025_03_06::KEYMAP_PATTERNS
268);
269define_query!(
270 KEYMAP_QUERY_2025_04_15,
271 migrations::m_2025_04_15::KEYMAP_PATTERNS
272);
273
274// settings
275define_query!(
276 SETTINGS_QUERY_2025_01_02,
277 migrations::m_2025_01_02::SETTINGS_PATTERNS
278);
279define_query!(
280 SETTINGS_QUERY_2025_01_29,
281 migrations::m_2025_01_29::SETTINGS_PATTERNS
282);
283define_query!(
284 SETTINGS_QUERY_2025_01_30,
285 migrations::m_2025_01_30::SETTINGS_PATTERNS
286);
287define_query!(
288 SETTINGS_QUERY_2025_03_29,
289 migrations::m_2025_03_29::SETTINGS_PATTERNS
290);
291define_query!(
292 SETTINGS_QUERY_2025_04_15,
293 migrations::m_2025_04_15::SETTINGS_PATTERNS
294);
295define_query!(
296 SETTINGS_QUERY_2025_04_21,
297 migrations::m_2025_04_21::SETTINGS_PATTERNS
298);
299define_query!(
300 SETTINGS_QUERY_2025_04_23,
301 migrations::m_2025_04_23::SETTINGS_PATTERNS
302);
303define_query!(
304 SETTINGS_QUERY_2025_05_05,
305 migrations::m_2025_05_05::SETTINGS_PATTERNS
306);
307define_query!(
308 SETTINGS_QUERY_2025_05_08,
309 migrations::m_2025_05_08::SETTINGS_PATTERNS
310);
311define_query!(
312 SETTINGS_QUERY_2025_05_29,
313 migrations::m_2025_05_29::SETTINGS_PATTERNS
314);
315define_query!(
316 SETTINGS_QUERY_2025_06_16,
317 migrations::m_2025_06_16::SETTINGS_PATTERNS
318);
319define_query!(
320 SETTINGS_QUERY_2025_06_25,
321 migrations::m_2025_06_25::SETTINGS_PATTERNS
322);
323define_query!(
324 SETTINGS_QUERY_2025_06_27,
325 migrations::m_2025_06_27::SETTINGS_PATTERNS
326);
327define_query!(
328 SETTINGS_QUERY_2025_07_08,
329 migrations::m_2025_07_08::SETTINGS_PATTERNS
330);
331define_query!(
332 SETTINGS_QUERY_2025_10_01,
333 migrations::m_2025_10_01::SETTINGS_PATTERNS
334);
335define_query!(
336 SETTINGS_QUERY_2025_10_03,
337 migrations::m_2025_10_03::SETTINGS_PATTERNS
338);
339
340// custom query
341static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
342 Query::new(
343 &tree_sitter_json::LANGUAGE.into(),
344 SETTINGS_NESTED_KEY_VALUE_PATTERN,
345 )
346 .unwrap()
347});
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use unindent::Unindent as _;
353
354 fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
355 match (&migrated, &expected) {
356 (Some(migrated), Some(expected)) => {
357 pretty_assertions::assert_str_eq!(migrated, expected);
358 }
359 _ => {
360 pretty_assertions::assert_eq!(migrated.as_deref(), expected);
361 }
362 }
363 }
364
365 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
366 let migrated = migrate_keymap(input).unwrap();
367 pretty_assertions::assert_eq!(migrated.as_deref(), output);
368 }
369
370 #[track_caller]
371 fn assert_migrate_settings(input: &str, output: Option<&str>) {
372 let migrated = migrate_settings(input).unwrap();
373 assert_migrated_correctly(migrated, output);
374 }
375
376 fn assert_migrate_settings_with_migrations(
377 migrations: &[MigrationType],
378 input: &str,
379 output: Option<&str>,
380 ) {
381 let migrated = run_migrations(input, migrations).unwrap();
382 assert_migrated_correctly(migrated, output);
383 }
384
385 #[test]
386 fn test_empty_content() {
387 assert_migrate_settings("", None)
388 }
389
390 #[test]
391 fn test_replace_array_with_single_string() {
392 assert_migrate_keymap(
393 r#"
394 [
395 {
396 "bindings": {
397 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
398 }
399 }
400 ]
401 "#,
402 Some(
403 r#"
404 [
405 {
406 "bindings": {
407 "cmd-1": "workspace::ActivatePaneUp"
408 }
409 }
410 ]
411 "#,
412 ),
413 )
414 }
415
416 #[test]
417 fn test_replace_action_argument_object_with_single_value() {
418 assert_migrate_keymap(
419 r#"
420 [
421 {
422 "bindings": {
423 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
424 }
425 }
426 ]
427 "#,
428 Some(
429 r#"
430 [
431 {
432 "bindings": {
433 "cmd-1": ["editor::FoldAtLevel", 1]
434 }
435 }
436 ]
437 "#,
438 ),
439 )
440 }
441
442 #[test]
443 fn test_replace_action_argument_object_with_single_value_2() {
444 assert_migrate_keymap(
445 r#"
446 [
447 {
448 "bindings": {
449 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
450 }
451 }
452 ]
453 "#,
454 Some(
455 r#"
456 [
457 {
458 "bindings": {
459 "cmd-1": ["vim::PushObject", { "some" : "value" }]
460 }
461 }
462 ]
463 "#,
464 ),
465 )
466 }
467
468 #[test]
469 fn test_rename_string_action() {
470 assert_migrate_keymap(
471 r#"
472 [
473 {
474 "bindings": {
475 "cmd-1": "inline_completion::ToggleMenu"
476 }
477 }
478 ]
479 "#,
480 Some(
481 r#"
482 [
483 {
484 "bindings": {
485 "cmd-1": "edit_prediction::ToggleMenu"
486 }
487 }
488 ]
489 "#,
490 ),
491 )
492 }
493
494 #[test]
495 fn test_rename_context_key() {
496 assert_migrate_keymap(
497 r#"
498 [
499 {
500 "context": "Editor && inline_completion && !showing_completions"
501 }
502 ]
503 "#,
504 Some(
505 r#"
506 [
507 {
508 "context": "Editor && edit_prediction && !showing_completions"
509 }
510 ]
511 "#,
512 ),
513 )
514 }
515
516 #[test]
517 fn test_incremental_migrations() {
518 // Here string transforms to array internally. Then, that array transforms back to string.
519 assert_migrate_keymap(
520 r#"
521 [
522 {
523 "bindings": {
524 "ctrl-q": "editor::GoToHunk", // should remain same
525 "ctrl-w": "editor::GoToPrevHunk", // should rename
526 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
527 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
528 }
529 }
530 ]
531 "#,
532 Some(
533 r#"
534 [
535 {
536 "bindings": {
537 "ctrl-q": "editor::GoToHunk", // should remain same
538 "ctrl-w": "editor::GoToPreviousHunk", // should rename
539 "ctrl-q": "editor::GoToHunk", // should transform
540 "ctrl-w": "editor::GoToPreviousHunk" // should transform
541 }
542 }
543 ]
544 "#,
545 ),
546 )
547 }
548
549 #[test]
550 fn test_action_argument_snake_case() {
551 // First performs transformations, then replacements
552 assert_migrate_keymap(
553 r#"
554 [
555 {
556 "bindings": {
557 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
558 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
559 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
560 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
561 }
562 }
563 ]
564 "#,
565 Some(
566 r#"
567 [
568 {
569 "bindings": {
570 "cmd-1": ["vim::PushObject", { "around": false }],
571 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
572 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
573 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
574 }
575 }
576 ]
577 "#,
578 ),
579 )
580 }
581
582 #[test]
583 fn test_replace_setting_name() {
584 assert_migrate_settings(
585 r#"
586 {
587 "show_inline_completions_in_menu": true,
588 "show_inline_completions": true,
589 "inline_completions_disabled_in": ["string"],
590 "inline_completions": { "some" : "value" }
591 }
592 "#,
593 Some(
594 r#"
595 {
596 "show_edit_predictions_in_menu": true,
597 "show_edit_predictions": true,
598 "edit_predictions_disabled_in": ["string"],
599 "edit_predictions": { "some" : "value" }
600 }
601 "#,
602 ),
603 )
604 }
605
606 #[test]
607 fn test_nested_string_replace_for_settings() {
608 assert_migrate_settings(
609 r#"
610 {
611 "features": {
612 "inline_completion_provider": "zed"
613 },
614 }
615 "#,
616 Some(
617 r#"
618 {
619 "features": {
620 "edit_prediction_provider": "zed"
621 },
622 }
623 "#,
624 ),
625 )
626 }
627
628 #[test]
629 fn test_replace_settings_in_languages() {
630 assert_migrate_settings(
631 r#"
632 {
633 "languages": {
634 "Astro": {
635 "show_inline_completions": true
636 }
637 }
638 }
639 "#,
640 Some(
641 r#"
642 {
643 "languages": {
644 "Astro": {
645 "show_edit_predictions": true
646 }
647 }
648 }
649 "#,
650 ),
651 )
652 }
653
654 #[test]
655 fn test_replace_settings_value() {
656 assert_migrate_settings(
657 r#"
658 {
659 "scrollbar": {
660 "diagnostics": true
661 },
662 "chat_panel": {
663 "button": true
664 }
665 }
666 "#,
667 Some(
668 r#"
669 {
670 "scrollbar": {
671 "diagnostics": "all"
672 },
673 "chat_panel": {
674 "button": "always"
675 }
676 }
677 "#,
678 ),
679 )
680 }
681
682 #[test]
683 fn test_replace_settings_name_and_value() {
684 assert_migrate_settings(
685 r#"
686 {
687 "tabs": {
688 "always_show_close_button": true
689 }
690 }
691 "#,
692 Some(
693 r#"
694 {
695 "tabs": {
696 "show_close_button": "always"
697 }
698 }
699 "#,
700 ),
701 )
702 }
703
704 #[test]
705 fn test_replace_bash_with_terminal_in_profiles() {
706 assert_migrate_settings(
707 r#"
708 {
709 "assistant": {
710 "profiles": {
711 "custom": {
712 "name": "Custom",
713 "tools": {
714 "bash": true,
715 "diagnostics": true
716 }
717 }
718 }
719 }
720 }
721 "#,
722 Some(
723 r#"
724 {
725 "agent": {
726 "profiles": {
727 "custom": {
728 "name": "Custom",
729 "tools": {
730 "terminal": true,
731 "diagnostics": true
732 }
733 }
734 }
735 }
736 }
737 "#,
738 ),
739 )
740 }
741
742 #[test]
743 fn test_replace_bash_false_with_terminal_in_profiles() {
744 assert_migrate_settings(
745 r#"
746 {
747 "assistant": {
748 "profiles": {
749 "custom": {
750 "name": "Custom",
751 "tools": {
752 "bash": false,
753 "diagnostics": true
754 }
755 }
756 }
757 }
758 }
759 "#,
760 Some(
761 r#"
762 {
763 "agent": {
764 "profiles": {
765 "custom": {
766 "name": "Custom",
767 "tools": {
768 "terminal": false,
769 "diagnostics": true
770 }
771 }
772 }
773 }
774 }
775 "#,
776 ),
777 )
778 }
779
780 #[test]
781 fn test_no_bash_in_profiles() {
782 assert_migrate_settings(
783 r#"
784 {
785 "assistant": {
786 "profiles": {
787 "custom": {
788 "name": "Custom",
789 "tools": {
790 "diagnostics": true,
791 "find_path": true,
792 "read_file": true
793 }
794 }
795 }
796 }
797 }
798 "#,
799 Some(
800 r#"
801 {
802 "agent": {
803 "profiles": {
804 "custom": {
805 "name": "Custom",
806 "tools": {
807 "diagnostics": true,
808 "find_path": true,
809 "read_file": true
810 }
811 }
812 }
813 }
814 }
815 "#,
816 ),
817 )
818 }
819
820 #[test]
821 fn test_rename_path_search_to_find_path() {
822 assert_migrate_settings(
823 r#"
824 {
825 "assistant": {
826 "profiles": {
827 "default": {
828 "tools": {
829 "path_search": true,
830 "read_file": true
831 }
832 }
833 }
834 }
835 }
836 "#,
837 Some(
838 r#"
839 {
840 "agent": {
841 "profiles": {
842 "default": {
843 "tools": {
844 "find_path": true,
845 "read_file": true
846 }
847 }
848 }
849 }
850 }
851 "#,
852 ),
853 );
854 }
855
856 #[test]
857 fn test_rename_assistant() {
858 assert_migrate_settings(
859 r#"{
860 "assistant": {
861 "foo": "bar"
862 },
863 "edit_predictions": {
864 "enabled_in_assistant": false,
865 }
866 }"#,
867 Some(
868 r#"{
869 "agent": {
870 "foo": "bar"
871 },
872 "edit_predictions": {
873 "enabled_in_text_threads": false,
874 }
875 }"#,
876 ),
877 );
878 }
879
880 #[test]
881 fn test_comment_duplicated_agent() {
882 assert_migrate_settings(
883 r#"{
884 "agent": {
885 "name": "assistant-1",
886 "model": "gpt-4", // weird formatting
887 "utf8": "привіт"
888 },
889 "something": "else",
890 "agent": {
891 "name": "assistant-2",
892 "model": "gemini-pro"
893 }
894 }
895 "#,
896 Some(
897 r#"{
898 /* Duplicated key auto-commented: "agent": {
899 "name": "assistant-1",
900 "model": "gpt-4", // weird formatting
901 "utf8": "привіт"
902 }, */
903 "something": "else",
904 "agent": {
905 "name": "assistant-2",
906 "model": "gemini-pro"
907 }
908 }
909 "#,
910 ),
911 );
912 }
913
914 #[test]
915 fn test_preferred_completion_mode_migration() {
916 assert_migrate_settings(
917 r#"{
918 "agent": {
919 "preferred_completion_mode": "max",
920 "enabled": true
921 }
922 }"#,
923 Some(
924 r#"{
925 "agent": {
926 "preferred_completion_mode": "burn",
927 "enabled": true
928 }
929 }"#,
930 ),
931 );
932
933 assert_migrate_settings(
934 r#"{
935 "agent": {
936 "preferred_completion_mode": "normal",
937 "enabled": true
938 }
939 }"#,
940 None,
941 );
942
943 assert_migrate_settings(
944 r#"{
945 "agent": {
946 "preferred_completion_mode": "burn",
947 "enabled": true
948 }
949 }"#,
950 None,
951 );
952
953 assert_migrate_settings(
954 r#"{
955 "other_section": {
956 "preferred_completion_mode": "max"
957 },
958 "agent": {
959 "preferred_completion_mode": "max"
960 }
961 }"#,
962 Some(
963 r#"{
964 "other_section": {
965 "preferred_completion_mode": "max"
966 },
967 "agent": {
968 "preferred_completion_mode": "burn"
969 }
970 }"#,
971 ),
972 );
973 }
974
975 #[test]
976 fn test_mcp_settings_migration() {
977 assert_migrate_settings_with_migrations(
978 &[MigrationType::TreeSitter(
979 migrations::m_2025_06_16::SETTINGS_PATTERNS,
980 &SETTINGS_QUERY_2025_06_16,
981 )],
982 r#"{
983 "context_servers": {
984 "empty_server": {},
985 "extension_server": {
986 "settings": {
987 "foo": "bar"
988 }
989 },
990 "custom_server": {
991 "command": {
992 "path": "foo",
993 "args": ["bar"],
994 "env": {
995 "FOO": "BAR"
996 }
997 }
998 },
999 "invalid_server": {
1000 "command": {
1001 "path": "foo",
1002 "args": ["bar"],
1003 "env": {
1004 "FOO": "BAR"
1005 }
1006 },
1007 "settings": {
1008 "foo": "bar"
1009 }
1010 },
1011 "empty_server2": {},
1012 "extension_server2": {
1013 "foo": "bar",
1014 "settings": {
1015 "foo": "bar"
1016 },
1017 "bar": "foo"
1018 },
1019 "custom_server2": {
1020 "foo": "bar",
1021 "command": {
1022 "path": "foo",
1023 "args": ["bar"],
1024 "env": {
1025 "FOO": "BAR"
1026 }
1027 },
1028 "bar": "foo"
1029 },
1030 "invalid_server2": {
1031 "foo": "bar",
1032 "command": {
1033 "path": "foo",
1034 "args": ["bar"],
1035 "env": {
1036 "FOO": "BAR"
1037 }
1038 },
1039 "bar": "foo",
1040 "settings": {
1041 "foo": "bar"
1042 }
1043 }
1044 }
1045}"#,
1046 Some(
1047 r#"{
1048 "context_servers": {
1049 "empty_server": {
1050 "source": "extension",
1051 "settings": {}
1052 },
1053 "extension_server": {
1054 "source": "extension",
1055 "settings": {
1056 "foo": "bar"
1057 }
1058 },
1059 "custom_server": {
1060 "source": "custom",
1061 "command": {
1062 "path": "foo",
1063 "args": ["bar"],
1064 "env": {
1065 "FOO": "BAR"
1066 }
1067 }
1068 },
1069 "invalid_server": {
1070 "source": "custom",
1071 "command": {
1072 "path": "foo",
1073 "args": ["bar"],
1074 "env": {
1075 "FOO": "BAR"
1076 }
1077 },
1078 "settings": {
1079 "foo": "bar"
1080 }
1081 },
1082 "empty_server2": {
1083 "source": "extension",
1084 "settings": {}
1085 },
1086 "extension_server2": {
1087 "source": "extension",
1088 "foo": "bar",
1089 "settings": {
1090 "foo": "bar"
1091 },
1092 "bar": "foo"
1093 },
1094 "custom_server2": {
1095 "source": "custom",
1096 "foo": "bar",
1097 "command": {
1098 "path": "foo",
1099 "args": ["bar"],
1100 "env": {
1101 "FOO": "BAR"
1102 }
1103 },
1104 "bar": "foo"
1105 },
1106 "invalid_server2": {
1107 "source": "custom",
1108 "foo": "bar",
1109 "command": {
1110 "path": "foo",
1111 "args": ["bar"],
1112 "env": {
1113 "FOO": "BAR"
1114 }
1115 },
1116 "bar": "foo",
1117 "settings": {
1118 "foo": "bar"
1119 }
1120 }
1121 }
1122}"#,
1123 ),
1124 );
1125 }
1126
1127 #[test]
1128 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1129 let settings = r#"{
1130 "context_servers": {
1131 "empty_server": {
1132 "source": "extension",
1133 "settings": {}
1134 },
1135 "extension_server": {
1136 "source": "extension",
1137 "settings": {
1138 "foo": "bar"
1139 }
1140 },
1141 "custom_server": {
1142 "source": "custom",
1143 "command": {
1144 "path": "foo",
1145 "args": ["bar"],
1146 "env": {
1147 "FOO": "BAR"
1148 }
1149 }
1150 },
1151 "invalid_server": {
1152 "source": "custom",
1153 "command": {
1154 "path": "foo",
1155 "args": ["bar"],
1156 "env": {
1157 "FOO": "BAR"
1158 }
1159 },
1160 "settings": {
1161 "foo": "bar"
1162 }
1163 }
1164 }
1165}"#;
1166 assert_migrate_settings_with_migrations(
1167 &[MigrationType::TreeSitter(
1168 migrations::m_2025_06_16::SETTINGS_PATTERNS,
1169 &SETTINGS_QUERY_2025_06_16,
1170 )],
1171 settings,
1172 None,
1173 );
1174 }
1175
1176 #[test]
1177 fn test_remove_version_fields() {
1178 assert_migrate_settings(
1179 r#"{
1180 "language_models": {
1181 "anthropic": {
1182 "version": "1",
1183 "api_url": "https://api.anthropic.com"
1184 },
1185 "openai": {
1186 "version": "1",
1187 "api_url": "https://api.openai.com/v1"
1188 }
1189 },
1190 "agent": {
1191 "version": "2",
1192 "enabled": true,
1193 "preferred_completion_mode": "normal",
1194 "button": true,
1195 "dock": "right",
1196 "default_width": 640,
1197 "default_height": 320,
1198 "default_model": {
1199 "provider": "zed.dev",
1200 "model": "claude-sonnet-4"
1201 }
1202 }
1203}"#,
1204 Some(
1205 r#"{
1206 "language_models": {
1207 "anthropic": {
1208 "api_url": "https://api.anthropic.com"
1209 },
1210 "openai": {
1211 "api_url": "https://api.openai.com/v1"
1212 }
1213 },
1214 "agent": {
1215 "enabled": true,
1216 "preferred_completion_mode": "normal",
1217 "button": true,
1218 "dock": "right",
1219 "default_width": 640,
1220 "default_height": 320,
1221 "default_model": {
1222 "provider": "zed.dev",
1223 "model": "claude-sonnet-4"
1224 }
1225 }
1226}"#,
1227 ),
1228 );
1229
1230 // Test that version fields in other contexts are not removed
1231 assert_migrate_settings(
1232 r#"{
1233 "language_models": {
1234 "other_provider": {
1235 "version": "1",
1236 "api_url": "https://api.example.com"
1237 }
1238 },
1239 "other_section": {
1240 "version": "1"
1241 }
1242}"#,
1243 None,
1244 );
1245 }
1246
1247 #[test]
1248 fn test_flatten_context_server_command() {
1249 assert_migrate_settings(
1250 r#"{
1251 "context_servers": {
1252 "some-mcp-server": {
1253 "source": "custom",
1254 "command": {
1255 "path": "npx",
1256 "args": [
1257 "-y",
1258 "@supabase/mcp-server-supabase@latest",
1259 "--read-only",
1260 "--project-ref=<project-ref>"
1261 ],
1262 "env": {
1263 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1264 }
1265 }
1266 }
1267 }
1268}"#,
1269 Some(
1270 r#"{
1271 "context_servers": {
1272 "some-mcp-server": {
1273 "source": "custom",
1274 "command": "npx",
1275 "args": [
1276 "-y",
1277 "@supabase/mcp-server-supabase@latest",
1278 "--read-only",
1279 "--project-ref=<project-ref>"
1280 ],
1281 "env": {
1282 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1283 }
1284 }
1285 }
1286}"#,
1287 ),
1288 );
1289
1290 // Test with additional keys in server object
1291 assert_migrate_settings(
1292 r#"{
1293 "context_servers": {
1294 "server-with-extras": {
1295 "source": "custom",
1296 "command": {
1297 "path": "/usr/bin/node",
1298 "args": ["server.js"]
1299 },
1300 "settings": {}
1301 }
1302 }
1303}"#,
1304 Some(
1305 r#"{
1306 "context_servers": {
1307 "server-with-extras": {
1308 "source": "custom",
1309 "command": "/usr/bin/node",
1310 "args": ["server.js"],
1311 "settings": {}
1312 }
1313 }
1314}"#,
1315 ),
1316 );
1317
1318 // Test command without args or env
1319 assert_migrate_settings(
1320 r#"{
1321 "context_servers": {
1322 "simple-server": {
1323 "source": "custom",
1324 "command": {
1325 "path": "simple-mcp-server"
1326 }
1327 }
1328 }
1329}"#,
1330 Some(
1331 r#"{
1332 "context_servers": {
1333 "simple-server": {
1334 "source": "custom",
1335 "command": "simple-mcp-server"
1336 }
1337 }
1338}"#,
1339 ),
1340 );
1341 }
1342
1343 #[test]
1344 fn test_flatten_code_action_formatters_basic_array() {
1345 assert_migrate_settings_with_migrations(
1346 &[MigrationType::TreeSitter(
1347 migrations::m_2025_10_01::SETTINGS_PATTERNS,
1348 &SETTINGS_QUERY_2025_10_01,
1349 )],
1350 &r#"{
1351 "formatter": [
1352 {
1353 "code_actions": {
1354 "included-1": true,
1355 "included-2": true,
1356 "excluded": false,
1357 }
1358 }
1359 ]
1360 }"#
1361 .unindent(),
1362 Some(
1363 &r#"{
1364 "formatter": [
1365 { "code_action": "included-1" },
1366 { "code_action": "included-2" }
1367 ]
1368 }"#
1369 .unindent(),
1370 ),
1371 );
1372 }
1373
1374 #[test]
1375 fn test_flatten_code_action_formatters_basic_object() {
1376 assert_migrate_settings_with_migrations(
1377 &[MigrationType::TreeSitter(
1378 migrations::m_2025_10_01::SETTINGS_PATTERNS,
1379 &SETTINGS_QUERY_2025_10_01,
1380 )],
1381 &r#"{
1382 "formatter": {
1383 "code_actions": {
1384 "included-1": true,
1385 "excluded": false,
1386 "included-2": true
1387 }
1388 }
1389 }"#
1390 .unindent(),
1391 Some(
1392 &r#"{
1393 "formatter": [
1394 { "code_action": "included-1" },
1395 { "code_action": "included-2" }
1396 ]
1397 }"#
1398 .unindent(),
1399 ),
1400 );
1401 }
1402
1403 #[test]
1404 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1405 assert_migrate_settings(
1406 r#"{
1407 "formatter": [
1408 {
1409 "code_actions": {
1410 "included-1": true,
1411 "included-2": true,
1412 "excluded": false,
1413 }
1414 },
1415 {
1416 "language_server": "ruff"
1417 },
1418 {
1419 "code_actions": {
1420 "excluded": false,
1421 "excluded-2": false,
1422 }
1423 }
1424 // some comment
1425 ,
1426 {
1427 "code_actions": {
1428 "excluded": false,
1429 "included-3": true,
1430 "included-4": true,
1431 }
1432 },
1433 ]
1434 }"#,
1435 Some(
1436 r#"{
1437 "formatter": [
1438 { "code_action": "included-1" },
1439 { "code_action": "included-2" },
1440 {
1441 "language_server": "ruff"
1442 },
1443 { "code_action": "included-3" },
1444 { "code_action": "included-4" },
1445 ]
1446 }"#,
1447 ),
1448 );
1449 }
1450
1451 #[test]
1452 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1453 assert_migrate_settings(
1454 &r#"{
1455 "languages": {
1456 "Rust": {
1457 "formatter": [
1458 {
1459 "code_actions": {
1460 "included-1": true,
1461 "included-2": true,
1462 "excluded": false,
1463 }
1464 },
1465 {
1466 "language_server": "ruff"
1467 },
1468 {
1469 "code_actions": {
1470 "excluded": false,
1471 "excluded-2": false,
1472 }
1473 }
1474 // some comment
1475 ,
1476 {
1477 "code_actions": {
1478 "excluded": false,
1479 "included-3": true,
1480 "included-4": true,
1481 }
1482 },
1483 ]
1484 }
1485 }
1486 }"#
1487 .unindent(),
1488 Some(
1489 &r#"{
1490 "languages": {
1491 "Rust": {
1492 "formatter": [
1493 { "code_action": "included-1" },
1494 { "code_action": "included-2" },
1495 {
1496 "language_server": "ruff"
1497 },
1498 { "code_action": "included-3" },
1499 { "code_action": "included-4" },
1500 ]
1501 }
1502 }
1503 }"#
1504 .unindent(),
1505 ),
1506 );
1507 }
1508
1509 #[test]
1510 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1511 {
1512 assert_migrate_settings_with_migrations(
1513 &[MigrationType::TreeSitter(
1514 migrations::m_2025_10_01::SETTINGS_PATTERNS,
1515 &SETTINGS_QUERY_2025_10_01,
1516 )],
1517 &r#"{
1518 "formatter": {
1519 "code_actions": {
1520 "default-1": true,
1521 "default-2": true,
1522 "default-3": true,
1523 "default-4": true,
1524 }
1525 },
1526 "languages": {
1527 "Rust": {
1528 "formatter": [
1529 {
1530 "code_actions": {
1531 "included-1": true,
1532 "included-2": true,
1533 "excluded": false,
1534 }
1535 },
1536 {
1537 "language_server": "ruff"
1538 },
1539 {
1540 "code_actions": {
1541 "excluded": false,
1542 "excluded-2": false,
1543 }
1544 }
1545 // some comment
1546 ,
1547 {
1548 "code_actions": {
1549 "excluded": false,
1550 "included-3": true,
1551 "included-4": true,
1552 }
1553 },
1554 ]
1555 },
1556 "Python": {
1557 "formatter": [
1558 {
1559 "language_server": "ruff"
1560 },
1561 {
1562 "code_actions": {
1563 "excluded": false,
1564 "excluded-2": false,
1565 }
1566 }
1567 // some comment
1568 ,
1569 {
1570 "code_actions": {
1571 "excluded": false,
1572 "included-3": true,
1573 "included-4": true,
1574 }
1575 },
1576 ]
1577 }
1578 }
1579 }"#
1580 .unindent(),
1581 Some(
1582 &r#"{
1583 "formatter": [
1584 { "code_action": "default-1" },
1585 { "code_action": "default-2" },
1586 { "code_action": "default-3" },
1587 { "code_action": "default-4" }
1588 ],
1589 "languages": {
1590 "Rust": {
1591 "formatter": [
1592 { "code_action": "included-1" },
1593 { "code_action": "included-2" },
1594 {
1595 "language_server": "ruff"
1596 },
1597 { "code_action": "included-3" },
1598 { "code_action": "included-4" },
1599 ]
1600 },
1601 "Python": {
1602 "formatter": [
1603 {
1604 "language_server": "ruff"
1605 },
1606 { "code_action": "included-3" },
1607 { "code_action": "included-4" },
1608 ]
1609 }
1610 }
1611 }"#
1612 .unindent(),
1613 ),
1614 );
1615 }
1616
1617 #[test]
1618 fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1619 assert_migrate_settings_with_migrations(
1620 &[MigrationType::TreeSitter(
1621 migrations::m_2025_10_01::SETTINGS_PATTERNS,
1622 &SETTINGS_QUERY_2025_10_01,
1623 )],
1624 &r#"{
1625 "formatter": {
1626 "code_actions": {
1627 "default-1": true,
1628 "default-2": true,
1629 "default-3": true,
1630 "default-4": true,
1631 }
1632 },
1633 "format_on_save": [
1634 {
1635 "code_actions": {
1636 "included-1": true,
1637 "included-2": true,
1638 "excluded": false,
1639 }
1640 },
1641 {
1642 "language_server": "ruff"
1643 },
1644 {
1645 "code_actions": {
1646 "excluded": false,
1647 "excluded-2": false,
1648 }
1649 }
1650 // some comment
1651 ,
1652 {
1653 "code_actions": {
1654 "excluded": false,
1655 "included-3": true,
1656 "included-4": true,
1657 }
1658 },
1659 ],
1660 "languages": {
1661 "Rust": {
1662 "format_on_save": "prettier",
1663 "formatter": [
1664 {
1665 "code_actions": {
1666 "included-1": true,
1667 "included-2": true,
1668 "excluded": false,
1669 }
1670 },
1671 {
1672 "language_server": "ruff"
1673 },
1674 {
1675 "code_actions": {
1676 "excluded": false,
1677 "excluded-2": false,
1678 }
1679 }
1680 // some comment
1681 ,
1682 {
1683 "code_actions": {
1684 "excluded": false,
1685 "included-3": true,
1686 "included-4": true,
1687 }
1688 },
1689 ]
1690 },
1691 "Python": {
1692 "format_on_save": {
1693 "code_actions": {
1694 "on-save-1": true,
1695 "on-save-2": true,
1696 }
1697 },
1698 "formatter": [
1699 {
1700 "language_server": "ruff"
1701 },
1702 {
1703 "code_actions": {
1704 "excluded": false,
1705 "excluded-2": false,
1706 }
1707 }
1708 // some comment
1709 ,
1710 {
1711 "code_actions": {
1712 "excluded": false,
1713 "included-3": true,
1714 "included-4": true,
1715 }
1716 },
1717 ]
1718 }
1719 }
1720 }"#
1721 .unindent(),
1722 Some(
1723 &r#"{
1724 "formatter": [
1725 { "code_action": "default-1" },
1726 { "code_action": "default-2" },
1727 { "code_action": "default-3" },
1728 { "code_action": "default-4" }
1729 ],
1730 "format_on_save": [
1731 { "code_action": "included-1" },
1732 { "code_action": "included-2" },
1733 {
1734 "language_server": "ruff"
1735 },
1736 { "code_action": "included-3" },
1737 { "code_action": "included-4" },
1738 ],
1739 "languages": {
1740 "Rust": {
1741 "format_on_save": "prettier",
1742 "formatter": [
1743 { "code_action": "included-1" },
1744 { "code_action": "included-2" },
1745 {
1746 "language_server": "ruff"
1747 },
1748 { "code_action": "included-3" },
1749 { "code_action": "included-4" },
1750 ]
1751 },
1752 "Python": {
1753 "format_on_save": [
1754 { "code_action": "on-save-1" },
1755 { "code_action": "on-save-2" }
1756 ],
1757 "formatter": [
1758 {
1759 "language_server": "ruff"
1760 },
1761 { "code_action": "included-3" },
1762 { "code_action": "included-4" },
1763 ]
1764 }
1765 }
1766 }"#
1767 .unindent(),
1768 ),
1769 );
1770 }
1771
1772 #[test]
1773 fn test_format_on_save_formatter_migration_basic() {
1774 assert_migrate_settings_with_migrations(
1775 &[MigrationType::Json(
1776 migrations::m_2025_10_02::remove_formatters_on_save,
1777 )],
1778 &r#"{
1779 "format_on_save": "prettier"
1780 }"#
1781 .unindent(),
1782 Some(
1783 &r#"{
1784 "formatter": "prettier",
1785 "format_on_save": "on"
1786 }"#
1787 .unindent(),
1788 ),
1789 );
1790 }
1791
1792 #[test]
1793 fn test_format_on_save_formatter_migration_array() {
1794 assert_migrate_settings_with_migrations(
1795 &[MigrationType::Json(
1796 migrations::m_2025_10_02::remove_formatters_on_save,
1797 )],
1798 &r#"{
1799 "format_on_save": ["prettier", {"language_server": "eslint"}]
1800 }"#
1801 .unindent(),
1802 Some(
1803 &r#"{
1804 "formatter": [
1805 "prettier",
1806 {
1807 "language_server": "eslint"
1808 }
1809 ],
1810 "format_on_save": "on"
1811 }"#
1812 .unindent(),
1813 ),
1814 );
1815 }
1816
1817 #[test]
1818 fn test_format_on_save_on_off_unchanged() {
1819 assert_migrate_settings_with_migrations(
1820 &[MigrationType::Json(
1821 migrations::m_2025_10_02::remove_formatters_on_save,
1822 )],
1823 &r#"{
1824 "format_on_save": "on"
1825 }"#
1826 .unindent(),
1827 None,
1828 );
1829
1830 assert_migrate_settings_with_migrations(
1831 &[MigrationType::Json(
1832 migrations::m_2025_10_02::remove_formatters_on_save,
1833 )],
1834 &r#"{
1835 "format_on_save": "off"
1836 }"#
1837 .unindent(),
1838 None,
1839 );
1840 }
1841
1842 #[test]
1843 fn test_format_on_save_formatter_migration_in_languages() {
1844 assert_migrate_settings_with_migrations(
1845 &[MigrationType::Json(
1846 migrations::m_2025_10_02::remove_formatters_on_save,
1847 )],
1848 &r#"{
1849 "languages": {
1850 "Rust": {
1851 "format_on_save": "rust-analyzer"
1852 },
1853 "Python": {
1854 "format_on_save": ["ruff", "black"]
1855 }
1856 }
1857 }"#
1858 .unindent(),
1859 Some(
1860 &r#"{
1861 "languages": {
1862 "Rust": {
1863 "formatter": "rust-analyzer",
1864 "format_on_save": "on"
1865 },
1866 "Python": {
1867 "formatter": [
1868 "ruff",
1869 "black"
1870 ],
1871 "format_on_save": "on"
1872 }
1873 }
1874 }"#
1875 .unindent(),
1876 ),
1877 );
1878 }
1879
1880 #[test]
1881 fn test_format_on_save_formatter_migration_mixed_global_and_languages() {
1882 assert_migrate_settings_with_migrations(
1883 &[MigrationType::Json(
1884 migrations::m_2025_10_02::remove_formatters_on_save,
1885 )],
1886 &r#"{
1887 "format_on_save": "prettier",
1888 "languages": {
1889 "Rust": {
1890 "format_on_save": "rust-analyzer"
1891 },
1892 "Python": {
1893 "format_on_save": "on"
1894 }
1895 }
1896 }"#
1897 .unindent(),
1898 Some(
1899 &r#"{
1900 "formatter": "prettier",
1901 "format_on_save": "on",
1902 "languages": {
1903 "Rust": {
1904 "formatter": "rust-analyzer",
1905 "format_on_save": "on"
1906 },
1907 "Python": {
1908 "format_on_save": "on"
1909 }
1910 }
1911 }"#
1912 .unindent(),
1913 ),
1914 );
1915 }
1916
1917 #[test]
1918 fn test_format_on_save_no_migration_when_no_format_on_save() {
1919 assert_migrate_settings_with_migrations(
1920 &[MigrationType::Json(
1921 migrations::m_2025_10_02::remove_formatters_on_save,
1922 )],
1923 &r#"{
1924 "formatter": ["prettier"]
1925 }"#
1926 .unindent(),
1927 None,
1928 );
1929 }
1930
1931 #[test]
1932 fn test_restore_code_actions_on_format() {
1933 assert_migrate_settings_with_migrations(
1934 &[MigrationType::Json(
1935 migrations::m_2025_10_16::restore_code_actions_on_format,
1936 )],
1937 &r#"{
1938 "formatter": {
1939 "code_action": "foo"
1940 }
1941 }"#
1942 .unindent(),
1943 Some(
1944 &r#"{
1945 "code_actions_on_format": {
1946 "foo": true
1947 }
1948 }
1949 "#
1950 .unindent(),
1951 ),
1952 );
1953
1954 assert_migrate_settings_with_migrations(
1955 &[MigrationType::Json(
1956 migrations::m_2025_10_16::restore_code_actions_on_format,
1957 )],
1958 &r#"{
1959 "formatter": [
1960 { "code_action": "foo" },
1961 "auto"
1962 ]
1963 }"#
1964 .unindent(),
1965 None,
1966 );
1967
1968 assert_migrate_settings_with_migrations(
1969 &[MigrationType::Json(
1970 migrations::m_2025_10_16::restore_code_actions_on_format,
1971 )],
1972 &r#"{
1973 "formatter": {
1974 "code_action": "foo"
1975 },
1976 "code_actions_on_format": {
1977 "bar": true,
1978 "baz": false
1979 }
1980 }"#
1981 .unindent(),
1982 Some(
1983 &r#"{
1984 "code_actions_on_format": {
1985 "foo": true,
1986 "bar": true,
1987 "baz": false
1988 }
1989 }"#
1990 .unindent(),
1991 ),
1992 );
1993
1994 assert_migrate_settings_with_migrations(
1995 &[MigrationType::Json(
1996 migrations::m_2025_10_16::restore_code_actions_on_format,
1997 )],
1998 &r#"{
1999 "formatter": [
2000 { "code_action": "foo" },
2001 { "code_action": "qux" },
2002 ],
2003 "code_actions_on_format": {
2004 "bar": true,
2005 "baz": false
2006 }
2007 }"#
2008 .unindent(),
2009 Some(
2010 &r#"{
2011 "code_actions_on_format": {
2012 "foo": true,
2013 "qux": true,
2014 "bar": true,
2015 "baz": false
2016 }
2017 }"#
2018 .unindent(),
2019 ),
2020 );
2021
2022 assert_migrate_settings_with_migrations(
2023 &[MigrationType::Json(
2024 migrations::m_2025_10_16::restore_code_actions_on_format,
2025 )],
2026 &r#"{
2027 "formatter": [],
2028 "code_actions_on_format": {
2029 "bar": true,
2030 "baz": false
2031 }
2032 }"#
2033 .unindent(),
2034 None,
2035 );
2036 }
2037}