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