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