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 settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text};
19use std::{cmp::Reverse, ops::Range, sync::LazyLock};
20use streaming_iterator::StreamingIterator;
21use tree_sitter::{Query, QueryMatch};
22
23use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
24
25mod migrations;
26mod patterns;
27
28fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
29 let mut parser = tree_sitter::Parser::new();
30 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
31 let syntax_tree = parser
32 .parse(text, None)
33 .context("failed to parse settings")?;
34
35 let mut cursor = tree_sitter::QueryCursor::new();
36 let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
37
38 let mut edits = vec![];
39 while let Some(mat) = matches.next() {
40 if let Some((_, callback)) = patterns.get(mat.pattern_index) {
41 edits.extend(callback(text, mat, query));
42 }
43 }
44
45 edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
46 edits.dedup_by(|(range_b, _), (range_a, _)| {
47 range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
48 });
49
50 if edits.is_empty() {
51 Ok(None)
52 } else {
53 let mut new_text = text.to_string();
54 for (range, replacement) in edits.iter().rev() {
55 new_text.replace_range(range.clone(), replacement);
56 }
57 if new_text == text {
58 log::error!(
59 "Edits computed for configuration migration do not cause a change: {:?}",
60 edits
61 );
62 Ok(None)
63 } else {
64 Ok(Some(new_text))
65 }
66 }
67}
68
69/// Runs the provided migrations on the given text.
70/// Will automatically return `Ok(None)` if there's no content to migrate.
71fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<String>> {
72 if text.is_empty() {
73 return Ok(None);
74 }
75
76 let mut current_text = text.to_string();
77 let mut result: Option<String> = None;
78 let json_indent_size = infer_json_indent_size(¤t_text);
79 for migration in migrations.iter() {
80 let migrated_text = match migration {
81 MigrationType::TreeSitter(patterns, query) => migrate(¤t_text, patterns, query)?,
82 MigrationType::Json(callback) => {
83 if current_text.trim().is_empty() {
84 return Ok(None);
85 }
86 let old_content: serde_json_lenient::Value =
87 parse_json_with_comments(¤t_text)?;
88 let old_value = serde_json::to_value(&old_content).unwrap();
89 let mut new_value = old_value.clone();
90 callback(&mut new_value)?;
91 if new_value != old_value {
92 let mut current = current_text.clone();
93 let mut edits = vec![];
94 update_value_in_json_text(
95 &mut current,
96 &mut vec![],
97 json_indent_size,
98 &old_value,
99 &new_value,
100 &mut edits,
101 );
102 let mut migrated_text = current_text.clone();
103 for (range, replacement) in edits.into_iter() {
104 migrated_text.replace_range(range, &replacement);
105 }
106 Some(migrated_text)
107 } else {
108 None
109 }
110 }
111 };
112 if let Some(migrated_text) = migrated_text {
113 current_text = migrated_text.clone();
114 result = Some(migrated_text);
115 }
116 }
117 Ok(result.filter(|new_text| text != new_text))
118}
119
120pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
121 let migrations: &[MigrationType] = &[
122 MigrationType::TreeSitter(
123 migrations::m_2025_01_29::KEYMAP_PATTERNS,
124 &KEYMAP_QUERY_2025_01_29,
125 ),
126 MigrationType::TreeSitter(
127 migrations::m_2025_01_30::KEYMAP_PATTERNS,
128 &KEYMAP_QUERY_2025_01_30,
129 ),
130 MigrationType::TreeSitter(
131 migrations::m_2025_03_03::KEYMAP_PATTERNS,
132 &KEYMAP_QUERY_2025_03_03,
133 ),
134 MigrationType::TreeSitter(
135 migrations::m_2025_03_06::KEYMAP_PATTERNS,
136 &KEYMAP_QUERY_2025_03_06,
137 ),
138 MigrationType::TreeSitter(
139 migrations::m_2025_04_15::KEYMAP_PATTERNS,
140 &KEYMAP_QUERY_2025_04_15,
141 ),
142 MigrationType::TreeSitter(
143 migrations::m_2025_12_08::KEYMAP_PATTERNS,
144 &KEYMAP_QUERY_2025_12_08,
145 ),
146 ];
147 run_migrations(text, migrations)
148}
149
150enum MigrationType<'a> {
151 TreeSitter(MigrationPatterns, &'a Query),
152 Json(fn(&mut serde_json::Value) -> Result<()>),
153}
154
155pub fn migrate_settings(text: &str) -> Result<Option<String>> {
156 let migrations: &[MigrationType] = &[
157 MigrationType::TreeSitter(
158 migrations::m_2025_01_02::SETTINGS_PATTERNS,
159 &SETTINGS_QUERY_2025_01_02,
160 ),
161 MigrationType::TreeSitter(
162 migrations::m_2025_01_29::SETTINGS_PATTERNS,
163 &SETTINGS_QUERY_2025_01_29,
164 ),
165 MigrationType::TreeSitter(
166 migrations::m_2025_01_30::SETTINGS_PATTERNS,
167 &SETTINGS_QUERY_2025_01_30,
168 ),
169 MigrationType::TreeSitter(
170 migrations::m_2025_03_29::SETTINGS_PATTERNS,
171 &SETTINGS_QUERY_2025_03_29,
172 ),
173 MigrationType::TreeSitter(
174 migrations::m_2025_04_15::SETTINGS_PATTERNS,
175 &SETTINGS_QUERY_2025_04_15,
176 ),
177 MigrationType::TreeSitter(
178 migrations::m_2025_04_21::SETTINGS_PATTERNS,
179 &SETTINGS_QUERY_2025_04_21,
180 ),
181 MigrationType::TreeSitter(
182 migrations::m_2025_04_23::SETTINGS_PATTERNS,
183 &SETTINGS_QUERY_2025_04_23,
184 ),
185 MigrationType::TreeSitter(
186 migrations::m_2025_05_05::SETTINGS_PATTERNS,
187 &SETTINGS_QUERY_2025_05_05,
188 ),
189 MigrationType::TreeSitter(
190 migrations::m_2025_05_08::SETTINGS_PATTERNS,
191 &SETTINGS_QUERY_2025_05_08,
192 ),
193 MigrationType::TreeSitter(
194 migrations::m_2025_06_16::SETTINGS_PATTERNS,
195 &SETTINGS_QUERY_2025_06_16,
196 ),
197 MigrationType::TreeSitter(
198 migrations::m_2025_06_25::SETTINGS_PATTERNS,
199 &SETTINGS_QUERY_2025_06_25,
200 ),
201 MigrationType::TreeSitter(
202 migrations::m_2025_06_27::SETTINGS_PATTERNS,
203 &SETTINGS_QUERY_2025_06_27,
204 ),
205 MigrationType::TreeSitter(
206 migrations::m_2025_07_08::SETTINGS_PATTERNS,
207 &SETTINGS_QUERY_2025_07_08,
208 ),
209 MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters),
210 MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save),
211 MigrationType::TreeSitter(
212 migrations::m_2025_10_03::SETTINGS_PATTERNS,
213 &SETTINGS_QUERY_2025_10_03,
214 ),
215 MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
216 MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum),
217 MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum),
218 MigrationType::TreeSitter(
219 migrations::m_2025_11_12::SETTINGS_PATTERNS,
220 &SETTINGS_QUERY_2025_11_12,
221 ),
222 MigrationType::TreeSitter(
223 migrations::m_2025_12_01::SETTINGS_PATTERNS,
224 &SETTINGS_QUERY_2025_12_01,
225 ),
226 MigrationType::TreeSitter(
227 migrations::m_2025_11_20::SETTINGS_PATTERNS,
228 &SETTINGS_QUERY_2025_11_20,
229 ),
230 MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source),
231 MigrationType::TreeSitter(
232 migrations::m_2025_12_15::SETTINGS_PATTERNS,
233 &SETTINGS_QUERY_2025_12_15,
234 ),
235 MigrationType::Json(
236 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
237 ),
238 MigrationType::Json(migrations::m_2026_02_03::migrate_experimental_sweep_mercury),
239 ];
240 run_migrations(text, migrations)
241}
242
243pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
244 migrate(
245 text,
246 &[(
247 SETTINGS_NESTED_KEY_VALUE_PATTERN,
248 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
249 )],
250 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
251 )
252}
253
254pub type MigrationPatterns = &'static [(
255 &'static str,
256 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
257)];
258
259macro_rules! define_query {
260 ($var_name:ident, $patterns_path:path) => {
261 static $var_name: LazyLock<Query> = LazyLock::new(|| {
262 Query::new(
263 &tree_sitter_json::LANGUAGE.into(),
264 &$patterns_path
265 .iter()
266 .map(|pattern| pattern.0)
267 .collect::<String>(),
268 )
269 .unwrap()
270 });
271 };
272}
273
274// keymap
275define_query!(
276 KEYMAP_QUERY_2025_01_29,
277 migrations::m_2025_01_29::KEYMAP_PATTERNS
278);
279define_query!(
280 KEYMAP_QUERY_2025_01_30,
281 migrations::m_2025_01_30::KEYMAP_PATTERNS
282);
283define_query!(
284 KEYMAP_QUERY_2025_03_03,
285 migrations::m_2025_03_03::KEYMAP_PATTERNS
286);
287define_query!(
288 KEYMAP_QUERY_2025_03_06,
289 migrations::m_2025_03_06::KEYMAP_PATTERNS
290);
291define_query!(
292 KEYMAP_QUERY_2025_04_15,
293 migrations::m_2025_04_15::KEYMAP_PATTERNS
294);
295
296// settings
297define_query!(
298 SETTINGS_QUERY_2025_01_02,
299 migrations::m_2025_01_02::SETTINGS_PATTERNS
300);
301define_query!(
302 SETTINGS_QUERY_2025_01_29,
303 migrations::m_2025_01_29::SETTINGS_PATTERNS
304);
305define_query!(
306 SETTINGS_QUERY_2025_01_30,
307 migrations::m_2025_01_30::SETTINGS_PATTERNS
308);
309define_query!(
310 SETTINGS_QUERY_2025_03_29,
311 migrations::m_2025_03_29::SETTINGS_PATTERNS
312);
313define_query!(
314 SETTINGS_QUERY_2025_04_15,
315 migrations::m_2025_04_15::SETTINGS_PATTERNS
316);
317define_query!(
318 SETTINGS_QUERY_2025_04_21,
319 migrations::m_2025_04_21::SETTINGS_PATTERNS
320);
321define_query!(
322 SETTINGS_QUERY_2025_04_23,
323 migrations::m_2025_04_23::SETTINGS_PATTERNS
324);
325define_query!(
326 SETTINGS_QUERY_2025_05_05,
327 migrations::m_2025_05_05::SETTINGS_PATTERNS
328);
329define_query!(
330 SETTINGS_QUERY_2025_05_08,
331 migrations::m_2025_05_08::SETTINGS_PATTERNS
332);
333define_query!(
334 SETTINGS_QUERY_2025_06_16,
335 migrations::m_2025_06_16::SETTINGS_PATTERNS
336);
337define_query!(
338 SETTINGS_QUERY_2025_06_25,
339 migrations::m_2025_06_25::SETTINGS_PATTERNS
340);
341define_query!(
342 SETTINGS_QUERY_2025_06_27,
343 migrations::m_2025_06_27::SETTINGS_PATTERNS
344);
345define_query!(
346 SETTINGS_QUERY_2025_07_08,
347 migrations::m_2025_07_08::SETTINGS_PATTERNS
348);
349define_query!(
350 SETTINGS_QUERY_2025_10_03,
351 migrations::m_2025_10_03::SETTINGS_PATTERNS
352);
353define_query!(
354 SETTINGS_QUERY_2025_11_12,
355 migrations::m_2025_11_12::SETTINGS_PATTERNS
356);
357define_query!(
358 SETTINGS_QUERY_2025_12_01,
359 migrations::m_2025_12_01::SETTINGS_PATTERNS
360);
361define_query!(
362 SETTINGS_QUERY_2025_11_20,
363 migrations::m_2025_11_20::SETTINGS_PATTERNS
364);
365define_query!(
366 KEYMAP_QUERY_2025_12_08,
367 migrations::m_2025_12_08::KEYMAP_PATTERNS
368);
369define_query!(
370 SETTINGS_QUERY_2025_12_15,
371 migrations::m_2025_12_15::SETTINGS_PATTERNS
372);
373
374// custom query
375static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
376 Query::new(
377 &tree_sitter_json::LANGUAGE.into(),
378 SETTINGS_NESTED_KEY_VALUE_PATTERN,
379 )
380 .unwrap()
381});
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use unindent::Unindent as _;
387
388 #[track_caller]
389 fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
390 match (&migrated, &expected) {
391 (Some(migrated), Some(expected)) => {
392 pretty_assertions::assert_str_eq!(expected, migrated);
393 }
394 _ => {
395 pretty_assertions::assert_eq!(migrated.as_deref(), expected);
396 }
397 }
398 }
399
400 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
401 let migrated = migrate_keymap(input).unwrap();
402 pretty_assertions::assert_eq!(migrated.as_deref(), output);
403 }
404
405 #[track_caller]
406 fn assert_migrate_settings(input: &str, output: Option<&str>) {
407 let migrated = migrate_settings(input).unwrap();
408 assert_migrated_correctly(migrated.clone(), output);
409
410 // expect that rerunning the migration does not result in another migration
411 if let Some(migrated) = migrated {
412 let rerun = migrate_settings(&migrated).unwrap();
413 assert_migrated_correctly(rerun, None);
414 }
415 }
416
417 #[track_caller]
418 fn assert_migrate_settings_with_migrations(
419 migrations: &[MigrationType],
420 input: &str,
421 output: Option<&str>,
422 ) {
423 let migrated = run_migrations(input, migrations).unwrap();
424 assert_migrated_correctly(migrated.clone(), output);
425
426 // expect that rerunning the migration does not result in another migration
427 if let Some(migrated) = migrated {
428 let rerun = run_migrations(&migrated, migrations).unwrap();
429 assert_migrated_correctly(rerun, None);
430 }
431 }
432
433 #[test]
434 fn test_empty_content() {
435 assert_migrate_settings("", None)
436 }
437
438 #[test]
439 fn test_replace_array_with_single_string() {
440 assert_migrate_keymap(
441 r#"
442 [
443 {
444 "bindings": {
445 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
446 }
447 }
448 ]
449 "#,
450 Some(
451 r#"
452 [
453 {
454 "bindings": {
455 "cmd-1": "workspace::ActivatePaneUp"
456 }
457 }
458 ]
459 "#,
460 ),
461 )
462 }
463
464 #[test]
465 fn test_replace_action_argument_object_with_single_value() {
466 assert_migrate_keymap(
467 r#"
468 [
469 {
470 "bindings": {
471 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
472 }
473 }
474 ]
475 "#,
476 Some(
477 r#"
478 [
479 {
480 "bindings": {
481 "cmd-1": ["editor::FoldAtLevel", 1]
482 }
483 }
484 ]
485 "#,
486 ),
487 )
488 }
489
490 #[test]
491 fn test_replace_action_argument_object_with_single_value_2() {
492 assert_migrate_keymap(
493 r#"
494 [
495 {
496 "bindings": {
497 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
498 }
499 }
500 ]
501 "#,
502 Some(
503 r#"
504 [
505 {
506 "bindings": {
507 "cmd-1": ["vim::PushObject", { "some" : "value" }]
508 }
509 }
510 ]
511 "#,
512 ),
513 )
514 }
515
516 #[test]
517 fn test_rename_string_action() {
518 assert_migrate_keymap(
519 r#"
520 [
521 {
522 "bindings": {
523 "cmd-1": "inline_completion::ToggleMenu"
524 }
525 }
526 ]
527 "#,
528 Some(
529 r#"
530 [
531 {
532 "bindings": {
533 "cmd-1": "edit_prediction::ToggleMenu"
534 }
535 }
536 ]
537 "#,
538 ),
539 )
540 }
541
542 #[test]
543 fn test_rename_context_key() {
544 assert_migrate_keymap(
545 r#"
546 [
547 {
548 "context": "Editor && inline_completion && !showing_completions"
549 }
550 ]
551 "#,
552 Some(
553 r#"
554 [
555 {
556 "context": "Editor && edit_prediction && !showing_completions"
557 }
558 ]
559 "#,
560 ),
561 )
562 }
563
564 #[test]
565 fn test_incremental_migrations() {
566 // Here string transforms to array internally. Then, that array transforms back to string.
567 assert_migrate_keymap(
568 r#"
569 [
570 {
571 "bindings": {
572 "ctrl-q": "editor::GoToHunk", // should remain same
573 "ctrl-w": "editor::GoToPrevHunk", // should rename
574 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
575 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
576 }
577 }
578 ]
579 "#,
580 Some(
581 r#"
582 [
583 {
584 "bindings": {
585 "ctrl-q": "editor::GoToHunk", // should remain same
586 "ctrl-w": "editor::GoToPreviousHunk", // should rename
587 "ctrl-q": "editor::GoToHunk", // should transform
588 "ctrl-w": "editor::GoToPreviousHunk" // should transform
589 }
590 }
591 ]
592 "#,
593 ),
594 )
595 }
596
597 #[test]
598 fn test_action_argument_snake_case() {
599 // First performs transformations, then replacements
600 assert_migrate_keymap(
601 r#"
602 [
603 {
604 "bindings": {
605 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
606 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
607 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
608 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
609 }
610 }
611 ]
612 "#,
613 Some(
614 r#"
615 [
616 {
617 "bindings": {
618 "cmd-1": ["vim::PushObject", { "around": false }],
619 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
620 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
621 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
622 }
623 }
624 ]
625 "#,
626 ),
627 )
628 }
629
630 #[test]
631 fn test_replace_setting_name() {
632 assert_migrate_settings(
633 r#"
634 {
635 "show_inline_completions_in_menu": true,
636 "show_inline_completions": true,
637 "inline_completions_disabled_in": ["string"],
638 "inline_completions": { "some" : "value" }
639 }
640 "#,
641 Some(
642 r#"
643 {
644 "show_edit_predictions_in_menu": true,
645 "show_edit_predictions": true,
646 "edit_predictions_disabled_in": ["string"],
647 "edit_predictions": { "some" : "value" }
648 }
649 "#,
650 ),
651 )
652 }
653
654 #[test]
655 fn test_nested_string_replace_for_settings() {
656 assert_migrate_settings(
657 &r#"
658 {
659 "features": {
660 "inline_completion_provider": "zed"
661 },
662 }
663 "#
664 .unindent(),
665 Some(
666 &r#"
667 {
668 "edit_predictions": {
669 "provider": "zed"
670 }
671 }
672 "#
673 .unindent(),
674 ),
675 )
676 }
677
678 #[test]
679 fn test_replace_settings_in_languages() {
680 assert_migrate_settings(
681 r#"
682 {
683 "languages": {
684 "Astro": {
685 "show_inline_completions": true
686 }
687 }
688 }
689 "#,
690 Some(
691 r#"
692 {
693 "languages": {
694 "Astro": {
695 "show_edit_predictions": true
696 }
697 }
698 }
699 "#,
700 ),
701 )
702 }
703
704 #[test]
705 fn test_replace_settings_value() {
706 assert_migrate_settings(
707 r#"
708 {
709 "scrollbar": {
710 "diagnostics": true
711 },
712 "chat_panel": {
713 "button": true
714 }
715 }
716 "#,
717 Some(
718 r#"
719 {
720 "scrollbar": {
721 "diagnostics": "all"
722 },
723 "chat_panel": {
724 "button": "always"
725 }
726 }
727 "#,
728 ),
729 )
730 }
731
732 #[test]
733 fn test_replace_settings_name_and_value() {
734 assert_migrate_settings(
735 r#"
736 {
737 "tabs": {
738 "always_show_close_button": true
739 }
740 }
741 "#,
742 Some(
743 r#"
744 {
745 "tabs": {
746 "show_close_button": "always"
747 }
748 }
749 "#,
750 ),
751 )
752 }
753
754 #[test]
755 fn test_replace_bash_with_terminal_in_profiles() {
756 assert_migrate_settings(
757 r#"
758 {
759 "assistant": {
760 "profiles": {
761 "custom": {
762 "name": "Custom",
763 "tools": {
764 "bash": true,
765 "diagnostics": true
766 }
767 }
768 }
769 }
770 }
771 "#,
772 Some(
773 r#"
774 {
775 "agent": {
776 "profiles": {
777 "custom": {
778 "name": "Custom",
779 "tools": {
780 "terminal": true,
781 "diagnostics": true
782 }
783 }
784 }
785 }
786 }
787 "#,
788 ),
789 )
790 }
791
792 #[test]
793 fn test_replace_bash_false_with_terminal_in_profiles() {
794 assert_migrate_settings(
795 r#"
796 {
797 "assistant": {
798 "profiles": {
799 "custom": {
800 "name": "Custom",
801 "tools": {
802 "bash": false,
803 "diagnostics": true
804 }
805 }
806 }
807 }
808 }
809 "#,
810 Some(
811 r#"
812 {
813 "agent": {
814 "profiles": {
815 "custom": {
816 "name": "Custom",
817 "tools": {
818 "terminal": false,
819 "diagnostics": true
820 }
821 }
822 }
823 }
824 }
825 "#,
826 ),
827 )
828 }
829
830 #[test]
831 fn test_no_bash_in_profiles() {
832 assert_migrate_settings(
833 r#"
834 {
835 "assistant": {
836 "profiles": {
837 "custom": {
838 "name": "Custom",
839 "tools": {
840 "diagnostics": true,
841 "find_path": true,
842 "read_file": true
843 }
844 }
845 }
846 }
847 }
848 "#,
849 Some(
850 r#"
851 {
852 "agent": {
853 "profiles": {
854 "custom": {
855 "name": "Custom",
856 "tools": {
857 "diagnostics": true,
858 "find_path": true,
859 "read_file": true
860 }
861 }
862 }
863 }
864 }
865 "#,
866 ),
867 )
868 }
869
870 #[test]
871 fn test_rename_path_search_to_find_path() {
872 assert_migrate_settings(
873 r#"
874 {
875 "assistant": {
876 "profiles": {
877 "default": {
878 "tools": {
879 "path_search": true,
880 "read_file": true
881 }
882 }
883 }
884 }
885 }
886 "#,
887 Some(
888 r#"
889 {
890 "agent": {
891 "profiles": {
892 "default": {
893 "tools": {
894 "find_path": true,
895 "read_file": true
896 }
897 }
898 }
899 }
900 }
901 "#,
902 ),
903 );
904 }
905
906 #[test]
907 fn test_rename_assistant() {
908 assert_migrate_settings(
909 r#"{
910 "assistant": {
911 "foo": "bar"
912 },
913 "edit_predictions": {
914 "enabled_in_assistant": false,
915 }
916 }"#,
917 Some(
918 r#"{
919 "agent": {
920 "foo": "bar"
921 },
922 "edit_predictions": {
923 "enabled_in_text_threads": false,
924 }
925 }"#,
926 ),
927 );
928 }
929
930 #[test]
931 fn test_comment_duplicated_agent() {
932 assert_migrate_settings(
933 r#"{
934 "agent": {
935 "name": "assistant-1",
936 "model": "gpt-4", // weird formatting
937 "utf8": "привіт"
938 },
939 "something": "else",
940 "agent": {
941 "name": "assistant-2",
942 "model": "gemini-pro"
943 }
944 }
945 "#,
946 Some(
947 r#"{
948 /* Duplicated key auto-commented: "agent": {
949 "name": "assistant-1",
950 "model": "gpt-4", // weird formatting
951 "utf8": "привіт"
952 }, */
953 "something": "else",
954 "agent": {
955 "name": "assistant-2",
956 "model": "gemini-pro"
957 }
958 }
959 "#,
960 ),
961 );
962 }
963
964 #[test]
965 fn test_mcp_settings_migration() {
966 assert_migrate_settings_with_migrations(
967 &[MigrationType::TreeSitter(
968 migrations::m_2025_06_16::SETTINGS_PATTERNS,
969 &SETTINGS_QUERY_2025_06_16,
970 )],
971 r#"{
972 "context_servers": {
973 "empty_server": {},
974 "extension_server": {
975 "settings": {
976 "foo": "bar"
977 }
978 },
979 "custom_server": {
980 "command": {
981 "path": "foo",
982 "args": ["bar"],
983 "env": {
984 "FOO": "BAR"
985 }
986 }
987 },
988 "invalid_server": {
989 "command": {
990 "path": "foo",
991 "args": ["bar"],
992 "env": {
993 "FOO": "BAR"
994 }
995 },
996 "settings": {
997 "foo": "bar"
998 }
999 },
1000 "empty_server2": {},
1001 "extension_server2": {
1002 "foo": "bar",
1003 "settings": {
1004 "foo": "bar"
1005 },
1006 "bar": "foo"
1007 },
1008 "custom_server2": {
1009 "foo": "bar",
1010 "command": {
1011 "path": "foo",
1012 "args": ["bar"],
1013 "env": {
1014 "FOO": "BAR"
1015 }
1016 },
1017 "bar": "foo"
1018 },
1019 "invalid_server2": {
1020 "foo": "bar",
1021 "command": {
1022 "path": "foo",
1023 "args": ["bar"],
1024 "env": {
1025 "FOO": "BAR"
1026 }
1027 },
1028 "bar": "foo",
1029 "settings": {
1030 "foo": "bar"
1031 }
1032 }
1033 }
1034}"#,
1035 Some(
1036 r#"{
1037 "context_servers": {
1038 "empty_server": {
1039 "source": "extension",
1040 "settings": {}
1041 },
1042 "extension_server": {
1043 "source": "extension",
1044 "settings": {
1045 "foo": "bar"
1046 }
1047 },
1048 "custom_server": {
1049 "source": "custom",
1050 "command": {
1051 "path": "foo",
1052 "args": ["bar"],
1053 "env": {
1054 "FOO": "BAR"
1055 }
1056 }
1057 },
1058 "invalid_server": {
1059 "source": "custom",
1060 "command": {
1061 "path": "foo",
1062 "args": ["bar"],
1063 "env": {
1064 "FOO": "BAR"
1065 }
1066 },
1067 "settings": {
1068 "foo": "bar"
1069 }
1070 },
1071 "empty_server2": {
1072 "source": "extension",
1073 "settings": {}
1074 },
1075 "extension_server2": {
1076 "source": "extension",
1077 "foo": "bar",
1078 "settings": {
1079 "foo": "bar"
1080 },
1081 "bar": "foo"
1082 },
1083 "custom_server2": {
1084 "source": "custom",
1085 "foo": "bar",
1086 "command": {
1087 "path": "foo",
1088 "args": ["bar"],
1089 "env": {
1090 "FOO": "BAR"
1091 }
1092 },
1093 "bar": "foo"
1094 },
1095 "invalid_server2": {
1096 "source": "custom",
1097 "foo": "bar",
1098 "command": {
1099 "path": "foo",
1100 "args": ["bar"],
1101 "env": {
1102 "FOO": "BAR"
1103 }
1104 },
1105 "bar": "foo",
1106 "settings": {
1107 "foo": "bar"
1108 }
1109 }
1110 }
1111}"#,
1112 ),
1113 );
1114 }
1115
1116 #[test]
1117 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1118 let settings = r#"{
1119 "context_servers": {
1120 "empty_server": {
1121 "source": "extension",
1122 "settings": {}
1123 },
1124 "extension_server": {
1125 "source": "extension",
1126 "settings": {
1127 "foo": "bar"
1128 }
1129 },
1130 "custom_server": {
1131 "source": "custom",
1132 "command": {
1133 "path": "foo",
1134 "args": ["bar"],
1135 "env": {
1136 "FOO": "BAR"
1137 }
1138 }
1139 },
1140 "invalid_server": {
1141 "source": "custom",
1142 "command": {
1143 "path": "foo",
1144 "args": ["bar"],
1145 "env": {
1146 "FOO": "BAR"
1147 }
1148 },
1149 "settings": {
1150 "foo": "bar"
1151 }
1152 }
1153 }
1154}"#;
1155 assert_migrate_settings_with_migrations(
1156 &[MigrationType::TreeSitter(
1157 migrations::m_2025_06_16::SETTINGS_PATTERNS,
1158 &SETTINGS_QUERY_2025_06_16,
1159 )],
1160 settings,
1161 None,
1162 );
1163 }
1164
1165 #[test]
1166 fn test_custom_agent_server_settings_migration() {
1167 assert_migrate_settings_with_migrations(
1168 &[MigrationType::TreeSitter(
1169 migrations::m_2025_11_20::SETTINGS_PATTERNS,
1170 &SETTINGS_QUERY_2025_11_20,
1171 )],
1172 r#"{
1173 "agent_servers": {
1174 "gemini": {
1175 "default_model": "gemini-1.5-pro"
1176 },
1177 "claude": {},
1178 "codex": {},
1179 "my-custom-agent": {
1180 "command": "/path/to/agent",
1181 "args": ["--foo"],
1182 "default_model": "my-model"
1183 },
1184 "already-migrated-agent": {
1185 "type": "custom",
1186 "command": "/path/to/agent"
1187 },
1188 "future-extension-agent": {
1189 "type": "extension",
1190 "default_model": "ext-model"
1191 }
1192 }
1193}"#,
1194 Some(
1195 r#"{
1196 "agent_servers": {
1197 "gemini": {
1198 "default_model": "gemini-1.5-pro"
1199 },
1200 "claude": {},
1201 "codex": {},
1202 "my-custom-agent": {
1203 "type": "custom",
1204 "command": "/path/to/agent",
1205 "args": ["--foo"],
1206 "default_model": "my-model"
1207 },
1208 "already-migrated-agent": {
1209 "type": "custom",
1210 "command": "/path/to/agent"
1211 },
1212 "future-extension-agent": {
1213 "type": "extension",
1214 "default_model": "ext-model"
1215 }
1216 }
1217}"#,
1218 ),
1219 );
1220 }
1221
1222 #[test]
1223 fn test_remove_version_fields() {
1224 assert_migrate_settings(
1225 r#"{
1226 "language_models": {
1227 "anthropic": {
1228 "version": "1",
1229 "api_url": "https://api.anthropic.com"
1230 },
1231 "openai": {
1232 "version": "1",
1233 "api_url": "https://api.openai.com/v1"
1234 }
1235 },
1236 "agent": {
1237 "version": "2",
1238 "enabled": true,
1239 "button": true,
1240 "dock": "right",
1241 "default_width": 640,
1242 "default_height": 320,
1243 "default_model": {
1244 "provider": "zed.dev",
1245 "model": "claude-sonnet-4"
1246 }
1247 }
1248}"#,
1249 Some(
1250 r#"{
1251 "language_models": {
1252 "anthropic": {
1253 "api_url": "https://api.anthropic.com"
1254 },
1255 "openai": {
1256 "api_url": "https://api.openai.com/v1"
1257 }
1258 },
1259 "agent": {
1260 "enabled": true,
1261 "button": true,
1262 "dock": "right",
1263 "default_width": 640,
1264 "default_height": 320,
1265 "default_model": {
1266 "provider": "zed.dev",
1267 "model": "claude-sonnet-4"
1268 }
1269 }
1270}"#,
1271 ),
1272 );
1273
1274 // Test that version fields in other contexts are not removed
1275 assert_migrate_settings(
1276 r#"{
1277 "language_models": {
1278 "other_provider": {
1279 "version": "1",
1280 "api_url": "https://api.example.com"
1281 }
1282 },
1283 "other_section": {
1284 "version": "1"
1285 }
1286}"#,
1287 None,
1288 );
1289 }
1290
1291 #[test]
1292 fn test_flatten_context_server_command() {
1293 assert_migrate_settings(
1294 r#"{
1295 "context_servers": {
1296 "some-mcp-server": {
1297 "command": {
1298 "path": "npx",
1299 "args": [
1300 "-y",
1301 "@supabase/mcp-server-supabase@latest",
1302 "--read-only",
1303 "--project-ref=<project-ref>"
1304 ],
1305 "env": {
1306 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1307 }
1308 }
1309 }
1310 }
1311}"#,
1312 Some(
1313 r#"{
1314 "context_servers": {
1315 "some-mcp-server": {
1316 "command": "npx",
1317 "args": [
1318 "-y",
1319 "@supabase/mcp-server-supabase@latest",
1320 "--read-only",
1321 "--project-ref=<project-ref>"
1322 ],
1323 "env": {
1324 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1325 }
1326 }
1327 }
1328}"#,
1329 ),
1330 );
1331
1332 // Test with additional keys in server object
1333 assert_migrate_settings(
1334 r#"{
1335 "context_servers": {
1336 "server-with-extras": {
1337 "command": {
1338 "path": "/usr/bin/node",
1339 "args": ["server.js"]
1340 },
1341 "settings": {}
1342 }
1343 }
1344}"#,
1345 Some(
1346 r#"{
1347 "context_servers": {
1348 "server-with-extras": {
1349 "command": "/usr/bin/node",
1350 "args": ["server.js"],
1351 "settings": {}
1352 }
1353 }
1354}"#,
1355 ),
1356 );
1357
1358 // Test command without args or env
1359 assert_migrate_settings(
1360 r#"{
1361 "context_servers": {
1362 "simple-server": {
1363 "command": {
1364 "path": "simple-mcp-server"
1365 }
1366 }
1367 }
1368}"#,
1369 Some(
1370 r#"{
1371 "context_servers": {
1372 "simple-server": {
1373 "command": "simple-mcp-server"
1374 }
1375 }
1376}"#,
1377 ),
1378 );
1379 }
1380
1381 #[test]
1382 fn test_flatten_code_action_formatters_basic_array() {
1383 assert_migrate_settings_with_migrations(
1384 &[MigrationType::Json(
1385 migrations::m_2025_10_01::flatten_code_actions_formatters,
1386 )],
1387 &r#"{
1388 "formatter": [
1389 {
1390 "code_actions": {
1391 "included-1": true,
1392 "included-2": true,
1393 "excluded": false,
1394 }
1395 }
1396 ]
1397 }"#
1398 .unindent(),
1399 Some(
1400 &r#"{
1401 "formatter": [
1402 {
1403 "code_action": "included-1"
1404 },
1405 {
1406 "code_action": "included-2"
1407 }
1408 ]
1409 }"#
1410 .unindent(),
1411 ),
1412 );
1413 }
1414
1415 #[test]
1416 fn test_flatten_code_action_formatters_basic_object() {
1417 assert_migrate_settings_with_migrations(
1418 &[MigrationType::Json(
1419 migrations::m_2025_10_01::flatten_code_actions_formatters,
1420 )],
1421 &r#"{
1422 "formatter": {
1423 "code_actions": {
1424 "included-1": true,
1425 "excluded": false,
1426 "included-2": true
1427 }
1428 }
1429 }"#
1430 .unindent(),
1431 Some(
1432 &r#"{
1433 "formatter": [
1434 {
1435 "code_action": "included-1"
1436 },
1437 {
1438 "code_action": "included-2"
1439 }
1440 ]
1441 }"#
1442 .unindent(),
1443 ),
1444 );
1445 }
1446
1447 #[test]
1448 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1449 assert_migrate_settings(
1450 &r#"{
1451 "formatter": [
1452 {
1453 "code_actions": {
1454 "included-1": true,
1455 "included-2": true,
1456 "excluded": false,
1457 }
1458 },
1459 {
1460 "language_server": "ruff"
1461 },
1462 {
1463 "code_actions": {
1464 "excluded": false,
1465 "excluded-2": false,
1466 }
1467 }
1468 // some comment
1469 ,
1470 {
1471 "code_actions": {
1472 "excluded": false,
1473 "included-3": true,
1474 "included-4": true,
1475 }
1476 },
1477 ]
1478 }"#
1479 .unindent(),
1480 Some(
1481 &r#"{
1482 "formatter": [
1483 {
1484 "code_action": "included-1"
1485 },
1486 {
1487 "code_action": "included-2"
1488 },
1489 {
1490 "language_server": "ruff"
1491 },
1492 {
1493 "code_action": "included-3"
1494 },
1495 {
1496 "code_action": "included-4"
1497 }
1498 ]
1499 }"#
1500 .unindent(),
1501 ),
1502 );
1503 }
1504
1505 #[test]
1506 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1507 assert_migrate_settings(
1508 &r#"{
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 }
1540 }"#
1541 .unindent(),
1542 Some(
1543 &r#"{
1544 "languages": {
1545 "Rust": {
1546 "formatter": [
1547 {
1548 "code_action": "included-1"
1549 },
1550 {
1551 "code_action": "included-2"
1552 },
1553 {
1554 "language_server": "ruff"
1555 },
1556 {
1557 "code_action": "included-3"
1558 },
1559 {
1560 "code_action": "included-4"
1561 }
1562 ]
1563 }
1564 }
1565 }"#
1566 .unindent(),
1567 ),
1568 );
1569 }
1570
1571 #[test]
1572 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1573 {
1574 assert_migrate_settings_with_migrations(
1575 &[MigrationType::Json(
1576 migrations::m_2025_10_01::flatten_code_actions_formatters,
1577 )],
1578 &r#"{
1579 "formatter": {
1580 "code_actions": {
1581 "default-1": true,
1582 "default-2": true,
1583 "default-3": true,
1584 "default-4": true,
1585 }
1586 },
1587 "languages": {
1588 "Rust": {
1589 "formatter": [
1590 {
1591 "code_actions": {
1592 "included-1": true,
1593 "included-2": true,
1594 "excluded": false,
1595 }
1596 },
1597 {
1598 "language_server": "ruff"
1599 },
1600 {
1601 "code_actions": {
1602 "excluded": false,
1603 "excluded-2": false,
1604 }
1605 }
1606 // some comment
1607 ,
1608 {
1609 "code_actions": {
1610 "excluded": false,
1611 "included-3": true,
1612 "included-4": true,
1613 }
1614 },
1615 ]
1616 },
1617 "Python": {
1618 "formatter": [
1619 {
1620 "language_server": "ruff"
1621 },
1622 {
1623 "code_actions": {
1624 "excluded": false,
1625 "excluded-2": false,
1626 }
1627 }
1628 // some comment
1629 ,
1630 {
1631 "code_actions": {
1632 "excluded": false,
1633 "included-3": true,
1634 "included-4": true,
1635 }
1636 },
1637 ]
1638 }
1639 }
1640 }"#
1641 .unindent(),
1642 Some(
1643 &r#"{
1644 "formatter": [
1645 {
1646 "code_action": "default-1"
1647 },
1648 {
1649 "code_action": "default-2"
1650 },
1651 {
1652 "code_action": "default-3"
1653 },
1654 {
1655 "code_action": "default-4"
1656 }
1657 ],
1658 "languages": {
1659 "Rust": {
1660 "formatter": [
1661 {
1662 "code_action": "included-1"
1663 },
1664 {
1665 "code_action": "included-2"
1666 },
1667 {
1668 "language_server": "ruff"
1669 },
1670 {
1671 "code_action": "included-3"
1672 },
1673 {
1674 "code_action": "included-4"
1675 }
1676 ]
1677 },
1678 "Python": {
1679 "formatter": [
1680 {
1681 "language_server": "ruff"
1682 },
1683 {
1684 "code_action": "included-3"
1685 },
1686 {
1687 "code_action": "included-4"
1688 }
1689 ]
1690 }
1691 }
1692 }"#
1693 .unindent(),
1694 ),
1695 );
1696 }
1697
1698 #[test]
1699 fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1700 assert_migrate_settings_with_migrations(
1701 &[MigrationType::Json(
1702 migrations::m_2025_10_01::flatten_code_actions_formatters,
1703 )],
1704 &r#"{
1705 "formatter": {
1706 "code_actions": {
1707 "default-1": true,
1708 "default-2": true,
1709 "default-3": true,
1710 "default-4": true,
1711 }
1712 },
1713 "format_on_save": [
1714 {
1715 "code_actions": {
1716 "included-1": true,
1717 "included-2": true,
1718 "excluded": false,
1719 }
1720 },
1721 {
1722 "language_server": "ruff"
1723 },
1724 {
1725 "code_actions": {
1726 "excluded": false,
1727 "excluded-2": false,
1728 }
1729 }
1730 // some comment
1731 ,
1732 {
1733 "code_actions": {
1734 "excluded": false,
1735 "included-3": true,
1736 "included-4": true,
1737 }
1738 },
1739 ],
1740 "languages": {
1741 "Rust": {
1742 "format_on_save": "prettier",
1743 "formatter": [
1744 {
1745 "code_actions": {
1746 "included-1": true,
1747 "included-2": true,
1748 "excluded": false,
1749 }
1750 },
1751 {
1752 "language_server": "ruff"
1753 },
1754 {
1755 "code_actions": {
1756 "excluded": false,
1757 "excluded-2": false,
1758 }
1759 }
1760 // some comment
1761 ,
1762 {
1763 "code_actions": {
1764 "excluded": false,
1765 "included-3": true,
1766 "included-4": true,
1767 }
1768 },
1769 ]
1770 },
1771 "Python": {
1772 "format_on_save": {
1773 "code_actions": {
1774 "on-save-1": true,
1775 "on-save-2": true,
1776 }
1777 },
1778 "formatter": [
1779 {
1780 "language_server": "ruff"
1781 },
1782 {
1783 "code_actions": {
1784 "excluded": false,
1785 "excluded-2": false,
1786 }
1787 }
1788 // some comment
1789 ,
1790 {
1791 "code_actions": {
1792 "excluded": false,
1793 "included-3": true,
1794 "included-4": true,
1795 }
1796 },
1797 ]
1798 }
1799 }
1800 }"#
1801 .unindent(),
1802 Some(
1803 &r#"
1804 {
1805 "formatter": [
1806 {
1807 "code_action": "default-1"
1808 },
1809 {
1810 "code_action": "default-2"
1811 },
1812 {
1813 "code_action": "default-3"
1814 },
1815 {
1816 "code_action": "default-4"
1817 }
1818 ],
1819 "format_on_save": [
1820 {
1821 "code_action": "included-1"
1822 },
1823 {
1824 "code_action": "included-2"
1825 },
1826 {
1827 "language_server": "ruff"
1828 },
1829 {
1830 "code_action": "included-3"
1831 },
1832 {
1833 "code_action": "included-4"
1834 }
1835 ],
1836 "languages": {
1837 "Rust": {
1838 "format_on_save": "prettier",
1839 "formatter": [
1840 {
1841 "code_action": "included-1"
1842 },
1843 {
1844 "code_action": "included-2"
1845 },
1846 {
1847 "language_server": "ruff"
1848 },
1849 {
1850 "code_action": "included-3"
1851 },
1852 {
1853 "code_action": "included-4"
1854 }
1855 ]
1856 },
1857 "Python": {
1858 "format_on_save": [
1859 {
1860 "code_action": "on-save-1"
1861 },
1862 {
1863 "code_action": "on-save-2"
1864 }
1865 ],
1866 "formatter": [
1867 {
1868 "language_server": "ruff"
1869 },
1870 {
1871 "code_action": "included-3"
1872 },
1873 {
1874 "code_action": "included-4"
1875 }
1876 ]
1877 }
1878 }
1879 }"#
1880 .unindent(),
1881 ),
1882 );
1883 }
1884
1885 #[test]
1886 fn test_format_on_save_formatter_migration_basic() {
1887 assert_migrate_settings_with_migrations(
1888 &[MigrationType::Json(
1889 migrations::m_2025_10_02::remove_formatters_on_save,
1890 )],
1891 &r#"{
1892 "format_on_save": "prettier"
1893 }"#
1894 .unindent(),
1895 Some(
1896 &r#"{
1897 "formatter": "prettier",
1898 "format_on_save": "on"
1899 }"#
1900 .unindent(),
1901 ),
1902 );
1903 }
1904
1905 #[test]
1906 fn test_format_on_save_formatter_migration_array() {
1907 assert_migrate_settings_with_migrations(
1908 &[MigrationType::Json(
1909 migrations::m_2025_10_02::remove_formatters_on_save,
1910 )],
1911 &r#"{
1912 "format_on_save": ["prettier", {"language_server": "eslint"}]
1913 }"#
1914 .unindent(),
1915 Some(
1916 &r#"{
1917 "formatter": [
1918 "prettier",
1919 {
1920 "language_server": "eslint"
1921 }
1922 ],
1923 "format_on_save": "on"
1924 }"#
1925 .unindent(),
1926 ),
1927 );
1928 }
1929
1930 #[test]
1931 fn test_format_on_save_on_off_unchanged() {
1932 assert_migrate_settings_with_migrations(
1933 &[MigrationType::Json(
1934 migrations::m_2025_10_02::remove_formatters_on_save,
1935 )],
1936 &r#"{
1937 "format_on_save": "on"
1938 }"#
1939 .unindent(),
1940 None,
1941 );
1942
1943 assert_migrate_settings_with_migrations(
1944 &[MigrationType::Json(
1945 migrations::m_2025_10_02::remove_formatters_on_save,
1946 )],
1947 &r#"{
1948 "format_on_save": "off"
1949 }"#
1950 .unindent(),
1951 None,
1952 );
1953 }
1954
1955 #[test]
1956 fn test_format_on_save_formatter_migration_in_languages() {
1957 assert_migrate_settings_with_migrations(
1958 &[MigrationType::Json(
1959 migrations::m_2025_10_02::remove_formatters_on_save,
1960 )],
1961 &r#"{
1962 "languages": {
1963 "Rust": {
1964 "format_on_save": "rust-analyzer"
1965 },
1966 "Python": {
1967 "format_on_save": ["ruff", "black"]
1968 }
1969 }
1970 }"#
1971 .unindent(),
1972 Some(
1973 &r#"{
1974 "languages": {
1975 "Rust": {
1976 "formatter": "rust-analyzer",
1977 "format_on_save": "on"
1978 },
1979 "Python": {
1980 "formatter": [
1981 "ruff",
1982 "black"
1983 ],
1984 "format_on_save": "on"
1985 }
1986 }
1987 }"#
1988 .unindent(),
1989 ),
1990 );
1991 }
1992
1993 #[test]
1994 fn test_format_on_save_formatter_migration_mixed_global_and_languages() {
1995 assert_migrate_settings_with_migrations(
1996 &[MigrationType::Json(
1997 migrations::m_2025_10_02::remove_formatters_on_save,
1998 )],
1999 &r#"{
2000 "format_on_save": "prettier",
2001 "languages": {
2002 "Rust": {
2003 "format_on_save": "rust-analyzer"
2004 },
2005 "Python": {
2006 "format_on_save": "on"
2007 }
2008 }
2009 }"#
2010 .unindent(),
2011 Some(
2012 &r#"{
2013 "formatter": "prettier",
2014 "format_on_save": "on",
2015 "languages": {
2016 "Rust": {
2017 "formatter": "rust-analyzer",
2018 "format_on_save": "on"
2019 },
2020 "Python": {
2021 "format_on_save": "on"
2022 }
2023 }
2024 }"#
2025 .unindent(),
2026 ),
2027 );
2028 }
2029
2030 #[test]
2031 fn test_format_on_save_no_migration_when_no_format_on_save() {
2032 assert_migrate_settings_with_migrations(
2033 &[MigrationType::Json(
2034 migrations::m_2025_10_02::remove_formatters_on_save,
2035 )],
2036 &r#"{
2037 "formatter": ["prettier"]
2038 }"#
2039 .unindent(),
2040 None,
2041 );
2042 }
2043
2044 #[test]
2045 fn test_restore_code_actions_on_format() {
2046 assert_migrate_settings_with_migrations(
2047 &[MigrationType::Json(
2048 migrations::m_2025_10_16::restore_code_actions_on_format,
2049 )],
2050 &r#"{
2051 "formatter": {
2052 "code_action": "foo"
2053 }
2054 }"#
2055 .unindent(),
2056 Some(
2057 &r#"{
2058 "code_actions_on_format": {
2059 "foo": true
2060 },
2061 "formatter": []
2062 }"#
2063 .unindent(),
2064 ),
2065 );
2066
2067 assert_migrate_settings_with_migrations(
2068 &[MigrationType::Json(
2069 migrations::m_2025_10_16::restore_code_actions_on_format,
2070 )],
2071 &r#"{
2072 "formatter": [
2073 { "code_action": "foo" },
2074 "auto"
2075 ]
2076 }"#
2077 .unindent(),
2078 None,
2079 );
2080
2081 assert_migrate_settings_with_migrations(
2082 &[MigrationType::Json(
2083 migrations::m_2025_10_16::restore_code_actions_on_format,
2084 )],
2085 &r#"{
2086 "formatter": {
2087 "code_action": "foo"
2088 },
2089 "code_actions_on_format": {
2090 "bar": true,
2091 "baz": false
2092 }
2093 }"#
2094 .unindent(),
2095 Some(
2096 &r#"{
2097 "formatter": [],
2098 "code_actions_on_format": {
2099 "foo": true,
2100 "bar": true,
2101 "baz": false
2102 }
2103 }"#
2104 .unindent(),
2105 ),
2106 );
2107
2108 assert_migrate_settings_with_migrations(
2109 &[MigrationType::Json(
2110 migrations::m_2025_10_16::restore_code_actions_on_format,
2111 )],
2112 &r#"{
2113 "formatter": [
2114 { "code_action": "foo" },
2115 { "code_action": "qux" },
2116 ],
2117 "code_actions_on_format": {
2118 "bar": true,
2119 "baz": false
2120 }
2121 }"#
2122 .unindent(),
2123 Some(
2124 &r#"{
2125 "formatter": [],
2126 "code_actions_on_format": {
2127 "foo": true,
2128 "qux": true,
2129 "bar": true,
2130 "baz": false
2131 }
2132 }"#
2133 .unindent(),
2134 ),
2135 );
2136
2137 assert_migrate_settings_with_migrations(
2138 &[MigrationType::Json(
2139 migrations::m_2025_10_16::restore_code_actions_on_format,
2140 )],
2141 &r#"{
2142 "formatter": [],
2143 "code_actions_on_format": {
2144 "bar": true,
2145 "baz": false
2146 }
2147 }"#
2148 .unindent(),
2149 None,
2150 );
2151 }
2152
2153 #[test]
2154 fn test_make_file_finder_include_ignored_an_enum() {
2155 assert_migrate_settings_with_migrations(
2156 &[MigrationType::Json(
2157 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2158 )],
2159 &r#"{ }"#.unindent(),
2160 None,
2161 );
2162
2163 assert_migrate_settings_with_migrations(
2164 &[MigrationType::Json(
2165 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2166 )],
2167 &r#"{
2168 "file_finder": {
2169 "include_ignored": true
2170 }
2171 }"#
2172 .unindent(),
2173 Some(
2174 &r#"{
2175 "file_finder": {
2176 "include_ignored": "all"
2177 }
2178 }"#
2179 .unindent(),
2180 ),
2181 );
2182
2183 assert_migrate_settings_with_migrations(
2184 &[MigrationType::Json(
2185 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2186 )],
2187 &r#"{
2188 "file_finder": {
2189 "include_ignored": false
2190 }
2191 }"#
2192 .unindent(),
2193 Some(
2194 &r#"{
2195 "file_finder": {
2196 "include_ignored": "indexed"
2197 }
2198 }"#
2199 .unindent(),
2200 ),
2201 );
2202
2203 assert_migrate_settings_with_migrations(
2204 &[MigrationType::Json(
2205 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2206 )],
2207 &r#"{
2208 "file_finder": {
2209 "include_ignored": null
2210 }
2211 }"#
2212 .unindent(),
2213 Some(
2214 &r#"{
2215 "file_finder": {
2216 "include_ignored": "smart"
2217 }
2218 }"#
2219 .unindent(),
2220 ),
2221 );
2222 }
2223
2224 #[test]
2225 fn test_remove_context_server_source() {
2226 assert_migrate_settings(
2227 &r#"
2228 {
2229 "context_servers": {
2230 "extension_server": {
2231 "source": "extension",
2232 "settings": {
2233 "foo": "bar"
2234 }
2235 },
2236 "custom_server": {
2237 "source": "custom",
2238 "command": "foo",
2239 "args": ["bar"],
2240 "env": {
2241 "FOO": "BAR"
2242 }
2243 },
2244 }
2245 }
2246 "#
2247 .unindent(),
2248 Some(
2249 &r#"
2250 {
2251 "context_servers": {
2252 "extension_server": {
2253 "settings": {
2254 "foo": "bar"
2255 }
2256 },
2257 "custom_server": {
2258 "command": "foo",
2259 "args": ["bar"],
2260 "env": {
2261 "FOO": "BAR"
2262 }
2263 },
2264 }
2265 }
2266 "#
2267 .unindent(),
2268 ),
2269 );
2270 }
2271
2272 #[test]
2273 fn test_project_panel_open_file_on_paste_migration() {
2274 assert_migrate_settings(
2275 &r#"
2276 {
2277 "project_panel": {
2278 "open_file_on_paste": true
2279 }
2280 }
2281 "#
2282 .unindent(),
2283 Some(
2284 &r#"
2285 {
2286 "project_panel": {
2287 "auto_open": { "on_paste": true }
2288 }
2289 }
2290 "#
2291 .unindent(),
2292 ),
2293 );
2294
2295 assert_migrate_settings(
2296 &r#"
2297 {
2298 "project_panel": {
2299 "open_file_on_paste": false
2300 }
2301 }
2302 "#
2303 .unindent(),
2304 Some(
2305 &r#"
2306 {
2307 "project_panel": {
2308 "auto_open": { "on_paste": false }
2309 }
2310 }
2311 "#
2312 .unindent(),
2313 ),
2314 );
2315 }
2316
2317 #[test]
2318 fn test_enable_preview_from_code_navigation_migration() {
2319 assert_migrate_settings(
2320 &r#"
2321 {
2322 "other_setting_1": 1,
2323 "preview_tabs": {
2324 "other_setting_2": 2,
2325 "enable_preview_from_code_navigation": false
2326 }
2327 }
2328 "#
2329 .unindent(),
2330 Some(
2331 &r#"
2332 {
2333 "other_setting_1": 1,
2334 "preview_tabs": {
2335 "other_setting_2": 2,
2336 "enable_keep_preview_on_code_navigation": false
2337 }
2338 }
2339 "#
2340 .unindent(),
2341 ),
2342 );
2343
2344 assert_migrate_settings(
2345 &r#"
2346 {
2347 "other_setting_1": 1,
2348 "preview_tabs": {
2349 "other_setting_2": 2,
2350 "enable_preview_from_code_navigation": true
2351 }
2352 }
2353 "#
2354 .unindent(),
2355 Some(
2356 &r#"
2357 {
2358 "other_setting_1": 1,
2359 "preview_tabs": {
2360 "other_setting_2": 2,
2361 "enable_keep_preview_on_code_navigation": true
2362 }
2363 }
2364 "#
2365 .unindent(),
2366 ),
2367 );
2368 }
2369
2370 #[test]
2371 fn test_move_edit_prediction_provider_to_edit_predictions() {
2372 assert_migrate_settings_with_migrations(
2373 &[MigrationType::Json(
2374 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2375 )],
2376 &r#"{ }"#.unindent(),
2377 None,
2378 );
2379
2380 assert_migrate_settings_with_migrations(
2381 &[MigrationType::Json(
2382 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2383 )],
2384 &r#"
2385 {
2386 "features": {
2387 "edit_prediction_provider": "copilot"
2388 }
2389 }
2390 "#
2391 .unindent(),
2392 Some(
2393 &r#"
2394 {
2395 "edit_predictions": {
2396 "provider": "copilot"
2397 }
2398 }
2399 "#
2400 .unindent(),
2401 ),
2402 );
2403
2404 assert_migrate_settings_with_migrations(
2405 &[MigrationType::Json(
2406 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2407 )],
2408 &r#"
2409 {
2410 "features": {
2411 "edit_prediction_provider": "zed"
2412 },
2413 "edit_predictions": {
2414 "mode": "eager"
2415 }
2416 }
2417 "#
2418 .unindent(),
2419 Some(
2420 &r#"
2421 {
2422 "edit_predictions": {
2423 "provider": "zed",
2424 "mode": "eager"
2425 }
2426 }
2427 "#
2428 .unindent(),
2429 ),
2430 );
2431
2432 assert_migrate_settings_with_migrations(
2433 &[MigrationType::Json(
2434 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2435 )],
2436 &r#"
2437 {
2438 "features": {
2439 "edit_prediction_provider": "supermaven"
2440 },
2441 "edit_predictions": {
2442 "provider": "copilot"
2443 }
2444 }
2445 "#
2446 .unindent(),
2447 Some(
2448 &r#"
2449 {
2450 "edit_predictions": {
2451 "provider": "copilot"
2452 }
2453 }
2454 "#
2455 .unindent(),
2456 ),
2457 );
2458
2459 assert_migrate_settings_with_migrations(
2460 &[MigrationType::Json(
2461 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2462 )],
2463 &r#"
2464 {
2465 "edit_predictions": {
2466 "provider": "zed"
2467 }
2468 }
2469 "#
2470 .unindent(),
2471 None,
2472 );
2473 }
2474
2475 #[test]
2476 fn test_migrate_experimental_sweep_mercury() {
2477 assert_migrate_settings_with_migrations(
2478 &[MigrationType::Json(
2479 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2480 )],
2481 &r#"{ }"#.unindent(),
2482 None,
2483 );
2484
2485 assert_migrate_settings_with_migrations(
2486 &[MigrationType::Json(
2487 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2488 )],
2489 &r#"
2490 {
2491 "edit_predictions": {
2492 "provider": {
2493 "experimental": "sweep"
2494 }
2495 }
2496 }
2497 "#
2498 .unindent(),
2499 Some(
2500 &r#"
2501 {
2502 "edit_predictions": {
2503 "provider": "sweep"
2504 }
2505 }
2506 "#
2507 .unindent(),
2508 ),
2509 );
2510
2511 assert_migrate_settings_with_migrations(
2512 &[MigrationType::Json(
2513 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2514 )],
2515 &r#"
2516 {
2517 "edit_predictions": {
2518 "provider": {
2519 "experimental": "mercury"
2520 }
2521 }
2522 }
2523 "#
2524 .unindent(),
2525 Some(
2526 &r#"
2527 {
2528 "edit_predictions": {
2529 "provider": "mercury"
2530 }
2531 }
2532 "#
2533 .unindent(),
2534 ),
2535 );
2536
2537 assert_migrate_settings_with_migrations(
2538 &[MigrationType::Json(
2539 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2540 )],
2541 &r#"
2542 {
2543 "features": {
2544 "edit_prediction_provider": {
2545 "experimental": "sweep"
2546 }
2547 }
2548 }
2549 "#
2550 .unindent(),
2551 Some(
2552 &r#"
2553 {
2554 "features": {
2555 "edit_prediction_provider": "sweep"
2556 }
2557 }
2558 "#
2559 .unindent(),
2560 ),
2561 );
2562
2563 assert_migrate_settings_with_migrations(
2564 &[MigrationType::Json(
2565 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2566 )],
2567 &r#"
2568 {
2569 "edit_predictions": {
2570 "provider": "zed"
2571 }
2572 }
2573 "#
2574 .unindent(),
2575 None,
2576 );
2577
2578 assert_migrate_settings_with_migrations(
2579 &[MigrationType::Json(
2580 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2581 )],
2582 &r#"
2583 {
2584 "edit_predictions": {
2585 "provider": {
2586 "experimental": "zeta2"
2587 }
2588 }
2589 }
2590 "#
2591 .unindent(),
2592 None,
2593 );
2594 }
2595}