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