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 run_migrations(text, migrations)
165}
166
167pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
168 migrate(
169 &text,
170 &[(
171 SETTINGS_NESTED_KEY_VALUE_PATTERN,
172 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
173 )],
174 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
175 )
176}
177
178pub type MigrationPatterns = &'static [(
179 &'static str,
180 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
181)];
182
183macro_rules! define_query {
184 ($var_name:ident, $patterns_path:path) => {
185 static $var_name: LazyLock<Query> = LazyLock::new(|| {
186 Query::new(
187 &tree_sitter_json::LANGUAGE.into(),
188 &$patterns_path
189 .iter()
190 .map(|pattern| pattern.0)
191 .collect::<String>(),
192 )
193 .unwrap()
194 });
195 };
196}
197
198// keymap
199define_query!(
200 KEYMAP_QUERY_2025_01_29,
201 migrations::m_2025_01_29::KEYMAP_PATTERNS
202);
203define_query!(
204 KEYMAP_QUERY_2025_01_30,
205 migrations::m_2025_01_30::KEYMAP_PATTERNS
206);
207define_query!(
208 KEYMAP_QUERY_2025_03_03,
209 migrations::m_2025_03_03::KEYMAP_PATTERNS
210);
211define_query!(
212 KEYMAP_QUERY_2025_03_06,
213 migrations::m_2025_03_06::KEYMAP_PATTERNS
214);
215define_query!(
216 KEYMAP_QUERY_2025_04_15,
217 migrations::m_2025_04_15::KEYMAP_PATTERNS
218);
219
220// settings
221define_query!(
222 SETTINGS_QUERY_2025_01_02,
223 migrations::m_2025_01_02::SETTINGS_PATTERNS
224);
225define_query!(
226 SETTINGS_QUERY_2025_01_29,
227 migrations::m_2025_01_29::SETTINGS_PATTERNS
228);
229define_query!(
230 SETTINGS_QUERY_2025_01_30,
231 migrations::m_2025_01_30::SETTINGS_PATTERNS
232);
233define_query!(
234 SETTINGS_QUERY_2025_03_29,
235 migrations::m_2025_03_29::SETTINGS_PATTERNS
236);
237define_query!(
238 SETTINGS_QUERY_2025_04_15,
239 migrations::m_2025_04_15::SETTINGS_PATTERNS
240);
241define_query!(
242 SETTINGS_QUERY_2025_04_21,
243 migrations::m_2025_04_21::SETTINGS_PATTERNS
244);
245define_query!(
246 SETTINGS_QUERY_2025_04_23,
247 migrations::m_2025_04_23::SETTINGS_PATTERNS
248);
249define_query!(
250 SETTINGS_QUERY_2025_05_05,
251 migrations::m_2025_05_05::SETTINGS_PATTERNS
252);
253define_query!(
254 SETTINGS_QUERY_2025_05_08,
255 migrations::m_2025_05_08::SETTINGS_PATTERNS
256);
257define_query!(
258 SETTINGS_QUERY_2025_05_29,
259 migrations::m_2025_05_29::SETTINGS_PATTERNS
260);
261define_query!(
262 SETTINGS_QUERY_2025_06_16,
263 migrations::m_2025_06_16::SETTINGS_PATTERNS
264);
265define_query!(
266 SETTINGS_QUERY_2025_06_25,
267 migrations::m_2025_06_25::SETTINGS_PATTERNS
268);
269define_query!(
270 SETTINGS_QUERY_2025_06_27,
271 migrations::m_2025_06_27::SETTINGS_PATTERNS
272);
273
274// custom query
275static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
276 Query::new(
277 &tree_sitter_json::LANGUAGE.into(),
278 SETTINGS_NESTED_KEY_VALUE_PATTERN,
279 )
280 .unwrap()
281});
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
288 let migrated = migrate_keymap(&input).unwrap();
289 pretty_assertions::assert_eq!(migrated.as_deref(), output);
290 }
291
292 fn assert_migrate_settings(input: &str, output: Option<&str>) {
293 let migrated = migrate_settings(&input).unwrap();
294 pretty_assertions::assert_eq!(migrated.as_deref(), output);
295 }
296
297 fn assert_migrate_settings_with_migrations(
298 migrations: &[(MigrationPatterns, &Query)],
299 input: &str,
300 output: Option<&str>,
301 ) {
302 let migrated = run_migrations(input, migrations).unwrap();
303 pretty_assertions::assert_eq!(migrated.as_deref(), output);
304 }
305
306 #[test]
307 fn test_replace_array_with_single_string() {
308 assert_migrate_keymap(
309 r#"
310 [
311 {
312 "bindings": {
313 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
314 }
315 }
316 ]
317 "#,
318 Some(
319 r#"
320 [
321 {
322 "bindings": {
323 "cmd-1": "workspace::ActivatePaneUp"
324 }
325 }
326 ]
327 "#,
328 ),
329 )
330 }
331
332 #[test]
333 fn test_replace_action_argument_object_with_single_value() {
334 assert_migrate_keymap(
335 r#"
336 [
337 {
338 "bindings": {
339 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
340 }
341 }
342 ]
343 "#,
344 Some(
345 r#"
346 [
347 {
348 "bindings": {
349 "cmd-1": ["editor::FoldAtLevel", 1]
350 }
351 }
352 ]
353 "#,
354 ),
355 )
356 }
357
358 #[test]
359 fn test_replace_action_argument_object_with_single_value_2() {
360 assert_migrate_keymap(
361 r#"
362 [
363 {
364 "bindings": {
365 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
366 }
367 }
368 ]
369 "#,
370 Some(
371 r#"
372 [
373 {
374 "bindings": {
375 "cmd-1": ["vim::PushObject", { "some" : "value" }]
376 }
377 }
378 ]
379 "#,
380 ),
381 )
382 }
383
384 #[test]
385 fn test_rename_string_action() {
386 assert_migrate_keymap(
387 r#"
388 [
389 {
390 "bindings": {
391 "cmd-1": "inline_completion::ToggleMenu"
392 }
393 }
394 ]
395 "#,
396 Some(
397 r#"
398 [
399 {
400 "bindings": {
401 "cmd-1": "edit_prediction::ToggleMenu"
402 }
403 }
404 ]
405 "#,
406 ),
407 )
408 }
409
410 #[test]
411 fn test_rename_context_key() {
412 assert_migrate_keymap(
413 r#"
414 [
415 {
416 "context": "Editor && inline_completion && !showing_completions"
417 }
418 ]
419 "#,
420 Some(
421 r#"
422 [
423 {
424 "context": "Editor && edit_prediction && !showing_completions"
425 }
426 ]
427 "#,
428 ),
429 )
430 }
431
432 #[test]
433 fn test_incremental_migrations() {
434 // Here string transforms to array internally. Then, that array transforms back to string.
435 assert_migrate_keymap(
436 r#"
437 [
438 {
439 "bindings": {
440 "ctrl-q": "editor::GoToHunk", // should remain same
441 "ctrl-w": "editor::GoToPrevHunk", // should rename
442 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
443 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
444 }
445 }
446 ]
447 "#,
448 Some(
449 r#"
450 [
451 {
452 "bindings": {
453 "ctrl-q": "editor::GoToHunk", // should remain same
454 "ctrl-w": "editor::GoToPreviousHunk", // should rename
455 "ctrl-q": "editor::GoToHunk", // should transform
456 "ctrl-w": "editor::GoToPreviousHunk" // should transform
457 }
458 }
459 ]
460 "#,
461 ),
462 )
463 }
464
465 #[test]
466 fn test_action_argument_snake_case() {
467 // First performs transformations, then replacements
468 assert_migrate_keymap(
469 r#"
470 [
471 {
472 "bindings": {
473 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
474 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
475 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
476 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
477 }
478 }
479 ]
480 "#,
481 Some(
482 r#"
483 [
484 {
485 "bindings": {
486 "cmd-1": ["vim::PushObject", { "around": false }],
487 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
488 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
489 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
490 }
491 }
492 ]
493 "#,
494 ),
495 )
496 }
497
498 #[test]
499 fn test_replace_setting_name() {
500 assert_migrate_settings(
501 r#"
502 {
503 "show_inline_completions_in_menu": true,
504 "show_inline_completions": true,
505 "inline_completions_disabled_in": ["string"],
506 "inline_completions": { "some" : "value" }
507 }
508 "#,
509 Some(
510 r#"
511 {
512 "show_edit_predictions_in_menu": true,
513 "show_edit_predictions": true,
514 "edit_predictions_disabled_in": ["string"],
515 "edit_predictions": { "some" : "value" }
516 }
517 "#,
518 ),
519 )
520 }
521
522 #[test]
523 fn test_nested_string_replace_for_settings() {
524 assert_migrate_settings(
525 r#"
526 {
527 "features": {
528 "inline_completion_provider": "zed"
529 },
530 }
531 "#,
532 Some(
533 r#"
534 {
535 "features": {
536 "edit_prediction_provider": "zed"
537 },
538 }
539 "#,
540 ),
541 )
542 }
543
544 #[test]
545 fn test_replace_settings_in_languages() {
546 assert_migrate_settings(
547 r#"
548 {
549 "languages": {
550 "Astro": {
551 "show_inline_completions": true
552 }
553 }
554 }
555 "#,
556 Some(
557 r#"
558 {
559 "languages": {
560 "Astro": {
561 "show_edit_predictions": true
562 }
563 }
564 }
565 "#,
566 ),
567 )
568 }
569
570 #[test]
571 fn test_replace_settings_value() {
572 assert_migrate_settings(
573 r#"
574 {
575 "scrollbar": {
576 "diagnostics": true
577 },
578 "chat_panel": {
579 "button": true
580 }
581 }
582 "#,
583 Some(
584 r#"
585 {
586 "scrollbar": {
587 "diagnostics": "all"
588 },
589 "chat_panel": {
590 "button": "always"
591 }
592 }
593 "#,
594 ),
595 )
596 }
597
598 #[test]
599 fn test_replace_settings_name_and_value() {
600 assert_migrate_settings(
601 r#"
602 {
603 "tabs": {
604 "always_show_close_button": true
605 }
606 }
607 "#,
608 Some(
609 r#"
610 {
611 "tabs": {
612 "show_close_button": "always"
613 }
614 }
615 "#,
616 ),
617 )
618 }
619
620 #[test]
621 fn test_replace_bash_with_terminal_in_profiles() {
622 assert_migrate_settings(
623 r#"
624 {
625 "assistant": {
626 "profiles": {
627 "custom": {
628 "name": "Custom",
629 "tools": {
630 "bash": true,
631 "diagnostics": true
632 }
633 }
634 }
635 }
636 }
637 "#,
638 Some(
639 r#"
640 {
641 "agent": {
642 "profiles": {
643 "custom": {
644 "name": "Custom",
645 "tools": {
646 "terminal": true,
647 "diagnostics": true
648 }
649 }
650 }
651 }
652 }
653 "#,
654 ),
655 )
656 }
657
658 #[test]
659 fn test_replace_bash_false_with_terminal_in_profiles() {
660 assert_migrate_settings(
661 r#"
662 {
663 "assistant": {
664 "profiles": {
665 "custom": {
666 "name": "Custom",
667 "tools": {
668 "bash": false,
669 "diagnostics": true
670 }
671 }
672 }
673 }
674 }
675 "#,
676 Some(
677 r#"
678 {
679 "agent": {
680 "profiles": {
681 "custom": {
682 "name": "Custom",
683 "tools": {
684 "terminal": false,
685 "diagnostics": true
686 }
687 }
688 }
689 }
690 }
691 "#,
692 ),
693 )
694 }
695
696 #[test]
697 fn test_no_bash_in_profiles() {
698 assert_migrate_settings(
699 r#"
700 {
701 "assistant": {
702 "profiles": {
703 "custom": {
704 "name": "Custom",
705 "tools": {
706 "diagnostics": true,
707 "find_path": true,
708 "read_file": true
709 }
710 }
711 }
712 }
713 }
714 "#,
715 Some(
716 r#"
717 {
718 "agent": {
719 "profiles": {
720 "custom": {
721 "name": "Custom",
722 "tools": {
723 "diagnostics": true,
724 "find_path": true,
725 "read_file": true
726 }
727 }
728 }
729 }
730 }
731 "#,
732 ),
733 )
734 }
735
736 #[test]
737 fn test_rename_path_search_to_find_path() {
738 assert_migrate_settings(
739 r#"
740 {
741 "assistant": {
742 "profiles": {
743 "default": {
744 "tools": {
745 "path_search": true,
746 "read_file": true
747 }
748 }
749 }
750 }
751 }
752 "#,
753 Some(
754 r#"
755 {
756 "agent": {
757 "profiles": {
758 "default": {
759 "tools": {
760 "find_path": true,
761 "read_file": true
762 }
763 }
764 }
765 }
766 }
767 "#,
768 ),
769 );
770 }
771
772 #[test]
773 fn test_rename_assistant() {
774 assert_migrate_settings(
775 r#"{
776 "assistant": {
777 "foo": "bar"
778 },
779 "edit_predictions": {
780 "enabled_in_assistant": false,
781 }
782 }"#,
783 Some(
784 r#"{
785 "agent": {
786 "foo": "bar"
787 },
788 "edit_predictions": {
789 "enabled_in_text_threads": false,
790 }
791 }"#,
792 ),
793 );
794 }
795
796 #[test]
797 fn test_comment_duplicated_agent() {
798 assert_migrate_settings(
799 r#"{
800 "agent": {
801 "name": "assistant-1",
802 "model": "gpt-4", // weird formatting
803 "utf8": "привіт"
804 },
805 "something": "else",
806 "agent": {
807 "name": "assistant-2",
808 "model": "gemini-pro"
809 }
810 }
811 "#,
812 Some(
813 r#"{
814 /* Duplicated key auto-commented: "agent": {
815 "name": "assistant-1",
816 "model": "gpt-4", // weird formatting
817 "utf8": "привіт"
818 }, */
819 "something": "else",
820 "agent": {
821 "name": "assistant-2",
822 "model": "gemini-pro"
823 }
824 }
825 "#,
826 ),
827 );
828 }
829
830 #[test]
831 fn test_preferred_completion_mode_migration() {
832 assert_migrate_settings(
833 r#"{
834 "agent": {
835 "preferred_completion_mode": "max",
836 "enabled": true
837 }
838 }"#,
839 Some(
840 r#"{
841 "agent": {
842 "preferred_completion_mode": "burn",
843 "enabled": true
844 }
845 }"#,
846 ),
847 );
848
849 assert_migrate_settings(
850 r#"{
851 "agent": {
852 "preferred_completion_mode": "normal",
853 "enabled": true
854 }
855 }"#,
856 None,
857 );
858
859 assert_migrate_settings(
860 r#"{
861 "agent": {
862 "preferred_completion_mode": "burn",
863 "enabled": true
864 }
865 }"#,
866 None,
867 );
868
869 assert_migrate_settings(
870 r#"{
871 "other_section": {
872 "preferred_completion_mode": "max"
873 },
874 "agent": {
875 "preferred_completion_mode": "max"
876 }
877 }"#,
878 Some(
879 r#"{
880 "other_section": {
881 "preferred_completion_mode": "max"
882 },
883 "agent": {
884 "preferred_completion_mode": "burn"
885 }
886 }"#,
887 ),
888 );
889 }
890
891 #[test]
892 fn test_mcp_settings_migration() {
893 assert_migrate_settings_with_migrations(
894 &[(
895 migrations::m_2025_06_16::SETTINGS_PATTERNS,
896 &SETTINGS_QUERY_2025_06_16,
897 )],
898 r#"{
899 "context_servers": {
900 "empty_server": {},
901 "extension_server": {
902 "settings": {
903 "foo": "bar"
904 }
905 },
906 "custom_server": {
907 "command": {
908 "path": "foo",
909 "args": ["bar"],
910 "env": {
911 "FOO": "BAR"
912 }
913 }
914 },
915 "invalid_server": {
916 "command": {
917 "path": "foo",
918 "args": ["bar"],
919 "env": {
920 "FOO": "BAR"
921 }
922 },
923 "settings": {
924 "foo": "bar"
925 }
926 },
927 "empty_server2": {},
928 "extension_server2": {
929 "foo": "bar",
930 "settings": {
931 "foo": "bar"
932 },
933 "bar": "foo"
934 },
935 "custom_server2": {
936 "foo": "bar",
937 "command": {
938 "path": "foo",
939 "args": ["bar"],
940 "env": {
941 "FOO": "BAR"
942 }
943 },
944 "bar": "foo"
945 },
946 "invalid_server2": {
947 "foo": "bar",
948 "command": {
949 "path": "foo",
950 "args": ["bar"],
951 "env": {
952 "FOO": "BAR"
953 }
954 },
955 "bar": "foo",
956 "settings": {
957 "foo": "bar"
958 }
959 }
960 }
961}"#,
962 Some(
963 r#"{
964 "context_servers": {
965 "empty_server": {
966 "source": "extension",
967 "settings": {}
968 },
969 "extension_server": {
970 "source": "extension",
971 "settings": {
972 "foo": "bar"
973 }
974 },
975 "custom_server": {
976 "source": "custom",
977 "command": {
978 "path": "foo",
979 "args": ["bar"],
980 "env": {
981 "FOO": "BAR"
982 }
983 }
984 },
985 "invalid_server": {
986 "source": "custom",
987 "command": {
988 "path": "foo",
989 "args": ["bar"],
990 "env": {
991 "FOO": "BAR"
992 }
993 },
994 "settings": {
995 "foo": "bar"
996 }
997 },
998 "empty_server2": {
999 "source": "extension",
1000 "settings": {}
1001 },
1002 "extension_server2": {
1003 "source": "extension",
1004 "foo": "bar",
1005 "settings": {
1006 "foo": "bar"
1007 },
1008 "bar": "foo"
1009 },
1010 "custom_server2": {
1011 "source": "custom",
1012 "foo": "bar",
1013 "command": {
1014 "path": "foo",
1015 "args": ["bar"],
1016 "env": {
1017 "FOO": "BAR"
1018 }
1019 },
1020 "bar": "foo"
1021 },
1022 "invalid_server2": {
1023 "source": "custom",
1024 "foo": "bar",
1025 "command": {
1026 "path": "foo",
1027 "args": ["bar"],
1028 "env": {
1029 "FOO": "BAR"
1030 }
1031 },
1032 "bar": "foo",
1033 "settings": {
1034 "foo": "bar"
1035 }
1036 }
1037 }
1038}"#,
1039 ),
1040 );
1041 }
1042
1043 #[test]
1044 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1045 let settings = r#"{
1046 "context_servers": {
1047 "empty_server": {
1048 "source": "extension",
1049 "settings": {}
1050 },
1051 "extension_server": {
1052 "source": "extension",
1053 "settings": {
1054 "foo": "bar"
1055 }
1056 },
1057 "custom_server": {
1058 "source": "custom",
1059 "command": {
1060 "path": "foo",
1061 "args": ["bar"],
1062 "env": {
1063 "FOO": "BAR"
1064 }
1065 }
1066 },
1067 "invalid_server": {
1068 "source": "custom",
1069 "command": {
1070 "path": "foo",
1071 "args": ["bar"],
1072 "env": {
1073 "FOO": "BAR"
1074 }
1075 },
1076 "settings": {
1077 "foo": "bar"
1078 }
1079 }
1080 }
1081}"#;
1082 assert_migrate_settings_with_migrations(
1083 &[(
1084 migrations::m_2025_06_16::SETTINGS_PATTERNS,
1085 &SETTINGS_QUERY_2025_06_16,
1086 )],
1087 settings,
1088 None,
1089 );
1090 }
1091
1092 #[test]
1093 fn test_remove_version_fields() {
1094 assert_migrate_settings(
1095 r#"{
1096 "language_models": {
1097 "anthropic": {
1098 "version": "1",
1099 "api_url": "https://api.anthropic.com"
1100 },
1101 "openai": {
1102 "version": "1",
1103 "api_url": "https://api.openai.com/v1"
1104 }
1105 },
1106 "agent": {
1107 "version": "2",
1108 "enabled": true,
1109 "preferred_completion_mode": "normal",
1110 "button": true,
1111 "dock": "right",
1112 "default_width": 640,
1113 "default_height": 320,
1114 "default_model": {
1115 "provider": "zed.dev",
1116 "model": "claude-sonnet-4"
1117 }
1118 }
1119}"#,
1120 Some(
1121 r#"{
1122 "language_models": {
1123 "anthropic": {
1124 "api_url": "https://api.anthropic.com"
1125 },
1126 "openai": {
1127 "api_url": "https://api.openai.com/v1"
1128 }
1129 },
1130 "agent": {
1131 "enabled": true,
1132 "preferred_completion_mode": "normal",
1133 "button": true,
1134 "dock": "right",
1135 "default_width": 640,
1136 "default_height": 320,
1137 "default_model": {
1138 "provider": "zed.dev",
1139 "model": "claude-sonnet-4"
1140 }
1141 }
1142}"#,
1143 ),
1144 );
1145
1146 // Test that version fields in other contexts are not removed
1147 assert_migrate_settings(
1148 r#"{
1149 "language_models": {
1150 "other_provider": {
1151 "version": "1",
1152 "api_url": "https://api.example.com"
1153 }
1154 },
1155 "other_section": {
1156 "version": "1"
1157 }
1158}"#,
1159 None,
1160 );
1161 }
1162
1163 #[test]
1164 fn test_flatten_context_server_command() {
1165 assert_migrate_settings(
1166 r#"{
1167 "context_servers": {
1168 "some-mcp-server": {
1169 "source": "custom",
1170 "command": {
1171 "path": "npx",
1172 "args": [
1173 "-y",
1174 "@supabase/mcp-server-supabase@latest",
1175 "--read-only",
1176 "--project-ref=<project-ref>"
1177 ],
1178 "env": {
1179 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1180 }
1181 }
1182 }
1183 }
1184}"#,
1185 Some(
1186 r#"{
1187 "context_servers": {
1188 "some-mcp-server": {
1189 "source": "custom",
1190 "command": "npx",
1191 "args": [
1192 "-y",
1193 "@supabase/mcp-server-supabase@latest",
1194 "--read-only",
1195 "--project-ref=<project-ref>"
1196 ],
1197 "env": {
1198 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1199 }
1200 }
1201 }
1202}"#,
1203 ),
1204 );
1205
1206 // Test with additional keys in server object
1207 assert_migrate_settings(
1208 r#"{
1209 "context_servers": {
1210 "server-with-extras": {
1211 "source": "custom",
1212 "command": {
1213 "path": "/usr/bin/node",
1214 "args": ["server.js"]
1215 },
1216 "settings": {}
1217 }
1218 }
1219}"#,
1220 Some(
1221 r#"{
1222 "context_servers": {
1223 "server-with-extras": {
1224 "source": "custom",
1225 "command": "/usr/bin/node",
1226 "args": ["server.js"],
1227 "settings": {}
1228 }
1229 }
1230}"#,
1231 ),
1232 );
1233
1234 // Test command without args or env
1235 assert_migrate_settings(
1236 r#"{
1237 "context_servers": {
1238 "simple-server": {
1239 "source": "custom",
1240 "command": {
1241 "path": "simple-mcp-server"
1242 }
1243 }
1244 }
1245}"#,
1246 Some(
1247 r#"{
1248 "context_servers": {
1249 "simple-server": {
1250 "source": "custom",
1251 "command": "simple-mcp-server"
1252 }
1253 }
1254}"#,
1255 ),
1256 );
1257 }
1258}