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