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