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