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