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