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