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