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