migrator.rs

   1//! ## When to create a migration and why?
   2//! A migration is necessary when keymap actions or settings are renamed or transformed (e.g., from an array to a string, a string to an array, a boolean to an enum, etc.).
   3//!
   4//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally.
   5//! It also provides a quick way to migrate their existing settings to the latest state using button in UI.
   6//!
   7//! ## How to create a migration?
   8//! Migrations use Tree-sitter to query commonly used patterns, such as actions with a string or actions with an array where the second argument is an object, etc.
   9//! Once queried, *you can filter out the modified items* and write the replacement logic.
  10//!
  11//! You *must not* modify previous migrations; always create new ones instead.
  12//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state.
  13//! Modifying existing migrations means they will only work for users upgrading from version x-1 to x, but not from x-2 to x, and so on, where x is the latest version.
  14//!
  15//! You only need to write replacement logic for x-1 to x because you can be certain that, internally, every user will be at x-1, regardless of their on disk state.
  16
  17use anyhow::{Context as _, Result};
  18use std::{cmp::Reverse, ops::Range, sync::LazyLock};
  19use streaming_iterator::StreamingIterator;
  20use tree_sitter::{Query, QueryMatch};
  21
  22use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
  23
  24mod migrations;
  25mod patterns;
  26
  27fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
  28    let mut parser = tree_sitter::Parser::new();
  29    parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
  30    let syntax_tree = parser
  31        .parse(&text, None)
  32        .context("failed to parse settings")?;
  33
  34    let mut cursor = tree_sitter::QueryCursor::new();
  35    let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
  36
  37    let mut edits = vec![];
  38    while let Some(mat) = matches.next() {
  39        if let Some((_, callback)) = patterns.get(mat.pattern_index) {
  40            edits.extend(callback(&text, &mat, query));
  41        }
  42    }
  43
  44    edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
  45    edits.dedup_by(|(range_b, _), (range_a, _)| {
  46        range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
  47    });
  48
  49    if edits.is_empty() {
  50        Ok(None)
  51    } else {
  52        let mut new_text = text.to_string();
  53        for (range, replacement) in edits.iter().rev() {
  54            new_text.replace_range(range.clone(), replacement);
  55        }
  56        if new_text == text {
  57            log::error!(
  58                "Edits computed for configuration migration do not cause a change: {:?}",
  59                edits
  60            );
  61            Ok(None)
  62        } else {
  63            Ok(Some(new_text))
  64        }
  65    }
  66}
  67
  68fn run_migrations(
  69    text: &str,
  70    migrations: &[(MigrationPatterns, &Query)],
  71) -> Result<Option<String>> {
  72    let mut current_text = text.to_string();
  73    let mut result: Option<String> = None;
  74    for (patterns, query) in migrations.iter() {
  75        if let Some(migrated_text) = migrate(&current_text, patterns, query)? {
  76            current_text = migrated_text.clone();
  77            result = Some(migrated_text);
  78        }
  79    }
  80    Ok(result.filter(|new_text| text != new_text))
  81}
  82
  83pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
  84    let migrations: &[(MigrationPatterns, &Query)] = &[
  85        (
  86            migrations::m_2025_01_29::KEYMAP_PATTERNS,
  87            &KEYMAP_QUERY_2025_01_29,
  88        ),
  89        (
  90            migrations::m_2025_01_30::KEYMAP_PATTERNS,
  91            &KEYMAP_QUERY_2025_01_30,
  92        ),
  93        (
  94            migrations::m_2025_03_03::KEYMAP_PATTERNS,
  95            &KEYMAP_QUERY_2025_03_03,
  96        ),
  97        (
  98            migrations::m_2025_03_06::KEYMAP_PATTERNS,
  99            &KEYMAP_QUERY_2025_03_06,
 100        ),
 101        (
 102            migrations::m_2025_04_15::KEYMAP_PATTERNS,
 103            &KEYMAP_QUERY_2025_04_15,
 104        ),
 105    ];
 106    run_migrations(text, migrations)
 107}
 108
 109pub fn migrate_settings(text: &str) -> Result<Option<String>> {
 110    let migrations: &[(MigrationPatterns, &Query)] = &[
 111        (
 112            migrations::m_2025_01_02::SETTINGS_PATTERNS,
 113            &SETTINGS_QUERY_2025_01_02,
 114        ),
 115        (
 116            migrations::m_2025_01_29::SETTINGS_PATTERNS,
 117            &SETTINGS_QUERY_2025_01_29,
 118        ),
 119        (
 120            migrations::m_2025_01_30::SETTINGS_PATTERNS,
 121            &SETTINGS_QUERY_2025_01_30,
 122        ),
 123        (
 124            migrations::m_2025_03_29::SETTINGS_PATTERNS,
 125            &SETTINGS_QUERY_2025_03_29,
 126        ),
 127        (
 128            migrations::m_2025_04_15::SETTINGS_PATTERNS,
 129            &SETTINGS_QUERY_2025_04_15,
 130        ),
 131        (
 132            migrations::m_2025_04_21::SETTINGS_PATTERNS,
 133            &SETTINGS_QUERY_2025_04_21,
 134        ),
 135        (
 136            migrations::m_2025_04_23::SETTINGS_PATTERNS,
 137            &SETTINGS_QUERY_2025_04_23,
 138        ),
 139        (
 140            migrations::m_2025_05_05::SETTINGS_PATTERNS,
 141            &SETTINGS_QUERY_2025_05_05,
 142        ),
 143        (
 144            migrations::m_2025_05_08::SETTINGS_PATTERNS,
 145            &SETTINGS_QUERY_2025_05_08,
 146        ),
 147        (
 148            migrations::m_2025_05_29::SETTINGS_PATTERNS,
 149            &SETTINGS_QUERY_2025_05_29,
 150        ),
 151        (
 152            migrations::m_2025_06_16::SETTINGS_PATTERNS,
 153            &SETTINGS_QUERY_2025_06_16,
 154        ),
 155        (
 156            migrations::m_2025_06_25::SETTINGS_PATTERNS,
 157            &SETTINGS_QUERY_2025_06_25,
 158        ),
 159    ];
 160    run_migrations(text, migrations)
 161}
 162
 163pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
 164    migrate(
 165        &text,
 166        &[(
 167            SETTINGS_NESTED_KEY_VALUE_PATTERN,
 168            migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
 169        )],
 170        &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
 171    )
 172}
 173
 174pub type MigrationPatterns = &'static [(
 175    &'static str,
 176    fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
 177)];
 178
 179macro_rules! define_query {
 180    ($var_name:ident, $patterns_path:path) => {
 181        static $var_name: LazyLock<Query> = LazyLock::new(|| {
 182            Query::new(
 183                &tree_sitter_json::LANGUAGE.into(),
 184                &$patterns_path
 185                    .iter()
 186                    .map(|pattern| pattern.0)
 187                    .collect::<String>(),
 188            )
 189            .unwrap()
 190        });
 191    };
 192}
 193
 194// keymap
 195define_query!(
 196    KEYMAP_QUERY_2025_01_29,
 197    migrations::m_2025_01_29::KEYMAP_PATTERNS
 198);
 199define_query!(
 200    KEYMAP_QUERY_2025_01_30,
 201    migrations::m_2025_01_30::KEYMAP_PATTERNS
 202);
 203define_query!(
 204    KEYMAP_QUERY_2025_03_03,
 205    migrations::m_2025_03_03::KEYMAP_PATTERNS
 206);
 207define_query!(
 208    KEYMAP_QUERY_2025_03_06,
 209    migrations::m_2025_03_06::KEYMAP_PATTERNS
 210);
 211define_query!(
 212    KEYMAP_QUERY_2025_04_15,
 213    migrations::m_2025_04_15::KEYMAP_PATTERNS
 214);
 215
 216// settings
 217define_query!(
 218    SETTINGS_QUERY_2025_01_02,
 219    migrations::m_2025_01_02::SETTINGS_PATTERNS
 220);
 221define_query!(
 222    SETTINGS_QUERY_2025_01_29,
 223    migrations::m_2025_01_29::SETTINGS_PATTERNS
 224);
 225define_query!(
 226    SETTINGS_QUERY_2025_01_30,
 227    migrations::m_2025_01_30::SETTINGS_PATTERNS
 228);
 229define_query!(
 230    SETTINGS_QUERY_2025_03_29,
 231    migrations::m_2025_03_29::SETTINGS_PATTERNS
 232);
 233define_query!(
 234    SETTINGS_QUERY_2025_04_15,
 235    migrations::m_2025_04_15::SETTINGS_PATTERNS
 236);
 237define_query!(
 238    SETTINGS_QUERY_2025_04_21,
 239    migrations::m_2025_04_21::SETTINGS_PATTERNS
 240);
 241define_query!(
 242    SETTINGS_QUERY_2025_04_23,
 243    migrations::m_2025_04_23::SETTINGS_PATTERNS
 244);
 245define_query!(
 246    SETTINGS_QUERY_2025_05_05,
 247    migrations::m_2025_05_05::SETTINGS_PATTERNS
 248);
 249define_query!(
 250    SETTINGS_QUERY_2025_05_08,
 251    migrations::m_2025_05_08::SETTINGS_PATTERNS
 252);
 253define_query!(
 254    SETTINGS_QUERY_2025_05_29,
 255    migrations::m_2025_05_29::SETTINGS_PATTERNS
 256);
 257define_query!(
 258    SETTINGS_QUERY_2025_06_16,
 259    migrations::m_2025_06_16::SETTINGS_PATTERNS
 260);
 261define_query!(
 262    SETTINGS_QUERY_2025_06_25,
 263    migrations::m_2025_06_25::SETTINGS_PATTERNS
 264);
 265
 266// custom query
 267static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 268    Query::new(
 269        &tree_sitter_json::LANGUAGE.into(),
 270        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 271    )
 272    .unwrap()
 273});
 274
 275#[cfg(test)]
 276mod tests {
 277    use super::*;
 278
 279    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 280        let migrated = migrate_keymap(&input).unwrap();
 281        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 282    }
 283
 284    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 285        let migrated = migrate_settings(&input).unwrap();
 286        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 287    }
 288
 289    #[test]
 290    fn test_replace_array_with_single_string() {
 291        assert_migrate_keymap(
 292            r#"
 293            [
 294                {
 295                    "bindings": {
 296                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 297                    }
 298                }
 299            ]
 300            "#,
 301            Some(
 302                r#"
 303            [
 304                {
 305                    "bindings": {
 306                        "cmd-1": "workspace::ActivatePaneUp"
 307                    }
 308                }
 309            ]
 310            "#,
 311            ),
 312        )
 313    }
 314
 315    #[test]
 316    fn test_replace_action_argument_object_with_single_value() {
 317        assert_migrate_keymap(
 318            r#"
 319            [
 320                {
 321                    "bindings": {
 322                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 323                    }
 324                }
 325            ]
 326            "#,
 327            Some(
 328                r#"
 329            [
 330                {
 331                    "bindings": {
 332                        "cmd-1": ["editor::FoldAtLevel", 1]
 333                    }
 334                }
 335            ]
 336            "#,
 337            ),
 338        )
 339    }
 340
 341    #[test]
 342    fn test_replace_action_argument_object_with_single_value_2() {
 343        assert_migrate_keymap(
 344            r#"
 345            [
 346                {
 347                    "bindings": {
 348                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 349                    }
 350                }
 351            ]
 352            "#,
 353            Some(
 354                r#"
 355            [
 356                {
 357                    "bindings": {
 358                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 359                    }
 360                }
 361            ]
 362            "#,
 363            ),
 364        )
 365    }
 366
 367    #[test]
 368    fn test_rename_string_action() {
 369        assert_migrate_keymap(
 370            r#"
 371                [
 372                    {
 373                        "bindings": {
 374                            "cmd-1": "inline_completion::ToggleMenu"
 375                        }
 376                    }
 377                ]
 378            "#,
 379            Some(
 380                r#"
 381                [
 382                    {
 383                        "bindings": {
 384                            "cmd-1": "edit_prediction::ToggleMenu"
 385                        }
 386                    }
 387                ]
 388            "#,
 389            ),
 390        )
 391    }
 392
 393    #[test]
 394    fn test_rename_context_key() {
 395        assert_migrate_keymap(
 396            r#"
 397                [
 398                    {
 399                        "context": "Editor && inline_completion && !showing_completions"
 400                    }
 401                ]
 402            "#,
 403            Some(
 404                r#"
 405                [
 406                    {
 407                        "context": "Editor && edit_prediction && !showing_completions"
 408                    }
 409                ]
 410            "#,
 411            ),
 412        )
 413    }
 414
 415    #[test]
 416    fn test_incremental_migrations() {
 417        // Here string transforms to array internally. Then, that array transforms back to string.
 418        assert_migrate_keymap(
 419            r#"
 420                [
 421                    {
 422                        "bindings": {
 423                            "ctrl-q": "editor::GoToHunk", // should remain same
 424                            "ctrl-w": "editor::GoToPrevHunk", // should rename
 425                            "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
 426                            "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
 427                        }
 428                    }
 429                ]
 430            "#,
 431            Some(
 432                r#"
 433                [
 434                    {
 435                        "bindings": {
 436                            "ctrl-q": "editor::GoToHunk", // should remain same
 437                            "ctrl-w": "editor::GoToPreviousHunk", // should rename
 438                            "ctrl-q": "editor::GoToHunk", // should transform
 439                            "ctrl-w": "editor::GoToPreviousHunk" // should transform
 440                        }
 441                    }
 442                ]
 443            "#,
 444            ),
 445        )
 446    }
 447
 448    #[test]
 449    fn test_action_argument_snake_case() {
 450        // First performs transformations, then replacements
 451        assert_migrate_keymap(
 452            r#"
 453            [
 454                {
 455                    "bindings": {
 456                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
 457                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
 458                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
 459                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 460                    }
 461                }
 462            ]
 463            "#,
 464            Some(
 465                r#"
 466            [
 467                {
 468                    "bindings": {
 469                        "cmd-1": ["vim::PushObject", { "around": false }],
 470                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
 471                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
 472                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 473                    }
 474                }
 475            ]
 476            "#,
 477            ),
 478        )
 479    }
 480
 481    #[test]
 482    fn test_replace_setting_name() {
 483        assert_migrate_settings(
 484            r#"
 485                {
 486                    "show_inline_completions_in_menu": true,
 487                    "show_inline_completions": true,
 488                    "inline_completions_disabled_in": ["string"],
 489                    "inline_completions": { "some" : "value" }
 490                }
 491            "#,
 492            Some(
 493                r#"
 494                {
 495                    "show_edit_predictions_in_menu": true,
 496                    "show_edit_predictions": true,
 497                    "edit_predictions_disabled_in": ["string"],
 498                    "edit_predictions": { "some" : "value" }
 499                }
 500            "#,
 501            ),
 502        )
 503    }
 504
 505    #[test]
 506    fn test_nested_string_replace_for_settings() {
 507        assert_migrate_settings(
 508            r#"
 509                {
 510                    "features": {
 511                        "inline_completion_provider": "zed"
 512                    },
 513                }
 514            "#,
 515            Some(
 516                r#"
 517                {
 518                    "features": {
 519                        "edit_prediction_provider": "zed"
 520                    },
 521                }
 522            "#,
 523            ),
 524        )
 525    }
 526
 527    #[test]
 528    fn test_replace_settings_in_languages() {
 529        assert_migrate_settings(
 530            r#"
 531                {
 532                    "languages": {
 533                        "Astro": {
 534                            "show_inline_completions": true
 535                        }
 536                    }
 537                }
 538            "#,
 539            Some(
 540                r#"
 541                {
 542                    "languages": {
 543                        "Astro": {
 544                            "show_edit_predictions": true
 545                        }
 546                    }
 547                }
 548            "#,
 549            ),
 550        )
 551    }
 552
 553    #[test]
 554    fn test_replace_settings_value() {
 555        assert_migrate_settings(
 556            r#"
 557                {
 558                    "scrollbar": {
 559                        "diagnostics": true
 560                    },
 561                    "chat_panel": {
 562                        "button": true
 563                    }
 564                }
 565            "#,
 566            Some(
 567                r#"
 568                {
 569                    "scrollbar": {
 570                        "diagnostics": "all"
 571                    },
 572                    "chat_panel": {
 573                        "button": "always"
 574                    }
 575                }
 576            "#,
 577            ),
 578        )
 579    }
 580
 581    #[test]
 582    fn test_replace_settings_name_and_value() {
 583        assert_migrate_settings(
 584            r#"
 585                {
 586                    "tabs": {
 587                        "always_show_close_button": true
 588                    }
 589                }
 590            "#,
 591            Some(
 592                r#"
 593                {
 594                    "tabs": {
 595                        "show_close_button": "always"
 596                    }
 597                }
 598            "#,
 599            ),
 600        )
 601    }
 602
 603    #[test]
 604    fn test_replace_bash_with_terminal_in_profiles() {
 605        assert_migrate_settings(
 606            r#"
 607                {
 608                    "assistant": {
 609                        "profiles": {
 610                            "custom": {
 611                                "name": "Custom",
 612                                "tools": {
 613                                    "bash": true,
 614                                    "diagnostics": true
 615                                }
 616                            }
 617                        }
 618                    }
 619                }
 620            "#,
 621            Some(
 622                r#"
 623                {
 624                    "agent": {
 625                        "profiles": {
 626                            "custom": {
 627                                "name": "Custom",
 628                                "tools": {
 629                                    "terminal": true,
 630                                    "diagnostics": true
 631                                }
 632                            }
 633                        }
 634                    }
 635                }
 636            "#,
 637            ),
 638        )
 639    }
 640
 641    #[test]
 642    fn test_replace_bash_false_with_terminal_in_profiles() {
 643        assert_migrate_settings(
 644            r#"
 645                {
 646                    "assistant": {
 647                        "profiles": {
 648                            "custom": {
 649                                "name": "Custom",
 650                                "tools": {
 651                                    "bash": false,
 652                                    "diagnostics": true
 653                                }
 654                            }
 655                        }
 656                    }
 657                }
 658            "#,
 659            Some(
 660                r#"
 661                {
 662                    "agent": {
 663                        "profiles": {
 664                            "custom": {
 665                                "name": "Custom",
 666                                "tools": {
 667                                    "terminal": false,
 668                                    "diagnostics": true
 669                                }
 670                            }
 671                        }
 672                    }
 673                }
 674            "#,
 675            ),
 676        )
 677    }
 678
 679    #[test]
 680    fn test_no_bash_in_profiles() {
 681        assert_migrate_settings(
 682            r#"
 683                {
 684                    "assistant": {
 685                        "profiles": {
 686                            "custom": {
 687                                "name": "Custom",
 688                                "tools": {
 689                                    "diagnostics": true,
 690                                    "find_path": true,
 691                                    "read_file": true
 692                                }
 693                            }
 694                        }
 695                    }
 696                }
 697            "#,
 698            Some(
 699                r#"
 700                {
 701                    "agent": {
 702                        "profiles": {
 703                            "custom": {
 704                                "name": "Custom",
 705                                "tools": {
 706                                    "diagnostics": true,
 707                                    "find_path": true,
 708                                    "read_file": true
 709                                }
 710                            }
 711                        }
 712                    }
 713                }
 714            "#,
 715            ),
 716        )
 717    }
 718
 719    #[test]
 720    fn test_rename_path_search_to_find_path() {
 721        assert_migrate_settings(
 722            r#"
 723                {
 724                    "assistant": {
 725                        "profiles": {
 726                            "default": {
 727                                "tools": {
 728                                    "path_search": true,
 729                                    "read_file": true
 730                                }
 731                            }
 732                        }
 733                    }
 734                }
 735            "#,
 736            Some(
 737                r#"
 738                {
 739                    "agent": {
 740                        "profiles": {
 741                            "default": {
 742                                "tools": {
 743                                    "find_path": true,
 744                                    "read_file": true
 745                                }
 746                            }
 747                        }
 748                    }
 749                }
 750            "#,
 751            ),
 752        );
 753    }
 754
 755    #[test]
 756    fn test_rename_assistant() {
 757        assert_migrate_settings(
 758            r#"{
 759                "assistant": {
 760                    "foo": "bar"
 761                },
 762                "edit_predictions": {
 763                    "enabled_in_assistant": false,
 764                }
 765            }"#,
 766            Some(
 767                r#"{
 768                "agent": {
 769                    "foo": "bar"
 770                },
 771                "edit_predictions": {
 772                    "enabled_in_text_threads": false,
 773                }
 774            }"#,
 775            ),
 776        );
 777    }
 778
 779    #[test]
 780    fn test_comment_duplicated_agent() {
 781        assert_migrate_settings(
 782            r#"{
 783                "agent": {
 784                    "name": "assistant-1",
 785                "model": "gpt-4", // weird formatting
 786                    "utf8": "привіт"
 787                },
 788                "something": "else",
 789                "agent": {
 790                    "name": "assistant-2",
 791                    "model": "gemini-pro"
 792                }
 793            }
 794        "#,
 795            Some(
 796                r#"{
 797                /* Duplicated key auto-commented: "agent": {
 798                    "name": "assistant-1",
 799                "model": "gpt-4", // weird formatting
 800                    "utf8": "привіт"
 801                }, */
 802                "something": "else",
 803                "agent": {
 804                    "name": "assistant-2",
 805                    "model": "gemini-pro"
 806                }
 807            }
 808        "#,
 809            ),
 810        );
 811    }
 812
 813    #[test]
 814    fn test_preferred_completion_mode_migration() {
 815        assert_migrate_settings(
 816            r#"{
 817                "agent": {
 818                    "preferred_completion_mode": "max",
 819                    "enabled": true
 820                }
 821            }"#,
 822            Some(
 823                r#"{
 824                "agent": {
 825                    "preferred_completion_mode": "burn",
 826                    "enabled": true
 827                }
 828            }"#,
 829            ),
 830        );
 831
 832        assert_migrate_settings(
 833            r#"{
 834                "agent": {
 835                    "preferred_completion_mode": "normal",
 836                    "enabled": true
 837                }
 838            }"#,
 839            None,
 840        );
 841
 842        assert_migrate_settings(
 843            r#"{
 844                "agent": {
 845                    "preferred_completion_mode": "burn",
 846                    "enabled": true
 847                }
 848            }"#,
 849            None,
 850        );
 851
 852        assert_migrate_settings(
 853            r#"{
 854                "other_section": {
 855                    "preferred_completion_mode": "max"
 856                },
 857                "agent": {
 858                    "preferred_completion_mode": "max"
 859                }
 860            }"#,
 861            Some(
 862                r#"{
 863                "other_section": {
 864                    "preferred_completion_mode": "max"
 865                },
 866                "agent": {
 867                    "preferred_completion_mode": "burn"
 868                }
 869            }"#,
 870            ),
 871        );
 872    }
 873
 874    #[test]
 875    fn test_mcp_settings_migration() {
 876        assert_migrate_settings(
 877            r#"{
 878    "context_servers": {
 879        "empty_server": {},
 880        "extension_server": {
 881            "settings": {
 882                "foo": "bar"
 883            }
 884        },
 885        "custom_server": {
 886            "command": {
 887                "path": "foo",
 888                "args": ["bar"],
 889                "env": {
 890                    "FOO": "BAR"
 891                }
 892            }
 893        },
 894        "invalid_server": {
 895            "command": {
 896                "path": "foo",
 897                "args": ["bar"],
 898                "env": {
 899                    "FOO": "BAR"
 900                }
 901            },
 902            "settings": {
 903                "foo": "bar"
 904            }
 905        },
 906        "empty_server2": {},
 907        "extension_server2": {
 908            "foo": "bar",
 909            "settings": {
 910                "foo": "bar"
 911            },
 912            "bar": "foo"
 913        },
 914        "custom_server2": {
 915            "foo": "bar",
 916            "command": {
 917                "path": "foo",
 918                "args": ["bar"],
 919                "env": {
 920                    "FOO": "BAR"
 921                }
 922            },
 923            "bar": "foo"
 924        },
 925        "invalid_server2": {
 926            "foo": "bar",
 927            "command": {
 928                "path": "foo",
 929                "args": ["bar"],
 930                "env": {
 931                    "FOO": "BAR"
 932                }
 933            },
 934            "bar": "foo",
 935            "settings": {
 936                "foo": "bar"
 937            }
 938        }
 939    }
 940}"#,
 941            Some(
 942                r#"{
 943    "context_servers": {
 944        "empty_server": {
 945            "source": "extension",
 946            "settings": {}
 947        },
 948        "extension_server": {
 949            "source": "extension",
 950            "settings": {
 951                "foo": "bar"
 952            }
 953        },
 954        "custom_server": {
 955            "source": "custom",
 956            "command": {
 957                "path": "foo",
 958                "args": ["bar"],
 959                "env": {
 960                    "FOO": "BAR"
 961                }
 962            }
 963        },
 964        "invalid_server": {
 965            "source": "custom",
 966            "command": {
 967                "path": "foo",
 968                "args": ["bar"],
 969                "env": {
 970                    "FOO": "BAR"
 971                }
 972            },
 973            "settings": {
 974                "foo": "bar"
 975            }
 976        },
 977        "empty_server2": {
 978            "source": "extension",
 979            "settings": {}
 980        },
 981        "extension_server2": {
 982            "source": "extension",
 983            "foo": "bar",
 984            "settings": {
 985                "foo": "bar"
 986            },
 987            "bar": "foo"
 988        },
 989        "custom_server2": {
 990            "source": "custom",
 991            "foo": "bar",
 992            "command": {
 993                "path": "foo",
 994                "args": ["bar"],
 995                "env": {
 996                    "FOO": "BAR"
 997                }
 998            },
 999            "bar": "foo"
1000        },
1001        "invalid_server2": {
1002            "source": "custom",
1003            "foo": "bar",
1004            "command": {
1005                "path": "foo",
1006                "args": ["bar"],
1007                "env": {
1008                    "FOO": "BAR"
1009                }
1010            },
1011            "bar": "foo",
1012            "settings": {
1013                "foo": "bar"
1014            }
1015        }
1016    }
1017}"#,
1018            ),
1019        );
1020    }
1021
1022    #[test]
1023    fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1024        let settings = r#"{
1025    "context_servers": {
1026        "empty_server": {
1027            "source": "extension",
1028            "settings": {}
1029        },
1030        "extension_server": {
1031            "source": "extension",
1032            "settings": {
1033                "foo": "bar"
1034            }
1035        },
1036        "custom_server": {
1037            "source": "custom",
1038            "command": {
1039                "path": "foo",
1040                "args": ["bar"],
1041                "env": {
1042                    "FOO": "BAR"
1043                }
1044            }
1045        },
1046        "invalid_server": {
1047            "source": "custom",
1048            "command": {
1049                "path": "foo",
1050                "args": ["bar"],
1051                "env": {
1052                    "FOO": "BAR"
1053                }
1054            },
1055            "settings": {
1056                "foo": "bar"
1057            }
1058        }
1059    }
1060}"#;
1061        assert_migrate_settings(settings, None);
1062    }
1063
1064    #[test]
1065    fn test_remove_version_fields() {
1066        assert_migrate_settings(
1067            r#"{
1068    "language_models": {
1069        "anthropic": {
1070            "version": "1",
1071            "api_url": "https://api.anthropic.com"
1072        },
1073        "openai": {
1074            "version": "1",
1075            "api_url": "https://api.openai.com/v1"
1076        }
1077    },
1078    "agent": {
1079        "version": "2",
1080        "enabled": true,
1081        "preferred_completion_mode": "normal",
1082        "button": true,
1083        "dock": "right",
1084        "default_width": 640,
1085        "default_height": 320,
1086        "default_model": {
1087            "provider": "zed.dev",
1088            "model": "claude-sonnet-4"
1089        }
1090    }
1091}"#,
1092            Some(
1093                r#"{
1094    "language_models": {
1095        "anthropic": {
1096            "api_url": "https://api.anthropic.com"
1097        },
1098        "openai": {
1099            "api_url": "https://api.openai.com/v1"
1100        }
1101    },
1102    "agent": {
1103        "enabled": true,
1104        "preferred_completion_mode": "normal",
1105        "button": true,
1106        "dock": "right",
1107        "default_width": 640,
1108        "default_height": 320,
1109        "default_model": {
1110            "provider": "zed.dev",
1111            "model": "claude-sonnet-4"
1112        }
1113    }
1114}"#,
1115            ),
1116        );
1117
1118        // Test that version fields in other contexts are not removed
1119        assert_migrate_settings(
1120            r#"{
1121    "language_models": {
1122        "other_provider": {
1123            "version": "1",
1124            "api_url": "https://api.example.com"
1125        }
1126    },
1127    "other_section": {
1128        "version": "1"
1129    }
1130}"#,
1131            None,
1132        );
1133    }
1134}