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