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