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