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