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