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