1//! ## When to create a migration and why?
2//! A migration is necessary when keymap actions or settings are renamed or transformed (e.g., from an array to a string, a string to an array, a boolean to an enum, etc.).
3//!
4//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally.
5//! It also provides a quick way to migrate their existing settings to the latest state using button in UI.
6//!
7//! ## How to create a migration?
8//! Migrations use Tree-sitter to query commonly used patterns, such as actions with a string or actions with an array where the second argument is an object, etc.
9//! Once queried, *you can filter out the modified items* and write the replacement logic.
10//!
11//! You *must not* modify previous migrations; always create new ones instead.
12//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state.
13//! Modifying existing migrations means they will only work for users upgrading from version x-1 to x, but not from x-2 to x, and so on, where x is the latest version.
14//!
15//! You only need to write replacement logic for x-1 to x because you can be certain that, internally, every user will be at x-1, regardless of their on disk state.
16
17use anyhow::{Context as _, Result};
18use settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text};
19use std::{cmp::Reverse, ops::Range, sync::LazyLock};
20use streaming_iterator::StreamingIterator;
21use tree_sitter::{Query, QueryMatch};
22
23use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
24
25mod migrations;
26mod patterns;
27
28fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
29 let mut parser = tree_sitter::Parser::new();
30 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
31 let syntax_tree = parser
32 .parse(text, None)
33 .context("failed to parse settings")?;
34
35 let mut cursor = tree_sitter::QueryCursor::new();
36 let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
37
38 let mut edits = vec![];
39 while let Some(mat) = matches.next() {
40 if let Some((_, callback)) = patterns.get(mat.pattern_index) {
41 edits.extend(callback(text, mat, query));
42 }
43 }
44
45 edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
46 edits.dedup_by(|(range_b, _), (range_a, _)| {
47 range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
48 });
49
50 if edits.is_empty() {
51 Ok(None)
52 } else {
53 let mut new_text = text.to_string();
54 for (range, replacement) in edits.iter().rev() {
55 new_text.replace_range(range.clone(), replacement);
56 }
57 if new_text == text {
58 log::error!(
59 "Edits computed for configuration migration do not cause a change: {:?}",
60 edits
61 );
62 Ok(None)
63 } else {
64 Ok(Some(new_text))
65 }
66 }
67}
68
69/// Runs the provided migrations on the given text.
70/// Will automatically return `Ok(None)` if there's no content to migrate.
71fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<String>> {
72 if text.is_empty() {
73 return Ok(None);
74 }
75
76 let mut current_text = text.to_string();
77 let mut result: Option<String> = None;
78 let json_indent_size = infer_json_indent_size(¤t_text);
79 for migration in migrations.iter() {
80 let migrated_text = match migration {
81 MigrationType::TreeSitter(patterns, query) => migrate(¤t_text, patterns, query)?,
82 MigrationType::Json(callback) => {
83 if current_text.trim().is_empty() {
84 return Ok(None);
85 }
86 let old_content: serde_json_lenient::Value =
87 parse_json_with_comments(¤t_text)?;
88 let old_value = serde_json::to_value(&old_content).unwrap();
89 let mut new_value = old_value.clone();
90 callback(&mut new_value)?;
91 if new_value != old_value {
92 let mut current = current_text.clone();
93 let mut edits = vec![];
94 update_value_in_json_text(
95 &mut current,
96 &mut vec![],
97 json_indent_size,
98 &old_value,
99 &new_value,
100 &mut edits,
101 );
102 let mut migrated_text = current_text.clone();
103 for (range, replacement) in edits.into_iter() {
104 migrated_text.replace_range(range, &replacement);
105 }
106 Some(migrated_text)
107 } else {
108 None
109 }
110 }
111 };
112 if let Some(migrated_text) = migrated_text {
113 current_text = migrated_text.clone();
114 result = Some(migrated_text);
115 }
116 }
117 Ok(result.filter(|new_text| text != new_text))
118}
119
120pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
121 let migrations: &[MigrationType] = &[
122 MigrationType::TreeSitter(
123 migrations::m_2025_01_29::KEYMAP_PATTERNS,
124 &KEYMAP_QUERY_2025_01_29,
125 ),
126 MigrationType::TreeSitter(
127 migrations::m_2025_01_30::KEYMAP_PATTERNS,
128 &KEYMAP_QUERY_2025_01_30,
129 ),
130 MigrationType::TreeSitter(
131 migrations::m_2025_03_03::KEYMAP_PATTERNS,
132 &KEYMAP_QUERY_2025_03_03,
133 ),
134 MigrationType::TreeSitter(
135 migrations::m_2025_03_06::KEYMAP_PATTERNS,
136 &KEYMAP_QUERY_2025_03_06,
137 ),
138 MigrationType::TreeSitter(
139 migrations::m_2025_04_15::KEYMAP_PATTERNS,
140 &KEYMAP_QUERY_2025_04_15,
141 ),
142 MigrationType::TreeSitter(
143 migrations::m_2025_12_08::KEYMAP_PATTERNS,
144 &KEYMAP_QUERY_2025_12_08,
145 ),
146 MigrationType::TreeSitter(
147 migrations::m_2026_03_23::KEYMAP_PATTERNS,
148 &KEYMAP_QUERY_2026_03_23,
149 ),
150 ];
151 run_migrations(text, migrations)
152}
153
154enum MigrationType<'a> {
155 TreeSitter(MigrationPatterns, &'a Query),
156 Json(fn(&mut serde_json::Value) -> Result<()>),
157}
158
159pub fn migrate_settings(text: &str) -> Result<Option<String>> {
160 let migrations: &[MigrationType] = &[
161 MigrationType::TreeSitter(
162 migrations::m_2025_01_02::SETTINGS_PATTERNS,
163 &SETTINGS_QUERY_2025_01_02,
164 ),
165 MigrationType::TreeSitter(
166 migrations::m_2025_01_29::SETTINGS_PATTERNS,
167 &SETTINGS_QUERY_2025_01_29,
168 ),
169 MigrationType::TreeSitter(
170 migrations::m_2025_01_30::SETTINGS_PATTERNS,
171 &SETTINGS_QUERY_2025_01_30,
172 ),
173 MigrationType::TreeSitter(
174 migrations::m_2025_03_29::SETTINGS_PATTERNS,
175 &SETTINGS_QUERY_2025_03_29,
176 ),
177 MigrationType::TreeSitter(
178 migrations::m_2025_04_15::SETTINGS_PATTERNS,
179 &SETTINGS_QUERY_2025_04_15,
180 ),
181 MigrationType::TreeSitter(
182 migrations::m_2025_04_21::SETTINGS_PATTERNS,
183 &SETTINGS_QUERY_2025_04_21,
184 ),
185 MigrationType::TreeSitter(
186 migrations::m_2025_04_23::SETTINGS_PATTERNS,
187 &SETTINGS_QUERY_2025_04_23,
188 ),
189 MigrationType::TreeSitter(
190 migrations::m_2025_05_05::SETTINGS_PATTERNS,
191 &SETTINGS_QUERY_2025_05_05,
192 ),
193 MigrationType::TreeSitter(
194 migrations::m_2025_05_08::SETTINGS_PATTERNS,
195 &SETTINGS_QUERY_2025_05_08,
196 ),
197 MigrationType::TreeSitter(
198 migrations::m_2025_06_16::SETTINGS_PATTERNS,
199 &SETTINGS_QUERY_2025_06_16,
200 ),
201 MigrationType::TreeSitter(
202 migrations::m_2025_06_25::SETTINGS_PATTERNS,
203 &SETTINGS_QUERY_2025_06_25,
204 ),
205 MigrationType::TreeSitter(
206 migrations::m_2025_06_27::SETTINGS_PATTERNS,
207 &SETTINGS_QUERY_2025_06_27,
208 ),
209 MigrationType::TreeSitter(
210 migrations::m_2025_07_08::SETTINGS_PATTERNS,
211 &SETTINGS_QUERY_2025_07_08,
212 ),
213 MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters),
214 MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save),
215 MigrationType::TreeSitter(
216 migrations::m_2025_10_03::SETTINGS_PATTERNS,
217 &SETTINGS_QUERY_2025_10_03,
218 ),
219 MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
220 MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum),
221 MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum),
222 MigrationType::TreeSitter(
223 migrations::m_2025_11_12::SETTINGS_PATTERNS,
224 &SETTINGS_QUERY_2025_11_12,
225 ),
226 MigrationType::TreeSitter(
227 migrations::m_2025_12_01::SETTINGS_PATTERNS,
228 &SETTINGS_QUERY_2025_12_01,
229 ),
230 MigrationType::TreeSitter(
231 migrations::m_2025_11_20::SETTINGS_PATTERNS,
232 &SETTINGS_QUERY_2025_11_20,
233 ),
234 MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source),
235 MigrationType::TreeSitter(
236 migrations::m_2025_12_15::SETTINGS_PATTERNS,
237 &SETTINGS_QUERY_2025_12_15,
238 ),
239 MigrationType::Json(migrations::m_2025_01_27::make_auto_indent_an_enum),
240 MigrationType::Json(
241 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
242 ),
243 MigrationType::Json(migrations::m_2026_02_03::migrate_experimental_sweep_mercury),
244 MigrationType::Json(migrations::m_2026_02_04::migrate_tool_permission_defaults),
245 MigrationType::Json(migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry),
246 MigrationType::TreeSitter(
247 migrations::m_2026_03_16::SETTINGS_PATTERNS,
248 &SETTINGS_QUERY_2026_03_16,
249 ),
250 MigrationType::Json(migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum),
251 MigrationType::Json(migrations::m_2026_04_01::restructure_profiles_with_settings_key),
252 MigrationType::Json(migrations::m_2026_04_10::rename_web_search_to_search_web),
253 ];
254 run_migrations(text, migrations)
255}
256
257pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
258 migrate(
259 text,
260 &[(
261 SETTINGS_NESTED_KEY_VALUE_PATTERN,
262 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
263 )],
264 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
265 )
266}
267
268pub type MigrationPatterns = &'static [(
269 &'static str,
270 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
271)];
272
273macro_rules! define_query {
274 ($var_name:ident, $patterns_path:path) => {
275 static $var_name: LazyLock<Query> = LazyLock::new(|| {
276 Query::new(
277 &tree_sitter_json::LANGUAGE.into(),
278 &$patterns_path
279 .iter()
280 .map(|pattern| pattern.0)
281 .collect::<String>(),
282 )
283 .unwrap()
284 });
285 };
286}
287
288// keymap
289define_query!(
290 KEYMAP_QUERY_2025_01_29,
291 migrations::m_2025_01_29::KEYMAP_PATTERNS
292);
293define_query!(
294 KEYMAP_QUERY_2025_01_30,
295 migrations::m_2025_01_30::KEYMAP_PATTERNS
296);
297define_query!(
298 KEYMAP_QUERY_2025_03_03,
299 migrations::m_2025_03_03::KEYMAP_PATTERNS
300);
301define_query!(
302 KEYMAP_QUERY_2025_03_06,
303 migrations::m_2025_03_06::KEYMAP_PATTERNS
304);
305define_query!(
306 KEYMAP_QUERY_2025_04_15,
307 migrations::m_2025_04_15::KEYMAP_PATTERNS
308);
309
310// settings
311define_query!(
312 SETTINGS_QUERY_2025_01_02,
313 migrations::m_2025_01_02::SETTINGS_PATTERNS
314);
315define_query!(
316 SETTINGS_QUERY_2025_01_29,
317 migrations::m_2025_01_29::SETTINGS_PATTERNS
318);
319define_query!(
320 SETTINGS_QUERY_2025_01_30,
321 migrations::m_2025_01_30::SETTINGS_PATTERNS
322);
323define_query!(
324 SETTINGS_QUERY_2025_03_29,
325 migrations::m_2025_03_29::SETTINGS_PATTERNS
326);
327define_query!(
328 SETTINGS_QUERY_2025_04_15,
329 migrations::m_2025_04_15::SETTINGS_PATTERNS
330);
331define_query!(
332 SETTINGS_QUERY_2025_04_21,
333 migrations::m_2025_04_21::SETTINGS_PATTERNS
334);
335define_query!(
336 SETTINGS_QUERY_2025_04_23,
337 migrations::m_2025_04_23::SETTINGS_PATTERNS
338);
339define_query!(
340 SETTINGS_QUERY_2025_05_05,
341 migrations::m_2025_05_05::SETTINGS_PATTERNS
342);
343define_query!(
344 SETTINGS_QUERY_2025_05_08,
345 migrations::m_2025_05_08::SETTINGS_PATTERNS
346);
347define_query!(
348 SETTINGS_QUERY_2025_06_16,
349 migrations::m_2025_06_16::SETTINGS_PATTERNS
350);
351define_query!(
352 SETTINGS_QUERY_2025_06_25,
353 migrations::m_2025_06_25::SETTINGS_PATTERNS
354);
355define_query!(
356 SETTINGS_QUERY_2025_06_27,
357 migrations::m_2025_06_27::SETTINGS_PATTERNS
358);
359define_query!(
360 SETTINGS_QUERY_2025_07_08,
361 migrations::m_2025_07_08::SETTINGS_PATTERNS
362);
363define_query!(
364 SETTINGS_QUERY_2025_10_03,
365 migrations::m_2025_10_03::SETTINGS_PATTERNS
366);
367define_query!(
368 SETTINGS_QUERY_2025_11_12,
369 migrations::m_2025_11_12::SETTINGS_PATTERNS
370);
371define_query!(
372 SETTINGS_QUERY_2025_12_01,
373 migrations::m_2025_12_01::SETTINGS_PATTERNS
374);
375define_query!(
376 SETTINGS_QUERY_2025_11_20,
377 migrations::m_2025_11_20::SETTINGS_PATTERNS
378);
379define_query!(
380 KEYMAP_QUERY_2025_12_08,
381 migrations::m_2025_12_08::KEYMAP_PATTERNS
382);
383define_query!(
384 SETTINGS_QUERY_2025_12_15,
385 migrations::m_2025_12_15::SETTINGS_PATTERNS
386);
387define_query!(
388 SETTINGS_QUERY_2026_03_16,
389 migrations::m_2026_03_16::SETTINGS_PATTERNS
390);
391define_query!(
392 KEYMAP_QUERY_2026_03_23,
393 migrations::m_2026_03_23::KEYMAP_PATTERNS
394);
395
396// custom query
397static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
398 Query::new(
399 &tree_sitter_json::LANGUAGE.into(),
400 SETTINGS_NESTED_KEY_VALUE_PATTERN,
401 )
402 .unwrap()
403});
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use unindent::Unindent as _;
409
410 #[track_caller]
411 fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
412 match (&migrated, &expected) {
413 (Some(migrated), Some(expected)) => {
414 pretty_assertions::assert_str_eq!(expected, migrated);
415 }
416 _ => {
417 pretty_assertions::assert_eq!(migrated.as_deref(), expected);
418 }
419 }
420 }
421
422 #[track_caller]
423 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
424 let migrated = migrate_keymap(input).unwrap();
425 pretty_assertions::assert_eq!(migrated.as_deref(), output);
426 }
427
428 #[track_caller]
429 fn assert_migrate_settings(input: &str, output: Option<&str>) {
430 let migrated = migrate_settings(input).unwrap();
431 assert_migrated_correctly(migrated.clone(), output);
432
433 // expect that rerunning the migration does not result in another migration
434 if let Some(migrated) = migrated {
435 let rerun = migrate_settings(&migrated).unwrap();
436 assert_migrated_correctly(rerun, None);
437 }
438 }
439
440 #[track_caller]
441 fn assert_migrate_with_migrations(
442 migrations: &[MigrationType],
443 input: &str,
444 output: Option<&str>,
445 ) {
446 let migrated = run_migrations(input, migrations).unwrap();
447 assert_migrated_correctly(migrated.clone(), output);
448
449 // expect that rerunning the migration does not result in another migration
450 if let Some(migrated) = migrated {
451 let rerun = run_migrations(&migrated, migrations).unwrap();
452 assert_migrated_correctly(rerun, None);
453 }
454 }
455
456 #[test]
457 fn test_empty_content() {
458 assert_migrate_settings("", None)
459 }
460
461 #[test]
462 fn test_replace_array_with_single_string() {
463 assert_migrate_keymap(
464 r#"
465 [
466 {
467 "bindings": {
468 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
469 }
470 }
471 ]
472 "#,
473 Some(
474 r#"
475 [
476 {
477 "bindings": {
478 "cmd-1": "workspace::ActivatePaneUp"
479 }
480 }
481 ]
482 "#,
483 ),
484 )
485 }
486
487 #[test]
488 fn test_replace_action_argument_object_with_single_value() {
489 assert_migrate_keymap(
490 r#"
491 [
492 {
493 "bindings": {
494 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
495 }
496 }
497 ]
498 "#,
499 Some(
500 r#"
501 [
502 {
503 "bindings": {
504 "cmd-1": ["editor::FoldAtLevel", 1]
505 }
506 }
507 ]
508 "#,
509 ),
510 )
511 }
512
513 #[test]
514 fn test_replace_action_argument_object_with_single_value_2() {
515 assert_migrate_keymap(
516 r#"
517 [
518 {
519 "bindings": {
520 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
521 }
522 }
523 ]
524 "#,
525 Some(
526 r#"
527 [
528 {
529 "bindings": {
530 "cmd-1": ["vim::PushObject", { "some" : "value" }]
531 }
532 }
533 ]
534 "#,
535 ),
536 )
537 }
538
539 #[test]
540 fn test_rename_string_action() {
541 assert_migrate_keymap(
542 r#"
543 [
544 {
545 "bindings": {
546 "cmd-1": "inline_completion::ToggleMenu"
547 }
548 }
549 ]
550 "#,
551 Some(
552 r#"
553 [
554 {
555 "bindings": {
556 "cmd-1": "edit_prediction::ToggleMenu"
557 }
558 }
559 ]
560 "#,
561 ),
562 )
563 }
564
565 #[test]
566 fn test_rename_context_key() {
567 assert_migrate_keymap(
568 r#"
569 [
570 {
571 "context": "Editor && inline_completion && !showing_completions"
572 }
573 ]
574 "#,
575 Some(
576 r#"
577 [
578 {
579 "context": "Editor && edit_prediction && !showing_completions"
580 }
581 ]
582 "#,
583 ),
584 )
585 }
586
587 #[test]
588 fn test_incremental_migrations() {
589 // Here string transforms to array internally. Then, that array transforms back to string.
590 assert_migrate_keymap(
591 r#"
592 [
593 {
594 "bindings": {
595 "ctrl-q": "editor::GoToHunk", // should remain same
596 "ctrl-w": "editor::GoToPrevHunk", // should rename
597 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
598 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
599 }
600 }
601 ]
602 "#,
603 Some(
604 r#"
605 [
606 {
607 "bindings": {
608 "ctrl-q": "editor::GoToHunk", // should remain same
609 "ctrl-w": "editor::GoToPreviousHunk", // should rename
610 "ctrl-q": "editor::GoToHunk", // should transform
611 "ctrl-w": "editor::GoToPreviousHunk" // should transform
612 }
613 }
614 ]
615 "#,
616 ),
617 )
618 }
619
620 #[test]
621 fn test_action_argument_snake_case() {
622 // First performs transformations, then replacements
623 assert_migrate_keymap(
624 r#"
625 [
626 {
627 "bindings": {
628 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
629 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
630 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
631 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
632 }
633 }
634 ]
635 "#,
636 Some(
637 r#"
638 [
639 {
640 "bindings": {
641 "cmd-1": ["vim::PushObject", { "around": false }],
642 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
643 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
644 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
645 }
646 }
647 ]
648 "#,
649 ),
650 )
651 }
652
653 #[test]
654 fn test_replace_setting_name() {
655 assert_migrate_settings(
656 r#"
657 {
658 "show_inline_completions_in_menu": true,
659 "show_inline_completions": true,
660 "inline_completions_disabled_in": ["string"],
661 "inline_completions": { "some" : "value" }
662 }
663 "#,
664 Some(
665 r#"
666 {
667 "show_edit_predictions_in_menu": true,
668 "show_edit_predictions": true,
669 "edit_predictions_disabled_in": ["string"],
670 "edit_predictions": { "some" : "value" }
671 }
672 "#,
673 ),
674 )
675 }
676
677 #[test]
678 fn test_nested_string_replace_for_settings() {
679 assert_migrate_settings(
680 &r#"
681 {
682 "features": {
683 "inline_completion_provider": "zed"
684 },
685 }
686 "#
687 .unindent(),
688 Some(
689 &r#"
690 {
691 "edit_predictions": {
692 "provider": "zed"
693 }
694 }
695 "#
696 .unindent(),
697 ),
698 )
699 }
700
701 #[test]
702 fn test_replace_settings_in_languages() {
703 assert_migrate_settings(
704 r#"
705 {
706 "languages": {
707 "Astro": {
708 "show_inline_completions": true
709 }
710 }
711 }
712 "#,
713 Some(
714 r#"
715 {
716 "languages": {
717 "Astro": {
718 "show_edit_predictions": true
719 }
720 }
721 }
722 "#,
723 ),
724 )
725 }
726
727 #[test]
728 fn test_replace_settings_value() {
729 assert_migrate_settings(
730 r#"
731 {
732 "scrollbar": {
733 "diagnostics": true
734 },
735 "chat_panel": {
736 "button": true
737 }
738 }
739 "#,
740 Some(
741 r#"
742 {
743 "scrollbar": {
744 "diagnostics": "all"
745 },
746 "chat_panel": {
747 "button": "always"
748 }
749 }
750 "#,
751 ),
752 )
753 }
754
755 #[test]
756 fn test_replace_settings_name_and_value() {
757 assert_migrate_settings(
758 r#"
759 {
760 "tabs": {
761 "always_show_close_button": true
762 }
763 }
764 "#,
765 Some(
766 r#"
767 {
768 "tabs": {
769 "show_close_button": "always"
770 }
771 }
772 "#,
773 ),
774 )
775 }
776
777 #[test]
778 fn test_replace_bash_with_terminal_in_profiles() {
779 assert_migrate_settings(
780 r#"
781 {
782 "assistant": {
783 "profiles": {
784 "custom": {
785 "name": "Custom",
786 "tools": {
787 "bash": true,
788 "diagnostics": true
789 }
790 }
791 }
792 }
793 }
794 "#,
795 Some(
796 r#"
797 {
798 "agent": {
799 "profiles": {
800 "custom": {
801 "name": "Custom",
802 "tools": {
803 "terminal": true,
804 "diagnostics": true
805 }
806 }
807 }
808 }
809 }
810 "#,
811 ),
812 )
813 }
814
815 #[test]
816 fn test_replace_bash_false_with_terminal_in_profiles() {
817 assert_migrate_settings(
818 r#"
819 {
820 "assistant": {
821 "profiles": {
822 "custom": {
823 "name": "Custom",
824 "tools": {
825 "bash": false,
826 "diagnostics": true
827 }
828 }
829 }
830 }
831 }
832 "#,
833 Some(
834 r#"
835 {
836 "agent": {
837 "profiles": {
838 "custom": {
839 "name": "Custom",
840 "tools": {
841 "terminal": false,
842 "diagnostics": true
843 }
844 }
845 }
846 }
847 }
848 "#,
849 ),
850 )
851 }
852
853 #[test]
854 fn test_no_bash_in_profiles() {
855 assert_migrate_settings(
856 r#"
857 {
858 "assistant": {
859 "profiles": {
860 "custom": {
861 "name": "Custom",
862 "tools": {
863 "diagnostics": true,
864 "find_path": true,
865 "read_file": true
866 }
867 }
868 }
869 }
870 }
871 "#,
872 Some(
873 r#"
874 {
875 "agent": {
876 "profiles": {
877 "custom": {
878 "name": "Custom",
879 "tools": {
880 "diagnostics": true,
881 "find_path": true,
882 "read_file": true
883 }
884 }
885 }
886 }
887 }
888 "#,
889 ),
890 )
891 }
892
893 #[test]
894 fn test_rename_path_search_to_find_path() {
895 assert_migrate_settings(
896 r#"
897 {
898 "assistant": {
899 "profiles": {
900 "default": {
901 "tools": {
902 "path_search": true,
903 "read_file": true
904 }
905 }
906 }
907 }
908 }
909 "#,
910 Some(
911 r#"
912 {
913 "agent": {
914 "profiles": {
915 "default": {
916 "tools": {
917 "find_path": true,
918 "read_file": true
919 }
920 }
921 }
922 }
923 }
924 "#,
925 ),
926 );
927 }
928
929 #[test]
930 fn test_rename_assistant() {
931 assert_migrate_settings(
932 r#"{
933 "assistant": {
934 "foo": "bar"
935 },
936 "edit_predictions": {
937 "enabled_in_assistant": false,
938 }
939 }"#,
940 Some(
941 r#"{
942 "agent": {
943 "foo": "bar"
944 },
945 "edit_predictions": {
946 "enabled_in_text_threads": false,
947 }
948 }"#,
949 ),
950 );
951 }
952
953 #[test]
954 fn test_comment_duplicated_agent() {
955 assert_migrate_settings(
956 r#"{
957 "agent": {
958 "name": "assistant-1",
959 "model": "gpt-4", // weird formatting
960 "utf8": "привіт"
961 },
962 "something": "else",
963 "agent": {
964 "name": "assistant-2",
965 "model": "gemini-pro"
966 }
967 }
968 "#,
969 Some(
970 r#"{
971 /* Duplicated key auto-commented: "agent": {
972 "name": "assistant-1",
973 "model": "gpt-4", // weird formatting
974 "utf8": "привіт"
975 }, */
976 "something": "else",
977 "agent": {
978 "name": "assistant-2",
979 "model": "gemini-pro"
980 }
981 }
982 "#,
983 ),
984 );
985 }
986
987 #[test]
988 fn test_mcp_settings_migration() {
989 assert_migrate_with_migrations(
990 &[MigrationType::TreeSitter(
991 migrations::m_2025_06_16::SETTINGS_PATTERNS,
992 &SETTINGS_QUERY_2025_06_16,
993 )],
994 r#"{
995 "context_servers": {
996 "empty_server": {},
997 "extension_server": {
998 "settings": {
999 "foo": "bar"
1000 }
1001 },
1002 "custom_server": {
1003 "command": {
1004 "path": "foo",
1005 "args": ["bar"],
1006 "env": {
1007 "FOO": "BAR"
1008 }
1009 }
1010 },
1011 "invalid_server": {
1012 "command": {
1013 "path": "foo",
1014 "args": ["bar"],
1015 "env": {
1016 "FOO": "BAR"
1017 }
1018 },
1019 "settings": {
1020 "foo": "bar"
1021 }
1022 },
1023 "empty_server2": {},
1024 "extension_server2": {
1025 "foo": "bar",
1026 "settings": {
1027 "foo": "bar"
1028 },
1029 "bar": "foo"
1030 },
1031 "custom_server2": {
1032 "foo": "bar",
1033 "command": {
1034 "path": "foo",
1035 "args": ["bar"],
1036 "env": {
1037 "FOO": "BAR"
1038 }
1039 },
1040 "bar": "foo"
1041 },
1042 "invalid_server2": {
1043 "foo": "bar",
1044 "command": {
1045 "path": "foo",
1046 "args": ["bar"],
1047 "env": {
1048 "FOO": "BAR"
1049 }
1050 },
1051 "bar": "foo",
1052 "settings": {
1053 "foo": "bar"
1054 }
1055 }
1056 }
1057}"#,
1058 Some(
1059 r#"{
1060 "context_servers": {
1061 "empty_server": {
1062 "source": "extension",
1063 "settings": {}
1064 },
1065 "extension_server": {
1066 "source": "extension",
1067 "settings": {
1068 "foo": "bar"
1069 }
1070 },
1071 "custom_server": {
1072 "source": "custom",
1073 "command": {
1074 "path": "foo",
1075 "args": ["bar"],
1076 "env": {
1077 "FOO": "BAR"
1078 }
1079 }
1080 },
1081 "invalid_server": {
1082 "source": "custom",
1083 "command": {
1084 "path": "foo",
1085 "args": ["bar"],
1086 "env": {
1087 "FOO": "BAR"
1088 }
1089 },
1090 "settings": {
1091 "foo": "bar"
1092 }
1093 },
1094 "empty_server2": {
1095 "source": "extension",
1096 "settings": {}
1097 },
1098 "extension_server2": {
1099 "source": "extension",
1100 "foo": "bar",
1101 "settings": {
1102 "foo": "bar"
1103 },
1104 "bar": "foo"
1105 },
1106 "custom_server2": {
1107 "source": "custom",
1108 "foo": "bar",
1109 "command": {
1110 "path": "foo",
1111 "args": ["bar"],
1112 "env": {
1113 "FOO": "BAR"
1114 }
1115 },
1116 "bar": "foo"
1117 },
1118 "invalid_server2": {
1119 "source": "custom",
1120 "foo": "bar",
1121 "command": {
1122 "path": "foo",
1123 "args": ["bar"],
1124 "env": {
1125 "FOO": "BAR"
1126 }
1127 },
1128 "bar": "foo",
1129 "settings": {
1130 "foo": "bar"
1131 }
1132 }
1133 }
1134}"#,
1135 ),
1136 );
1137 }
1138
1139 #[test]
1140 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1141 let settings = r#"{
1142 "context_servers": {
1143 "empty_server": {
1144 "source": "extension",
1145 "settings": {}
1146 },
1147 "extension_server": {
1148 "source": "extension",
1149 "settings": {
1150 "foo": "bar"
1151 }
1152 },
1153 "custom_server": {
1154 "source": "custom",
1155 "command": {
1156 "path": "foo",
1157 "args": ["bar"],
1158 "env": {
1159 "FOO": "BAR"
1160 }
1161 }
1162 },
1163 "invalid_server": {
1164 "source": "custom",
1165 "command": {
1166 "path": "foo",
1167 "args": ["bar"],
1168 "env": {
1169 "FOO": "BAR"
1170 }
1171 },
1172 "settings": {
1173 "foo": "bar"
1174 }
1175 }
1176 }
1177}"#;
1178 assert_migrate_with_migrations(
1179 &[MigrationType::TreeSitter(
1180 migrations::m_2025_06_16::SETTINGS_PATTERNS,
1181 &SETTINGS_QUERY_2025_06_16,
1182 )],
1183 settings,
1184 None,
1185 );
1186 }
1187
1188 #[test]
1189 fn test_custom_agent_server_settings_migration() {
1190 assert_migrate_with_migrations(
1191 &[MigrationType::TreeSitter(
1192 migrations::m_2025_11_20::SETTINGS_PATTERNS,
1193 &SETTINGS_QUERY_2025_11_20,
1194 )],
1195 r#"{
1196 "agent_servers": {
1197 "gemini": {
1198 "default_model": "gemini-1.5-pro"
1199 },
1200 "claude": {},
1201 "codex": {},
1202 "my-custom-agent": {
1203 "command": "/path/to/agent",
1204 "args": ["--foo"],
1205 "default_model": "my-model"
1206 },
1207 "already-migrated-agent": {
1208 "type": "custom",
1209 "command": "/path/to/agent"
1210 },
1211 "future-extension-agent": {
1212 "type": "extension",
1213 "default_model": "ext-model"
1214 }
1215 }
1216}"#,
1217 Some(
1218 r#"{
1219 "agent_servers": {
1220 "gemini": {
1221 "default_model": "gemini-1.5-pro"
1222 },
1223 "claude": {},
1224 "codex": {},
1225 "my-custom-agent": {
1226 "type": "custom",
1227 "command": "/path/to/agent",
1228 "args": ["--foo"],
1229 "default_model": "my-model"
1230 },
1231 "already-migrated-agent": {
1232 "type": "custom",
1233 "command": "/path/to/agent"
1234 },
1235 "future-extension-agent": {
1236 "type": "extension",
1237 "default_model": "ext-model"
1238 }
1239 }
1240}"#,
1241 ),
1242 );
1243 }
1244
1245 #[test]
1246 fn test_remove_version_fields() {
1247 assert_migrate_settings(
1248 r#"{
1249 "language_models": {
1250 "anthropic": {
1251 "version": "1",
1252 "api_url": "https://api.anthropic.com"
1253 },
1254 "openai": {
1255 "version": "1",
1256 "api_url": "https://api.openai.com/v1"
1257 }
1258 },
1259 "agent": {
1260 "version": "2",
1261 "enabled": true,
1262 "button": true,
1263 "dock": "right",
1264 "default_width": 640,
1265 "default_height": 320,
1266 "default_model": {
1267 "provider": "zed.dev",
1268 "model": "claude-sonnet-4"
1269 }
1270 }
1271}"#,
1272 Some(
1273 r#"{
1274 "language_models": {
1275 "anthropic": {
1276 "api_url": "https://api.anthropic.com"
1277 },
1278 "openai": {
1279 "api_url": "https://api.openai.com/v1"
1280 }
1281 },
1282 "agent": {
1283 "enabled": true,
1284 "button": true,
1285 "dock": "right",
1286 "default_width": 640,
1287 "default_height": 320,
1288 "default_model": {
1289 "provider": "zed.dev",
1290 "model": "claude-sonnet-4"
1291 }
1292 }
1293}"#,
1294 ),
1295 );
1296
1297 // Test that version fields in other contexts are not removed
1298 assert_migrate_settings(
1299 r#"{
1300 "language_models": {
1301 "other_provider": {
1302 "version": "1",
1303 "api_url": "https://api.example.com"
1304 }
1305 },
1306 "other_section": {
1307 "version": "1"
1308 }
1309}"#,
1310 None,
1311 );
1312 }
1313
1314 #[test]
1315 fn test_flatten_context_server_command() {
1316 assert_migrate_settings(
1317 r#"{
1318 "context_servers": {
1319 "some-mcp-server": {
1320 "command": {
1321 "path": "npx",
1322 "args": [
1323 "-y",
1324 "@supabase/mcp-server-supabase@latest",
1325 "--read-only",
1326 "--project-ref=<project-ref>"
1327 ],
1328 "env": {
1329 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1330 }
1331 }
1332 }
1333 }
1334}"#,
1335 Some(
1336 r#"{
1337 "context_servers": {
1338 "some-mcp-server": {
1339 "command": "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 );
1354
1355 // Test with additional keys in server object
1356 assert_migrate_settings(
1357 r#"{
1358 "context_servers": {
1359 "server-with-extras": {
1360 "command": {
1361 "path": "/usr/bin/node",
1362 "args": ["server.js"]
1363 },
1364 "settings": {}
1365 }
1366 }
1367}"#,
1368 Some(
1369 r#"{
1370 "context_servers": {
1371 "server-with-extras": {
1372 "command": "/usr/bin/node",
1373 "args": ["server.js"],
1374 "settings": {}
1375 }
1376 }
1377}"#,
1378 ),
1379 );
1380
1381 // Test command without args or env
1382 assert_migrate_settings(
1383 r#"{
1384 "context_servers": {
1385 "simple-server": {
1386 "command": {
1387 "path": "simple-mcp-server"
1388 }
1389 }
1390 }
1391}"#,
1392 Some(
1393 r#"{
1394 "context_servers": {
1395 "simple-server": {
1396 "command": "simple-mcp-server"
1397 }
1398 }
1399}"#,
1400 ),
1401 );
1402 }
1403
1404 #[test]
1405 fn test_flatten_code_action_formatters_basic_array() {
1406 assert_migrate_with_migrations(
1407 &[MigrationType::Json(
1408 migrations::m_2025_10_01::flatten_code_actions_formatters,
1409 )],
1410 &r#"{
1411 "formatter": [
1412 {
1413 "code_actions": {
1414 "included-1": true,
1415 "included-2": true,
1416 "excluded": false,
1417 }
1418 }
1419 ]
1420 }"#
1421 .unindent(),
1422 Some(
1423 &r#"{
1424 "formatter": [
1425 {
1426 "code_action": "included-1"
1427 },
1428 {
1429 "code_action": "included-2"
1430 }
1431 ]
1432 }"#
1433 .unindent(),
1434 ),
1435 );
1436 }
1437
1438 #[test]
1439 fn test_flatten_code_action_formatters_basic_object() {
1440 assert_migrate_with_migrations(
1441 &[MigrationType::Json(
1442 migrations::m_2025_10_01::flatten_code_actions_formatters,
1443 )],
1444 &r#"{
1445 "formatter": {
1446 "code_actions": {
1447 "included-1": true,
1448 "excluded": false,
1449 "included-2": true
1450 }
1451 }
1452 }"#
1453 .unindent(),
1454 Some(
1455 &r#"{
1456 "formatter": [
1457 {
1458 "code_action": "included-1"
1459 },
1460 {
1461 "code_action": "included-2"
1462 }
1463 ]
1464 }"#
1465 .unindent(),
1466 ),
1467 );
1468 }
1469
1470 #[test]
1471 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1472 assert_migrate_settings(
1473 &r#"{
1474 "formatter": [
1475 {
1476 "code_actions": {
1477 "included-1": true,
1478 "included-2": true,
1479 "excluded": false,
1480 }
1481 },
1482 {
1483 "language_server": "ruff"
1484 },
1485 {
1486 "code_actions": {
1487 "excluded": false,
1488 "excluded-2": false,
1489 }
1490 }
1491 // some comment
1492 ,
1493 {
1494 "code_actions": {
1495 "excluded": false,
1496 "included-3": true,
1497 "included-4": true,
1498 }
1499 },
1500 ]
1501 }"#
1502 .unindent(),
1503 Some(
1504 &r#"{
1505 "formatter": [
1506 {
1507 "code_action": "included-1"
1508 },
1509 {
1510 "code_action": "included-2"
1511 },
1512 {
1513 "language_server": "ruff"
1514 },
1515 {
1516 "code_action": "included-3"
1517 },
1518 {
1519 "code_action": "included-4"
1520 }
1521 ]
1522 }"#
1523 .unindent(),
1524 ),
1525 );
1526 }
1527
1528 #[test]
1529 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1530 assert_migrate_settings(
1531 &r#"{
1532 "languages": {
1533 "Rust": {
1534 "formatter": [
1535 {
1536 "code_actions": {
1537 "included-1": true,
1538 "included-2": true,
1539 "excluded": false,
1540 }
1541 },
1542 {
1543 "language_server": "ruff"
1544 },
1545 {
1546 "code_actions": {
1547 "excluded": false,
1548 "excluded-2": false,
1549 }
1550 }
1551 // some comment
1552 ,
1553 {
1554 "code_actions": {
1555 "excluded": false,
1556 "included-3": true,
1557 "included-4": true,
1558 }
1559 },
1560 ]
1561 }
1562 }
1563 }"#
1564 .unindent(),
1565 Some(
1566 &r#"{
1567 "languages": {
1568 "Rust": {
1569 "formatter": [
1570 {
1571 "code_action": "included-1"
1572 },
1573 {
1574 "code_action": "included-2"
1575 },
1576 {
1577 "language_server": "ruff"
1578 },
1579 {
1580 "code_action": "included-3"
1581 },
1582 {
1583 "code_action": "included-4"
1584 }
1585 ]
1586 }
1587 }
1588 }"#
1589 .unindent(),
1590 ),
1591 );
1592 }
1593
1594 #[test]
1595 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1596 {
1597 assert_migrate_with_migrations(
1598 &[MigrationType::Json(
1599 migrations::m_2025_10_01::flatten_code_actions_formatters,
1600 )],
1601 &r#"{
1602 "formatter": {
1603 "code_actions": {
1604 "default-1": true,
1605 "default-2": true,
1606 "default-3": true,
1607 "default-4": true,
1608 }
1609 },
1610 "languages": {
1611 "Rust": {
1612 "formatter": [
1613 {
1614 "code_actions": {
1615 "included-1": true,
1616 "included-2": true,
1617 "excluded": false,
1618 }
1619 },
1620 {
1621 "language_server": "ruff"
1622 },
1623 {
1624 "code_actions": {
1625 "excluded": false,
1626 "excluded-2": false,
1627 }
1628 }
1629 // some comment
1630 ,
1631 {
1632 "code_actions": {
1633 "excluded": false,
1634 "included-3": true,
1635 "included-4": true,
1636 }
1637 },
1638 ]
1639 },
1640 "Python": {
1641 "formatter": [
1642 {
1643 "language_server": "ruff"
1644 },
1645 {
1646 "code_actions": {
1647 "excluded": false,
1648 "excluded-2": false,
1649 }
1650 }
1651 // some comment
1652 ,
1653 {
1654 "code_actions": {
1655 "excluded": false,
1656 "included-3": true,
1657 "included-4": true,
1658 }
1659 },
1660 ]
1661 }
1662 }
1663 }"#
1664 .unindent(),
1665 Some(
1666 &r#"{
1667 "formatter": [
1668 {
1669 "code_action": "default-1"
1670 },
1671 {
1672 "code_action": "default-2"
1673 },
1674 {
1675 "code_action": "default-3"
1676 },
1677 {
1678 "code_action": "default-4"
1679 }
1680 ],
1681 "languages": {
1682 "Rust": {
1683 "formatter": [
1684 {
1685 "code_action": "included-1"
1686 },
1687 {
1688 "code_action": "included-2"
1689 },
1690 {
1691 "language_server": "ruff"
1692 },
1693 {
1694 "code_action": "included-3"
1695 },
1696 {
1697 "code_action": "included-4"
1698 }
1699 ]
1700 },
1701 "Python": {
1702 "formatter": [
1703 {
1704 "language_server": "ruff"
1705 },
1706 {
1707 "code_action": "included-3"
1708 },
1709 {
1710 "code_action": "included-4"
1711 }
1712 ]
1713 }
1714 }
1715 }"#
1716 .unindent(),
1717 ),
1718 );
1719 }
1720
1721 #[test]
1722 fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1723 assert_migrate_with_migrations(
1724 &[MigrationType::Json(
1725 migrations::m_2025_10_01::flatten_code_actions_formatters,
1726 )],
1727 &r#"{
1728 "formatter": {
1729 "code_actions": {
1730 "default-1": true,
1731 "default-2": true,
1732 "default-3": true,
1733 "default-4": true,
1734 }
1735 },
1736 "format_on_save": [
1737 {
1738 "code_actions": {
1739 "included-1": true,
1740 "included-2": true,
1741 "excluded": false,
1742 }
1743 },
1744 {
1745 "language_server": "ruff"
1746 },
1747 {
1748 "code_actions": {
1749 "excluded": false,
1750 "excluded-2": false,
1751 }
1752 }
1753 // some comment
1754 ,
1755 {
1756 "code_actions": {
1757 "excluded": false,
1758 "included-3": true,
1759 "included-4": true,
1760 }
1761 },
1762 ],
1763 "languages": {
1764 "Rust": {
1765 "format_on_save": "prettier",
1766 "formatter": [
1767 {
1768 "code_actions": {
1769 "included-1": true,
1770 "included-2": true,
1771 "excluded": false,
1772 }
1773 },
1774 {
1775 "language_server": "ruff"
1776 },
1777 {
1778 "code_actions": {
1779 "excluded": false,
1780 "excluded-2": false,
1781 }
1782 }
1783 // some comment
1784 ,
1785 {
1786 "code_actions": {
1787 "excluded": false,
1788 "included-3": true,
1789 "included-4": true,
1790 }
1791 },
1792 ]
1793 },
1794 "Python": {
1795 "format_on_save": {
1796 "code_actions": {
1797 "on-save-1": true,
1798 "on-save-2": true,
1799 }
1800 },
1801 "formatter": [
1802 {
1803 "language_server": "ruff"
1804 },
1805 {
1806 "code_actions": {
1807 "excluded": false,
1808 "excluded-2": false,
1809 }
1810 }
1811 // some comment
1812 ,
1813 {
1814 "code_actions": {
1815 "excluded": false,
1816 "included-3": true,
1817 "included-4": true,
1818 }
1819 },
1820 ]
1821 }
1822 }
1823 }"#
1824 .unindent(),
1825 Some(
1826 &r#"
1827 {
1828 "formatter": [
1829 {
1830 "code_action": "default-1"
1831 },
1832 {
1833 "code_action": "default-2"
1834 },
1835 {
1836 "code_action": "default-3"
1837 },
1838 {
1839 "code_action": "default-4"
1840 }
1841 ],
1842 "format_on_save": [
1843 {
1844 "code_action": "included-1"
1845 },
1846 {
1847 "code_action": "included-2"
1848 },
1849 {
1850 "language_server": "ruff"
1851 },
1852 {
1853 "code_action": "included-3"
1854 },
1855 {
1856 "code_action": "included-4"
1857 }
1858 ],
1859 "languages": {
1860 "Rust": {
1861 "format_on_save": "prettier",
1862 "formatter": [
1863 {
1864 "code_action": "included-1"
1865 },
1866 {
1867 "code_action": "included-2"
1868 },
1869 {
1870 "language_server": "ruff"
1871 },
1872 {
1873 "code_action": "included-3"
1874 },
1875 {
1876 "code_action": "included-4"
1877 }
1878 ]
1879 },
1880 "Python": {
1881 "format_on_save": [
1882 {
1883 "code_action": "on-save-1"
1884 },
1885 {
1886 "code_action": "on-save-2"
1887 }
1888 ],
1889 "formatter": [
1890 {
1891 "language_server": "ruff"
1892 },
1893 {
1894 "code_action": "included-3"
1895 },
1896 {
1897 "code_action": "included-4"
1898 }
1899 ]
1900 }
1901 }
1902 }"#
1903 .unindent(),
1904 ),
1905 );
1906 }
1907
1908 #[test]
1909 fn test_format_on_save_formatter_migration_basic() {
1910 assert_migrate_with_migrations(
1911 &[MigrationType::Json(
1912 migrations::m_2025_10_02::remove_formatters_on_save,
1913 )],
1914 &r#"{
1915 "format_on_save": "prettier"
1916 }"#
1917 .unindent(),
1918 Some(
1919 &r#"{
1920 "formatter": "prettier",
1921 "format_on_save": "on"
1922 }"#
1923 .unindent(),
1924 ),
1925 );
1926 }
1927
1928 #[test]
1929 fn test_format_on_save_formatter_migration_array() {
1930 assert_migrate_with_migrations(
1931 &[MigrationType::Json(
1932 migrations::m_2025_10_02::remove_formatters_on_save,
1933 )],
1934 &r#"{
1935 "format_on_save": ["prettier", {"language_server": "eslint"}]
1936 }"#
1937 .unindent(),
1938 Some(
1939 &r#"{
1940 "formatter": [
1941 "prettier",
1942 {
1943 "language_server": "eslint"
1944 }
1945 ],
1946 "format_on_save": "on"
1947 }"#
1948 .unindent(),
1949 ),
1950 );
1951 }
1952
1953 #[test]
1954 fn test_format_on_save_on_off_unchanged() {
1955 assert_migrate_with_migrations(
1956 &[MigrationType::Json(
1957 migrations::m_2025_10_02::remove_formatters_on_save,
1958 )],
1959 &r#"{
1960 "format_on_save": "on"
1961 }"#
1962 .unindent(),
1963 None,
1964 );
1965
1966 assert_migrate_with_migrations(
1967 &[MigrationType::Json(
1968 migrations::m_2025_10_02::remove_formatters_on_save,
1969 )],
1970 &r#"{
1971 "format_on_save": "off"
1972 }"#
1973 .unindent(),
1974 None,
1975 );
1976 }
1977
1978 #[test]
1979 fn test_format_on_save_formatter_migration_in_languages() {
1980 assert_migrate_with_migrations(
1981 &[MigrationType::Json(
1982 migrations::m_2025_10_02::remove_formatters_on_save,
1983 )],
1984 &r#"{
1985 "languages": {
1986 "Rust": {
1987 "format_on_save": "rust-analyzer"
1988 },
1989 "Python": {
1990 "format_on_save": ["ruff", "black"]
1991 }
1992 }
1993 }"#
1994 .unindent(),
1995 Some(
1996 &r#"{
1997 "languages": {
1998 "Rust": {
1999 "formatter": "rust-analyzer",
2000 "format_on_save": "on"
2001 },
2002 "Python": {
2003 "formatter": [
2004 "ruff",
2005 "black"
2006 ],
2007 "format_on_save": "on"
2008 }
2009 }
2010 }"#
2011 .unindent(),
2012 ),
2013 );
2014 }
2015
2016 #[test]
2017 fn test_format_on_save_formatter_migration_mixed_global_and_languages() {
2018 assert_migrate_with_migrations(
2019 &[MigrationType::Json(
2020 migrations::m_2025_10_02::remove_formatters_on_save,
2021 )],
2022 &r#"{
2023 "format_on_save": "prettier",
2024 "languages": {
2025 "Rust": {
2026 "format_on_save": "rust-analyzer"
2027 },
2028 "Python": {
2029 "format_on_save": "on"
2030 }
2031 }
2032 }"#
2033 .unindent(),
2034 Some(
2035 &r#"{
2036 "formatter": "prettier",
2037 "format_on_save": "on",
2038 "languages": {
2039 "Rust": {
2040 "formatter": "rust-analyzer",
2041 "format_on_save": "on"
2042 },
2043 "Python": {
2044 "format_on_save": "on"
2045 }
2046 }
2047 }"#
2048 .unindent(),
2049 ),
2050 );
2051 }
2052
2053 #[test]
2054 fn test_format_on_save_no_migration_when_no_format_on_save() {
2055 assert_migrate_with_migrations(
2056 &[MigrationType::Json(
2057 migrations::m_2025_10_02::remove_formatters_on_save,
2058 )],
2059 &r#"{
2060 "formatter": ["prettier"]
2061 }"#
2062 .unindent(),
2063 None,
2064 );
2065 }
2066
2067 #[test]
2068 fn test_restore_code_actions_on_format() {
2069 assert_migrate_with_migrations(
2070 &[MigrationType::Json(
2071 migrations::m_2025_10_16::restore_code_actions_on_format,
2072 )],
2073 &r#"{
2074 "formatter": {
2075 "code_action": "foo"
2076 }
2077 }"#
2078 .unindent(),
2079 Some(
2080 &r#"{
2081 "code_actions_on_format": {
2082 "foo": true
2083 },
2084 "formatter": []
2085 }"#
2086 .unindent(),
2087 ),
2088 );
2089
2090 assert_migrate_with_migrations(
2091 &[MigrationType::Json(
2092 migrations::m_2025_10_16::restore_code_actions_on_format,
2093 )],
2094 &r#"{
2095 "formatter": [
2096 { "code_action": "foo" },
2097 "auto"
2098 ]
2099 }"#
2100 .unindent(),
2101 None,
2102 );
2103
2104 assert_migrate_with_migrations(
2105 &[MigrationType::Json(
2106 migrations::m_2025_10_16::restore_code_actions_on_format,
2107 )],
2108 &r#"{
2109 "formatter": {
2110 "code_action": "foo"
2111 },
2112 "code_actions_on_format": {
2113 "bar": true,
2114 "baz": false
2115 }
2116 }"#
2117 .unindent(),
2118 Some(
2119 &r#"{
2120 "formatter": [],
2121 "code_actions_on_format": {
2122 "foo": true,
2123 "bar": true,
2124 "baz": false
2125 }
2126 }"#
2127 .unindent(),
2128 ),
2129 );
2130
2131 assert_migrate_with_migrations(
2132 &[MigrationType::Json(
2133 migrations::m_2025_10_16::restore_code_actions_on_format,
2134 )],
2135 &r#"{
2136 "formatter": [
2137 { "code_action": "foo" },
2138 { "code_action": "qux" },
2139 ],
2140 "code_actions_on_format": {
2141 "bar": true,
2142 "baz": false
2143 }
2144 }"#
2145 .unindent(),
2146 Some(
2147 &r#"{
2148 "formatter": [],
2149 "code_actions_on_format": {
2150 "foo": true,
2151 "qux": true,
2152 "bar": true,
2153 "baz": false
2154 }
2155 }"#
2156 .unindent(),
2157 ),
2158 );
2159
2160 assert_migrate_with_migrations(
2161 &[MigrationType::Json(
2162 migrations::m_2025_10_16::restore_code_actions_on_format,
2163 )],
2164 &r#"{
2165 "formatter": [],
2166 "code_actions_on_format": {
2167 "bar": true,
2168 "baz": false
2169 }
2170 }"#
2171 .unindent(),
2172 None,
2173 );
2174 }
2175
2176 #[test]
2177 fn test_make_file_finder_include_ignored_an_enum() {
2178 assert_migrate_with_migrations(
2179 &[MigrationType::Json(
2180 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2181 )],
2182 &r#"{ }"#.unindent(),
2183 None,
2184 );
2185
2186 assert_migrate_with_migrations(
2187 &[MigrationType::Json(
2188 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2189 )],
2190 &r#"{
2191 "file_finder": {
2192 "include_ignored": true
2193 }
2194 }"#
2195 .unindent(),
2196 Some(
2197 &r#"{
2198 "file_finder": {
2199 "include_ignored": "all"
2200 }
2201 }"#
2202 .unindent(),
2203 ),
2204 );
2205
2206 assert_migrate_with_migrations(
2207 &[MigrationType::Json(
2208 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2209 )],
2210 &r#"{
2211 "file_finder": {
2212 "include_ignored": false
2213 }
2214 }"#
2215 .unindent(),
2216 Some(
2217 &r#"{
2218 "file_finder": {
2219 "include_ignored": "indexed"
2220 }
2221 }"#
2222 .unindent(),
2223 ),
2224 );
2225
2226 assert_migrate_with_migrations(
2227 &[MigrationType::Json(
2228 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2229 )],
2230 &r#"{
2231 "file_finder": {
2232 "include_ignored": null
2233 }
2234 }"#
2235 .unindent(),
2236 Some(
2237 &r#"{
2238 "file_finder": {
2239 "include_ignored": "smart"
2240 }
2241 }"#
2242 .unindent(),
2243 ),
2244 );
2245
2246 // Platform key: settings nested inside "linux" should be migrated
2247 assert_migrate_with_migrations(
2248 &[MigrationType::Json(
2249 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2250 )],
2251 &r#"
2252 {
2253 "linux": {
2254 "file_finder": {
2255 "include_ignored": true
2256 }
2257 }
2258 }
2259 "#
2260 .unindent(),
2261 Some(
2262 &r#"
2263 {
2264 "linux": {
2265 "file_finder": {
2266 "include_ignored": "all"
2267 }
2268 }
2269 }
2270 "#
2271 .unindent(),
2272 ),
2273 );
2274
2275 // Profile: settings nested inside profiles should be migrated
2276 assert_migrate_with_migrations(
2277 &[MigrationType::Json(
2278 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2279 )],
2280 &r#"
2281 {
2282 "profiles": {
2283 "work": {
2284 "file_finder": {
2285 "include_ignored": false
2286 }
2287 }
2288 }
2289 }
2290 "#
2291 .unindent(),
2292 Some(
2293 &r#"
2294 {
2295 "profiles": {
2296 "work": {
2297 "file_finder": {
2298 "include_ignored": "indexed"
2299 }
2300 }
2301 }
2302 }
2303 "#
2304 .unindent(),
2305 ),
2306 );
2307 }
2308
2309 #[test]
2310 fn test_make_relative_line_numbers_an_enum() {
2311 assert_migrate_with_migrations(
2312 &[MigrationType::Json(
2313 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2314 )],
2315 &r#"{ }"#.unindent(),
2316 None,
2317 );
2318
2319 assert_migrate_with_migrations(
2320 &[MigrationType::Json(
2321 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2322 )],
2323 &r#"{
2324 "relative_line_numbers": true
2325 }"#
2326 .unindent(),
2327 Some(
2328 &r#"{
2329 "relative_line_numbers": "enabled"
2330 }"#
2331 .unindent(),
2332 ),
2333 );
2334
2335 assert_migrate_with_migrations(
2336 &[MigrationType::Json(
2337 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2338 )],
2339 &r#"{
2340 "relative_line_numbers": false
2341 }"#
2342 .unindent(),
2343 Some(
2344 &r#"{
2345 "relative_line_numbers": "disabled"
2346 }"#
2347 .unindent(),
2348 ),
2349 );
2350
2351 // Platform key: settings nested inside "macos" should be migrated
2352 assert_migrate_with_migrations(
2353 &[MigrationType::Json(
2354 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2355 )],
2356 &r#"
2357 {
2358 "macos": {
2359 "relative_line_numbers": true
2360 }
2361 }
2362 "#
2363 .unindent(),
2364 Some(
2365 &r#"
2366 {
2367 "macos": {
2368 "relative_line_numbers": "enabled"
2369 }
2370 }
2371 "#
2372 .unindent(),
2373 ),
2374 );
2375
2376 // Profile: settings nested inside profiles should be migrated
2377 assert_migrate_with_migrations(
2378 &[MigrationType::Json(
2379 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2380 )],
2381 &r#"
2382 {
2383 "profiles": {
2384 "dev": {
2385 "relative_line_numbers": false
2386 }
2387 }
2388 }
2389 "#
2390 .unindent(),
2391 Some(
2392 &r#"
2393 {
2394 "profiles": {
2395 "dev": {
2396 "relative_line_numbers": "disabled"
2397 }
2398 }
2399 }
2400 "#
2401 .unindent(),
2402 ),
2403 );
2404 }
2405
2406 #[test]
2407 fn test_make_play_sound_when_agent_done_an_enum() {
2408 assert_migrate_with_migrations(
2409 &[MigrationType::Json(
2410 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2411 )],
2412 &r#"{ }"#.unindent(),
2413 None,
2414 );
2415
2416 assert_migrate_with_migrations(
2417 &[MigrationType::Json(
2418 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2419 )],
2420 &r#"{
2421 "agent": {
2422 "play_sound_when_agent_done": true
2423 }
2424 }"#
2425 .unindent(),
2426 Some(
2427 &r#"{
2428 "agent": {
2429 "play_sound_when_agent_done": "always"
2430 }
2431 }"#
2432 .unindent(),
2433 ),
2434 );
2435
2436 assert_migrate_with_migrations(
2437 &[MigrationType::Json(
2438 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2439 )],
2440 &r#"{
2441 "agent": {
2442 "play_sound_when_agent_done": false
2443 }
2444 }"#
2445 .unindent(),
2446 Some(
2447 &r#"{
2448 "agent": {
2449 "play_sound_when_agent_done": "never"
2450 }
2451 }"#
2452 .unindent(),
2453 ),
2454 );
2455
2456 assert_migrate_with_migrations(
2457 &[MigrationType::Json(
2458 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2459 )],
2460 &r#"{
2461 "agent": {
2462 "play_sound_when_agent_done": "when_hidden"
2463 }
2464 }"#
2465 .unindent(),
2466 None,
2467 );
2468
2469 // Platform key: settings nested inside "macos" should be migrated
2470 assert_migrate_with_migrations(
2471 &[MigrationType::Json(
2472 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2473 )],
2474 &r#"
2475 {
2476 "macos": {
2477 "agent": {
2478 "play_sound_when_agent_done": true
2479 }
2480 }
2481 }
2482 "#
2483 .unindent(),
2484 Some(
2485 &r#"
2486 {
2487 "macos": {
2488 "agent": {
2489 "play_sound_when_agent_done": "always"
2490 }
2491 }
2492 }
2493 "#
2494 .unindent(),
2495 ),
2496 );
2497
2498 // Profile: settings nested inside profiles should be migrated
2499 assert_migrate_with_migrations(
2500 &[MigrationType::Json(
2501 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2502 )],
2503 &r#"
2504 {
2505 "profiles": {
2506 "work": {
2507 "agent": {
2508 "play_sound_when_agent_done": false
2509 }
2510 }
2511 }
2512 }
2513 "#
2514 .unindent(),
2515 Some(
2516 &r#"
2517 {
2518 "profiles": {
2519 "work": {
2520 "agent": {
2521 "play_sound_when_agent_done": "never"
2522 }
2523 }
2524 }
2525 }
2526 "#
2527 .unindent(),
2528 ),
2529 );
2530 }
2531
2532 #[test]
2533 fn test_remove_context_server_source() {
2534 assert_migrate_settings(
2535 &r#"
2536 {
2537 "context_servers": {
2538 "extension_server": {
2539 "source": "extension",
2540 "settings": {
2541 "foo": "bar"
2542 }
2543 },
2544 "custom_server": {
2545 "source": "custom",
2546 "command": "foo",
2547 "args": ["bar"],
2548 "env": {
2549 "FOO": "BAR"
2550 }
2551 },
2552 }
2553 }
2554 "#
2555 .unindent(),
2556 Some(
2557 &r#"
2558 {
2559 "context_servers": {
2560 "extension_server": {
2561 "settings": {
2562 "foo": "bar"
2563 }
2564 },
2565 "custom_server": {
2566 "command": "foo",
2567 "args": ["bar"],
2568 "env": {
2569 "FOO": "BAR"
2570 }
2571 },
2572 }
2573 }
2574 "#
2575 .unindent(),
2576 ),
2577 );
2578
2579 // Platform key: settings nested inside "linux" should be migrated
2580 assert_migrate_with_migrations(
2581 &[MigrationType::Json(
2582 migrations::m_2025_11_25::remove_context_server_source,
2583 )],
2584 &r#"
2585 {
2586 "linux": {
2587 "context_servers": {
2588 "my_server": {
2589 "source": "extension",
2590 "settings": {
2591 "key": "value"
2592 }
2593 }
2594 }
2595 }
2596 }
2597 "#
2598 .unindent(),
2599 Some(
2600 &r#"
2601 {
2602 "linux": {
2603 "context_servers": {
2604 "my_server": {
2605 "settings": {
2606 "key": "value"
2607 }
2608 }
2609 }
2610 }
2611 }
2612 "#
2613 .unindent(),
2614 ),
2615 );
2616
2617 // Profile: settings nested inside profiles should be migrated
2618 assert_migrate_with_migrations(
2619 &[MigrationType::Json(
2620 migrations::m_2025_11_25::remove_context_server_source,
2621 )],
2622 &r#"
2623 {
2624 "profiles": {
2625 "work": {
2626 "context_servers": {
2627 "my_server": {
2628 "source": "custom",
2629 "command": "foo",
2630 "args": ["bar"]
2631 }
2632 }
2633 }
2634 }
2635 }
2636 "#
2637 .unindent(),
2638 Some(
2639 &r#"
2640 {
2641 "profiles": {
2642 "work": {
2643 "context_servers": {
2644 "my_server": {
2645 "command": "foo",
2646 "args": ["bar"]
2647 }
2648 }
2649 }
2650 }
2651 }
2652 "#
2653 .unindent(),
2654 ),
2655 );
2656 }
2657
2658 #[test]
2659 fn test_project_panel_open_file_on_paste_migration() {
2660 assert_migrate_settings(
2661 &r#"
2662 {
2663 "project_panel": {
2664 "open_file_on_paste": true
2665 }
2666 }
2667 "#
2668 .unindent(),
2669 Some(
2670 &r#"
2671 {
2672 "project_panel": {
2673 "auto_open": { "on_paste": true }
2674 }
2675 }
2676 "#
2677 .unindent(),
2678 ),
2679 );
2680
2681 assert_migrate_settings(
2682 &r#"
2683 {
2684 "project_panel": {
2685 "open_file_on_paste": false
2686 }
2687 }
2688 "#
2689 .unindent(),
2690 Some(
2691 &r#"
2692 {
2693 "project_panel": {
2694 "auto_open": { "on_paste": false }
2695 }
2696 }
2697 "#
2698 .unindent(),
2699 ),
2700 );
2701 }
2702
2703 #[test]
2704 fn test_enable_preview_from_code_navigation_migration() {
2705 assert_migrate_settings(
2706 &r#"
2707 {
2708 "other_setting_1": 1,
2709 "preview_tabs": {
2710 "other_setting_2": 2,
2711 "enable_preview_from_code_navigation": false
2712 }
2713 }
2714 "#
2715 .unindent(),
2716 Some(
2717 &r#"
2718 {
2719 "other_setting_1": 1,
2720 "preview_tabs": {
2721 "other_setting_2": 2,
2722 "enable_keep_preview_on_code_navigation": false
2723 }
2724 }
2725 "#
2726 .unindent(),
2727 ),
2728 );
2729
2730 assert_migrate_settings(
2731 &r#"
2732 {
2733 "other_setting_1": 1,
2734 "preview_tabs": {
2735 "other_setting_2": 2,
2736 "enable_preview_from_code_navigation": true
2737 }
2738 }
2739 "#
2740 .unindent(),
2741 Some(
2742 &r#"
2743 {
2744 "other_setting_1": 1,
2745 "preview_tabs": {
2746 "other_setting_2": 2,
2747 "enable_keep_preview_on_code_navigation": true
2748 }
2749 }
2750 "#
2751 .unindent(),
2752 ),
2753 );
2754 }
2755
2756 #[test]
2757 fn test_make_auto_indent_an_enum() {
2758 // Empty settings should not change
2759 assert_migrate_with_migrations(
2760 &[MigrationType::Json(
2761 migrations::m_2025_01_27::make_auto_indent_an_enum,
2762 )],
2763 &r#"{ }"#.unindent(),
2764 None,
2765 );
2766
2767 // true should become "syntax_aware"
2768 assert_migrate_with_migrations(
2769 &[MigrationType::Json(
2770 migrations::m_2025_01_27::make_auto_indent_an_enum,
2771 )],
2772 &r#"{
2773 "auto_indent": true
2774 }"#
2775 .unindent(),
2776 Some(
2777 &r#"{
2778 "auto_indent": "syntax_aware"
2779 }"#
2780 .unindent(),
2781 ),
2782 );
2783
2784 // false should become "none"
2785 assert_migrate_with_migrations(
2786 &[MigrationType::Json(
2787 migrations::m_2025_01_27::make_auto_indent_an_enum,
2788 )],
2789 &r#"{
2790 "auto_indent": false
2791 }"#
2792 .unindent(),
2793 Some(
2794 &r#"{
2795 "auto_indent": "none"
2796 }"#
2797 .unindent(),
2798 ),
2799 );
2800
2801 // Already valid enum values should not change
2802 assert_migrate_with_migrations(
2803 &[MigrationType::Json(
2804 migrations::m_2025_01_27::make_auto_indent_an_enum,
2805 )],
2806 &r#"{
2807 "auto_indent": "preserve_indent"
2808 }"#
2809 .unindent(),
2810 None,
2811 );
2812
2813 // Should also work inside languages
2814 assert_migrate_with_migrations(
2815 &[MigrationType::Json(
2816 migrations::m_2025_01_27::make_auto_indent_an_enum,
2817 )],
2818 &r#"{
2819 "auto_indent": true,
2820 "languages": {
2821 "Python": {
2822 "auto_indent": false
2823 }
2824 }
2825 }"#
2826 .unindent(),
2827 Some(
2828 &r#"{
2829 "auto_indent": "syntax_aware",
2830 "languages": {
2831 "Python": {
2832 "auto_indent": "none"
2833 }
2834 }
2835 }"#
2836 .unindent(),
2837 ),
2838 );
2839 }
2840
2841 #[test]
2842 fn test_move_edit_prediction_provider_to_edit_predictions() {
2843 assert_migrate_with_migrations(
2844 &[MigrationType::Json(
2845 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2846 )],
2847 &r#"{ }"#.unindent(),
2848 None,
2849 );
2850
2851 assert_migrate_with_migrations(
2852 &[MigrationType::Json(
2853 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2854 )],
2855 &r#"
2856 {
2857 "features": {
2858 "edit_prediction_provider": "copilot"
2859 }
2860 }
2861 "#
2862 .unindent(),
2863 Some(
2864 &r#"
2865 {
2866 "edit_predictions": {
2867 "provider": "copilot"
2868 }
2869 }
2870 "#
2871 .unindent(),
2872 ),
2873 );
2874
2875 assert_migrate_with_migrations(
2876 &[MigrationType::Json(
2877 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2878 )],
2879 &r#"
2880 {
2881 "features": {
2882 "edit_prediction_provider": "zed"
2883 },
2884 "edit_predictions": {
2885 "mode": "eager"
2886 }
2887 }
2888 "#
2889 .unindent(),
2890 Some(
2891 &r#"
2892 {
2893 "edit_predictions": {
2894 "provider": "zed",
2895 "mode": "eager"
2896 }
2897 }
2898 "#
2899 .unindent(),
2900 ),
2901 );
2902
2903 assert_migrate_with_migrations(
2904 &[MigrationType::Json(
2905 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2906 )],
2907 &r#"
2908 {
2909 "features": {
2910 "edit_prediction_provider": "supermaven"
2911 },
2912 "edit_predictions": {
2913 "provider": "copilot"
2914 }
2915 }
2916 "#
2917 .unindent(),
2918 Some(
2919 &r#"
2920 {
2921 "edit_predictions": {
2922 "provider": "copilot"
2923 }
2924 }
2925 "#
2926 .unindent(),
2927 ),
2928 );
2929
2930 assert_migrate_with_migrations(
2931 &[MigrationType::Json(
2932 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2933 )],
2934 &r#"
2935 {
2936 "edit_predictions": {
2937 "provider": "zed"
2938 }
2939 }
2940 "#
2941 .unindent(),
2942 None,
2943 );
2944
2945 // Non-object edit_predictions (e.g. true) should gracefully skip
2946 // instead of bail!-ing and aborting the entire migration chain.
2947 assert_migrate_with_migrations(
2948 &[MigrationType::Json(
2949 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2950 )],
2951 &r#"
2952 {
2953 "features": {
2954 "edit_prediction_provider": "copilot"
2955 },
2956 "edit_predictions": true
2957 }
2958 "#
2959 .unindent(),
2960 Some(
2961 &r#"
2962 {
2963 "edit_predictions": true
2964 }
2965 "#
2966 .unindent(),
2967 ),
2968 );
2969
2970 // Platform key: settings nested inside "macos" should be migrated
2971 assert_migrate_with_migrations(
2972 &[MigrationType::Json(
2973 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2974 )],
2975 &r#"
2976 {
2977 "macos": {
2978 "features": {
2979 "edit_prediction_provider": "copilot"
2980 }
2981 }
2982 }
2983 "#
2984 .unindent(),
2985 Some(
2986 &r#"
2987 {
2988 "macos": {
2989 "edit_predictions": {
2990 "provider": "copilot"
2991 }
2992 }
2993 }
2994 "#
2995 .unindent(),
2996 ),
2997 );
2998
2999 // Profile: settings nested inside profiles should be migrated
3000 assert_migrate_with_migrations(
3001 &[MigrationType::Json(
3002 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
3003 )],
3004 &r#"
3005 {
3006 "profiles": {
3007 "work": {
3008 "features": {
3009 "edit_prediction_provider": "copilot"
3010 }
3011 }
3012 }
3013 }
3014 "#
3015 .unindent(),
3016 Some(
3017 &r#"
3018 {
3019 "profiles": {
3020 "work": {
3021 "edit_predictions": {
3022 "provider": "copilot"
3023 }
3024 }
3025 }
3026 }
3027 "#
3028 .unindent(),
3029 ),
3030 );
3031
3032 // Combined: root + platform + profile should all be migrated simultaneously
3033 assert_migrate_with_migrations(
3034 &[MigrationType::Json(
3035 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
3036 )],
3037 &r#"
3038 {
3039 "features": {
3040 "edit_prediction_provider": "copilot"
3041 },
3042 "macos": {
3043 "features": {
3044 "edit_prediction_provider": "zed"
3045 }
3046 },
3047 "profiles": {
3048 "work": {
3049 "features": {
3050 "edit_prediction_provider": "supermaven"
3051 }
3052 }
3053 }
3054 }
3055 "#
3056 .unindent(),
3057 Some(
3058 &r#"
3059 {
3060 "edit_predictions": {
3061 "provider": "copilot"
3062 },
3063 "macos": {
3064 "edit_predictions": {
3065 "provider": "zed"
3066 }
3067 },
3068 "profiles": {
3069 "work": {
3070 "edit_predictions": {
3071 "provider": "supermaven"
3072 }
3073 }
3074 }
3075 }
3076 "#
3077 .unindent(),
3078 ),
3079 );
3080 }
3081
3082 #[test]
3083 fn test_migrate_experimental_sweep_mercury() {
3084 assert_migrate_with_migrations(
3085 &[MigrationType::Json(
3086 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3087 )],
3088 &r#"{ }"#.unindent(),
3089 None,
3090 );
3091
3092 assert_migrate_with_migrations(
3093 &[MigrationType::Json(
3094 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3095 )],
3096 &r#"
3097 {
3098 "edit_predictions": {
3099 "provider": {
3100 "experimental": "sweep"
3101 }
3102 }
3103 }
3104 "#
3105 .unindent(),
3106 Some(
3107 &r#"
3108 {
3109 "edit_predictions": {
3110 "provider": "sweep"
3111 }
3112 }
3113 "#
3114 .unindent(),
3115 ),
3116 );
3117
3118 assert_migrate_with_migrations(
3119 &[MigrationType::Json(
3120 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3121 )],
3122 &r#"
3123 {
3124 "edit_predictions": {
3125 "provider": {
3126 "experimental": "mercury"
3127 }
3128 }
3129 }
3130 "#
3131 .unindent(),
3132 Some(
3133 &r#"
3134 {
3135 "edit_predictions": {
3136 "provider": "mercury"
3137 }
3138 }
3139 "#
3140 .unindent(),
3141 ),
3142 );
3143
3144 assert_migrate_with_migrations(
3145 &[MigrationType::Json(
3146 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3147 )],
3148 &r#"
3149 {
3150 "features": {
3151 "edit_prediction_provider": {
3152 "experimental": "sweep"
3153 }
3154 }
3155 }
3156 "#
3157 .unindent(),
3158 Some(
3159 &r#"
3160 {
3161 "features": {
3162 "edit_prediction_provider": "sweep"
3163 }
3164 }
3165 "#
3166 .unindent(),
3167 ),
3168 );
3169
3170 assert_migrate_with_migrations(
3171 &[MigrationType::Json(
3172 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3173 )],
3174 &r#"
3175 {
3176 "edit_predictions": {
3177 "provider": "zed"
3178 }
3179 }
3180 "#
3181 .unindent(),
3182 None,
3183 );
3184
3185 assert_migrate_with_migrations(
3186 &[MigrationType::Json(
3187 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3188 )],
3189 &r#"
3190 {
3191 "edit_predictions": {
3192 "provider": {
3193 "experimental": "zeta2"
3194 }
3195 }
3196 }
3197 "#
3198 .unindent(),
3199 None,
3200 );
3201
3202 // Platform key: settings nested inside "linux" should be migrated
3203 assert_migrate_with_migrations(
3204 &[MigrationType::Json(
3205 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3206 )],
3207 &r#"
3208 {
3209 "linux": {
3210 "edit_predictions": {
3211 "provider": {
3212 "experimental": "sweep"
3213 }
3214 }
3215 }
3216 }
3217 "#
3218 .unindent(),
3219 Some(
3220 &r#"
3221 {
3222 "linux": {
3223 "edit_predictions": {
3224 "provider": "sweep"
3225 }
3226 }
3227 }
3228 "#
3229 .unindent(),
3230 ),
3231 );
3232
3233 // Profile: settings nested inside profiles should be migrated
3234 assert_migrate_with_migrations(
3235 &[MigrationType::Json(
3236 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3237 )],
3238 &r#"
3239 {
3240 "profiles": {
3241 "dev": {
3242 "edit_predictions": {
3243 "provider": {
3244 "experimental": "mercury"
3245 }
3246 }
3247 }
3248 }
3249 }
3250 "#
3251 .unindent(),
3252 Some(
3253 &r#"
3254 {
3255 "profiles": {
3256 "dev": {
3257 "edit_predictions": {
3258 "provider": "mercury"
3259 }
3260 }
3261 }
3262 }
3263 "#
3264 .unindent(),
3265 ),
3266 );
3267
3268 // Combined: root + platform + profile should all be migrated simultaneously
3269 assert_migrate_with_migrations(
3270 &[MigrationType::Json(
3271 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3272 )],
3273 &r#"
3274 {
3275 "edit_predictions": {
3276 "provider": {
3277 "experimental": "sweep"
3278 }
3279 },
3280 "linux": {
3281 "edit_predictions": {
3282 "provider": {
3283 "experimental": "mercury"
3284 }
3285 }
3286 },
3287 "profiles": {
3288 "dev": {
3289 "edit_predictions": {
3290 "provider": {
3291 "experimental": "sweep"
3292 }
3293 }
3294 }
3295 }
3296 }
3297 "#
3298 .unindent(),
3299 Some(
3300 &r#"
3301 {
3302 "edit_predictions": {
3303 "provider": "sweep"
3304 },
3305 "linux": {
3306 "edit_predictions": {
3307 "provider": "mercury"
3308 }
3309 },
3310 "profiles": {
3311 "dev": {
3312 "edit_predictions": {
3313 "provider": "sweep"
3314 }
3315 }
3316 }
3317 }
3318 "#
3319 .unindent(),
3320 ),
3321 );
3322 }
3323
3324 #[test]
3325 fn test_migrate_always_allow_tool_actions_to_default() {
3326 // No agent settings - no change
3327 assert_migrate_with_migrations(
3328 &[MigrationType::Json(
3329 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3330 )],
3331 &r#"{ }"#.unindent(),
3332 None,
3333 );
3334
3335 // always_allow_tool_actions: true -> tool_permissions.default: "allow"
3336 assert_migrate_with_migrations(
3337 &[MigrationType::Json(
3338 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3339 )],
3340 &r#"
3341 {
3342 "agent": {
3343 "always_allow_tool_actions": true
3344 }
3345 }
3346 "#
3347 .unindent(),
3348 Some(
3349 &r#"
3350 {
3351 "agent": {
3352 "tool_permissions": {
3353 "default": "allow"
3354 }
3355 }
3356 }
3357 "#
3358 .unindent(),
3359 ),
3360 );
3361
3362 // always_allow_tool_actions: false -> just remove it
3363 assert_migrate_with_migrations(
3364 &[MigrationType::Json(
3365 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3366 )],
3367 &r#"
3368 {
3369 "agent": {
3370 "always_allow_tool_actions": false
3371 }
3372 }
3373 "#
3374 .unindent(),
3375 Some(
3376 // The blank line has spaces because the migration preserves the original indentation
3377 "{\n \"agent\": {\n \n }\n}\n",
3378 ),
3379 );
3380
3381 // Preserve existing tool_permissions.tools when migrating
3382 assert_migrate_with_migrations(
3383 &[MigrationType::Json(
3384 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3385 )],
3386 &r#"
3387 {
3388 "agent": {
3389 "always_allow_tool_actions": true,
3390 "tool_permissions": {
3391 "tools": {
3392 "terminal": {
3393 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3394 }
3395 }
3396 }
3397 }
3398 }
3399 "#
3400 .unindent(),
3401 Some(
3402 &r#"
3403 {
3404 "agent": {
3405 "tool_permissions": {
3406 "default": "allow",
3407 "tools": {
3408 "terminal": {
3409 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3410 }
3411 }
3412 }
3413 }
3414 }
3415 "#
3416 .unindent(),
3417 ),
3418 );
3419
3420 // Don't override existing default (and migrate default_mode to default)
3421 assert_migrate_with_migrations(
3422 &[MigrationType::Json(
3423 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3424 )],
3425 &r#"
3426 {
3427 "agent": {
3428 "always_allow_tool_actions": true,
3429 "tool_permissions": {
3430 "default_mode": "confirm"
3431 }
3432 }
3433 }
3434 "#
3435 .unindent(),
3436 Some(
3437 &r#"
3438 {
3439 "agent": {
3440 "tool_permissions": {
3441 "default": "confirm"
3442 }
3443 }
3444 }
3445 "#
3446 .unindent(),
3447 ),
3448 );
3449
3450 // Migrate existing default_mode to default (no always_allow_tool_actions)
3451 assert_migrate_with_migrations(
3452 &[MigrationType::Json(
3453 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3454 )],
3455 &r#"
3456 {
3457 "agent": {
3458 "tool_permissions": {
3459 "default_mode": "allow"
3460 }
3461 }
3462 }
3463 "#
3464 .unindent(),
3465 Some(
3466 &r#"
3467 {
3468 "agent": {
3469 "tool_permissions": {
3470 "default": "allow"
3471 }
3472 }
3473 }
3474 "#
3475 .unindent(),
3476 ),
3477 );
3478
3479 // No migration needed if already using new format with "default"
3480 assert_migrate_with_migrations(
3481 &[MigrationType::Json(
3482 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3483 )],
3484 &r#"
3485 {
3486 "agent": {
3487 "tool_permissions": {
3488 "default": "allow"
3489 }
3490 }
3491 }
3492 "#
3493 .unindent(),
3494 None,
3495 );
3496
3497 // Migrate default_mode to default in tool-specific rules
3498 assert_migrate_with_migrations(
3499 &[MigrationType::Json(
3500 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3501 )],
3502 &r#"
3503 {
3504 "agent": {
3505 "tool_permissions": {
3506 "default_mode": "confirm",
3507 "tools": {
3508 "terminal": {
3509 "default_mode": "allow"
3510 }
3511 }
3512 }
3513 }
3514 }
3515 "#
3516 .unindent(),
3517 Some(
3518 &r#"
3519 {
3520 "agent": {
3521 "tool_permissions": {
3522 "default": "confirm",
3523 "tools": {
3524 "terminal": {
3525 "default": "allow"
3526 }
3527 }
3528 }
3529 }
3530 }
3531 "#
3532 .unindent(),
3533 ),
3534 );
3535
3536 // When tool_permissions is null, replace it so always_allow is preserved
3537 assert_migrate_with_migrations(
3538 &[MigrationType::Json(
3539 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3540 )],
3541 &r#"
3542 {
3543 "agent": {
3544 "always_allow_tool_actions": true,
3545 "tool_permissions": null
3546 }
3547 }
3548 "#
3549 .unindent(),
3550 Some(
3551 &r#"
3552 {
3553 "agent": {
3554 "tool_permissions": {
3555 "default": "allow"
3556 }
3557 }
3558 }
3559 "#
3560 .unindent(),
3561 ),
3562 );
3563
3564 // Platform-specific agent migration
3565 assert_migrate_with_migrations(
3566 &[MigrationType::Json(
3567 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3568 )],
3569 &r#"
3570 {
3571 "linux": {
3572 "agent": {
3573 "always_allow_tool_actions": true
3574 }
3575 }
3576 }
3577 "#
3578 .unindent(),
3579 Some(
3580 &r#"
3581 {
3582 "linux": {
3583 "agent": {
3584 "tool_permissions": {
3585 "default": "allow"
3586 }
3587 }
3588 }
3589 }
3590 "#
3591 .unindent(),
3592 ),
3593 );
3594
3595 // Channel-specific agent migration
3596 assert_migrate_with_migrations(
3597 &[MigrationType::Json(
3598 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3599 )],
3600 &r#"
3601 {
3602 "agent": {
3603 "always_allow_tool_actions": true
3604 },
3605 "nightly": {
3606 "agent": {
3607 "tool_permissions": {
3608 "default_mode": "confirm"
3609 }
3610 }
3611 }
3612 }
3613 "#
3614 .unindent(),
3615 Some(
3616 &r#"
3617 {
3618 "agent": {
3619 "tool_permissions": {
3620 "default": "allow"
3621 }
3622 },
3623 "nightly": {
3624 "agent": {
3625 "tool_permissions": {
3626 "default": "confirm"
3627 }
3628 }
3629 }
3630 }
3631 "#
3632 .unindent(),
3633 ),
3634 );
3635
3636 // Profile-level migration
3637 assert_migrate_with_migrations(
3638 &[MigrationType::Json(
3639 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3640 )],
3641 &r#"
3642 {
3643 "agent": {
3644 "profiles": {
3645 "custom": {
3646 "always_allow_tool_actions": true,
3647 "tool_permissions": {
3648 "default_mode": "allow"
3649 }
3650 }
3651 }
3652 }
3653 }
3654 "#
3655 .unindent(),
3656 Some(
3657 &r#"
3658 {
3659 "agent": {
3660 "profiles": {
3661 "custom": {
3662 "tool_permissions": {
3663 "default": "allow"
3664 }
3665 }
3666 }
3667 }
3668 }
3669 "#
3670 .unindent(),
3671 ),
3672 );
3673
3674 // Platform-specific agent with profiles
3675 assert_migrate_with_migrations(
3676 &[MigrationType::Json(
3677 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3678 )],
3679 &r#"
3680 {
3681 "macos": {
3682 "agent": {
3683 "always_allow_tool_actions": true,
3684 "profiles": {
3685 "strict": {
3686 "tool_permissions": {
3687 "default_mode": "deny"
3688 }
3689 }
3690 }
3691 }
3692 }
3693 }
3694 "#
3695 .unindent(),
3696 Some(
3697 &r#"
3698 {
3699 "macos": {
3700 "agent": {
3701 "tool_permissions": {
3702 "default": "allow"
3703 },
3704 "profiles": {
3705 "strict": {
3706 "tool_permissions": {
3707 "default": "deny"
3708 }
3709 }
3710 }
3711 }
3712 }
3713 }
3714 "#
3715 .unindent(),
3716 ),
3717 );
3718
3719 // Root-level profile with always_allow_tool_actions
3720 assert_migrate_with_migrations(
3721 &[MigrationType::Json(
3722 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3723 )],
3724 &r#"
3725 {
3726 "profiles": {
3727 "work": {
3728 "agent": {
3729 "always_allow_tool_actions": true
3730 }
3731 }
3732 }
3733 }
3734 "#
3735 .unindent(),
3736 Some(
3737 &r#"
3738 {
3739 "profiles": {
3740 "work": {
3741 "agent": {
3742 "tool_permissions": {
3743 "default": "allow"
3744 }
3745 }
3746 }
3747 }
3748 }
3749 "#
3750 .unindent(),
3751 ),
3752 );
3753
3754 // Root-level profile with default_mode
3755 assert_migrate_with_migrations(
3756 &[MigrationType::Json(
3757 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3758 )],
3759 &r#"
3760 {
3761 "profiles": {
3762 "work": {
3763 "agent": {
3764 "tool_permissions": {
3765 "default_mode": "allow"
3766 }
3767 }
3768 }
3769 }
3770 }
3771 "#
3772 .unindent(),
3773 Some(
3774 &r#"
3775 {
3776 "profiles": {
3777 "work": {
3778 "agent": {
3779 "tool_permissions": {
3780 "default": "allow"
3781 }
3782 }
3783 }
3784 }
3785 }
3786 "#
3787 .unindent(),
3788 ),
3789 );
3790
3791 // Root-level profile + root-level agent both migrated
3792 assert_migrate_with_migrations(
3793 &[MigrationType::Json(
3794 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3795 )],
3796 &r#"
3797 {
3798 "agent": {
3799 "always_allow_tool_actions": true
3800 },
3801 "profiles": {
3802 "strict": {
3803 "agent": {
3804 "tool_permissions": {
3805 "default_mode": "deny"
3806 }
3807 }
3808 }
3809 }
3810 }
3811 "#
3812 .unindent(),
3813 Some(
3814 &r#"
3815 {
3816 "agent": {
3817 "tool_permissions": {
3818 "default": "allow"
3819 }
3820 },
3821 "profiles": {
3822 "strict": {
3823 "agent": {
3824 "tool_permissions": {
3825 "default": "deny"
3826 }
3827 }
3828 }
3829 }
3830 }
3831 "#
3832 .unindent(),
3833 ),
3834 );
3835
3836 // Non-boolean always_allow_tool_actions (string "true") is left in place
3837 // so the schema validator can report it, rather than silently dropping user data.
3838 assert_migrate_with_migrations(
3839 &[MigrationType::Json(
3840 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3841 )],
3842 &r#"
3843 {
3844 "agent": {
3845 "always_allow_tool_actions": "true"
3846 }
3847 }
3848 "#
3849 .unindent(),
3850 None,
3851 );
3852
3853 // null always_allow_tool_actions is removed (treated as false)
3854 assert_migrate_with_migrations(
3855 &[MigrationType::Json(
3856 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3857 )],
3858 &r#"
3859 {
3860 "agent": {
3861 "always_allow_tool_actions": null
3862 }
3863 }
3864 "#
3865 .unindent(),
3866 Some(&"{\n \"agent\": {\n \n }\n}\n"),
3867 );
3868
3869 // Project-local settings (.zed/settings.json) with always_allow_tool_actions
3870 // These files have no platform/channel overrides or root-level profiles.
3871 assert_migrate_with_migrations(
3872 &[MigrationType::Json(
3873 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3874 )],
3875 &r#"
3876 {
3877 "agent": {
3878 "always_allow_tool_actions": true,
3879 "tool_permissions": {
3880 "tools": {
3881 "terminal": {
3882 "default_mode": "confirm",
3883 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3884 }
3885 }
3886 }
3887 }
3888 }
3889 "#
3890 .unindent(),
3891 Some(
3892 &r#"
3893 {
3894 "agent": {
3895 "tool_permissions": {
3896 "default": "allow",
3897 "tools": {
3898 "terminal": {
3899 "default": "confirm",
3900 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3901 }
3902 }
3903 }
3904 }
3905 }
3906 "#
3907 .unindent(),
3908 ),
3909 );
3910
3911 // Project-local settings with only default_mode (no always_allow_tool_actions)
3912 assert_migrate_with_migrations(
3913 &[MigrationType::Json(
3914 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3915 )],
3916 &r#"
3917 {
3918 "agent": {
3919 "tool_permissions": {
3920 "default_mode": "deny"
3921 }
3922 }
3923 }
3924 "#
3925 .unindent(),
3926 Some(
3927 &r#"
3928 {
3929 "agent": {
3930 "tool_permissions": {
3931 "default": "deny"
3932 }
3933 }
3934 }
3935 "#
3936 .unindent(),
3937 ),
3938 );
3939
3940 // Project-local settings with no agent section at all - no change
3941 assert_migrate_with_migrations(
3942 &[MigrationType::Json(
3943 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3944 )],
3945 &r#"
3946 {
3947 "tab_size": 4,
3948 "format_on_save": "on"
3949 }
3950 "#
3951 .unindent(),
3952 None,
3953 );
3954
3955 // Existing agent_servers are left untouched
3956 assert_migrate_with_migrations(
3957 &[MigrationType::Json(
3958 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3959 )],
3960 &r#"
3961 {
3962 "agent": {
3963 "always_allow_tool_actions": true
3964 },
3965 "agent_servers": {
3966 "claude": {
3967 "default_mode": "plan"
3968 },
3969 "codex": {
3970 "default_mode": "read-only"
3971 }
3972 }
3973 }
3974 "#
3975 .unindent(),
3976 Some(
3977 &r#"
3978 {
3979 "agent": {
3980 "tool_permissions": {
3981 "default": "allow"
3982 }
3983 },
3984 "agent_servers": {
3985 "claude": {
3986 "default_mode": "plan"
3987 },
3988 "codex": {
3989 "default_mode": "read-only"
3990 }
3991 }
3992 }
3993 "#
3994 .unindent(),
3995 ),
3996 );
3997
3998 // Existing agent_servers are left untouched even with partial entries
3999 assert_migrate_with_migrations(
4000 &[MigrationType::Json(
4001 migrations::m_2026_02_04::migrate_tool_permission_defaults,
4002 )],
4003 &r#"
4004 {
4005 "agent": {
4006 "always_allow_tool_actions": true
4007 },
4008 "agent_servers": {
4009 "claude": {
4010 "default_mode": "plan"
4011 }
4012 }
4013 }
4014 "#
4015 .unindent(),
4016 Some(
4017 &r#"
4018 {
4019 "agent": {
4020 "tool_permissions": {
4021 "default": "allow"
4022 }
4023 },
4024 "agent_servers": {
4025 "claude": {
4026 "default_mode": "plan"
4027 }
4028 }
4029 }
4030 "#
4031 .unindent(),
4032 ),
4033 );
4034
4035 // always_allow_tool_actions: false leaves agent_servers untouched
4036 assert_migrate_with_migrations(
4037 &[MigrationType::Json(
4038 migrations::m_2026_02_04::migrate_tool_permission_defaults,
4039 )],
4040 &r#"
4041 {
4042 "agent": {
4043 "always_allow_tool_actions": false
4044 },
4045 "agent_servers": {
4046 "claude": {}
4047 }
4048 }
4049 "#
4050 .unindent(),
4051 Some(
4052 "{\n \"agent\": {\n \n },\n \"agent_servers\": {\n \"claude\": {}\n }\n}\n",
4053 ),
4054 );
4055 }
4056
4057 #[test]
4058 fn test_migrate_builtin_agent_servers_to_registry_simple() {
4059 assert_migrate_with_migrations(
4060 &[MigrationType::Json(
4061 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4062 )],
4063 r#"{
4064 "agent_servers": {
4065 "gemini": {
4066 "default_model": "gemini-2.0-flash"
4067 },
4068 "claude": {
4069 "default_mode": "plan"
4070 },
4071 "codex": {
4072 "default_model": "o4-mini"
4073 }
4074 }
4075}"#,
4076 Some(
4077 r#"{
4078 "agent_servers": {
4079 "codex-acp": {
4080 "type": "registry",
4081 "default_model": "o4-mini"
4082 },
4083 "claude-acp": {
4084 "type": "registry",
4085 "default_mode": "plan"
4086 },
4087 "gemini": {
4088 "type": "registry",
4089 "default_model": "gemini-2.0-flash"
4090 }
4091 }
4092}"#,
4093 ),
4094 );
4095 }
4096
4097 #[test]
4098 fn test_migrate_builtin_agent_servers_empty_entries() {
4099 assert_migrate_with_migrations(
4100 &[MigrationType::Json(
4101 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4102 )],
4103 r#"{
4104 "agent_servers": {
4105 "gemini": {},
4106 "claude": {},
4107 "codex": {}
4108 }
4109}"#,
4110 Some(
4111 r#"{
4112 "agent_servers": {
4113 "codex-acp": {
4114 "type": "registry"
4115 },
4116 "claude-acp": {
4117 "type": "registry"
4118 },
4119 "gemini": {
4120 "type": "registry"
4121 }
4122 }
4123}"#,
4124 ),
4125 );
4126 }
4127
4128 #[test]
4129 fn test_migrate_builtin_agent_servers_with_command() {
4130 assert_migrate_with_migrations(
4131 &[MigrationType::Json(
4132 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4133 )],
4134 r#"{
4135 "agent_servers": {
4136 "claude": {
4137 "command": "/usr/local/bin/claude",
4138 "args": ["--verbose"],
4139 "env": {"CLAUDE_KEY": "abc123"},
4140 "default_mode": "plan",
4141 "default_model": "claude-sonnet-4"
4142 }
4143 }
4144}"#,
4145 Some(
4146 r#"{
4147 "agent_servers": {
4148 "claude-acp-custom": {
4149 "type": "custom",
4150 "command": "/usr/local/bin/claude",
4151 "args": [
4152 "--verbose"
4153 ],
4154 "env": {
4155 "CLAUDE_KEY": "abc123"
4156 },
4157 "default_mode": "plan",
4158 "default_model": "claude-sonnet-4"
4159 }
4160 }
4161}"#,
4162 ),
4163 );
4164 }
4165
4166 #[test]
4167 fn test_migrate_builtin_agent_servers_gemini_with_command() {
4168 assert_migrate_with_migrations(
4169 &[MigrationType::Json(
4170 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4171 )],
4172 r#"{
4173 "agent_servers": {
4174 "gemini": {
4175 "command": "/opt/gemini/bin/gemini",
4176 "default_model": "gemini-2.0-flash"
4177 }
4178 }
4179}"#,
4180 Some(
4181 r#"{
4182 "agent_servers": {
4183 "gemini-custom": {
4184 "type": "custom",
4185 "command": "/opt/gemini/bin/gemini",
4186 "default_model": "gemini-2.0-flash"
4187 }
4188 }
4189}"#,
4190 ),
4191 );
4192 }
4193
4194 #[test]
4195 fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_false() {
4196 assert_migrate_with_migrations(
4197 &[MigrationType::Json(
4198 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4199 )],
4200 r#"{
4201 "agent_servers": {
4202 "gemini": {
4203 "ignore_system_version": false,
4204 "default_model": "gemini-2.0-flash"
4205 }
4206 }
4207}"#,
4208 Some(
4209 r#"{
4210 "agent_servers": {
4211 "gemini-custom": {
4212 "type": "custom",
4213 "command": "gemini",
4214 "default_model": "gemini-2.0-flash"
4215 }
4216 }
4217}"#,
4218 ),
4219 );
4220 }
4221
4222 #[test]
4223 fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_true() {
4224 assert_migrate_with_migrations(
4225 &[MigrationType::Json(
4226 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4227 )],
4228 r#"{
4229 "agent_servers": {
4230 "gemini": {
4231 "ignore_system_version": true,
4232 "default_model": "gemini-2.0-flash"
4233 }
4234 }
4235}"#,
4236 Some(
4237 r#"{
4238 "agent_servers": {
4239 "gemini": {
4240 "type": "registry",
4241 "default_model": "gemini-2.0-flash"
4242 }
4243 }
4244}"#,
4245 ),
4246 );
4247 }
4248
4249 #[test]
4250 fn test_migrate_builtin_agent_servers_already_typed_unchanged() {
4251 assert_migrate_with_migrations(
4252 &[MigrationType::Json(
4253 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4254 )],
4255 r#"{
4256 "agent_servers": {
4257 "gemini": {
4258 "type": "registry",
4259 "default_model": "gemini-2.0-flash"
4260 },
4261 "claude-acp": {
4262 "type": "registry",
4263 "default_mode": "plan"
4264 }
4265 }
4266}"#,
4267 None,
4268 );
4269 }
4270
4271 #[test]
4272 fn test_migrate_builtin_agent_servers_preserves_custom_entries() {
4273 assert_migrate_with_migrations(
4274 &[MigrationType::Json(
4275 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4276 )],
4277 r#"{
4278 "agent_servers": {
4279 "claude": {
4280 "default_mode": "plan"
4281 },
4282 "my-custom-agent": {
4283 "type": "custom",
4284 "command": "/path/to/agent"
4285 }
4286 }
4287}"#,
4288 Some(
4289 r#"{
4290 "agent_servers": {
4291 "claude-acp": {
4292 "type": "registry",
4293 "default_mode": "plan"
4294 },
4295 "my-custom-agent": {
4296 "type": "custom",
4297 "command": "/path/to/agent"
4298 }
4299 }
4300}"#,
4301 ),
4302 );
4303 }
4304
4305 #[test]
4306 fn test_migrate_builtin_agent_servers_target_already_exists() {
4307 assert_migrate_with_migrations(
4308 &[MigrationType::Json(
4309 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4310 )],
4311 r#"{
4312 "agent_servers": {
4313 "claude": {
4314 "default_mode": "plan"
4315 },
4316 "claude-acp": {
4317 "type": "registry",
4318 "default_model": "claude-sonnet-4"
4319 }
4320 }
4321}"#,
4322 Some(
4323 r#"{
4324 "agent_servers": {
4325 "claude-acp": {
4326 "type": "registry",
4327 "default_model": "claude-sonnet-4"
4328 }
4329 }
4330}"#,
4331 ),
4332 );
4333 }
4334
4335 #[test]
4336 fn test_migrate_builtin_agent_servers_no_agent_servers_key() {
4337 assert_migrate_with_migrations(
4338 &[MigrationType::Json(
4339 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4340 )],
4341 r#"{
4342 "agent": {
4343 "enabled": true
4344 }
4345}"#,
4346 None,
4347 );
4348 }
4349
4350 #[test]
4351 fn test_migrate_builtin_agent_servers_all_fields() {
4352 assert_migrate_with_migrations(
4353 &[MigrationType::Json(
4354 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4355 )],
4356 r#"{
4357 "agent_servers": {
4358 "codex": {
4359 "env": {"OPENAI_API_KEY": "sk-123"},
4360 "default_mode": "read-only",
4361 "default_model": "o4-mini",
4362 "favorite_models": ["o4-mini", "codex-mini-latest"],
4363 "default_config_options": {"approval_mode": "auto-edit"},
4364 "favorite_config_option_values": {"approval_mode": ["auto-edit", "suggest"]}
4365 }
4366 }
4367}"#,
4368 Some(
4369 r#"{
4370 "agent_servers": {
4371 "codex-acp": {
4372 "type": "registry",
4373 "env": {
4374 "OPENAI_API_KEY": "sk-123"
4375 },
4376 "default_mode": "read-only",
4377 "default_model": "o4-mini",
4378 "favorite_models": [
4379 "o4-mini",
4380 "codex-mini-latest"
4381 ],
4382 "default_config_options": {
4383 "approval_mode": "auto-edit"
4384 },
4385 "favorite_config_option_values": {
4386 "approval_mode": [
4387 "auto-edit",
4388 "suggest"
4389 ]
4390 }
4391 }
4392 }
4393}"#,
4394 ),
4395 );
4396 }
4397
4398 #[test]
4399 fn test_migrate_builtin_agent_servers_codex_with_command() {
4400 assert_migrate_with_migrations(
4401 &[MigrationType::Json(
4402 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4403 )],
4404 r#"{
4405 "agent_servers": {
4406 "codex": {
4407 "command": "/usr/local/bin/codex",
4408 "args": ["--full-auto"],
4409 "default_model": "o4-mini"
4410 }
4411 }
4412}"#,
4413 Some(
4414 r#"{
4415 "agent_servers": {
4416 "codex-acp-custom": {
4417 "type": "custom",
4418 "command": "/usr/local/bin/codex",
4419 "args": [
4420 "--full-auto"
4421 ],
4422 "default_model": "o4-mini"
4423 }
4424 }
4425}"#,
4426 ),
4427 );
4428 }
4429
4430 #[test]
4431 fn test_migrate_builtin_agent_servers_mixed_migrated_and_not() {
4432 assert_migrate_with_migrations(
4433 &[MigrationType::Json(
4434 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4435 )],
4436 r#"{
4437 "agent_servers": {
4438 "gemini": {
4439 "type": "registry",
4440 "default_model": "gemini-2.0-flash"
4441 },
4442 "claude": {
4443 "default_mode": "plan"
4444 },
4445 "codex": {}
4446 }
4447}"#,
4448 Some(
4449 r#"{
4450 "agent_servers": {
4451 "codex-acp": {
4452 "type": "registry"
4453 },
4454 "claude-acp": {
4455 "type": "registry",
4456 "default_mode": "plan"
4457 },
4458 "gemini": {
4459 "type": "registry",
4460 "default_model": "gemini-2.0-flash"
4461 }
4462 }
4463}"#,
4464 ),
4465 );
4466 }
4467
4468 #[test]
4469 fn test_migrate_edit_prediction_conflict_context() {
4470 assert_migrate_with_migrations(
4471 &[MigrationType::TreeSitter(
4472 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4473 &KEYMAP_QUERY_2026_03_23,
4474 )],
4475 &r#"
4476 [
4477 {
4478 "context": "Editor && edit_prediction_conflict",
4479 "bindings": {
4480 "ctrl-enter": "editor::AcceptEditPrediction" // Example of a modified keybinding
4481 }
4482 }
4483 ]
4484 "#.unindent(),
4485 Some(
4486 &r#"
4487 [
4488 {
4489 "context": "Editor && (edit_prediction && (showing_completions || in_leading_whitespace))",
4490 "bindings": {
4491 "ctrl-enter": "editor::AcceptEditPrediction" // Example of a modified keybinding
4492 }
4493 }
4494 ]
4495 "#.unindent(),
4496 ),
4497 );
4498
4499 assert_migrate_with_migrations(
4500 &[MigrationType::TreeSitter(
4501 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4502 &KEYMAP_QUERY_2026_03_23,
4503 )],
4504 &r#"
4505 [
4506 {
4507 "context": "Editor && edit_prediction_conflict && !showing_completions",
4508 "bindings": {
4509 // Here we don't require a modifier unless there's a language server completion
4510 "tab": "editor::AcceptEditPrediction"
4511 }
4512 }
4513 ]
4514 "#.unindent(),
4515 Some(
4516 &r#"
4517 [
4518 {
4519 "context": "Editor && (edit_prediction && in_leading_whitespace)",
4520 "bindings": {
4521 // Here we don't require a modifier unless there's a language server completion
4522 "tab": "editor::AcceptEditPrediction"
4523 }
4524 }
4525 ]
4526 "#.unindent(),
4527 ),
4528 );
4529
4530 assert_migrate_with_migrations(
4531 &[MigrationType::TreeSitter(
4532 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4533 &KEYMAP_QUERY_2026_03_23,
4534 )],
4535 &r#"
4536 [
4537 {
4538 "context": "Editor && edit_prediction_conflict && showing_completions",
4539 "bindings": {
4540 "tab": "editor::AcceptEditPrediction"
4541 }
4542 }
4543 ]
4544 "#
4545 .unindent(),
4546 Some(
4547 &r#"
4548 [
4549 {
4550 "context": "Editor && (edit_prediction && showing_completions)",
4551 "bindings": {
4552 "tab": "editor::AcceptEditPrediction"
4553 }
4554 }
4555 ]
4556 "#
4557 .unindent(),
4558 ),
4559 );
4560
4561 assert_migrate_with_migrations(
4562 &[MigrationType::TreeSitter(
4563 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4564 &KEYMAP_QUERY_2026_03_23,
4565 )],
4566 &r#"
4567 [
4568 {
4569 "context": "Editor && edit_prediction",
4570 "bindings": {
4571 "tab": "editor::AcceptEditPrediction",
4572 // Optional: This makes the default `alt-l` binding do nothing.
4573 "alt-l": null
4574 }
4575 },
4576 {
4577 "context": "Editor && edit_prediction_conflict",
4578 "bindings": {
4579 "alt-tab": "editor::AcceptEditPrediction",
4580 // Optional: This makes the default `alt-l` binding do nothing.
4581 "alt-l": null
4582 }
4583 },
4584 ]
4585 "#
4586 .unindent(),
4587 Some(
4588 &r#"
4589 [
4590 {
4591 "context": "Editor && edit_prediction",
4592 "bindings": {
4593 "tab": "editor::AcceptEditPrediction",
4594 // Optional: This makes the default `alt-l` binding do nothing.
4595 "alt-l": null
4596 }
4597 },
4598 {
4599 "context": "Editor && (edit_prediction && (showing_completions || in_leading_whitespace))",
4600 "bindings": {
4601 "alt-tab": "editor::AcceptEditPrediction",
4602 // Optional: This makes the default `alt-l` binding do nothing.
4603 "alt-l": null
4604 }
4605 },
4606 ]
4607 "#
4608 .unindent(),
4609 ),
4610 );
4611 }
4612
4613 #[test]
4614 fn test_restructure_profiles_with_settings_key() {
4615 assert_migrate_settings(
4616 &r#"
4617 {
4618 "buffer_font_size": 14,
4619 "profiles": {
4620 "Presenting": {
4621 "buffer_font_size": 20,
4622 "theme": "One Light"
4623 },
4624 "Minimal": {
4625 "vim_mode": true
4626 }
4627 }
4628 }
4629 "#
4630 .unindent(),
4631 Some(
4632 &r#"
4633 {
4634 "buffer_font_size": 14,
4635 "profiles": {
4636 "Presenting": {
4637 "settings": {
4638 "buffer_font_size": 20,
4639 "theme": "One Light"
4640 }
4641 },
4642 "Minimal": {
4643 "settings": {
4644 "vim_mode": true
4645 }
4646 }
4647 }
4648 }
4649 "#
4650 .unindent(),
4651 ),
4652 );
4653 }
4654
4655 #[test]
4656 fn test_restructure_profiles_with_settings_key_already_migrated() {
4657 assert_migrate_settings(
4658 &r#"
4659 {
4660 "profiles": {
4661 "Presenting": {
4662 "settings": {
4663 "buffer_font_size": 20
4664 }
4665 }
4666 }
4667 }
4668 "#
4669 .unindent(),
4670 None,
4671 );
4672 }
4673
4674 #[test]
4675 fn test_restructure_profiles_with_settings_key_no_profiles() {
4676 assert_migrate_settings(
4677 &r#"
4678 {
4679 "buffer_font_size": 14
4680 }
4681 "#
4682 .unindent(),
4683 None,
4684 );
4685 }
4686
4687 #[test]
4688 fn test_rename_web_search_to_search_web_in_tool_permissions() {
4689 assert_migrate_with_migrations(
4690 &[MigrationType::Json(
4691 migrations::m_2026_04_10::rename_web_search_to_search_web,
4692 )],
4693 &r#"
4694 {
4695 "agent": {
4696 "tool_permissions": {
4697 "tools": {
4698 "web_search": {
4699 "allow": true
4700 }
4701 }
4702 }
4703 }
4704 }
4705 "#
4706 .unindent(),
4707 Some(
4708 &r#"
4709 {
4710 "agent": {
4711 "tool_permissions": {
4712 "tools": {
4713 "search_web": {
4714 "allow": true
4715 }
4716 }
4717 }
4718 }
4719 }
4720 "#
4721 .unindent(),
4722 ),
4723 );
4724 }
4725
4726 #[test]
4727 fn test_rename_web_search_to_search_web_in_profiles() {
4728 assert_migrate_with_migrations(
4729 &[MigrationType::Json(
4730 migrations::m_2026_04_10::rename_web_search_to_search_web,
4731 )],
4732 &r#"
4733 {
4734 "agent": {
4735 "profiles": {
4736 "write": {
4737 "tools": {
4738 "web_search": false
4739 }
4740 }
4741 }
4742 }
4743 }
4744 "#
4745 .unindent(),
4746 Some(
4747 &r#"
4748 {
4749 "agent": {
4750 "profiles": {
4751 "write": {
4752 "tools": {
4753 "search_web": false
4754 }
4755 }
4756 }
4757 }
4758 }
4759 "#
4760 .unindent(),
4761 ),
4762 );
4763 }
4764
4765 #[test]
4766 fn test_rename_web_search_to_search_web_no_change_when_already_migrated() {
4767 assert_migrate_with_migrations(
4768 &[MigrationType::Json(
4769 migrations::m_2026_04_10::rename_web_search_to_search_web,
4770 )],
4771 &r#"
4772 {
4773 "agent": {
4774 "tool_permissions": {
4775 "tools": {
4776 "search_web": {
4777 "allow": true
4778 }
4779 }
4780 }
4781 }
4782 }
4783 "#
4784 .unindent(),
4785 None,
4786 );
4787 }
4788
4789 #[test]
4790 fn test_rename_web_search_to_search_web_no_clobber() {
4791 assert_migrate_with_migrations(
4792 &[MigrationType::Json(
4793 migrations::m_2026_04_10::rename_web_search_to_search_web,
4794 )],
4795 &r#"
4796 {
4797 "agent": {
4798 "tool_permissions": {
4799 "tools": {
4800 "web_search": {
4801 "allow": false
4802 },
4803 "search_web": {
4804 "allow": true
4805 }
4806 }
4807 }
4808 }
4809 }
4810 "#
4811 .unindent(),
4812 Some(
4813 &r#"
4814 {
4815 "agent": {
4816 "tool_permissions": {
4817 "tools": {
4818 "search_web": {
4819 "allow": false
4820 }
4821 }
4822 }
4823 }
4824 }
4825 "#
4826 .unindent(),
4827 ),
4828 );
4829 }
4830
4831 #[test]
4832 fn test_rename_web_search_to_search_web_platform_override() {
4833 assert_migrate_with_migrations(
4834 &[MigrationType::Json(
4835 migrations::m_2026_04_10::rename_web_search_to_search_web,
4836 )],
4837 &r#"
4838 {
4839 "linux": {
4840 "agent": {
4841 "tool_permissions": {
4842 "tools": {
4843 "web_search": {
4844 "allow": true
4845 }
4846 }
4847 }
4848 }
4849 }
4850 }
4851 "#
4852 .unindent(),
4853 Some(
4854 &r#"
4855 {
4856 "linux": {
4857 "agent": {
4858 "tool_permissions": {
4859 "tools": {
4860 "search_web": {
4861 "allow": true
4862 }
4863 }
4864 }
4865 }
4866 }
4867 }
4868 "#
4869 .unindent(),
4870 ),
4871 );
4872 }
4873
4874 #[test]
4875 fn test_rename_web_search_to_search_web_release_channel_override() {
4876 assert_migrate_with_migrations(
4877 &[MigrationType::Json(
4878 migrations::m_2026_04_10::rename_web_search_to_search_web,
4879 )],
4880 &r#"
4881 {
4882 "nightly": {
4883 "agent": {
4884 "tool_permissions": {
4885 "tools": {
4886 "web_search": {
4887 "default": "allow"
4888 }
4889 }
4890 }
4891 }
4892 }
4893 }
4894 "#
4895 .unindent(),
4896 Some(
4897 &r#"
4898 {
4899 "nightly": {
4900 "agent": {
4901 "tool_permissions": {
4902 "tools": {
4903 "search_web": {
4904 "default": "allow"
4905 }
4906 }
4907 }
4908 }
4909 }
4910 }
4911 "#
4912 .unindent(),
4913 ),
4914 );
4915 }
4916
4917 #[test]
4918 fn test_rename_web_search_to_search_web_no_agent() {
4919 assert_migrate_with_migrations(
4920 &[MigrationType::Json(
4921 migrations::m_2026_04_10::rename_web_search_to_search_web,
4922 )],
4923 &r#"
4924 {
4925 "buffer_font_size": 14
4926 }
4927 "#
4928 .unindent(),
4929 None,
4930 );
4931 }
4932
4933 #[test]
4934 fn test_rename_web_search_to_search_web_root_level_profile() {
4935 assert_migrate_with_migrations(
4936 &[MigrationType::Json(
4937 migrations::m_2026_04_10::rename_web_search_to_search_web,
4938 )],
4939 &r#"
4940 {
4941 "profiles": {
4942 "Work": {
4943 "settings": {
4944 "agent": {
4945 "tool_permissions": {
4946 "tools": {
4947 "web_search": {
4948 "default": "allow"
4949 }
4950 }
4951 }
4952 }
4953 }
4954 }
4955 }
4956 }
4957 "#
4958 .unindent(),
4959 Some(
4960 &r#"
4961 {
4962 "profiles": {
4963 "Work": {
4964 "settings": {
4965 "agent": {
4966 "tool_permissions": {
4967 "tools": {
4968 "search_web": {
4969 "default": "allow"
4970 }
4971 }
4972 }
4973 }
4974 }
4975 }
4976 }
4977 }
4978 "#
4979 .unindent(),
4980 ),
4981 );
4982 }
4983}