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 MigrationType::Json(migrations::m_2026_02_04::migrate_tool_permission_defaults),
240 ];
241 run_migrations(text, migrations)
242}
243
244pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
245 migrate(
246 text,
247 &[(
248 SETTINGS_NESTED_KEY_VALUE_PATTERN,
249 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
250 )],
251 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
252 )
253}
254
255pub type MigrationPatterns = &'static [(
256 &'static str,
257 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
258)];
259
260macro_rules! define_query {
261 ($var_name:ident, $patterns_path:path) => {
262 static $var_name: LazyLock<Query> = LazyLock::new(|| {
263 Query::new(
264 &tree_sitter_json::LANGUAGE.into(),
265 &$patterns_path
266 .iter()
267 .map(|pattern| pattern.0)
268 .collect::<String>(),
269 )
270 .unwrap()
271 });
272 };
273}
274
275// keymap
276define_query!(
277 KEYMAP_QUERY_2025_01_29,
278 migrations::m_2025_01_29::KEYMAP_PATTERNS
279);
280define_query!(
281 KEYMAP_QUERY_2025_01_30,
282 migrations::m_2025_01_30::KEYMAP_PATTERNS
283);
284define_query!(
285 KEYMAP_QUERY_2025_03_03,
286 migrations::m_2025_03_03::KEYMAP_PATTERNS
287);
288define_query!(
289 KEYMAP_QUERY_2025_03_06,
290 migrations::m_2025_03_06::KEYMAP_PATTERNS
291);
292define_query!(
293 KEYMAP_QUERY_2025_04_15,
294 migrations::m_2025_04_15::KEYMAP_PATTERNS
295);
296
297// settings
298define_query!(
299 SETTINGS_QUERY_2025_01_02,
300 migrations::m_2025_01_02::SETTINGS_PATTERNS
301);
302define_query!(
303 SETTINGS_QUERY_2025_01_29,
304 migrations::m_2025_01_29::SETTINGS_PATTERNS
305);
306define_query!(
307 SETTINGS_QUERY_2025_01_30,
308 migrations::m_2025_01_30::SETTINGS_PATTERNS
309);
310define_query!(
311 SETTINGS_QUERY_2025_03_29,
312 migrations::m_2025_03_29::SETTINGS_PATTERNS
313);
314define_query!(
315 SETTINGS_QUERY_2025_04_15,
316 migrations::m_2025_04_15::SETTINGS_PATTERNS
317);
318define_query!(
319 SETTINGS_QUERY_2025_04_21,
320 migrations::m_2025_04_21::SETTINGS_PATTERNS
321);
322define_query!(
323 SETTINGS_QUERY_2025_04_23,
324 migrations::m_2025_04_23::SETTINGS_PATTERNS
325);
326define_query!(
327 SETTINGS_QUERY_2025_05_05,
328 migrations::m_2025_05_05::SETTINGS_PATTERNS
329);
330define_query!(
331 SETTINGS_QUERY_2025_05_08,
332 migrations::m_2025_05_08::SETTINGS_PATTERNS
333);
334define_query!(
335 SETTINGS_QUERY_2025_06_16,
336 migrations::m_2025_06_16::SETTINGS_PATTERNS
337);
338define_query!(
339 SETTINGS_QUERY_2025_06_25,
340 migrations::m_2025_06_25::SETTINGS_PATTERNS
341);
342define_query!(
343 SETTINGS_QUERY_2025_06_27,
344 migrations::m_2025_06_27::SETTINGS_PATTERNS
345);
346define_query!(
347 SETTINGS_QUERY_2025_07_08,
348 migrations::m_2025_07_08::SETTINGS_PATTERNS
349);
350define_query!(
351 SETTINGS_QUERY_2025_10_03,
352 migrations::m_2025_10_03::SETTINGS_PATTERNS
353);
354define_query!(
355 SETTINGS_QUERY_2025_11_12,
356 migrations::m_2025_11_12::SETTINGS_PATTERNS
357);
358define_query!(
359 SETTINGS_QUERY_2025_12_01,
360 migrations::m_2025_12_01::SETTINGS_PATTERNS
361);
362define_query!(
363 SETTINGS_QUERY_2025_11_20,
364 migrations::m_2025_11_20::SETTINGS_PATTERNS
365);
366define_query!(
367 KEYMAP_QUERY_2025_12_08,
368 migrations::m_2025_12_08::KEYMAP_PATTERNS
369);
370define_query!(
371 SETTINGS_QUERY_2025_12_15,
372 migrations::m_2025_12_15::SETTINGS_PATTERNS
373);
374
375// custom query
376static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
377 Query::new(
378 &tree_sitter_json::LANGUAGE.into(),
379 SETTINGS_NESTED_KEY_VALUE_PATTERN,
380 )
381 .unwrap()
382});
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use unindent::Unindent as _;
388
389 #[track_caller]
390 fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
391 match (&migrated, &expected) {
392 (Some(migrated), Some(expected)) => {
393 pretty_assertions::assert_str_eq!(expected, migrated);
394 }
395 _ => {
396 pretty_assertions::assert_eq!(migrated.as_deref(), expected);
397 }
398 }
399 }
400
401 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
402 let migrated = migrate_keymap(input).unwrap();
403 pretty_assertions::assert_eq!(migrated.as_deref(), output);
404 }
405
406 #[track_caller]
407 fn assert_migrate_settings(input: &str, output: Option<&str>) {
408 let migrated = migrate_settings(input).unwrap();
409 assert_migrated_correctly(migrated.clone(), output);
410
411 // expect that rerunning the migration does not result in another migration
412 if let Some(migrated) = migrated {
413 let rerun = migrate_settings(&migrated).unwrap();
414 assert_migrated_correctly(rerun, None);
415 }
416 }
417
418 #[track_caller]
419 fn assert_migrate_settings_with_migrations(
420 migrations: &[MigrationType],
421 input: &str,
422 output: Option<&str>,
423 ) {
424 let migrated = run_migrations(input, migrations).unwrap();
425 assert_migrated_correctly(migrated.clone(), output);
426
427 // expect that rerunning the migration does not result in another migration
428 if let Some(migrated) = migrated {
429 let rerun = run_migrations(&migrated, migrations).unwrap();
430 assert_migrated_correctly(rerun, None);
431 }
432 }
433
434 #[test]
435 fn test_empty_content() {
436 assert_migrate_settings("", None)
437 }
438
439 #[test]
440 fn test_replace_array_with_single_string() {
441 assert_migrate_keymap(
442 r#"
443 [
444 {
445 "bindings": {
446 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
447 }
448 }
449 ]
450 "#,
451 Some(
452 r#"
453 [
454 {
455 "bindings": {
456 "cmd-1": "workspace::ActivatePaneUp"
457 }
458 }
459 ]
460 "#,
461 ),
462 )
463 }
464
465 #[test]
466 fn test_replace_action_argument_object_with_single_value() {
467 assert_migrate_keymap(
468 r#"
469 [
470 {
471 "bindings": {
472 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
473 }
474 }
475 ]
476 "#,
477 Some(
478 r#"
479 [
480 {
481 "bindings": {
482 "cmd-1": ["editor::FoldAtLevel", 1]
483 }
484 }
485 ]
486 "#,
487 ),
488 )
489 }
490
491 #[test]
492 fn test_replace_action_argument_object_with_single_value_2() {
493 assert_migrate_keymap(
494 r#"
495 [
496 {
497 "bindings": {
498 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
499 }
500 }
501 ]
502 "#,
503 Some(
504 r#"
505 [
506 {
507 "bindings": {
508 "cmd-1": ["vim::PushObject", { "some" : "value" }]
509 }
510 }
511 ]
512 "#,
513 ),
514 )
515 }
516
517 #[test]
518 fn test_rename_string_action() {
519 assert_migrate_keymap(
520 r#"
521 [
522 {
523 "bindings": {
524 "cmd-1": "inline_completion::ToggleMenu"
525 }
526 }
527 ]
528 "#,
529 Some(
530 r#"
531 [
532 {
533 "bindings": {
534 "cmd-1": "edit_prediction::ToggleMenu"
535 }
536 }
537 ]
538 "#,
539 ),
540 )
541 }
542
543 #[test]
544 fn test_rename_context_key() {
545 assert_migrate_keymap(
546 r#"
547 [
548 {
549 "context": "Editor && inline_completion && !showing_completions"
550 }
551 ]
552 "#,
553 Some(
554 r#"
555 [
556 {
557 "context": "Editor && edit_prediction && !showing_completions"
558 }
559 ]
560 "#,
561 ),
562 )
563 }
564
565 #[test]
566 fn test_incremental_migrations() {
567 // Here string transforms to array internally. Then, that array transforms back to string.
568 assert_migrate_keymap(
569 r#"
570 [
571 {
572 "bindings": {
573 "ctrl-q": "editor::GoToHunk", // should remain same
574 "ctrl-w": "editor::GoToPrevHunk", // should rename
575 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
576 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
577 }
578 }
579 ]
580 "#,
581 Some(
582 r#"
583 [
584 {
585 "bindings": {
586 "ctrl-q": "editor::GoToHunk", // should remain same
587 "ctrl-w": "editor::GoToPreviousHunk", // should rename
588 "ctrl-q": "editor::GoToHunk", // should transform
589 "ctrl-w": "editor::GoToPreviousHunk" // should transform
590 }
591 }
592 ]
593 "#,
594 ),
595 )
596 }
597
598 #[test]
599 fn test_action_argument_snake_case() {
600 // First performs transformations, then replacements
601 assert_migrate_keymap(
602 r#"
603 [
604 {
605 "bindings": {
606 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
607 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
608 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
609 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
610 }
611 }
612 ]
613 "#,
614 Some(
615 r#"
616 [
617 {
618 "bindings": {
619 "cmd-1": ["vim::PushObject", { "around": false }],
620 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
621 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
622 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
623 }
624 }
625 ]
626 "#,
627 ),
628 )
629 }
630
631 #[test]
632 fn test_replace_setting_name() {
633 assert_migrate_settings(
634 r#"
635 {
636 "show_inline_completions_in_menu": true,
637 "show_inline_completions": true,
638 "inline_completions_disabled_in": ["string"],
639 "inline_completions": { "some" : "value" }
640 }
641 "#,
642 Some(
643 r#"
644 {
645 "show_edit_predictions_in_menu": true,
646 "show_edit_predictions": true,
647 "edit_predictions_disabled_in": ["string"],
648 "edit_predictions": { "some" : "value" }
649 }
650 "#,
651 ),
652 )
653 }
654
655 #[test]
656 fn test_nested_string_replace_for_settings() {
657 assert_migrate_settings(
658 &r#"
659 {
660 "features": {
661 "inline_completion_provider": "zed"
662 },
663 }
664 "#
665 .unindent(),
666 Some(
667 &r#"
668 {
669 "edit_predictions": {
670 "provider": "zed"
671 }
672 }
673 "#
674 .unindent(),
675 ),
676 )
677 }
678
679 #[test]
680 fn test_replace_settings_in_languages() {
681 assert_migrate_settings(
682 r#"
683 {
684 "languages": {
685 "Astro": {
686 "show_inline_completions": true
687 }
688 }
689 }
690 "#,
691 Some(
692 r#"
693 {
694 "languages": {
695 "Astro": {
696 "show_edit_predictions": true
697 }
698 }
699 }
700 "#,
701 ),
702 )
703 }
704
705 #[test]
706 fn test_replace_settings_value() {
707 assert_migrate_settings(
708 r#"
709 {
710 "scrollbar": {
711 "diagnostics": true
712 },
713 "chat_panel": {
714 "button": true
715 }
716 }
717 "#,
718 Some(
719 r#"
720 {
721 "scrollbar": {
722 "diagnostics": "all"
723 },
724 "chat_panel": {
725 "button": "always"
726 }
727 }
728 "#,
729 ),
730 )
731 }
732
733 #[test]
734 fn test_replace_settings_name_and_value() {
735 assert_migrate_settings(
736 r#"
737 {
738 "tabs": {
739 "always_show_close_button": true
740 }
741 }
742 "#,
743 Some(
744 r#"
745 {
746 "tabs": {
747 "show_close_button": "always"
748 }
749 }
750 "#,
751 ),
752 )
753 }
754
755 #[test]
756 fn test_replace_bash_with_terminal_in_profiles() {
757 assert_migrate_settings(
758 r#"
759 {
760 "assistant": {
761 "profiles": {
762 "custom": {
763 "name": "Custom",
764 "tools": {
765 "bash": true,
766 "diagnostics": true
767 }
768 }
769 }
770 }
771 }
772 "#,
773 Some(
774 r#"
775 {
776 "agent": {
777 "profiles": {
778 "custom": {
779 "name": "Custom",
780 "tools": {
781 "terminal": true,
782 "diagnostics": true
783 }
784 }
785 }
786 }
787 }
788 "#,
789 ),
790 )
791 }
792
793 #[test]
794 fn test_replace_bash_false_with_terminal_in_profiles() {
795 assert_migrate_settings(
796 r#"
797 {
798 "assistant": {
799 "profiles": {
800 "custom": {
801 "name": "Custom",
802 "tools": {
803 "bash": false,
804 "diagnostics": true
805 }
806 }
807 }
808 }
809 }
810 "#,
811 Some(
812 r#"
813 {
814 "agent": {
815 "profiles": {
816 "custom": {
817 "name": "Custom",
818 "tools": {
819 "terminal": false,
820 "diagnostics": true
821 }
822 }
823 }
824 }
825 }
826 "#,
827 ),
828 )
829 }
830
831 #[test]
832 fn test_no_bash_in_profiles() {
833 assert_migrate_settings(
834 r#"
835 {
836 "assistant": {
837 "profiles": {
838 "custom": {
839 "name": "Custom",
840 "tools": {
841 "diagnostics": true,
842 "find_path": true,
843 "read_file": true
844 }
845 }
846 }
847 }
848 }
849 "#,
850 Some(
851 r#"
852 {
853 "agent": {
854 "profiles": {
855 "custom": {
856 "name": "Custom",
857 "tools": {
858 "diagnostics": true,
859 "find_path": true,
860 "read_file": true
861 }
862 }
863 }
864 }
865 }
866 "#,
867 ),
868 )
869 }
870
871 #[test]
872 fn test_rename_path_search_to_find_path() {
873 assert_migrate_settings(
874 r#"
875 {
876 "assistant": {
877 "profiles": {
878 "default": {
879 "tools": {
880 "path_search": true,
881 "read_file": true
882 }
883 }
884 }
885 }
886 }
887 "#,
888 Some(
889 r#"
890 {
891 "agent": {
892 "profiles": {
893 "default": {
894 "tools": {
895 "find_path": true,
896 "read_file": true
897 }
898 }
899 }
900 }
901 }
902 "#,
903 ),
904 );
905 }
906
907 #[test]
908 fn test_rename_assistant() {
909 assert_migrate_settings(
910 r#"{
911 "assistant": {
912 "foo": "bar"
913 },
914 "edit_predictions": {
915 "enabled_in_assistant": false,
916 }
917 }"#,
918 Some(
919 r#"{
920 "agent": {
921 "foo": "bar"
922 },
923 "edit_predictions": {
924 "enabled_in_text_threads": false,
925 }
926 }"#,
927 ),
928 );
929 }
930
931 #[test]
932 fn test_comment_duplicated_agent() {
933 assert_migrate_settings(
934 r#"{
935 "agent": {
936 "name": "assistant-1",
937 "model": "gpt-4", // weird formatting
938 "utf8": "привіт"
939 },
940 "something": "else",
941 "agent": {
942 "name": "assistant-2",
943 "model": "gemini-pro"
944 }
945 }
946 "#,
947 Some(
948 r#"{
949 /* Duplicated key auto-commented: "agent": {
950 "name": "assistant-1",
951 "model": "gpt-4", // weird formatting
952 "utf8": "привіт"
953 }, */
954 "something": "else",
955 "agent": {
956 "name": "assistant-2",
957 "model": "gemini-pro"
958 }
959 }
960 "#,
961 ),
962 );
963 }
964
965 #[test]
966 fn test_mcp_settings_migration() {
967 assert_migrate_settings_with_migrations(
968 &[MigrationType::TreeSitter(
969 migrations::m_2025_06_16::SETTINGS_PATTERNS,
970 &SETTINGS_QUERY_2025_06_16,
971 )],
972 r#"{
973 "context_servers": {
974 "empty_server": {},
975 "extension_server": {
976 "settings": {
977 "foo": "bar"
978 }
979 },
980 "custom_server": {
981 "command": {
982 "path": "foo",
983 "args": ["bar"],
984 "env": {
985 "FOO": "BAR"
986 }
987 }
988 },
989 "invalid_server": {
990 "command": {
991 "path": "foo",
992 "args": ["bar"],
993 "env": {
994 "FOO": "BAR"
995 }
996 },
997 "settings": {
998 "foo": "bar"
999 }
1000 },
1001 "empty_server2": {},
1002 "extension_server2": {
1003 "foo": "bar",
1004 "settings": {
1005 "foo": "bar"
1006 },
1007 "bar": "foo"
1008 },
1009 "custom_server2": {
1010 "foo": "bar",
1011 "command": {
1012 "path": "foo",
1013 "args": ["bar"],
1014 "env": {
1015 "FOO": "BAR"
1016 }
1017 },
1018 "bar": "foo"
1019 },
1020 "invalid_server2": {
1021 "foo": "bar",
1022 "command": {
1023 "path": "foo",
1024 "args": ["bar"],
1025 "env": {
1026 "FOO": "BAR"
1027 }
1028 },
1029 "bar": "foo",
1030 "settings": {
1031 "foo": "bar"
1032 }
1033 }
1034 }
1035}"#,
1036 Some(
1037 r#"{
1038 "context_servers": {
1039 "empty_server": {
1040 "source": "extension",
1041 "settings": {}
1042 },
1043 "extension_server": {
1044 "source": "extension",
1045 "settings": {
1046 "foo": "bar"
1047 }
1048 },
1049 "custom_server": {
1050 "source": "custom",
1051 "command": {
1052 "path": "foo",
1053 "args": ["bar"],
1054 "env": {
1055 "FOO": "BAR"
1056 }
1057 }
1058 },
1059 "invalid_server": {
1060 "source": "custom",
1061 "command": {
1062 "path": "foo",
1063 "args": ["bar"],
1064 "env": {
1065 "FOO": "BAR"
1066 }
1067 },
1068 "settings": {
1069 "foo": "bar"
1070 }
1071 },
1072 "empty_server2": {
1073 "source": "extension",
1074 "settings": {}
1075 },
1076 "extension_server2": {
1077 "source": "extension",
1078 "foo": "bar",
1079 "settings": {
1080 "foo": "bar"
1081 },
1082 "bar": "foo"
1083 },
1084 "custom_server2": {
1085 "source": "custom",
1086 "foo": "bar",
1087 "command": {
1088 "path": "foo",
1089 "args": ["bar"],
1090 "env": {
1091 "FOO": "BAR"
1092 }
1093 },
1094 "bar": "foo"
1095 },
1096 "invalid_server2": {
1097 "source": "custom",
1098 "foo": "bar",
1099 "command": {
1100 "path": "foo",
1101 "args": ["bar"],
1102 "env": {
1103 "FOO": "BAR"
1104 }
1105 },
1106 "bar": "foo",
1107 "settings": {
1108 "foo": "bar"
1109 }
1110 }
1111 }
1112}"#,
1113 ),
1114 );
1115 }
1116
1117 #[test]
1118 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1119 let settings = r#"{
1120 "context_servers": {
1121 "empty_server": {
1122 "source": "extension",
1123 "settings": {}
1124 },
1125 "extension_server": {
1126 "source": "extension",
1127 "settings": {
1128 "foo": "bar"
1129 }
1130 },
1131 "custom_server": {
1132 "source": "custom",
1133 "command": {
1134 "path": "foo",
1135 "args": ["bar"],
1136 "env": {
1137 "FOO": "BAR"
1138 }
1139 }
1140 },
1141 "invalid_server": {
1142 "source": "custom",
1143 "command": {
1144 "path": "foo",
1145 "args": ["bar"],
1146 "env": {
1147 "FOO": "BAR"
1148 }
1149 },
1150 "settings": {
1151 "foo": "bar"
1152 }
1153 }
1154 }
1155}"#;
1156 assert_migrate_settings_with_migrations(
1157 &[MigrationType::TreeSitter(
1158 migrations::m_2025_06_16::SETTINGS_PATTERNS,
1159 &SETTINGS_QUERY_2025_06_16,
1160 )],
1161 settings,
1162 None,
1163 );
1164 }
1165
1166 #[test]
1167 fn test_custom_agent_server_settings_migration() {
1168 assert_migrate_settings_with_migrations(
1169 &[MigrationType::TreeSitter(
1170 migrations::m_2025_11_20::SETTINGS_PATTERNS,
1171 &SETTINGS_QUERY_2025_11_20,
1172 )],
1173 r#"{
1174 "agent_servers": {
1175 "gemini": {
1176 "default_model": "gemini-1.5-pro"
1177 },
1178 "claude": {},
1179 "codex": {},
1180 "my-custom-agent": {
1181 "command": "/path/to/agent",
1182 "args": ["--foo"],
1183 "default_model": "my-model"
1184 },
1185 "already-migrated-agent": {
1186 "type": "custom",
1187 "command": "/path/to/agent"
1188 },
1189 "future-extension-agent": {
1190 "type": "extension",
1191 "default_model": "ext-model"
1192 }
1193 }
1194}"#,
1195 Some(
1196 r#"{
1197 "agent_servers": {
1198 "gemini": {
1199 "default_model": "gemini-1.5-pro"
1200 },
1201 "claude": {},
1202 "codex": {},
1203 "my-custom-agent": {
1204 "type": "custom",
1205 "command": "/path/to/agent",
1206 "args": ["--foo"],
1207 "default_model": "my-model"
1208 },
1209 "already-migrated-agent": {
1210 "type": "custom",
1211 "command": "/path/to/agent"
1212 },
1213 "future-extension-agent": {
1214 "type": "extension",
1215 "default_model": "ext-model"
1216 }
1217 }
1218}"#,
1219 ),
1220 );
1221 }
1222
1223 #[test]
1224 fn test_remove_version_fields() {
1225 assert_migrate_settings(
1226 r#"{
1227 "language_models": {
1228 "anthropic": {
1229 "version": "1",
1230 "api_url": "https://api.anthropic.com"
1231 },
1232 "openai": {
1233 "version": "1",
1234 "api_url": "https://api.openai.com/v1"
1235 }
1236 },
1237 "agent": {
1238 "version": "2",
1239 "enabled": true,
1240 "button": true,
1241 "dock": "right",
1242 "default_width": 640,
1243 "default_height": 320,
1244 "default_model": {
1245 "provider": "zed.dev",
1246 "model": "claude-sonnet-4"
1247 }
1248 }
1249}"#,
1250 Some(
1251 r#"{
1252 "language_models": {
1253 "anthropic": {
1254 "api_url": "https://api.anthropic.com"
1255 },
1256 "openai": {
1257 "api_url": "https://api.openai.com/v1"
1258 }
1259 },
1260 "agent": {
1261 "enabled": true,
1262 "button": true,
1263 "dock": "right",
1264 "default_width": 640,
1265 "default_height": 320,
1266 "default_model": {
1267 "provider": "zed.dev",
1268 "model": "claude-sonnet-4"
1269 }
1270 }
1271}"#,
1272 ),
1273 );
1274
1275 // Test that version fields in other contexts are not removed
1276 assert_migrate_settings(
1277 r#"{
1278 "language_models": {
1279 "other_provider": {
1280 "version": "1",
1281 "api_url": "https://api.example.com"
1282 }
1283 },
1284 "other_section": {
1285 "version": "1"
1286 }
1287}"#,
1288 None,
1289 );
1290 }
1291
1292 #[test]
1293 fn test_flatten_context_server_command() {
1294 assert_migrate_settings(
1295 r#"{
1296 "context_servers": {
1297 "some-mcp-server": {
1298 "command": {
1299 "path": "npx",
1300 "args": [
1301 "-y",
1302 "@supabase/mcp-server-supabase@latest",
1303 "--read-only",
1304 "--project-ref=<project-ref>"
1305 ],
1306 "env": {
1307 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1308 }
1309 }
1310 }
1311 }
1312}"#,
1313 Some(
1314 r#"{
1315 "context_servers": {
1316 "some-mcp-server": {
1317 "command": "npx",
1318 "args": [
1319 "-y",
1320 "@supabase/mcp-server-supabase@latest",
1321 "--read-only",
1322 "--project-ref=<project-ref>"
1323 ],
1324 "env": {
1325 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1326 }
1327 }
1328 }
1329}"#,
1330 ),
1331 );
1332
1333 // Test with additional keys in server object
1334 assert_migrate_settings(
1335 r#"{
1336 "context_servers": {
1337 "server-with-extras": {
1338 "command": {
1339 "path": "/usr/bin/node",
1340 "args": ["server.js"]
1341 },
1342 "settings": {}
1343 }
1344 }
1345}"#,
1346 Some(
1347 r#"{
1348 "context_servers": {
1349 "server-with-extras": {
1350 "command": "/usr/bin/node",
1351 "args": ["server.js"],
1352 "settings": {}
1353 }
1354 }
1355}"#,
1356 ),
1357 );
1358
1359 // Test command without args or env
1360 assert_migrate_settings(
1361 r#"{
1362 "context_servers": {
1363 "simple-server": {
1364 "command": {
1365 "path": "simple-mcp-server"
1366 }
1367 }
1368 }
1369}"#,
1370 Some(
1371 r#"{
1372 "context_servers": {
1373 "simple-server": {
1374 "command": "simple-mcp-server"
1375 }
1376 }
1377}"#,
1378 ),
1379 );
1380 }
1381
1382 #[test]
1383 fn test_flatten_code_action_formatters_basic_array() {
1384 assert_migrate_settings_with_migrations(
1385 &[MigrationType::Json(
1386 migrations::m_2025_10_01::flatten_code_actions_formatters,
1387 )],
1388 &r#"{
1389 "formatter": [
1390 {
1391 "code_actions": {
1392 "included-1": true,
1393 "included-2": true,
1394 "excluded": false,
1395 }
1396 }
1397 ]
1398 }"#
1399 .unindent(),
1400 Some(
1401 &r#"{
1402 "formatter": [
1403 {
1404 "code_action": "included-1"
1405 },
1406 {
1407 "code_action": "included-2"
1408 }
1409 ]
1410 }"#
1411 .unindent(),
1412 ),
1413 );
1414 }
1415
1416 #[test]
1417 fn test_flatten_code_action_formatters_basic_object() {
1418 assert_migrate_settings_with_migrations(
1419 &[MigrationType::Json(
1420 migrations::m_2025_10_01::flatten_code_actions_formatters,
1421 )],
1422 &r#"{
1423 "formatter": {
1424 "code_actions": {
1425 "included-1": true,
1426 "excluded": false,
1427 "included-2": true
1428 }
1429 }
1430 }"#
1431 .unindent(),
1432 Some(
1433 &r#"{
1434 "formatter": [
1435 {
1436 "code_action": "included-1"
1437 },
1438 {
1439 "code_action": "included-2"
1440 }
1441 ]
1442 }"#
1443 .unindent(),
1444 ),
1445 );
1446 }
1447
1448 #[test]
1449 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1450 assert_migrate_settings(
1451 &r#"{
1452 "formatter": [
1453 {
1454 "code_actions": {
1455 "included-1": true,
1456 "included-2": true,
1457 "excluded": false,
1458 }
1459 },
1460 {
1461 "language_server": "ruff"
1462 },
1463 {
1464 "code_actions": {
1465 "excluded": false,
1466 "excluded-2": false,
1467 }
1468 }
1469 // some comment
1470 ,
1471 {
1472 "code_actions": {
1473 "excluded": false,
1474 "included-3": true,
1475 "included-4": true,
1476 }
1477 },
1478 ]
1479 }"#
1480 .unindent(),
1481 Some(
1482 &r#"{
1483 "formatter": [
1484 {
1485 "code_action": "included-1"
1486 },
1487 {
1488 "code_action": "included-2"
1489 },
1490 {
1491 "language_server": "ruff"
1492 },
1493 {
1494 "code_action": "included-3"
1495 },
1496 {
1497 "code_action": "included-4"
1498 }
1499 ]
1500 }"#
1501 .unindent(),
1502 ),
1503 );
1504 }
1505
1506 #[test]
1507 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1508 assert_migrate_settings(
1509 &r#"{
1510 "languages": {
1511 "Rust": {
1512 "formatter": [
1513 {
1514 "code_actions": {
1515 "included-1": true,
1516 "included-2": true,
1517 "excluded": false,
1518 }
1519 },
1520 {
1521 "language_server": "ruff"
1522 },
1523 {
1524 "code_actions": {
1525 "excluded": false,
1526 "excluded-2": false,
1527 }
1528 }
1529 // some comment
1530 ,
1531 {
1532 "code_actions": {
1533 "excluded": false,
1534 "included-3": true,
1535 "included-4": true,
1536 }
1537 },
1538 ]
1539 }
1540 }
1541 }"#
1542 .unindent(),
1543 Some(
1544 &r#"{
1545 "languages": {
1546 "Rust": {
1547 "formatter": [
1548 {
1549 "code_action": "included-1"
1550 },
1551 {
1552 "code_action": "included-2"
1553 },
1554 {
1555 "language_server": "ruff"
1556 },
1557 {
1558 "code_action": "included-3"
1559 },
1560 {
1561 "code_action": "included-4"
1562 }
1563 ]
1564 }
1565 }
1566 }"#
1567 .unindent(),
1568 ),
1569 );
1570 }
1571
1572 #[test]
1573 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1574 {
1575 assert_migrate_settings_with_migrations(
1576 &[MigrationType::Json(
1577 migrations::m_2025_10_01::flatten_code_actions_formatters,
1578 )],
1579 &r#"{
1580 "formatter": {
1581 "code_actions": {
1582 "default-1": true,
1583 "default-2": true,
1584 "default-3": true,
1585 "default-4": true,
1586 }
1587 },
1588 "languages": {
1589 "Rust": {
1590 "formatter": [
1591 {
1592 "code_actions": {
1593 "included-1": true,
1594 "included-2": true,
1595 "excluded": false,
1596 }
1597 },
1598 {
1599 "language_server": "ruff"
1600 },
1601 {
1602 "code_actions": {
1603 "excluded": false,
1604 "excluded-2": false,
1605 }
1606 }
1607 // some comment
1608 ,
1609 {
1610 "code_actions": {
1611 "excluded": false,
1612 "included-3": true,
1613 "included-4": true,
1614 }
1615 },
1616 ]
1617 },
1618 "Python": {
1619 "formatter": [
1620 {
1621 "language_server": "ruff"
1622 },
1623 {
1624 "code_actions": {
1625 "excluded": false,
1626 "excluded-2": false,
1627 }
1628 }
1629 // some comment
1630 ,
1631 {
1632 "code_actions": {
1633 "excluded": false,
1634 "included-3": true,
1635 "included-4": true,
1636 }
1637 },
1638 ]
1639 }
1640 }
1641 }"#
1642 .unindent(),
1643 Some(
1644 &r#"{
1645 "formatter": [
1646 {
1647 "code_action": "default-1"
1648 },
1649 {
1650 "code_action": "default-2"
1651 },
1652 {
1653 "code_action": "default-3"
1654 },
1655 {
1656 "code_action": "default-4"
1657 }
1658 ],
1659 "languages": {
1660 "Rust": {
1661 "formatter": [
1662 {
1663 "code_action": "included-1"
1664 },
1665 {
1666 "code_action": "included-2"
1667 },
1668 {
1669 "language_server": "ruff"
1670 },
1671 {
1672 "code_action": "included-3"
1673 },
1674 {
1675 "code_action": "included-4"
1676 }
1677 ]
1678 },
1679 "Python": {
1680 "formatter": [
1681 {
1682 "language_server": "ruff"
1683 },
1684 {
1685 "code_action": "included-3"
1686 },
1687 {
1688 "code_action": "included-4"
1689 }
1690 ]
1691 }
1692 }
1693 }"#
1694 .unindent(),
1695 ),
1696 );
1697 }
1698
1699 #[test]
1700 fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1701 assert_migrate_settings_with_migrations(
1702 &[MigrationType::Json(
1703 migrations::m_2025_10_01::flatten_code_actions_formatters,
1704 )],
1705 &r#"{
1706 "formatter": {
1707 "code_actions": {
1708 "default-1": true,
1709 "default-2": true,
1710 "default-3": true,
1711 "default-4": true,
1712 }
1713 },
1714 "format_on_save": [
1715 {
1716 "code_actions": {
1717 "included-1": true,
1718 "included-2": true,
1719 "excluded": false,
1720 }
1721 },
1722 {
1723 "language_server": "ruff"
1724 },
1725 {
1726 "code_actions": {
1727 "excluded": false,
1728 "excluded-2": false,
1729 }
1730 }
1731 // some comment
1732 ,
1733 {
1734 "code_actions": {
1735 "excluded": false,
1736 "included-3": true,
1737 "included-4": true,
1738 }
1739 },
1740 ],
1741 "languages": {
1742 "Rust": {
1743 "format_on_save": "prettier",
1744 "formatter": [
1745 {
1746 "code_actions": {
1747 "included-1": true,
1748 "included-2": true,
1749 "excluded": false,
1750 }
1751 },
1752 {
1753 "language_server": "ruff"
1754 },
1755 {
1756 "code_actions": {
1757 "excluded": false,
1758 "excluded-2": false,
1759 }
1760 }
1761 // some comment
1762 ,
1763 {
1764 "code_actions": {
1765 "excluded": false,
1766 "included-3": true,
1767 "included-4": true,
1768 }
1769 },
1770 ]
1771 },
1772 "Python": {
1773 "format_on_save": {
1774 "code_actions": {
1775 "on-save-1": true,
1776 "on-save-2": true,
1777 }
1778 },
1779 "formatter": [
1780 {
1781 "language_server": "ruff"
1782 },
1783 {
1784 "code_actions": {
1785 "excluded": false,
1786 "excluded-2": false,
1787 }
1788 }
1789 // some comment
1790 ,
1791 {
1792 "code_actions": {
1793 "excluded": false,
1794 "included-3": true,
1795 "included-4": true,
1796 }
1797 },
1798 ]
1799 }
1800 }
1801 }"#
1802 .unindent(),
1803 Some(
1804 &r#"
1805 {
1806 "formatter": [
1807 {
1808 "code_action": "default-1"
1809 },
1810 {
1811 "code_action": "default-2"
1812 },
1813 {
1814 "code_action": "default-3"
1815 },
1816 {
1817 "code_action": "default-4"
1818 }
1819 ],
1820 "format_on_save": [
1821 {
1822 "code_action": "included-1"
1823 },
1824 {
1825 "code_action": "included-2"
1826 },
1827 {
1828 "language_server": "ruff"
1829 },
1830 {
1831 "code_action": "included-3"
1832 },
1833 {
1834 "code_action": "included-4"
1835 }
1836 ],
1837 "languages": {
1838 "Rust": {
1839 "format_on_save": "prettier",
1840 "formatter": [
1841 {
1842 "code_action": "included-1"
1843 },
1844 {
1845 "code_action": "included-2"
1846 },
1847 {
1848 "language_server": "ruff"
1849 },
1850 {
1851 "code_action": "included-3"
1852 },
1853 {
1854 "code_action": "included-4"
1855 }
1856 ]
1857 },
1858 "Python": {
1859 "format_on_save": [
1860 {
1861 "code_action": "on-save-1"
1862 },
1863 {
1864 "code_action": "on-save-2"
1865 }
1866 ],
1867 "formatter": [
1868 {
1869 "language_server": "ruff"
1870 },
1871 {
1872 "code_action": "included-3"
1873 },
1874 {
1875 "code_action": "included-4"
1876 }
1877 ]
1878 }
1879 }
1880 }"#
1881 .unindent(),
1882 ),
1883 );
1884 }
1885
1886 #[test]
1887 fn test_format_on_save_formatter_migration_basic() {
1888 assert_migrate_settings_with_migrations(
1889 &[MigrationType::Json(
1890 migrations::m_2025_10_02::remove_formatters_on_save,
1891 )],
1892 &r#"{
1893 "format_on_save": "prettier"
1894 }"#
1895 .unindent(),
1896 Some(
1897 &r#"{
1898 "formatter": "prettier",
1899 "format_on_save": "on"
1900 }"#
1901 .unindent(),
1902 ),
1903 );
1904 }
1905
1906 #[test]
1907 fn test_format_on_save_formatter_migration_array() {
1908 assert_migrate_settings_with_migrations(
1909 &[MigrationType::Json(
1910 migrations::m_2025_10_02::remove_formatters_on_save,
1911 )],
1912 &r#"{
1913 "format_on_save": ["prettier", {"language_server": "eslint"}]
1914 }"#
1915 .unindent(),
1916 Some(
1917 &r#"{
1918 "formatter": [
1919 "prettier",
1920 {
1921 "language_server": "eslint"
1922 }
1923 ],
1924 "format_on_save": "on"
1925 }"#
1926 .unindent(),
1927 ),
1928 );
1929 }
1930
1931 #[test]
1932 fn test_format_on_save_on_off_unchanged() {
1933 assert_migrate_settings_with_migrations(
1934 &[MigrationType::Json(
1935 migrations::m_2025_10_02::remove_formatters_on_save,
1936 )],
1937 &r#"{
1938 "format_on_save": "on"
1939 }"#
1940 .unindent(),
1941 None,
1942 );
1943
1944 assert_migrate_settings_with_migrations(
1945 &[MigrationType::Json(
1946 migrations::m_2025_10_02::remove_formatters_on_save,
1947 )],
1948 &r#"{
1949 "format_on_save": "off"
1950 }"#
1951 .unindent(),
1952 None,
1953 );
1954 }
1955
1956 #[test]
1957 fn test_format_on_save_formatter_migration_in_languages() {
1958 assert_migrate_settings_with_migrations(
1959 &[MigrationType::Json(
1960 migrations::m_2025_10_02::remove_formatters_on_save,
1961 )],
1962 &r#"{
1963 "languages": {
1964 "Rust": {
1965 "format_on_save": "rust-analyzer"
1966 },
1967 "Python": {
1968 "format_on_save": ["ruff", "black"]
1969 }
1970 }
1971 }"#
1972 .unindent(),
1973 Some(
1974 &r#"{
1975 "languages": {
1976 "Rust": {
1977 "formatter": "rust-analyzer",
1978 "format_on_save": "on"
1979 },
1980 "Python": {
1981 "formatter": [
1982 "ruff",
1983 "black"
1984 ],
1985 "format_on_save": "on"
1986 }
1987 }
1988 }"#
1989 .unindent(),
1990 ),
1991 );
1992 }
1993
1994 #[test]
1995 fn test_format_on_save_formatter_migration_mixed_global_and_languages() {
1996 assert_migrate_settings_with_migrations(
1997 &[MigrationType::Json(
1998 migrations::m_2025_10_02::remove_formatters_on_save,
1999 )],
2000 &r#"{
2001 "format_on_save": "prettier",
2002 "languages": {
2003 "Rust": {
2004 "format_on_save": "rust-analyzer"
2005 },
2006 "Python": {
2007 "format_on_save": "on"
2008 }
2009 }
2010 }"#
2011 .unindent(),
2012 Some(
2013 &r#"{
2014 "formatter": "prettier",
2015 "format_on_save": "on",
2016 "languages": {
2017 "Rust": {
2018 "formatter": "rust-analyzer",
2019 "format_on_save": "on"
2020 },
2021 "Python": {
2022 "format_on_save": "on"
2023 }
2024 }
2025 }"#
2026 .unindent(),
2027 ),
2028 );
2029 }
2030
2031 #[test]
2032 fn test_format_on_save_no_migration_when_no_format_on_save() {
2033 assert_migrate_settings_with_migrations(
2034 &[MigrationType::Json(
2035 migrations::m_2025_10_02::remove_formatters_on_save,
2036 )],
2037 &r#"{
2038 "formatter": ["prettier"]
2039 }"#
2040 .unindent(),
2041 None,
2042 );
2043 }
2044
2045 #[test]
2046 fn test_restore_code_actions_on_format() {
2047 assert_migrate_settings_with_migrations(
2048 &[MigrationType::Json(
2049 migrations::m_2025_10_16::restore_code_actions_on_format,
2050 )],
2051 &r#"{
2052 "formatter": {
2053 "code_action": "foo"
2054 }
2055 }"#
2056 .unindent(),
2057 Some(
2058 &r#"{
2059 "code_actions_on_format": {
2060 "foo": true
2061 },
2062 "formatter": []
2063 }"#
2064 .unindent(),
2065 ),
2066 );
2067
2068 assert_migrate_settings_with_migrations(
2069 &[MigrationType::Json(
2070 migrations::m_2025_10_16::restore_code_actions_on_format,
2071 )],
2072 &r#"{
2073 "formatter": [
2074 { "code_action": "foo" },
2075 "auto"
2076 ]
2077 }"#
2078 .unindent(),
2079 None,
2080 );
2081
2082 assert_migrate_settings_with_migrations(
2083 &[MigrationType::Json(
2084 migrations::m_2025_10_16::restore_code_actions_on_format,
2085 )],
2086 &r#"{
2087 "formatter": {
2088 "code_action": "foo"
2089 },
2090 "code_actions_on_format": {
2091 "bar": true,
2092 "baz": false
2093 }
2094 }"#
2095 .unindent(),
2096 Some(
2097 &r#"{
2098 "formatter": [],
2099 "code_actions_on_format": {
2100 "foo": true,
2101 "bar": true,
2102 "baz": false
2103 }
2104 }"#
2105 .unindent(),
2106 ),
2107 );
2108
2109 assert_migrate_settings_with_migrations(
2110 &[MigrationType::Json(
2111 migrations::m_2025_10_16::restore_code_actions_on_format,
2112 )],
2113 &r#"{
2114 "formatter": [
2115 { "code_action": "foo" },
2116 { "code_action": "qux" },
2117 ],
2118 "code_actions_on_format": {
2119 "bar": true,
2120 "baz": false
2121 }
2122 }"#
2123 .unindent(),
2124 Some(
2125 &r#"{
2126 "formatter": [],
2127 "code_actions_on_format": {
2128 "foo": true,
2129 "qux": true,
2130 "bar": true,
2131 "baz": false
2132 }
2133 }"#
2134 .unindent(),
2135 ),
2136 );
2137
2138 assert_migrate_settings_with_migrations(
2139 &[MigrationType::Json(
2140 migrations::m_2025_10_16::restore_code_actions_on_format,
2141 )],
2142 &r#"{
2143 "formatter": [],
2144 "code_actions_on_format": {
2145 "bar": true,
2146 "baz": false
2147 }
2148 }"#
2149 .unindent(),
2150 None,
2151 );
2152 }
2153
2154 #[test]
2155 fn test_make_file_finder_include_ignored_an_enum() {
2156 assert_migrate_settings_with_migrations(
2157 &[MigrationType::Json(
2158 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2159 )],
2160 &r#"{ }"#.unindent(),
2161 None,
2162 );
2163
2164 assert_migrate_settings_with_migrations(
2165 &[MigrationType::Json(
2166 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2167 )],
2168 &r#"{
2169 "file_finder": {
2170 "include_ignored": true
2171 }
2172 }"#
2173 .unindent(),
2174 Some(
2175 &r#"{
2176 "file_finder": {
2177 "include_ignored": "all"
2178 }
2179 }"#
2180 .unindent(),
2181 ),
2182 );
2183
2184 assert_migrate_settings_with_migrations(
2185 &[MigrationType::Json(
2186 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2187 )],
2188 &r#"{
2189 "file_finder": {
2190 "include_ignored": false
2191 }
2192 }"#
2193 .unindent(),
2194 Some(
2195 &r#"{
2196 "file_finder": {
2197 "include_ignored": "indexed"
2198 }
2199 }"#
2200 .unindent(),
2201 ),
2202 );
2203
2204 assert_migrate_settings_with_migrations(
2205 &[MigrationType::Json(
2206 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2207 )],
2208 &r#"{
2209 "file_finder": {
2210 "include_ignored": null
2211 }
2212 }"#
2213 .unindent(),
2214 Some(
2215 &r#"{
2216 "file_finder": {
2217 "include_ignored": "smart"
2218 }
2219 }"#
2220 .unindent(),
2221 ),
2222 );
2223
2224 // Platform key: settings nested inside "linux" should be migrated
2225 assert_migrate_settings_with_migrations(
2226 &[MigrationType::Json(
2227 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2228 )],
2229 &r#"
2230 {
2231 "linux": {
2232 "file_finder": {
2233 "include_ignored": true
2234 }
2235 }
2236 }
2237 "#
2238 .unindent(),
2239 Some(
2240 &r#"
2241 {
2242 "linux": {
2243 "file_finder": {
2244 "include_ignored": "all"
2245 }
2246 }
2247 }
2248 "#
2249 .unindent(),
2250 ),
2251 );
2252
2253 // Profile: settings nested inside profiles should be migrated
2254 assert_migrate_settings_with_migrations(
2255 &[MigrationType::Json(
2256 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2257 )],
2258 &r#"
2259 {
2260 "profiles": {
2261 "work": {
2262 "file_finder": {
2263 "include_ignored": false
2264 }
2265 }
2266 }
2267 }
2268 "#
2269 .unindent(),
2270 Some(
2271 &r#"
2272 {
2273 "profiles": {
2274 "work": {
2275 "file_finder": {
2276 "include_ignored": "indexed"
2277 }
2278 }
2279 }
2280 }
2281 "#
2282 .unindent(),
2283 ),
2284 );
2285 }
2286
2287 #[test]
2288 fn test_make_relative_line_numbers_an_enum() {
2289 assert_migrate_settings_with_migrations(
2290 &[MigrationType::Json(
2291 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2292 )],
2293 &r#"{ }"#.unindent(),
2294 None,
2295 );
2296
2297 assert_migrate_settings_with_migrations(
2298 &[MigrationType::Json(
2299 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2300 )],
2301 &r#"{
2302 "relative_line_numbers": true
2303 }"#
2304 .unindent(),
2305 Some(
2306 &r#"{
2307 "relative_line_numbers": "enabled"
2308 }"#
2309 .unindent(),
2310 ),
2311 );
2312
2313 assert_migrate_settings_with_migrations(
2314 &[MigrationType::Json(
2315 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2316 )],
2317 &r#"{
2318 "relative_line_numbers": false
2319 }"#
2320 .unindent(),
2321 Some(
2322 &r#"{
2323 "relative_line_numbers": "disabled"
2324 }"#
2325 .unindent(),
2326 ),
2327 );
2328
2329 // Platform key: settings nested inside "macos" should be migrated
2330 assert_migrate_settings_with_migrations(
2331 &[MigrationType::Json(
2332 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2333 )],
2334 &r#"
2335 {
2336 "macos": {
2337 "relative_line_numbers": true
2338 }
2339 }
2340 "#
2341 .unindent(),
2342 Some(
2343 &r#"
2344 {
2345 "macos": {
2346 "relative_line_numbers": "enabled"
2347 }
2348 }
2349 "#
2350 .unindent(),
2351 ),
2352 );
2353
2354 // Profile: settings nested inside profiles should be migrated
2355 assert_migrate_settings_with_migrations(
2356 &[MigrationType::Json(
2357 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2358 )],
2359 &r#"
2360 {
2361 "profiles": {
2362 "dev": {
2363 "relative_line_numbers": false
2364 }
2365 }
2366 }
2367 "#
2368 .unindent(),
2369 Some(
2370 &r#"
2371 {
2372 "profiles": {
2373 "dev": {
2374 "relative_line_numbers": "disabled"
2375 }
2376 }
2377 }
2378 "#
2379 .unindent(),
2380 ),
2381 );
2382 }
2383
2384 #[test]
2385 fn test_remove_context_server_source() {
2386 assert_migrate_settings(
2387 &r#"
2388 {
2389 "context_servers": {
2390 "extension_server": {
2391 "source": "extension",
2392 "settings": {
2393 "foo": "bar"
2394 }
2395 },
2396 "custom_server": {
2397 "source": "custom",
2398 "command": "foo",
2399 "args": ["bar"],
2400 "env": {
2401 "FOO": "BAR"
2402 }
2403 },
2404 }
2405 }
2406 "#
2407 .unindent(),
2408 Some(
2409 &r#"
2410 {
2411 "context_servers": {
2412 "extension_server": {
2413 "settings": {
2414 "foo": "bar"
2415 }
2416 },
2417 "custom_server": {
2418 "command": "foo",
2419 "args": ["bar"],
2420 "env": {
2421 "FOO": "BAR"
2422 }
2423 },
2424 }
2425 }
2426 "#
2427 .unindent(),
2428 ),
2429 );
2430
2431 // Platform key: settings nested inside "linux" should be migrated
2432 assert_migrate_settings_with_migrations(
2433 &[MigrationType::Json(
2434 migrations::m_2025_11_25::remove_context_server_source,
2435 )],
2436 &r#"
2437 {
2438 "linux": {
2439 "context_servers": {
2440 "my_server": {
2441 "source": "extension",
2442 "settings": {
2443 "key": "value"
2444 }
2445 }
2446 }
2447 }
2448 }
2449 "#
2450 .unindent(),
2451 Some(
2452 &r#"
2453 {
2454 "linux": {
2455 "context_servers": {
2456 "my_server": {
2457 "settings": {
2458 "key": "value"
2459 }
2460 }
2461 }
2462 }
2463 }
2464 "#
2465 .unindent(),
2466 ),
2467 );
2468
2469 // Profile: settings nested inside profiles should be migrated
2470 assert_migrate_settings_with_migrations(
2471 &[MigrationType::Json(
2472 migrations::m_2025_11_25::remove_context_server_source,
2473 )],
2474 &r#"
2475 {
2476 "profiles": {
2477 "work": {
2478 "context_servers": {
2479 "my_server": {
2480 "source": "custom",
2481 "command": "foo",
2482 "args": ["bar"]
2483 }
2484 }
2485 }
2486 }
2487 }
2488 "#
2489 .unindent(),
2490 Some(
2491 &r#"
2492 {
2493 "profiles": {
2494 "work": {
2495 "context_servers": {
2496 "my_server": {
2497 "command": "foo",
2498 "args": ["bar"]
2499 }
2500 }
2501 }
2502 }
2503 }
2504 "#
2505 .unindent(),
2506 ),
2507 );
2508 }
2509
2510 #[test]
2511 fn test_project_panel_open_file_on_paste_migration() {
2512 assert_migrate_settings(
2513 &r#"
2514 {
2515 "project_panel": {
2516 "open_file_on_paste": true
2517 }
2518 }
2519 "#
2520 .unindent(),
2521 Some(
2522 &r#"
2523 {
2524 "project_panel": {
2525 "auto_open": { "on_paste": true }
2526 }
2527 }
2528 "#
2529 .unindent(),
2530 ),
2531 );
2532
2533 assert_migrate_settings(
2534 &r#"
2535 {
2536 "project_panel": {
2537 "open_file_on_paste": false
2538 }
2539 }
2540 "#
2541 .unindent(),
2542 Some(
2543 &r#"
2544 {
2545 "project_panel": {
2546 "auto_open": { "on_paste": false }
2547 }
2548 }
2549 "#
2550 .unindent(),
2551 ),
2552 );
2553 }
2554
2555 #[test]
2556 fn test_enable_preview_from_code_navigation_migration() {
2557 assert_migrate_settings(
2558 &r#"
2559 {
2560 "other_setting_1": 1,
2561 "preview_tabs": {
2562 "other_setting_2": 2,
2563 "enable_preview_from_code_navigation": false
2564 }
2565 }
2566 "#
2567 .unindent(),
2568 Some(
2569 &r#"
2570 {
2571 "other_setting_1": 1,
2572 "preview_tabs": {
2573 "other_setting_2": 2,
2574 "enable_keep_preview_on_code_navigation": false
2575 }
2576 }
2577 "#
2578 .unindent(),
2579 ),
2580 );
2581
2582 assert_migrate_settings(
2583 &r#"
2584 {
2585 "other_setting_1": 1,
2586 "preview_tabs": {
2587 "other_setting_2": 2,
2588 "enable_preview_from_code_navigation": true
2589 }
2590 }
2591 "#
2592 .unindent(),
2593 Some(
2594 &r#"
2595 {
2596 "other_setting_1": 1,
2597 "preview_tabs": {
2598 "other_setting_2": 2,
2599 "enable_keep_preview_on_code_navigation": true
2600 }
2601 }
2602 "#
2603 .unindent(),
2604 ),
2605 );
2606 }
2607
2608 #[test]
2609 fn test_move_edit_prediction_provider_to_edit_predictions() {
2610 assert_migrate_settings_with_migrations(
2611 &[MigrationType::Json(
2612 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2613 )],
2614 &r#"{ }"#.unindent(),
2615 None,
2616 );
2617
2618 assert_migrate_settings_with_migrations(
2619 &[MigrationType::Json(
2620 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2621 )],
2622 &r#"
2623 {
2624 "features": {
2625 "edit_prediction_provider": "copilot"
2626 }
2627 }
2628 "#
2629 .unindent(),
2630 Some(
2631 &r#"
2632 {
2633 "edit_predictions": {
2634 "provider": "copilot"
2635 }
2636 }
2637 "#
2638 .unindent(),
2639 ),
2640 );
2641
2642 assert_migrate_settings_with_migrations(
2643 &[MigrationType::Json(
2644 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2645 )],
2646 &r#"
2647 {
2648 "features": {
2649 "edit_prediction_provider": "zed"
2650 },
2651 "edit_predictions": {
2652 "mode": "eager"
2653 }
2654 }
2655 "#
2656 .unindent(),
2657 Some(
2658 &r#"
2659 {
2660 "edit_predictions": {
2661 "provider": "zed",
2662 "mode": "eager"
2663 }
2664 }
2665 "#
2666 .unindent(),
2667 ),
2668 );
2669
2670 assert_migrate_settings_with_migrations(
2671 &[MigrationType::Json(
2672 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2673 )],
2674 &r#"
2675 {
2676 "features": {
2677 "edit_prediction_provider": "supermaven"
2678 },
2679 "edit_predictions": {
2680 "provider": "copilot"
2681 }
2682 }
2683 "#
2684 .unindent(),
2685 Some(
2686 &r#"
2687 {
2688 "edit_predictions": {
2689 "provider": "copilot"
2690 }
2691 }
2692 "#
2693 .unindent(),
2694 ),
2695 );
2696
2697 assert_migrate_settings_with_migrations(
2698 &[MigrationType::Json(
2699 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2700 )],
2701 &r#"
2702 {
2703 "edit_predictions": {
2704 "provider": "zed"
2705 }
2706 }
2707 "#
2708 .unindent(),
2709 None,
2710 );
2711
2712 // Non-object edit_predictions (e.g. true) should gracefully skip
2713 // instead of bail!-ing and aborting the entire migration chain.
2714 assert_migrate_settings_with_migrations(
2715 &[MigrationType::Json(
2716 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2717 )],
2718 &r#"
2719 {
2720 "features": {
2721 "edit_prediction_provider": "copilot"
2722 },
2723 "edit_predictions": true
2724 }
2725 "#
2726 .unindent(),
2727 Some(
2728 &r#"
2729 {
2730 "edit_predictions": true
2731 }
2732 "#
2733 .unindent(),
2734 ),
2735 );
2736
2737 // Platform key: settings nested inside "macos" should be migrated
2738 assert_migrate_settings_with_migrations(
2739 &[MigrationType::Json(
2740 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2741 )],
2742 &r#"
2743 {
2744 "macos": {
2745 "features": {
2746 "edit_prediction_provider": "copilot"
2747 }
2748 }
2749 }
2750 "#
2751 .unindent(),
2752 Some(
2753 &r#"
2754 {
2755 "macos": {
2756 "edit_predictions": {
2757 "provider": "copilot"
2758 }
2759 }
2760 }
2761 "#
2762 .unindent(),
2763 ),
2764 );
2765
2766 // Profile: settings nested inside profiles should be migrated
2767 assert_migrate_settings_with_migrations(
2768 &[MigrationType::Json(
2769 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2770 )],
2771 &r#"
2772 {
2773 "profiles": {
2774 "work": {
2775 "features": {
2776 "edit_prediction_provider": "copilot"
2777 }
2778 }
2779 }
2780 }
2781 "#
2782 .unindent(),
2783 Some(
2784 &r#"
2785 {
2786 "profiles": {
2787 "work": {
2788 "edit_predictions": {
2789 "provider": "copilot"
2790 }
2791 }
2792 }
2793 }
2794 "#
2795 .unindent(),
2796 ),
2797 );
2798
2799 // Combined: root + platform + profile should all be migrated simultaneously
2800 assert_migrate_settings_with_migrations(
2801 &[MigrationType::Json(
2802 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2803 )],
2804 &r#"
2805 {
2806 "features": {
2807 "edit_prediction_provider": "copilot"
2808 },
2809 "macos": {
2810 "features": {
2811 "edit_prediction_provider": "zed"
2812 }
2813 },
2814 "profiles": {
2815 "work": {
2816 "features": {
2817 "edit_prediction_provider": "supermaven"
2818 }
2819 }
2820 }
2821 }
2822 "#
2823 .unindent(),
2824 Some(
2825 &r#"
2826 {
2827 "edit_predictions": {
2828 "provider": "copilot"
2829 },
2830 "macos": {
2831 "edit_predictions": {
2832 "provider": "zed"
2833 }
2834 },
2835 "profiles": {
2836 "work": {
2837 "edit_predictions": {
2838 "provider": "supermaven"
2839 }
2840 }
2841 }
2842 }
2843 "#
2844 .unindent(),
2845 ),
2846 );
2847 }
2848
2849 #[test]
2850 fn test_migrate_experimental_sweep_mercury() {
2851 assert_migrate_settings_with_migrations(
2852 &[MigrationType::Json(
2853 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2854 )],
2855 &r#"{ }"#.unindent(),
2856 None,
2857 );
2858
2859 assert_migrate_settings_with_migrations(
2860 &[MigrationType::Json(
2861 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2862 )],
2863 &r#"
2864 {
2865 "edit_predictions": {
2866 "provider": {
2867 "experimental": "sweep"
2868 }
2869 }
2870 }
2871 "#
2872 .unindent(),
2873 Some(
2874 &r#"
2875 {
2876 "edit_predictions": {
2877 "provider": "sweep"
2878 }
2879 }
2880 "#
2881 .unindent(),
2882 ),
2883 );
2884
2885 assert_migrate_settings_with_migrations(
2886 &[MigrationType::Json(
2887 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2888 )],
2889 &r#"
2890 {
2891 "edit_predictions": {
2892 "provider": {
2893 "experimental": "mercury"
2894 }
2895 }
2896 }
2897 "#
2898 .unindent(),
2899 Some(
2900 &r#"
2901 {
2902 "edit_predictions": {
2903 "provider": "mercury"
2904 }
2905 }
2906 "#
2907 .unindent(),
2908 ),
2909 );
2910
2911 assert_migrate_settings_with_migrations(
2912 &[MigrationType::Json(
2913 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2914 )],
2915 &r#"
2916 {
2917 "features": {
2918 "edit_prediction_provider": {
2919 "experimental": "sweep"
2920 }
2921 }
2922 }
2923 "#
2924 .unindent(),
2925 Some(
2926 &r#"
2927 {
2928 "features": {
2929 "edit_prediction_provider": "sweep"
2930 }
2931 }
2932 "#
2933 .unindent(),
2934 ),
2935 );
2936
2937 assert_migrate_settings_with_migrations(
2938 &[MigrationType::Json(
2939 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2940 )],
2941 &r#"
2942 {
2943 "edit_predictions": {
2944 "provider": "zed"
2945 }
2946 }
2947 "#
2948 .unindent(),
2949 None,
2950 );
2951
2952 assert_migrate_settings_with_migrations(
2953 &[MigrationType::Json(
2954 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2955 )],
2956 &r#"
2957 {
2958 "edit_predictions": {
2959 "provider": {
2960 "experimental": "zeta2"
2961 }
2962 }
2963 }
2964 "#
2965 .unindent(),
2966 None,
2967 );
2968
2969 // Platform key: settings nested inside "linux" should be migrated
2970 assert_migrate_settings_with_migrations(
2971 &[MigrationType::Json(
2972 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2973 )],
2974 &r#"
2975 {
2976 "linux": {
2977 "edit_predictions": {
2978 "provider": {
2979 "experimental": "sweep"
2980 }
2981 }
2982 }
2983 }
2984 "#
2985 .unindent(),
2986 Some(
2987 &r#"
2988 {
2989 "linux": {
2990 "edit_predictions": {
2991 "provider": "sweep"
2992 }
2993 }
2994 }
2995 "#
2996 .unindent(),
2997 ),
2998 );
2999
3000 // Profile: settings nested inside profiles should be migrated
3001 assert_migrate_settings_with_migrations(
3002 &[MigrationType::Json(
3003 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3004 )],
3005 &r#"
3006 {
3007 "profiles": {
3008 "dev": {
3009 "edit_predictions": {
3010 "provider": {
3011 "experimental": "mercury"
3012 }
3013 }
3014 }
3015 }
3016 }
3017 "#
3018 .unindent(),
3019 Some(
3020 &r#"
3021 {
3022 "profiles": {
3023 "dev": {
3024 "edit_predictions": {
3025 "provider": "mercury"
3026 }
3027 }
3028 }
3029 }
3030 "#
3031 .unindent(),
3032 ),
3033 );
3034
3035 // Combined: root + platform + profile should all be migrated simultaneously
3036 assert_migrate_settings_with_migrations(
3037 &[MigrationType::Json(
3038 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3039 )],
3040 &r#"
3041 {
3042 "edit_predictions": {
3043 "provider": {
3044 "experimental": "sweep"
3045 }
3046 },
3047 "linux": {
3048 "edit_predictions": {
3049 "provider": {
3050 "experimental": "mercury"
3051 }
3052 }
3053 },
3054 "profiles": {
3055 "dev": {
3056 "edit_predictions": {
3057 "provider": {
3058 "experimental": "sweep"
3059 }
3060 }
3061 }
3062 }
3063 }
3064 "#
3065 .unindent(),
3066 Some(
3067 &r#"
3068 {
3069 "edit_predictions": {
3070 "provider": "sweep"
3071 },
3072 "linux": {
3073 "edit_predictions": {
3074 "provider": "mercury"
3075 }
3076 },
3077 "profiles": {
3078 "dev": {
3079 "edit_predictions": {
3080 "provider": "sweep"
3081 }
3082 }
3083 }
3084 }
3085 "#
3086 .unindent(),
3087 ),
3088 );
3089 }
3090
3091 #[test]
3092 fn test_migrate_always_allow_tool_actions_to_default() {
3093 // No agent settings - no change
3094 assert_migrate_settings_with_migrations(
3095 &[MigrationType::Json(
3096 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3097 )],
3098 &r#"{ }"#.unindent(),
3099 None,
3100 );
3101
3102 // always_allow_tool_actions: true -> tool_permissions.default: "allow"
3103 assert_migrate_settings_with_migrations(
3104 &[MigrationType::Json(
3105 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3106 )],
3107 &r#"
3108 {
3109 "agent": {
3110 "always_allow_tool_actions": true
3111 }
3112 }
3113 "#
3114 .unindent(),
3115 Some(
3116 &r#"
3117 {
3118 "agent": {
3119 "tool_permissions": {
3120 "default": "allow"
3121 }
3122 }
3123 }
3124 "#
3125 .unindent(),
3126 ),
3127 );
3128
3129 // always_allow_tool_actions: false -> just remove it
3130 assert_migrate_settings_with_migrations(
3131 &[MigrationType::Json(
3132 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3133 )],
3134 &r#"
3135 {
3136 "agent": {
3137 "always_allow_tool_actions": false
3138 }
3139 }
3140 "#
3141 .unindent(),
3142 Some(
3143 // The blank line has spaces because the migration preserves the original indentation
3144 "{\n \"agent\": {\n \n }\n}\n",
3145 ),
3146 );
3147
3148 // Preserve existing tool_permissions.tools when migrating
3149 assert_migrate_settings_with_migrations(
3150 &[MigrationType::Json(
3151 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3152 )],
3153 &r#"
3154 {
3155 "agent": {
3156 "always_allow_tool_actions": true,
3157 "tool_permissions": {
3158 "tools": {
3159 "terminal": {
3160 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3161 }
3162 }
3163 }
3164 }
3165 }
3166 "#
3167 .unindent(),
3168 Some(
3169 &r#"
3170 {
3171 "agent": {
3172 "tool_permissions": {
3173 "default": "allow",
3174 "tools": {
3175 "terminal": {
3176 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3177 }
3178 }
3179 }
3180 }
3181 }
3182 "#
3183 .unindent(),
3184 ),
3185 );
3186
3187 // Don't override existing default (and migrate default_mode to default)
3188 assert_migrate_settings_with_migrations(
3189 &[MigrationType::Json(
3190 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3191 )],
3192 &r#"
3193 {
3194 "agent": {
3195 "always_allow_tool_actions": true,
3196 "tool_permissions": {
3197 "default_mode": "confirm"
3198 }
3199 }
3200 }
3201 "#
3202 .unindent(),
3203 Some(
3204 &r#"
3205 {
3206 "agent": {
3207 "tool_permissions": {
3208 "default": "confirm"
3209 }
3210 }
3211 }
3212 "#
3213 .unindent(),
3214 ),
3215 );
3216
3217 // Migrate existing default_mode to default (no always_allow_tool_actions)
3218 assert_migrate_settings_with_migrations(
3219 &[MigrationType::Json(
3220 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3221 )],
3222 &r#"
3223 {
3224 "agent": {
3225 "tool_permissions": {
3226 "default_mode": "allow"
3227 }
3228 }
3229 }
3230 "#
3231 .unindent(),
3232 Some(
3233 &r#"
3234 {
3235 "agent": {
3236 "tool_permissions": {
3237 "default": "allow"
3238 }
3239 }
3240 }
3241 "#
3242 .unindent(),
3243 ),
3244 );
3245
3246 // No migration needed if already using new format with "default"
3247 assert_migrate_settings_with_migrations(
3248 &[MigrationType::Json(
3249 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3250 )],
3251 &r#"
3252 {
3253 "agent": {
3254 "tool_permissions": {
3255 "default": "allow"
3256 }
3257 }
3258 }
3259 "#
3260 .unindent(),
3261 None,
3262 );
3263
3264 // Migrate default_mode to default in tool-specific rules
3265 assert_migrate_settings_with_migrations(
3266 &[MigrationType::Json(
3267 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3268 )],
3269 &r#"
3270 {
3271 "agent": {
3272 "tool_permissions": {
3273 "default_mode": "confirm",
3274 "tools": {
3275 "terminal": {
3276 "default_mode": "allow"
3277 }
3278 }
3279 }
3280 }
3281 }
3282 "#
3283 .unindent(),
3284 Some(
3285 &r#"
3286 {
3287 "agent": {
3288 "tool_permissions": {
3289 "default": "confirm",
3290 "tools": {
3291 "terminal": {
3292 "default": "allow"
3293 }
3294 }
3295 }
3296 }
3297 }
3298 "#
3299 .unindent(),
3300 ),
3301 );
3302
3303 // When tool_permissions is null, replace it so always_allow is preserved
3304 assert_migrate_settings_with_migrations(
3305 &[MigrationType::Json(
3306 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3307 )],
3308 &r#"
3309 {
3310 "agent": {
3311 "always_allow_tool_actions": true,
3312 "tool_permissions": null
3313 }
3314 }
3315 "#
3316 .unindent(),
3317 Some(
3318 &r#"
3319 {
3320 "agent": {
3321 "tool_permissions": {
3322 "default": "allow"
3323 }
3324 }
3325 }
3326 "#
3327 .unindent(),
3328 ),
3329 );
3330
3331 // Platform-specific agent migration
3332 assert_migrate_settings_with_migrations(
3333 &[MigrationType::Json(
3334 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3335 )],
3336 &r#"
3337 {
3338 "linux": {
3339 "agent": {
3340 "always_allow_tool_actions": true
3341 }
3342 }
3343 }
3344 "#
3345 .unindent(),
3346 Some(
3347 &r#"
3348 {
3349 "linux": {
3350 "agent": {
3351 "tool_permissions": {
3352 "default": "allow"
3353 }
3354 }
3355 }
3356 }
3357 "#
3358 .unindent(),
3359 ),
3360 );
3361
3362 // Channel-specific agent migration
3363 assert_migrate_settings_with_migrations(
3364 &[MigrationType::Json(
3365 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3366 )],
3367 &r#"
3368 {
3369 "agent": {
3370 "always_allow_tool_actions": true
3371 },
3372 "nightly": {
3373 "agent": {
3374 "tool_permissions": {
3375 "default_mode": "confirm"
3376 }
3377 }
3378 }
3379 }
3380 "#
3381 .unindent(),
3382 Some(
3383 &r#"
3384 {
3385 "agent": {
3386 "tool_permissions": {
3387 "default": "allow"
3388 }
3389 },
3390 "nightly": {
3391 "agent": {
3392 "tool_permissions": {
3393 "default": "confirm"
3394 }
3395 }
3396 }
3397 }
3398 "#
3399 .unindent(),
3400 ),
3401 );
3402
3403 // Profile-level migration
3404 assert_migrate_settings_with_migrations(
3405 &[MigrationType::Json(
3406 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3407 )],
3408 &r#"
3409 {
3410 "agent": {
3411 "profiles": {
3412 "custom": {
3413 "always_allow_tool_actions": true,
3414 "tool_permissions": {
3415 "default_mode": "allow"
3416 }
3417 }
3418 }
3419 }
3420 }
3421 "#
3422 .unindent(),
3423 Some(
3424 &r#"
3425 {
3426 "agent": {
3427 "profiles": {
3428 "custom": {
3429 "tool_permissions": {
3430 "default": "allow"
3431 }
3432 }
3433 }
3434 }
3435 }
3436 "#
3437 .unindent(),
3438 ),
3439 );
3440
3441 // Platform-specific agent with profiles
3442 assert_migrate_settings_with_migrations(
3443 &[MigrationType::Json(
3444 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3445 )],
3446 &r#"
3447 {
3448 "macos": {
3449 "agent": {
3450 "always_allow_tool_actions": true,
3451 "profiles": {
3452 "strict": {
3453 "tool_permissions": {
3454 "default_mode": "deny"
3455 }
3456 }
3457 }
3458 }
3459 }
3460 }
3461 "#
3462 .unindent(),
3463 Some(
3464 &r#"
3465 {
3466 "macos": {
3467 "agent": {
3468 "tool_permissions": {
3469 "default": "allow"
3470 },
3471 "profiles": {
3472 "strict": {
3473 "tool_permissions": {
3474 "default": "deny"
3475 }
3476 }
3477 }
3478 }
3479 }
3480 }
3481 "#
3482 .unindent(),
3483 ),
3484 );
3485
3486 // Root-level profile with always_allow_tool_actions
3487 assert_migrate_settings_with_migrations(
3488 &[MigrationType::Json(
3489 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3490 )],
3491 &r#"
3492 {
3493 "profiles": {
3494 "work": {
3495 "agent": {
3496 "always_allow_tool_actions": true
3497 }
3498 }
3499 }
3500 }
3501 "#
3502 .unindent(),
3503 Some(
3504 &r#"
3505 {
3506 "profiles": {
3507 "work": {
3508 "agent": {
3509 "tool_permissions": {
3510 "default": "allow"
3511 }
3512 }
3513 }
3514 }
3515 }
3516 "#
3517 .unindent(),
3518 ),
3519 );
3520
3521 // Root-level profile with default_mode
3522 assert_migrate_settings_with_migrations(
3523 &[MigrationType::Json(
3524 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3525 )],
3526 &r#"
3527 {
3528 "profiles": {
3529 "work": {
3530 "agent": {
3531 "tool_permissions": {
3532 "default_mode": "allow"
3533 }
3534 }
3535 }
3536 }
3537 }
3538 "#
3539 .unindent(),
3540 Some(
3541 &r#"
3542 {
3543 "profiles": {
3544 "work": {
3545 "agent": {
3546 "tool_permissions": {
3547 "default": "allow"
3548 }
3549 }
3550 }
3551 }
3552 }
3553 "#
3554 .unindent(),
3555 ),
3556 );
3557
3558 // Root-level profile + root-level agent both migrated
3559 assert_migrate_settings_with_migrations(
3560 &[MigrationType::Json(
3561 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3562 )],
3563 &r#"
3564 {
3565 "agent": {
3566 "always_allow_tool_actions": true
3567 },
3568 "profiles": {
3569 "strict": {
3570 "agent": {
3571 "tool_permissions": {
3572 "default_mode": "deny"
3573 }
3574 }
3575 }
3576 }
3577 }
3578 "#
3579 .unindent(),
3580 Some(
3581 &r#"
3582 {
3583 "agent": {
3584 "tool_permissions": {
3585 "default": "allow"
3586 }
3587 },
3588 "profiles": {
3589 "strict": {
3590 "agent": {
3591 "tool_permissions": {
3592 "default": "deny"
3593 }
3594 }
3595 }
3596 }
3597 }
3598 "#
3599 .unindent(),
3600 ),
3601 );
3602
3603 // Non-boolean always_allow_tool_actions (string "true") is left in place
3604 // so the schema validator can report it, rather than silently dropping user data.
3605 assert_migrate_settings_with_migrations(
3606 &[MigrationType::Json(
3607 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3608 )],
3609 &r#"
3610 {
3611 "agent": {
3612 "always_allow_tool_actions": "true"
3613 }
3614 }
3615 "#
3616 .unindent(),
3617 None,
3618 );
3619
3620 // null always_allow_tool_actions is removed (treated as false)
3621 assert_migrate_settings_with_migrations(
3622 &[MigrationType::Json(
3623 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3624 )],
3625 &r#"
3626 {
3627 "agent": {
3628 "always_allow_tool_actions": null
3629 }
3630 }
3631 "#
3632 .unindent(),
3633 Some(&"{\n \"agent\": {\n \n }\n}\n"),
3634 );
3635
3636 // Project-local settings (.zed/settings.json) with always_allow_tool_actions
3637 // These files have no platform/channel overrides or root-level profiles.
3638 assert_migrate_settings_with_migrations(
3639 &[MigrationType::Json(
3640 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3641 )],
3642 &r#"
3643 {
3644 "agent": {
3645 "always_allow_tool_actions": true,
3646 "tool_permissions": {
3647 "tools": {
3648 "terminal": {
3649 "default_mode": "confirm",
3650 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3651 }
3652 }
3653 }
3654 }
3655 }
3656 "#
3657 .unindent(),
3658 Some(
3659 &r#"
3660 {
3661 "agent": {
3662 "tool_permissions": {
3663 "default": "allow",
3664 "tools": {
3665 "terminal": {
3666 "default": "confirm",
3667 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3668 }
3669 }
3670 }
3671 }
3672 }
3673 "#
3674 .unindent(),
3675 ),
3676 );
3677
3678 // Project-local settings with only default_mode (no always_allow_tool_actions)
3679 assert_migrate_settings_with_migrations(
3680 &[MigrationType::Json(
3681 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3682 )],
3683 &r#"
3684 {
3685 "agent": {
3686 "tool_permissions": {
3687 "default_mode": "deny"
3688 }
3689 }
3690 }
3691 "#
3692 .unindent(),
3693 Some(
3694 &r#"
3695 {
3696 "agent": {
3697 "tool_permissions": {
3698 "default": "deny"
3699 }
3700 }
3701 }
3702 "#
3703 .unindent(),
3704 ),
3705 );
3706
3707 // Project-local settings with no agent section at all - no change
3708 assert_migrate_settings_with_migrations(
3709 &[MigrationType::Json(
3710 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3711 )],
3712 &r#"
3713 {
3714 "tab_size": 4,
3715 "format_on_save": "on"
3716 }
3717 "#
3718 .unindent(),
3719 None,
3720 );
3721
3722 // Existing agent_servers are left untouched
3723 assert_migrate_settings_with_migrations(
3724 &[MigrationType::Json(
3725 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3726 )],
3727 &r#"
3728 {
3729 "agent": {
3730 "always_allow_tool_actions": true
3731 },
3732 "agent_servers": {
3733 "claude": {
3734 "default_mode": "plan"
3735 },
3736 "codex": {
3737 "default_mode": "read-only"
3738 }
3739 }
3740 }
3741 "#
3742 .unindent(),
3743 Some(
3744 &r#"
3745 {
3746 "agent": {
3747 "tool_permissions": {
3748 "default": "allow"
3749 }
3750 },
3751 "agent_servers": {
3752 "claude": {
3753 "default_mode": "plan"
3754 },
3755 "codex": {
3756 "default_mode": "read-only"
3757 }
3758 }
3759 }
3760 "#
3761 .unindent(),
3762 ),
3763 );
3764
3765 // Existing agent_servers are left untouched even with partial entries
3766 assert_migrate_settings_with_migrations(
3767 &[MigrationType::Json(
3768 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3769 )],
3770 &r#"
3771 {
3772 "agent": {
3773 "always_allow_tool_actions": true
3774 },
3775 "agent_servers": {
3776 "claude": {
3777 "default_mode": "plan"
3778 }
3779 }
3780 }
3781 "#
3782 .unindent(),
3783 Some(
3784 &r#"
3785 {
3786 "agent": {
3787 "tool_permissions": {
3788 "default": "allow"
3789 }
3790 },
3791 "agent_servers": {
3792 "claude": {
3793 "default_mode": "plan"
3794 }
3795 }
3796 }
3797 "#
3798 .unindent(),
3799 ),
3800 );
3801
3802 // always_allow_tool_actions: false leaves agent_servers untouched
3803 assert_migrate_settings_with_migrations(
3804 &[MigrationType::Json(
3805 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3806 )],
3807 &r#"
3808 {
3809 "agent": {
3810 "always_allow_tool_actions": false
3811 },
3812 "agent_servers": {
3813 "claude": {}
3814 }
3815 }
3816 "#
3817 .unindent(),
3818 Some(
3819 "{\n \"agent\": {\n \n },\n \"agent_servers\": {\n \"claude\": {}\n }\n}\n",
3820 ),
3821 );
3822 }
3823}