1//! ## When to create a migration and why?
2//! A migration is necessary when keymap actions or settings are renamed or transformed (e.g., from an array to a string, a string to an array, a boolean to an enum, etc.).
3//!
4//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally.
5//! It also provides a quick way to migrate their existing settings to the latest state using button in UI.
6//!
7//! ## How to create a migration?
8//! Migrations use Tree-sitter to query commonly used patterns, such as actions with a string or actions with an array where the second argument is an object, etc.
9//! Once queried, *you can filter out the modified items* and write the replacement logic.
10//!
11//! You *must not* modify previous migrations; always create new ones instead.
12//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state.
13//! Modifying existing migrations means they will only work for users upgrading from version x-1 to x, but not from x-2 to x, and so on, where x is the latest version.
14//!
15//! You only need to write replacement logic for x-1 to x because you can be certain that, internally, every user will be at x-1, regardless of their on disk state.
16
17use anyhow::{Context as _, Result};
18use std::{cmp::Reverse, ops::Range, sync::LazyLock};
19use streaming_iterator::StreamingIterator;
20use tree_sitter::{Query, QueryMatch};
21
22use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
23
24mod migrations;
25mod patterns;
26
27fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
28 let mut parser = tree_sitter::Parser::new();
29 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
30 let syntax_tree = parser
31 .parse(text, None)
32 .context("failed to parse settings")?;
33
34 let mut cursor = tree_sitter::QueryCursor::new();
35 let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
36
37 let mut edits = vec![];
38 while let Some(mat) = matches.next() {
39 if let Some((_, callback)) = patterns.get(mat.pattern_index) {
40 edits.extend(callback(text, mat, query));
41 }
42 }
43
44 edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
45 edits.dedup_by(|(range_b, _), (range_a, _)| {
46 range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
47 });
48
49 if edits.is_empty() {
50 Ok(None)
51 } else {
52 let mut new_text = text.to_string();
53 for (range, replacement) in edits.iter().rev() {
54 new_text.replace_range(range.clone(), replacement);
55 }
56 if new_text == text {
57 log::error!(
58 "Edits computed for configuration migration do not cause a change: {:?}",
59 edits
60 );
61 Ok(None)
62 } else {
63 Ok(Some(new_text))
64 }
65 }
66}
67
68fn run_migrations(
69 text: &str,
70 migrations: &[(MigrationPatterns, &Query)],
71) -> Result<Option<String>> {
72 let mut current_text = text.to_string();
73 let mut result: Option<String> = None;
74 for (patterns, query) in migrations.iter() {
75 if let Some(migrated_text) = migrate(¤t_text, patterns, query)? {
76 current_text = migrated_text.clone();
77 result = Some(migrated_text);
78 }
79 }
80 Ok(result.filter(|new_text| text != new_text))
81}
82
83pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
84 let migrations: &[(MigrationPatterns, &Query)] = &[
85 (
86 migrations::m_2025_01_29::KEYMAP_PATTERNS,
87 &KEYMAP_QUERY_2025_01_29,
88 ),
89 (
90 migrations::m_2025_01_30::KEYMAP_PATTERNS,
91 &KEYMAP_QUERY_2025_01_30,
92 ),
93 (
94 migrations::m_2025_03_03::KEYMAP_PATTERNS,
95 &KEYMAP_QUERY_2025_03_03,
96 ),
97 (
98 migrations::m_2025_03_06::KEYMAP_PATTERNS,
99 &KEYMAP_QUERY_2025_03_06,
100 ),
101 (
102 migrations::m_2025_04_15::KEYMAP_PATTERNS,
103 &KEYMAP_QUERY_2025_04_15,
104 ),
105 ];
106 run_migrations(text, migrations)
107}
108
109pub fn migrate_settings(text: &str) -> Result<Option<String>> {
110 let migrations: &[(MigrationPatterns, &Query)] = &[
111 (
112 migrations::m_2025_01_02::SETTINGS_PATTERNS,
113 &SETTINGS_QUERY_2025_01_02,
114 ),
115 (
116 migrations::m_2025_01_29::SETTINGS_PATTERNS,
117 &SETTINGS_QUERY_2025_01_29,
118 ),
119 (
120 migrations::m_2025_01_30::SETTINGS_PATTERNS,
121 &SETTINGS_QUERY_2025_01_30,
122 ),
123 (
124 migrations::m_2025_03_29::SETTINGS_PATTERNS,
125 &SETTINGS_QUERY_2025_03_29,
126 ),
127 (
128 migrations::m_2025_04_15::SETTINGS_PATTERNS,
129 &SETTINGS_QUERY_2025_04_15,
130 ),
131 (
132 migrations::m_2025_04_21::SETTINGS_PATTERNS,
133 &SETTINGS_QUERY_2025_04_21,
134 ),
135 (
136 migrations::m_2025_04_23::SETTINGS_PATTERNS,
137 &SETTINGS_QUERY_2025_04_23,
138 ),
139 (
140 migrations::m_2025_05_05::SETTINGS_PATTERNS,
141 &SETTINGS_QUERY_2025_05_05,
142 ),
143 (
144 migrations::m_2025_05_08::SETTINGS_PATTERNS,
145 &SETTINGS_QUERY_2025_05_08,
146 ),
147 (
148 migrations::m_2025_05_29::SETTINGS_PATTERNS,
149 &SETTINGS_QUERY_2025_05_29,
150 ),
151 (
152 migrations::m_2025_06_16::SETTINGS_PATTERNS,
153 &SETTINGS_QUERY_2025_06_16,
154 ),
155 (
156 migrations::m_2025_06_25::SETTINGS_PATTERNS,
157 &SETTINGS_QUERY_2025_06_25,
158 ),
159 (
160 migrations::m_2025_06_27::SETTINGS_PATTERNS,
161 &SETTINGS_QUERY_2025_06_27,
162 ),
163 (
164 migrations::m_2025_07_08::SETTINGS_PATTERNS,
165 &SETTINGS_QUERY_2025_07_08,
166 ),
167 (
168 migrations::m_2025_10_01::SETTINGS_PATTERNS,
169 &SETTINGS_QUERY_2025_10_01,
170 ),
171 ];
172 run_migrations(text, migrations)
173}
174
175pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
176 migrate(
177 text,
178 &[(
179 SETTINGS_NESTED_KEY_VALUE_PATTERN,
180 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
181 )],
182 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
183 )
184}
185
186pub type MigrationPatterns = &'static [(
187 &'static str,
188 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
189)];
190
191macro_rules! define_query {
192 ($var_name:ident, $patterns_path:path) => {
193 static $var_name: LazyLock<Query> = LazyLock::new(|| {
194 Query::new(
195 &tree_sitter_json::LANGUAGE.into(),
196 &$patterns_path
197 .iter()
198 .map(|pattern| pattern.0)
199 .collect::<String>(),
200 )
201 .unwrap()
202 });
203 };
204}
205
206// keymap
207define_query!(
208 KEYMAP_QUERY_2025_01_29,
209 migrations::m_2025_01_29::KEYMAP_PATTERNS
210);
211define_query!(
212 KEYMAP_QUERY_2025_01_30,
213 migrations::m_2025_01_30::KEYMAP_PATTERNS
214);
215define_query!(
216 KEYMAP_QUERY_2025_03_03,
217 migrations::m_2025_03_03::KEYMAP_PATTERNS
218);
219define_query!(
220 KEYMAP_QUERY_2025_03_06,
221 migrations::m_2025_03_06::KEYMAP_PATTERNS
222);
223define_query!(
224 KEYMAP_QUERY_2025_04_15,
225 migrations::m_2025_04_15::KEYMAP_PATTERNS
226);
227
228// settings
229define_query!(
230 SETTINGS_QUERY_2025_01_02,
231 migrations::m_2025_01_02::SETTINGS_PATTERNS
232);
233define_query!(
234 SETTINGS_QUERY_2025_01_29,
235 migrations::m_2025_01_29::SETTINGS_PATTERNS
236);
237define_query!(
238 SETTINGS_QUERY_2025_01_30,
239 migrations::m_2025_01_30::SETTINGS_PATTERNS
240);
241define_query!(
242 SETTINGS_QUERY_2025_03_29,
243 migrations::m_2025_03_29::SETTINGS_PATTERNS
244);
245define_query!(
246 SETTINGS_QUERY_2025_04_15,
247 migrations::m_2025_04_15::SETTINGS_PATTERNS
248);
249define_query!(
250 SETTINGS_QUERY_2025_04_21,
251 migrations::m_2025_04_21::SETTINGS_PATTERNS
252);
253define_query!(
254 SETTINGS_QUERY_2025_04_23,
255 migrations::m_2025_04_23::SETTINGS_PATTERNS
256);
257define_query!(
258 SETTINGS_QUERY_2025_05_05,
259 migrations::m_2025_05_05::SETTINGS_PATTERNS
260);
261define_query!(
262 SETTINGS_QUERY_2025_05_08,
263 migrations::m_2025_05_08::SETTINGS_PATTERNS
264);
265define_query!(
266 SETTINGS_QUERY_2025_05_29,
267 migrations::m_2025_05_29::SETTINGS_PATTERNS
268);
269define_query!(
270 SETTINGS_QUERY_2025_06_16,
271 migrations::m_2025_06_16::SETTINGS_PATTERNS
272);
273define_query!(
274 SETTINGS_QUERY_2025_06_25,
275 migrations::m_2025_06_25::SETTINGS_PATTERNS
276);
277define_query!(
278 SETTINGS_QUERY_2025_06_27,
279 migrations::m_2025_06_27::SETTINGS_PATTERNS
280);
281define_query!(
282 SETTINGS_QUERY_2025_07_08,
283 migrations::m_2025_07_08::SETTINGS_PATTERNS
284);
285define_query!(
286 SETTINGS_QUERY_2025_10_01,
287 migrations::m_2025_10_01::SETTINGS_PATTERNS
288);
289
290// custom query
291static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
292 Query::new(
293 &tree_sitter_json::LANGUAGE.into(),
294 SETTINGS_NESTED_KEY_VALUE_PATTERN,
295 )
296 .unwrap()
297});
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use unindent::Unindent as _;
303
304 fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
305 match (&migrated, &expected) {
306 (Some(migrated), Some(expected)) => {
307 pretty_assertions::assert_str_eq!(migrated, expected);
308 }
309 _ => {
310 pretty_assertions::assert_eq!(migrated.as_deref(), expected);
311 }
312 }
313 }
314
315 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
316 let migrated = migrate_keymap(input).unwrap();
317 pretty_assertions::assert_eq!(migrated.as_deref(), output);
318 }
319
320 fn assert_migrate_settings(input: &str, output: Option<&str>) {
321 let migrated = migrate_settings(input).unwrap();
322 assert_migrated_correctly(migrated, output);
323 }
324
325 fn assert_migrate_settings_with_migrations(
326 migrations: &[(MigrationPatterns, &Query)],
327 input: &str,
328 output: Option<&str>,
329 ) {
330 let migrated = run_migrations(input, migrations).unwrap();
331 pretty_assertions::assert_eq!(migrated.as_deref(), output);
332 }
333
334 #[test]
335 fn test_replace_array_with_single_string() {
336 assert_migrate_keymap(
337 r#"
338 [
339 {
340 "bindings": {
341 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
342 }
343 }
344 ]
345 "#,
346 Some(
347 r#"
348 [
349 {
350 "bindings": {
351 "cmd-1": "workspace::ActivatePaneUp"
352 }
353 }
354 ]
355 "#,
356 ),
357 )
358 }
359
360 #[test]
361 fn test_replace_action_argument_object_with_single_value() {
362 assert_migrate_keymap(
363 r#"
364 [
365 {
366 "bindings": {
367 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
368 }
369 }
370 ]
371 "#,
372 Some(
373 r#"
374 [
375 {
376 "bindings": {
377 "cmd-1": ["editor::FoldAtLevel", 1]
378 }
379 }
380 ]
381 "#,
382 ),
383 )
384 }
385
386 #[test]
387 fn test_replace_action_argument_object_with_single_value_2() {
388 assert_migrate_keymap(
389 r#"
390 [
391 {
392 "bindings": {
393 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
394 }
395 }
396 ]
397 "#,
398 Some(
399 r#"
400 [
401 {
402 "bindings": {
403 "cmd-1": ["vim::PushObject", { "some" : "value" }]
404 }
405 }
406 ]
407 "#,
408 ),
409 )
410 }
411
412 #[test]
413 fn test_rename_string_action() {
414 assert_migrate_keymap(
415 r#"
416 [
417 {
418 "bindings": {
419 "cmd-1": "inline_completion::ToggleMenu"
420 }
421 }
422 ]
423 "#,
424 Some(
425 r#"
426 [
427 {
428 "bindings": {
429 "cmd-1": "edit_prediction::ToggleMenu"
430 }
431 }
432 ]
433 "#,
434 ),
435 )
436 }
437
438 #[test]
439 fn test_rename_context_key() {
440 assert_migrate_keymap(
441 r#"
442 [
443 {
444 "context": "Editor && inline_completion && !showing_completions"
445 }
446 ]
447 "#,
448 Some(
449 r#"
450 [
451 {
452 "context": "Editor && edit_prediction && !showing_completions"
453 }
454 ]
455 "#,
456 ),
457 )
458 }
459
460 #[test]
461 fn test_incremental_migrations() {
462 // Here string transforms to array internally. Then, that array transforms back to string.
463 assert_migrate_keymap(
464 r#"
465 [
466 {
467 "bindings": {
468 "ctrl-q": "editor::GoToHunk", // should remain same
469 "ctrl-w": "editor::GoToPrevHunk", // should rename
470 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
471 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
472 }
473 }
474 ]
475 "#,
476 Some(
477 r#"
478 [
479 {
480 "bindings": {
481 "ctrl-q": "editor::GoToHunk", // should remain same
482 "ctrl-w": "editor::GoToPreviousHunk", // should rename
483 "ctrl-q": "editor::GoToHunk", // should transform
484 "ctrl-w": "editor::GoToPreviousHunk" // should transform
485 }
486 }
487 ]
488 "#,
489 ),
490 )
491 }
492
493 #[test]
494 fn test_action_argument_snake_case() {
495 // First performs transformations, then replacements
496 assert_migrate_keymap(
497 r#"
498 [
499 {
500 "bindings": {
501 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
502 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
503 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
504 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
505 }
506 }
507 ]
508 "#,
509 Some(
510 r#"
511 [
512 {
513 "bindings": {
514 "cmd-1": ["vim::PushObject", { "around": false }],
515 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
516 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
517 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
518 }
519 }
520 ]
521 "#,
522 ),
523 )
524 }
525
526 #[test]
527 fn test_replace_setting_name() {
528 assert_migrate_settings(
529 r#"
530 {
531 "show_inline_completions_in_menu": true,
532 "show_inline_completions": true,
533 "inline_completions_disabled_in": ["string"],
534 "inline_completions": { "some" : "value" }
535 }
536 "#,
537 Some(
538 r#"
539 {
540 "show_edit_predictions_in_menu": true,
541 "show_edit_predictions": true,
542 "edit_predictions_disabled_in": ["string"],
543 "edit_predictions": { "some" : "value" }
544 }
545 "#,
546 ),
547 )
548 }
549
550 #[test]
551 fn test_nested_string_replace_for_settings() {
552 assert_migrate_settings(
553 r#"
554 {
555 "features": {
556 "inline_completion_provider": "zed"
557 },
558 }
559 "#,
560 Some(
561 r#"
562 {
563 "features": {
564 "edit_prediction_provider": "zed"
565 },
566 }
567 "#,
568 ),
569 )
570 }
571
572 #[test]
573 fn test_replace_settings_in_languages() {
574 assert_migrate_settings(
575 r#"
576 {
577 "languages": {
578 "Astro": {
579 "show_inline_completions": true
580 }
581 }
582 }
583 "#,
584 Some(
585 r#"
586 {
587 "languages": {
588 "Astro": {
589 "show_edit_predictions": true
590 }
591 }
592 }
593 "#,
594 ),
595 )
596 }
597
598 #[test]
599 fn test_replace_settings_value() {
600 assert_migrate_settings(
601 r#"
602 {
603 "scrollbar": {
604 "diagnostics": true
605 },
606 "chat_panel": {
607 "button": true
608 }
609 }
610 "#,
611 Some(
612 r#"
613 {
614 "scrollbar": {
615 "diagnostics": "all"
616 },
617 "chat_panel": {
618 "button": "always"
619 }
620 }
621 "#,
622 ),
623 )
624 }
625
626 #[test]
627 fn test_replace_settings_name_and_value() {
628 assert_migrate_settings(
629 r#"
630 {
631 "tabs": {
632 "always_show_close_button": true
633 }
634 }
635 "#,
636 Some(
637 r#"
638 {
639 "tabs": {
640 "show_close_button": "always"
641 }
642 }
643 "#,
644 ),
645 )
646 }
647
648 #[test]
649 fn test_replace_bash_with_terminal_in_profiles() {
650 assert_migrate_settings(
651 r#"
652 {
653 "assistant": {
654 "profiles": {
655 "custom": {
656 "name": "Custom",
657 "tools": {
658 "bash": true,
659 "diagnostics": true
660 }
661 }
662 }
663 }
664 }
665 "#,
666 Some(
667 r#"
668 {
669 "agent": {
670 "profiles": {
671 "custom": {
672 "name": "Custom",
673 "tools": {
674 "terminal": true,
675 "diagnostics": true
676 }
677 }
678 }
679 }
680 }
681 "#,
682 ),
683 )
684 }
685
686 #[test]
687 fn test_replace_bash_false_with_terminal_in_profiles() {
688 assert_migrate_settings(
689 r#"
690 {
691 "assistant": {
692 "profiles": {
693 "custom": {
694 "name": "Custom",
695 "tools": {
696 "bash": false,
697 "diagnostics": true
698 }
699 }
700 }
701 }
702 }
703 "#,
704 Some(
705 r#"
706 {
707 "agent": {
708 "profiles": {
709 "custom": {
710 "name": "Custom",
711 "tools": {
712 "terminal": false,
713 "diagnostics": true
714 }
715 }
716 }
717 }
718 }
719 "#,
720 ),
721 )
722 }
723
724 #[test]
725 fn test_no_bash_in_profiles() {
726 assert_migrate_settings(
727 r#"
728 {
729 "assistant": {
730 "profiles": {
731 "custom": {
732 "name": "Custom",
733 "tools": {
734 "diagnostics": true,
735 "find_path": true,
736 "read_file": true
737 }
738 }
739 }
740 }
741 }
742 "#,
743 Some(
744 r#"
745 {
746 "agent": {
747 "profiles": {
748 "custom": {
749 "name": "Custom",
750 "tools": {
751 "diagnostics": true,
752 "find_path": true,
753 "read_file": true
754 }
755 }
756 }
757 }
758 }
759 "#,
760 ),
761 )
762 }
763
764 #[test]
765 fn test_rename_path_search_to_find_path() {
766 assert_migrate_settings(
767 r#"
768 {
769 "assistant": {
770 "profiles": {
771 "default": {
772 "tools": {
773 "path_search": true,
774 "read_file": true
775 }
776 }
777 }
778 }
779 }
780 "#,
781 Some(
782 r#"
783 {
784 "agent": {
785 "profiles": {
786 "default": {
787 "tools": {
788 "find_path": true,
789 "read_file": true
790 }
791 }
792 }
793 }
794 }
795 "#,
796 ),
797 );
798 }
799
800 #[test]
801 fn test_rename_assistant() {
802 assert_migrate_settings(
803 r#"{
804 "assistant": {
805 "foo": "bar"
806 },
807 "edit_predictions": {
808 "enabled_in_assistant": false,
809 }
810 }"#,
811 Some(
812 r#"{
813 "agent": {
814 "foo": "bar"
815 },
816 "edit_predictions": {
817 "enabled_in_text_threads": false,
818 }
819 }"#,
820 ),
821 );
822 }
823
824 #[test]
825 fn test_comment_duplicated_agent() {
826 assert_migrate_settings(
827 r#"{
828 "agent": {
829 "name": "assistant-1",
830 "model": "gpt-4", // weird formatting
831 "utf8": "привіт"
832 },
833 "something": "else",
834 "agent": {
835 "name": "assistant-2",
836 "model": "gemini-pro"
837 }
838 }
839 "#,
840 Some(
841 r#"{
842 /* Duplicated key auto-commented: "agent": {
843 "name": "assistant-1",
844 "model": "gpt-4", // weird formatting
845 "utf8": "привіт"
846 }, */
847 "something": "else",
848 "agent": {
849 "name": "assistant-2",
850 "model": "gemini-pro"
851 }
852 }
853 "#,
854 ),
855 );
856 }
857
858 #[test]
859 fn test_preferred_completion_mode_migration() {
860 assert_migrate_settings(
861 r#"{
862 "agent": {
863 "preferred_completion_mode": "max",
864 "enabled": true
865 }
866 }"#,
867 Some(
868 r#"{
869 "agent": {
870 "preferred_completion_mode": "burn",
871 "enabled": true
872 }
873 }"#,
874 ),
875 );
876
877 assert_migrate_settings(
878 r#"{
879 "agent": {
880 "preferred_completion_mode": "normal",
881 "enabled": true
882 }
883 }"#,
884 None,
885 );
886
887 assert_migrate_settings(
888 r#"{
889 "agent": {
890 "preferred_completion_mode": "burn",
891 "enabled": true
892 }
893 }"#,
894 None,
895 );
896
897 assert_migrate_settings(
898 r#"{
899 "other_section": {
900 "preferred_completion_mode": "max"
901 },
902 "agent": {
903 "preferred_completion_mode": "max"
904 }
905 }"#,
906 Some(
907 r#"{
908 "other_section": {
909 "preferred_completion_mode": "max"
910 },
911 "agent": {
912 "preferred_completion_mode": "burn"
913 }
914 }"#,
915 ),
916 );
917 }
918
919 #[test]
920 fn test_mcp_settings_migration() {
921 assert_migrate_settings_with_migrations(
922 &[(
923 migrations::m_2025_06_16::SETTINGS_PATTERNS,
924 &SETTINGS_QUERY_2025_06_16,
925 )],
926 r#"{
927 "context_servers": {
928 "empty_server": {},
929 "extension_server": {
930 "settings": {
931 "foo": "bar"
932 }
933 },
934 "custom_server": {
935 "command": {
936 "path": "foo",
937 "args": ["bar"],
938 "env": {
939 "FOO": "BAR"
940 }
941 }
942 },
943 "invalid_server": {
944 "command": {
945 "path": "foo",
946 "args": ["bar"],
947 "env": {
948 "FOO": "BAR"
949 }
950 },
951 "settings": {
952 "foo": "bar"
953 }
954 },
955 "empty_server2": {},
956 "extension_server2": {
957 "foo": "bar",
958 "settings": {
959 "foo": "bar"
960 },
961 "bar": "foo"
962 },
963 "custom_server2": {
964 "foo": "bar",
965 "command": {
966 "path": "foo",
967 "args": ["bar"],
968 "env": {
969 "FOO": "BAR"
970 }
971 },
972 "bar": "foo"
973 },
974 "invalid_server2": {
975 "foo": "bar",
976 "command": {
977 "path": "foo",
978 "args": ["bar"],
979 "env": {
980 "FOO": "BAR"
981 }
982 },
983 "bar": "foo",
984 "settings": {
985 "foo": "bar"
986 }
987 }
988 }
989}"#,
990 Some(
991 r#"{
992 "context_servers": {
993 "empty_server": {
994 "source": "extension",
995 "settings": {}
996 },
997 "extension_server": {
998 "source": "extension",
999 "settings": {
1000 "foo": "bar"
1001 }
1002 },
1003 "custom_server": {
1004 "source": "custom",
1005 "command": {
1006 "path": "foo",
1007 "args": ["bar"],
1008 "env": {
1009 "FOO": "BAR"
1010 }
1011 }
1012 },
1013 "invalid_server": {
1014 "source": "custom",
1015 "command": {
1016 "path": "foo",
1017 "args": ["bar"],
1018 "env": {
1019 "FOO": "BAR"
1020 }
1021 },
1022 "settings": {
1023 "foo": "bar"
1024 }
1025 },
1026 "empty_server2": {
1027 "source": "extension",
1028 "settings": {}
1029 },
1030 "extension_server2": {
1031 "source": "extension",
1032 "foo": "bar",
1033 "settings": {
1034 "foo": "bar"
1035 },
1036 "bar": "foo"
1037 },
1038 "custom_server2": {
1039 "source": "custom",
1040 "foo": "bar",
1041 "command": {
1042 "path": "foo",
1043 "args": ["bar"],
1044 "env": {
1045 "FOO": "BAR"
1046 }
1047 },
1048 "bar": "foo"
1049 },
1050 "invalid_server2": {
1051 "source": "custom",
1052 "foo": "bar",
1053 "command": {
1054 "path": "foo",
1055 "args": ["bar"],
1056 "env": {
1057 "FOO": "BAR"
1058 }
1059 },
1060 "bar": "foo",
1061 "settings": {
1062 "foo": "bar"
1063 }
1064 }
1065 }
1066}"#,
1067 ),
1068 );
1069 }
1070
1071 #[test]
1072 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1073 let settings = r#"{
1074 "context_servers": {
1075 "empty_server": {
1076 "source": "extension",
1077 "settings": {}
1078 },
1079 "extension_server": {
1080 "source": "extension",
1081 "settings": {
1082 "foo": "bar"
1083 }
1084 },
1085 "custom_server": {
1086 "source": "custom",
1087 "command": {
1088 "path": "foo",
1089 "args": ["bar"],
1090 "env": {
1091 "FOO": "BAR"
1092 }
1093 }
1094 },
1095 "invalid_server": {
1096 "source": "custom",
1097 "command": {
1098 "path": "foo",
1099 "args": ["bar"],
1100 "env": {
1101 "FOO": "BAR"
1102 }
1103 },
1104 "settings": {
1105 "foo": "bar"
1106 }
1107 }
1108 }
1109}"#;
1110 assert_migrate_settings_with_migrations(
1111 &[(
1112 migrations::m_2025_06_16::SETTINGS_PATTERNS,
1113 &SETTINGS_QUERY_2025_06_16,
1114 )],
1115 settings,
1116 None,
1117 );
1118 }
1119
1120 #[test]
1121 fn test_remove_version_fields() {
1122 assert_migrate_settings(
1123 r#"{
1124 "language_models": {
1125 "anthropic": {
1126 "version": "1",
1127 "api_url": "https://api.anthropic.com"
1128 },
1129 "openai": {
1130 "version": "1",
1131 "api_url": "https://api.openai.com/v1"
1132 }
1133 },
1134 "agent": {
1135 "version": "2",
1136 "enabled": true,
1137 "preferred_completion_mode": "normal",
1138 "button": true,
1139 "dock": "right",
1140 "default_width": 640,
1141 "default_height": 320,
1142 "default_model": {
1143 "provider": "zed.dev",
1144 "model": "claude-sonnet-4"
1145 }
1146 }
1147}"#,
1148 Some(
1149 r#"{
1150 "language_models": {
1151 "anthropic": {
1152 "api_url": "https://api.anthropic.com"
1153 },
1154 "openai": {
1155 "api_url": "https://api.openai.com/v1"
1156 }
1157 },
1158 "agent": {
1159 "enabled": true,
1160 "preferred_completion_mode": "normal",
1161 "button": true,
1162 "dock": "right",
1163 "default_width": 640,
1164 "default_height": 320,
1165 "default_model": {
1166 "provider": "zed.dev",
1167 "model": "claude-sonnet-4"
1168 }
1169 }
1170}"#,
1171 ),
1172 );
1173
1174 // Test that version fields in other contexts are not removed
1175 assert_migrate_settings(
1176 r#"{
1177 "language_models": {
1178 "other_provider": {
1179 "version": "1",
1180 "api_url": "https://api.example.com"
1181 }
1182 },
1183 "other_section": {
1184 "version": "1"
1185 }
1186}"#,
1187 None,
1188 );
1189 }
1190
1191 #[test]
1192 fn test_flatten_context_server_command() {
1193 assert_migrate_settings(
1194 r#"{
1195 "context_servers": {
1196 "some-mcp-server": {
1197 "source": "custom",
1198 "command": {
1199 "path": "npx",
1200 "args": [
1201 "-y",
1202 "@supabase/mcp-server-supabase@latest",
1203 "--read-only",
1204 "--project-ref=<project-ref>"
1205 ],
1206 "env": {
1207 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1208 }
1209 }
1210 }
1211 }
1212}"#,
1213 Some(
1214 r#"{
1215 "context_servers": {
1216 "some-mcp-server": {
1217 "source": "custom",
1218 "command": "npx",
1219 "args": [
1220 "-y",
1221 "@supabase/mcp-server-supabase@latest",
1222 "--read-only",
1223 "--project-ref=<project-ref>"
1224 ],
1225 "env": {
1226 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1227 }
1228 }
1229 }
1230}"#,
1231 ),
1232 );
1233
1234 // Test with additional keys in server object
1235 assert_migrate_settings(
1236 r#"{
1237 "context_servers": {
1238 "server-with-extras": {
1239 "source": "custom",
1240 "command": {
1241 "path": "/usr/bin/node",
1242 "args": ["server.js"]
1243 },
1244 "settings": {}
1245 }
1246 }
1247}"#,
1248 Some(
1249 r#"{
1250 "context_servers": {
1251 "server-with-extras": {
1252 "source": "custom",
1253 "command": "/usr/bin/node",
1254 "args": ["server.js"],
1255 "settings": {}
1256 }
1257 }
1258}"#,
1259 ),
1260 );
1261
1262 // Test command without args or env
1263 assert_migrate_settings(
1264 r#"{
1265 "context_servers": {
1266 "simple-server": {
1267 "source": "custom",
1268 "command": {
1269 "path": "simple-mcp-server"
1270 }
1271 }
1272 }
1273}"#,
1274 Some(
1275 r#"{
1276 "context_servers": {
1277 "simple-server": {
1278 "source": "custom",
1279 "command": "simple-mcp-server"
1280 }
1281 }
1282}"#,
1283 ),
1284 );
1285 }
1286
1287 #[test]
1288 fn test_flatten_code_action_formatters_basic_array() {
1289 assert_migrate_settings(
1290 &r#"{
1291 "formatter": [
1292 {
1293 "code_actions": {
1294 "included-1": true,
1295 "included-2": true,
1296 "excluded": false,
1297 }
1298 }
1299 ]
1300 }"#
1301 .unindent(),
1302 Some(
1303 &r#"{
1304 "formatter": [
1305 { "code_action": "included-1" },
1306 { "code_action": "included-2" }
1307 ]
1308 }"#
1309 .unindent(),
1310 ),
1311 );
1312 }
1313
1314 #[test]
1315 fn test_flatten_code_action_formatters_basic_object() {
1316 assert_migrate_settings(
1317 &r#"{
1318 "formatter": {
1319 "code_actions": {
1320 "included-1": true,
1321 "excluded": false,
1322 "included-2": true
1323 }
1324 }
1325 }"#
1326 .unindent(),
1327 Some(
1328 &r#"{
1329 "formatter": [
1330 { "code_action": "included-1" },
1331 { "code_action": "included-2" }
1332 ]
1333 }"#
1334 .unindent(),
1335 ),
1336 );
1337 }
1338
1339 #[test]
1340 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1341 assert_migrate_settings(
1342 r#"{
1343 "formatter": [
1344 {
1345 "code_actions": {
1346 "included-1": true,
1347 "included-2": true,
1348 "excluded": false,
1349 }
1350 },
1351 {
1352 "language_server": "ruff"
1353 },
1354 {
1355 "code_actions": {
1356 "excluded": false,
1357 "excluded-2": false,
1358 }
1359 }
1360 // some comment
1361 ,
1362 {
1363 "code_actions": {
1364 "excluded": false,
1365 "included-3": true,
1366 "included-4": true,
1367 }
1368 },
1369 ]
1370 }"#,
1371 Some(
1372 r#"{
1373 "formatter": [
1374 { "code_action": "included-1" },
1375 { "code_action": "included-2" },
1376 {
1377 "language_server": "ruff"
1378 },
1379 { "code_action": "included-3" },
1380 { "code_action": "included-4" },
1381 ]
1382 }"#,
1383 ),
1384 );
1385 }
1386
1387 #[test]
1388 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1389 assert_migrate_settings(
1390 &r#"{
1391 "languages": {
1392 "Rust": {
1393 "formatter": [
1394 {
1395 "code_actions": {
1396 "included-1": true,
1397 "included-2": true,
1398 "excluded": false,
1399 }
1400 },
1401 {
1402 "language_server": "ruff"
1403 },
1404 {
1405 "code_actions": {
1406 "excluded": false,
1407 "excluded-2": false,
1408 }
1409 }
1410 // some comment
1411 ,
1412 {
1413 "code_actions": {
1414 "excluded": false,
1415 "included-3": true,
1416 "included-4": true,
1417 }
1418 },
1419 ]
1420 }
1421 }
1422 }"#
1423 .unindent(),
1424 Some(
1425 &r#"{
1426 "languages": {
1427 "Rust": {
1428 "formatter": [
1429 { "code_action": "included-1" },
1430 { "code_action": "included-2" },
1431 {
1432 "language_server": "ruff"
1433 },
1434 { "code_action": "included-3" },
1435 { "code_action": "included-4" },
1436 ]
1437 }
1438 }
1439 }"#
1440 .unindent(),
1441 ),
1442 );
1443 }
1444
1445 #[test]
1446 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1447 {
1448 assert_migrate_settings(
1449 &r#"{
1450 "formatter": {
1451 "code_actions": {
1452 "default-1": true,
1453 "default-2": true,
1454 "default-3": true,
1455 "default-4": true,
1456 }
1457 }
1458 "languages": {
1459 "Rust": {
1460 "formatter": [
1461 {
1462 "code_actions": {
1463 "included-1": true,
1464 "included-2": true,
1465 "excluded": false,
1466 }
1467 },
1468 {
1469 "language_server": "ruff"
1470 },
1471 {
1472 "code_actions": {
1473 "excluded": false,
1474 "excluded-2": false,
1475 }
1476 }
1477 // some comment
1478 ,
1479 {
1480 "code_actions": {
1481 "excluded": false,
1482 "included-3": true,
1483 "included-4": true,
1484 }
1485 },
1486 ]
1487 },
1488 "Python": {
1489 "formatter": [
1490 {
1491 "language_server": "ruff"
1492 },
1493 {
1494 "code_actions": {
1495 "excluded": false,
1496 "excluded-2": false,
1497 }
1498 }
1499 // some comment
1500 ,
1501 {
1502 "code_actions": {
1503 "excluded": false,
1504 "included-3": true,
1505 "included-4": true,
1506 }
1507 },
1508 ]
1509 }
1510 }
1511 }"#
1512 .unindent(),
1513 Some(
1514 &r#"{
1515 "formatter": [
1516 { "code_action": "default-1" },
1517 { "code_action": "default-2" },
1518 { "code_action": "default-3" },
1519 { "code_action": "default-4" }
1520 ]
1521 "languages": {
1522 "Rust": {
1523 "formatter": [
1524 { "code_action": "included-1" },
1525 { "code_action": "included-2" },
1526 {
1527 "language_server": "ruff"
1528 },
1529 { "code_action": "included-3" },
1530 { "code_action": "included-4" },
1531 ]
1532 },
1533 "Python": {
1534 "formatter": [
1535 {
1536 "language_server": "ruff"
1537 },
1538 { "code_action": "included-3" },
1539 { "code_action": "included-4" },
1540 ]
1541 }
1542 }
1543 }"#
1544 .unindent(),
1545 ),
1546 );
1547 }
1548
1549 #[test]
1550 fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1551 assert_migrate_settings(
1552 &r#"{
1553 "formatter": {
1554 "code_actions": {
1555 "default-1": true,
1556 "default-2": true,
1557 "default-3": true,
1558 "default-4": true,
1559 }
1560 },
1561 "format_on_save": [
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 "languages": {
1589 "Rust": {
1590 "format_on_save": "prettier",
1591 "formatter": [
1592 {
1593 "code_actions": {
1594 "included-1": true,
1595 "included-2": true,
1596 "excluded": false,
1597 }
1598 },
1599 {
1600 "language_server": "ruff"
1601 },
1602 {
1603 "code_actions": {
1604 "excluded": false,
1605 "excluded-2": false,
1606 }
1607 }
1608 // some comment
1609 ,
1610 {
1611 "code_actions": {
1612 "excluded": false,
1613 "included-3": true,
1614 "included-4": true,
1615 }
1616 },
1617 ]
1618 },
1619 "Python": {
1620 "format_on_save": {
1621 "code_actions": {
1622 "on-save-1": true,
1623 "on-save-2": true,
1624 }
1625 },
1626 "formatter": [
1627 {
1628 "language_server": "ruff"
1629 },
1630 {
1631 "code_actions": {
1632 "excluded": false,
1633 "excluded-2": false,
1634 }
1635 }
1636 // some comment
1637 ,
1638 {
1639 "code_actions": {
1640 "excluded": false,
1641 "included-3": true,
1642 "included-4": true,
1643 }
1644 },
1645 ]
1646 }
1647 }
1648 }"#
1649 .unindent(),
1650 Some(
1651 &r#"{
1652 "formatter": [
1653 { "code_action": "default-1" },
1654 { "code_action": "default-2" },
1655 { "code_action": "default-3" },
1656 { "code_action": "default-4" }
1657 ],
1658 "format_on_save": [
1659 { "code_action": "included-1" },
1660 { "code_action": "included-2" },
1661 {
1662 "language_server": "ruff"
1663 },
1664 { "code_action": "included-3" },
1665 { "code_action": "included-4" },
1666 ],
1667 "languages": {
1668 "Rust": {
1669 "format_on_save": "prettier",
1670 "formatter": [
1671 { "code_action": "included-1" },
1672 { "code_action": "included-2" },
1673 {
1674 "language_server": "ruff"
1675 },
1676 { "code_action": "included-3" },
1677 { "code_action": "included-4" },
1678 ]
1679 },
1680 "Python": {
1681 "format_on_save": [
1682 { "code_action": "on-save-1" },
1683 { "code_action": "on-save-2" }
1684 ],
1685 "formatter": [
1686 {
1687 "language_server": "ruff"
1688 },
1689 { "code_action": "included-3" },
1690 { "code_action": "included-4" },
1691 ]
1692 }
1693 }
1694 }"#
1695 .unindent(),
1696 ),
1697 );
1698 }
1699}