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