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