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            migrations::m_2025_06_27::SETTINGS_PATTERNS,
 161            &SETTINGS_QUERY_2025_06_27,
 162        ),
 163        (
 164            migrations::m_2025_07_08::SETTINGS_PATTERNS,
 165            &SETTINGS_QUERY_2025_07_08,
 166        ),
 167        (
 168            migrations::m_2025_10_01::SETTINGS_PATTERNS,
 169            &SETTINGS_QUERY_2025_10_01,
 170        ),
 171    ];
 172    run_migrations(text, migrations)
 173}
 174
 175pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
 176    migrate(
 177        text,
 178        &[(
 179            SETTINGS_NESTED_KEY_VALUE_PATTERN,
 180            migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
 181        )],
 182        &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
 183    )
 184}
 185
 186pub type MigrationPatterns = &'static [(
 187    &'static str,
 188    fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
 189)];
 190
 191macro_rules! define_query {
 192    ($var_name:ident, $patterns_path:path) => {
 193        static $var_name: LazyLock<Query> = LazyLock::new(|| {
 194            Query::new(
 195                &tree_sitter_json::LANGUAGE.into(),
 196                &$patterns_path
 197                    .iter()
 198                    .map(|pattern| pattern.0)
 199                    .collect::<String>(),
 200            )
 201            .unwrap()
 202        });
 203    };
 204}
 205
 206// keymap
 207define_query!(
 208    KEYMAP_QUERY_2025_01_29,
 209    migrations::m_2025_01_29::KEYMAP_PATTERNS
 210);
 211define_query!(
 212    KEYMAP_QUERY_2025_01_30,
 213    migrations::m_2025_01_30::KEYMAP_PATTERNS
 214);
 215define_query!(
 216    KEYMAP_QUERY_2025_03_03,
 217    migrations::m_2025_03_03::KEYMAP_PATTERNS
 218);
 219define_query!(
 220    KEYMAP_QUERY_2025_03_06,
 221    migrations::m_2025_03_06::KEYMAP_PATTERNS
 222);
 223define_query!(
 224    KEYMAP_QUERY_2025_04_15,
 225    migrations::m_2025_04_15::KEYMAP_PATTERNS
 226);
 227
 228// settings
 229define_query!(
 230    SETTINGS_QUERY_2025_01_02,
 231    migrations::m_2025_01_02::SETTINGS_PATTERNS
 232);
 233define_query!(
 234    SETTINGS_QUERY_2025_01_29,
 235    migrations::m_2025_01_29::SETTINGS_PATTERNS
 236);
 237define_query!(
 238    SETTINGS_QUERY_2025_01_30,
 239    migrations::m_2025_01_30::SETTINGS_PATTERNS
 240);
 241define_query!(
 242    SETTINGS_QUERY_2025_03_29,
 243    migrations::m_2025_03_29::SETTINGS_PATTERNS
 244);
 245define_query!(
 246    SETTINGS_QUERY_2025_04_15,
 247    migrations::m_2025_04_15::SETTINGS_PATTERNS
 248);
 249define_query!(
 250    SETTINGS_QUERY_2025_04_21,
 251    migrations::m_2025_04_21::SETTINGS_PATTERNS
 252);
 253define_query!(
 254    SETTINGS_QUERY_2025_04_23,
 255    migrations::m_2025_04_23::SETTINGS_PATTERNS
 256);
 257define_query!(
 258    SETTINGS_QUERY_2025_05_05,
 259    migrations::m_2025_05_05::SETTINGS_PATTERNS
 260);
 261define_query!(
 262    SETTINGS_QUERY_2025_05_08,
 263    migrations::m_2025_05_08::SETTINGS_PATTERNS
 264);
 265define_query!(
 266    SETTINGS_QUERY_2025_05_29,
 267    migrations::m_2025_05_29::SETTINGS_PATTERNS
 268);
 269define_query!(
 270    SETTINGS_QUERY_2025_06_16,
 271    migrations::m_2025_06_16::SETTINGS_PATTERNS
 272);
 273define_query!(
 274    SETTINGS_QUERY_2025_06_25,
 275    migrations::m_2025_06_25::SETTINGS_PATTERNS
 276);
 277define_query!(
 278    SETTINGS_QUERY_2025_06_27,
 279    migrations::m_2025_06_27::SETTINGS_PATTERNS
 280);
 281define_query!(
 282    SETTINGS_QUERY_2025_07_08,
 283    migrations::m_2025_07_08::SETTINGS_PATTERNS
 284);
 285define_query!(
 286    SETTINGS_QUERY_2025_10_01,
 287    migrations::m_2025_10_01::SETTINGS_PATTERNS
 288);
 289
 290// custom query
 291static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 292    Query::new(
 293        &tree_sitter_json::LANGUAGE.into(),
 294        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 295    )
 296    .unwrap()
 297});
 298
 299#[cfg(test)]
 300mod tests {
 301    use super::*;
 302    use unindent::Unindent as _;
 303
 304    fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
 305        match (&migrated, &expected) {
 306            (Some(migrated), Some(expected)) => {
 307                pretty_assertions::assert_str_eq!(migrated, expected);
 308            }
 309            _ => {
 310                pretty_assertions::assert_eq!(migrated.as_deref(), expected);
 311            }
 312        }
 313    }
 314
 315    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 316        let migrated = migrate_keymap(input).unwrap();
 317        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 318    }
 319
 320    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 321        let migrated = migrate_settings(input).unwrap();
 322        assert_migrated_correctly(migrated, output);
 323    }
 324
 325    fn assert_migrate_settings_with_migrations(
 326        migrations: &[(MigrationPatterns, &Query)],
 327        input: &str,
 328        output: Option<&str>,
 329    ) {
 330        let migrated = run_migrations(input, migrations).unwrap();
 331        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 332    }
 333
 334    #[test]
 335    fn test_replace_array_with_single_string() {
 336        assert_migrate_keymap(
 337            r#"
 338            [
 339                {
 340                    "bindings": {
 341                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 342                    }
 343                }
 344            ]
 345            "#,
 346            Some(
 347                r#"
 348            [
 349                {
 350                    "bindings": {
 351                        "cmd-1": "workspace::ActivatePaneUp"
 352                    }
 353                }
 354            ]
 355            "#,
 356            ),
 357        )
 358    }
 359
 360    #[test]
 361    fn test_replace_action_argument_object_with_single_value() {
 362        assert_migrate_keymap(
 363            r#"
 364            [
 365                {
 366                    "bindings": {
 367                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 368                    }
 369                }
 370            ]
 371            "#,
 372            Some(
 373                r#"
 374            [
 375                {
 376                    "bindings": {
 377                        "cmd-1": ["editor::FoldAtLevel", 1]
 378                    }
 379                }
 380            ]
 381            "#,
 382            ),
 383        )
 384    }
 385
 386    #[test]
 387    fn test_replace_action_argument_object_with_single_value_2() {
 388        assert_migrate_keymap(
 389            r#"
 390            [
 391                {
 392                    "bindings": {
 393                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 394                    }
 395                }
 396            ]
 397            "#,
 398            Some(
 399                r#"
 400            [
 401                {
 402                    "bindings": {
 403                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 404                    }
 405                }
 406            ]
 407            "#,
 408            ),
 409        )
 410    }
 411
 412    #[test]
 413    fn test_rename_string_action() {
 414        assert_migrate_keymap(
 415            r#"
 416                [
 417                    {
 418                        "bindings": {
 419                            "cmd-1": "inline_completion::ToggleMenu"
 420                        }
 421                    }
 422                ]
 423            "#,
 424            Some(
 425                r#"
 426                [
 427                    {
 428                        "bindings": {
 429                            "cmd-1": "edit_prediction::ToggleMenu"
 430                        }
 431                    }
 432                ]
 433            "#,
 434            ),
 435        )
 436    }
 437
 438    #[test]
 439    fn test_rename_context_key() {
 440        assert_migrate_keymap(
 441            r#"
 442                [
 443                    {
 444                        "context": "Editor && inline_completion && !showing_completions"
 445                    }
 446                ]
 447            "#,
 448            Some(
 449                r#"
 450                [
 451                    {
 452                        "context": "Editor && edit_prediction && !showing_completions"
 453                    }
 454                ]
 455            "#,
 456            ),
 457        )
 458    }
 459
 460    #[test]
 461    fn test_incremental_migrations() {
 462        // Here string transforms to array internally. Then, that array transforms back to string.
 463        assert_migrate_keymap(
 464            r#"
 465                [
 466                    {
 467                        "bindings": {
 468                            "ctrl-q": "editor::GoToHunk", // should remain same
 469                            "ctrl-w": "editor::GoToPrevHunk", // should rename
 470                            "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
 471                            "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
 472                        }
 473                    }
 474                ]
 475            "#,
 476            Some(
 477                r#"
 478                [
 479                    {
 480                        "bindings": {
 481                            "ctrl-q": "editor::GoToHunk", // should remain same
 482                            "ctrl-w": "editor::GoToPreviousHunk", // should rename
 483                            "ctrl-q": "editor::GoToHunk", // should transform
 484                            "ctrl-w": "editor::GoToPreviousHunk" // should transform
 485                        }
 486                    }
 487                ]
 488            "#,
 489            ),
 490        )
 491    }
 492
 493    #[test]
 494    fn test_action_argument_snake_case() {
 495        // First performs transformations, then replacements
 496        assert_migrate_keymap(
 497            r#"
 498            [
 499                {
 500                    "bindings": {
 501                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
 502                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
 503                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
 504                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 505                    }
 506                }
 507            ]
 508            "#,
 509            Some(
 510                r#"
 511            [
 512                {
 513                    "bindings": {
 514                        "cmd-1": ["vim::PushObject", { "around": false }],
 515                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
 516                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
 517                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 518                    }
 519                }
 520            ]
 521            "#,
 522            ),
 523        )
 524    }
 525
 526    #[test]
 527    fn test_replace_setting_name() {
 528        assert_migrate_settings(
 529            r#"
 530                {
 531                    "show_inline_completions_in_menu": true,
 532                    "show_inline_completions": true,
 533                    "inline_completions_disabled_in": ["string"],
 534                    "inline_completions": { "some" : "value" }
 535                }
 536            "#,
 537            Some(
 538                r#"
 539                {
 540                    "show_edit_predictions_in_menu": true,
 541                    "show_edit_predictions": true,
 542                    "edit_predictions_disabled_in": ["string"],
 543                    "edit_predictions": { "some" : "value" }
 544                }
 545            "#,
 546            ),
 547        )
 548    }
 549
 550    #[test]
 551    fn test_nested_string_replace_for_settings() {
 552        assert_migrate_settings(
 553            r#"
 554                {
 555                    "features": {
 556                        "inline_completion_provider": "zed"
 557                    },
 558                }
 559            "#,
 560            Some(
 561                r#"
 562                {
 563                    "features": {
 564                        "edit_prediction_provider": "zed"
 565                    },
 566                }
 567            "#,
 568            ),
 569        )
 570    }
 571
 572    #[test]
 573    fn test_replace_settings_in_languages() {
 574        assert_migrate_settings(
 575            r#"
 576                {
 577                    "languages": {
 578                        "Astro": {
 579                            "show_inline_completions": true
 580                        }
 581                    }
 582                }
 583            "#,
 584            Some(
 585                r#"
 586                {
 587                    "languages": {
 588                        "Astro": {
 589                            "show_edit_predictions": true
 590                        }
 591                    }
 592                }
 593            "#,
 594            ),
 595        )
 596    }
 597
 598    #[test]
 599    fn test_replace_settings_value() {
 600        assert_migrate_settings(
 601            r#"
 602                {
 603                    "scrollbar": {
 604                        "diagnostics": true
 605                    },
 606                    "chat_panel": {
 607                        "button": true
 608                    }
 609                }
 610            "#,
 611            Some(
 612                r#"
 613                {
 614                    "scrollbar": {
 615                        "diagnostics": "all"
 616                    },
 617                    "chat_panel": {
 618                        "button": "always"
 619                    }
 620                }
 621            "#,
 622            ),
 623        )
 624    }
 625
 626    #[test]
 627    fn test_replace_settings_name_and_value() {
 628        assert_migrate_settings(
 629            r#"
 630                {
 631                    "tabs": {
 632                        "always_show_close_button": true
 633                    }
 634                }
 635            "#,
 636            Some(
 637                r#"
 638                {
 639                    "tabs": {
 640                        "show_close_button": "always"
 641                    }
 642                }
 643            "#,
 644            ),
 645        )
 646    }
 647
 648    #[test]
 649    fn test_replace_bash_with_terminal_in_profiles() {
 650        assert_migrate_settings(
 651            r#"
 652                {
 653                    "assistant": {
 654                        "profiles": {
 655                            "custom": {
 656                                "name": "Custom",
 657                                "tools": {
 658                                    "bash": true,
 659                                    "diagnostics": true
 660                                }
 661                            }
 662                        }
 663                    }
 664                }
 665            "#,
 666            Some(
 667                r#"
 668                {
 669                    "agent": {
 670                        "profiles": {
 671                            "custom": {
 672                                "name": "Custom",
 673                                "tools": {
 674                                    "terminal": true,
 675                                    "diagnostics": true
 676                                }
 677                            }
 678                        }
 679                    }
 680                }
 681            "#,
 682            ),
 683        )
 684    }
 685
 686    #[test]
 687    fn test_replace_bash_false_with_terminal_in_profiles() {
 688        assert_migrate_settings(
 689            r#"
 690                {
 691                    "assistant": {
 692                        "profiles": {
 693                            "custom": {
 694                                "name": "Custom",
 695                                "tools": {
 696                                    "bash": false,
 697                                    "diagnostics": true
 698                                }
 699                            }
 700                        }
 701                    }
 702                }
 703            "#,
 704            Some(
 705                r#"
 706                {
 707                    "agent": {
 708                        "profiles": {
 709                            "custom": {
 710                                "name": "Custom",
 711                                "tools": {
 712                                    "terminal": false,
 713                                    "diagnostics": true
 714                                }
 715                            }
 716                        }
 717                    }
 718                }
 719            "#,
 720            ),
 721        )
 722    }
 723
 724    #[test]
 725    fn test_no_bash_in_profiles() {
 726        assert_migrate_settings(
 727            r#"
 728                {
 729                    "assistant": {
 730                        "profiles": {
 731                            "custom": {
 732                                "name": "Custom",
 733                                "tools": {
 734                                    "diagnostics": true,
 735                                    "find_path": true,
 736                                    "read_file": true
 737                                }
 738                            }
 739                        }
 740                    }
 741                }
 742            "#,
 743            Some(
 744                r#"
 745                {
 746                    "agent": {
 747                        "profiles": {
 748                            "custom": {
 749                                "name": "Custom",
 750                                "tools": {
 751                                    "diagnostics": true,
 752                                    "find_path": true,
 753                                    "read_file": true
 754                                }
 755                            }
 756                        }
 757                    }
 758                }
 759            "#,
 760            ),
 761        )
 762    }
 763
 764    #[test]
 765    fn test_rename_path_search_to_find_path() {
 766        assert_migrate_settings(
 767            r#"
 768                {
 769                    "assistant": {
 770                        "profiles": {
 771                            "default": {
 772                                "tools": {
 773                                    "path_search": true,
 774                                    "read_file": true
 775                                }
 776                            }
 777                        }
 778                    }
 779                }
 780            "#,
 781            Some(
 782                r#"
 783                {
 784                    "agent": {
 785                        "profiles": {
 786                            "default": {
 787                                "tools": {
 788                                    "find_path": true,
 789                                    "read_file": true
 790                                }
 791                            }
 792                        }
 793                    }
 794                }
 795            "#,
 796            ),
 797        );
 798    }
 799
 800    #[test]
 801    fn test_rename_assistant() {
 802        assert_migrate_settings(
 803            r#"{
 804                "assistant": {
 805                    "foo": "bar"
 806                },
 807                "edit_predictions": {
 808                    "enabled_in_assistant": false,
 809                }
 810            }"#,
 811            Some(
 812                r#"{
 813                "agent": {
 814                    "foo": "bar"
 815                },
 816                "edit_predictions": {
 817                    "enabled_in_text_threads": false,
 818                }
 819            }"#,
 820            ),
 821        );
 822    }
 823
 824    #[test]
 825    fn test_comment_duplicated_agent() {
 826        assert_migrate_settings(
 827            r#"{
 828                "agent": {
 829                    "name": "assistant-1",
 830                "model": "gpt-4", // weird formatting
 831                    "utf8": "привіт"
 832                },
 833                "something": "else",
 834                "agent": {
 835                    "name": "assistant-2",
 836                    "model": "gemini-pro"
 837                }
 838            }
 839        "#,
 840            Some(
 841                r#"{
 842                /* Duplicated key auto-commented: "agent": {
 843                    "name": "assistant-1",
 844                "model": "gpt-4", // weird formatting
 845                    "utf8": "привіт"
 846                }, */
 847                "something": "else",
 848                "agent": {
 849                    "name": "assistant-2",
 850                    "model": "gemini-pro"
 851                }
 852            }
 853        "#,
 854            ),
 855        );
 856    }
 857
 858    #[test]
 859    fn test_preferred_completion_mode_migration() {
 860        assert_migrate_settings(
 861            r#"{
 862                "agent": {
 863                    "preferred_completion_mode": "max",
 864                    "enabled": true
 865                }
 866            }"#,
 867            Some(
 868                r#"{
 869                "agent": {
 870                    "preferred_completion_mode": "burn",
 871                    "enabled": true
 872                }
 873            }"#,
 874            ),
 875        );
 876
 877        assert_migrate_settings(
 878            r#"{
 879                "agent": {
 880                    "preferred_completion_mode": "normal",
 881                    "enabled": true
 882                }
 883            }"#,
 884            None,
 885        );
 886
 887        assert_migrate_settings(
 888            r#"{
 889                "agent": {
 890                    "preferred_completion_mode": "burn",
 891                    "enabled": true
 892                }
 893            }"#,
 894            None,
 895        );
 896
 897        assert_migrate_settings(
 898            r#"{
 899                "other_section": {
 900                    "preferred_completion_mode": "max"
 901                },
 902                "agent": {
 903                    "preferred_completion_mode": "max"
 904                }
 905            }"#,
 906            Some(
 907                r#"{
 908                "other_section": {
 909                    "preferred_completion_mode": "max"
 910                },
 911                "agent": {
 912                    "preferred_completion_mode": "burn"
 913                }
 914            }"#,
 915            ),
 916        );
 917    }
 918
 919    #[test]
 920    fn test_mcp_settings_migration() {
 921        assert_migrate_settings_with_migrations(
 922            &[(
 923                migrations::m_2025_06_16::SETTINGS_PATTERNS,
 924                &SETTINGS_QUERY_2025_06_16,
 925            )],
 926            r#"{
 927    "context_servers": {
 928        "empty_server": {},
 929        "extension_server": {
 930            "settings": {
 931                "foo": "bar"
 932            }
 933        },
 934        "custom_server": {
 935            "command": {
 936                "path": "foo",
 937                "args": ["bar"],
 938                "env": {
 939                    "FOO": "BAR"
 940                }
 941            }
 942        },
 943        "invalid_server": {
 944            "command": {
 945                "path": "foo",
 946                "args": ["bar"],
 947                "env": {
 948                    "FOO": "BAR"
 949                }
 950            },
 951            "settings": {
 952                "foo": "bar"
 953            }
 954        },
 955        "empty_server2": {},
 956        "extension_server2": {
 957            "foo": "bar",
 958            "settings": {
 959                "foo": "bar"
 960            },
 961            "bar": "foo"
 962        },
 963        "custom_server2": {
 964            "foo": "bar",
 965            "command": {
 966                "path": "foo",
 967                "args": ["bar"],
 968                "env": {
 969                    "FOO": "BAR"
 970                }
 971            },
 972            "bar": "foo"
 973        },
 974        "invalid_server2": {
 975            "foo": "bar",
 976            "command": {
 977                "path": "foo",
 978                "args": ["bar"],
 979                "env": {
 980                    "FOO": "BAR"
 981                }
 982            },
 983            "bar": "foo",
 984            "settings": {
 985                "foo": "bar"
 986            }
 987        }
 988    }
 989}"#,
 990            Some(
 991                r#"{
 992    "context_servers": {
 993        "empty_server": {
 994            "source": "extension",
 995            "settings": {}
 996        },
 997        "extension_server": {
 998            "source": "extension",
 999            "settings": {
1000                "foo": "bar"
1001            }
1002        },
1003        "custom_server": {
1004            "source": "custom",
1005            "command": {
1006                "path": "foo",
1007                "args": ["bar"],
1008                "env": {
1009                    "FOO": "BAR"
1010                }
1011            }
1012        },
1013        "invalid_server": {
1014            "source": "custom",
1015            "command": {
1016                "path": "foo",
1017                "args": ["bar"],
1018                "env": {
1019                    "FOO": "BAR"
1020                }
1021            },
1022            "settings": {
1023                "foo": "bar"
1024            }
1025        },
1026        "empty_server2": {
1027            "source": "extension",
1028            "settings": {}
1029        },
1030        "extension_server2": {
1031            "source": "extension",
1032            "foo": "bar",
1033            "settings": {
1034                "foo": "bar"
1035            },
1036            "bar": "foo"
1037        },
1038        "custom_server2": {
1039            "source": "custom",
1040            "foo": "bar",
1041            "command": {
1042                "path": "foo",
1043                "args": ["bar"],
1044                "env": {
1045                    "FOO": "BAR"
1046                }
1047            },
1048            "bar": "foo"
1049        },
1050        "invalid_server2": {
1051            "source": "custom",
1052            "foo": "bar",
1053            "command": {
1054                "path": "foo",
1055                "args": ["bar"],
1056                "env": {
1057                    "FOO": "BAR"
1058                }
1059            },
1060            "bar": "foo",
1061            "settings": {
1062                "foo": "bar"
1063            }
1064        }
1065    }
1066}"#,
1067            ),
1068        );
1069    }
1070
1071    #[test]
1072    fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1073        let settings = r#"{
1074    "context_servers": {
1075        "empty_server": {
1076            "source": "extension",
1077            "settings": {}
1078        },
1079        "extension_server": {
1080            "source": "extension",
1081            "settings": {
1082                "foo": "bar"
1083            }
1084        },
1085        "custom_server": {
1086            "source": "custom",
1087            "command": {
1088                "path": "foo",
1089                "args": ["bar"],
1090                "env": {
1091                    "FOO": "BAR"
1092                }
1093            }
1094        },
1095        "invalid_server": {
1096            "source": "custom",
1097            "command": {
1098                "path": "foo",
1099                "args": ["bar"],
1100                "env": {
1101                    "FOO": "BAR"
1102                }
1103            },
1104            "settings": {
1105                "foo": "bar"
1106            }
1107        }
1108    }
1109}"#;
1110        assert_migrate_settings_with_migrations(
1111            &[(
1112                migrations::m_2025_06_16::SETTINGS_PATTERNS,
1113                &SETTINGS_QUERY_2025_06_16,
1114            )],
1115            settings,
1116            None,
1117        );
1118    }
1119
1120    #[test]
1121    fn test_remove_version_fields() {
1122        assert_migrate_settings(
1123            r#"{
1124    "language_models": {
1125        "anthropic": {
1126            "version": "1",
1127            "api_url": "https://api.anthropic.com"
1128        },
1129        "openai": {
1130            "version": "1",
1131            "api_url": "https://api.openai.com/v1"
1132        }
1133    },
1134    "agent": {
1135        "version": "2",
1136        "enabled": true,
1137        "preferred_completion_mode": "normal",
1138        "button": true,
1139        "dock": "right",
1140        "default_width": 640,
1141        "default_height": 320,
1142        "default_model": {
1143            "provider": "zed.dev",
1144            "model": "claude-sonnet-4"
1145        }
1146    }
1147}"#,
1148            Some(
1149                r#"{
1150    "language_models": {
1151        "anthropic": {
1152            "api_url": "https://api.anthropic.com"
1153        },
1154        "openai": {
1155            "api_url": "https://api.openai.com/v1"
1156        }
1157    },
1158    "agent": {
1159        "enabled": true,
1160        "preferred_completion_mode": "normal",
1161        "button": true,
1162        "dock": "right",
1163        "default_width": 640,
1164        "default_height": 320,
1165        "default_model": {
1166            "provider": "zed.dev",
1167            "model": "claude-sonnet-4"
1168        }
1169    }
1170}"#,
1171            ),
1172        );
1173
1174        // Test that version fields in other contexts are not removed
1175        assert_migrate_settings(
1176            r#"{
1177    "language_models": {
1178        "other_provider": {
1179            "version": "1",
1180            "api_url": "https://api.example.com"
1181        }
1182    },
1183    "other_section": {
1184        "version": "1"
1185    }
1186}"#,
1187            None,
1188        );
1189    }
1190
1191    #[test]
1192    fn test_flatten_context_server_command() {
1193        assert_migrate_settings(
1194            r#"{
1195    "context_servers": {
1196        "some-mcp-server": {
1197            "source": "custom",
1198            "command": {
1199                "path": "npx",
1200                "args": [
1201                    "-y",
1202                    "@supabase/mcp-server-supabase@latest",
1203                    "--read-only",
1204                    "--project-ref=<project-ref>"
1205                ],
1206                "env": {
1207                    "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1208                }
1209            }
1210        }
1211    }
1212}"#,
1213            Some(
1214                r#"{
1215    "context_servers": {
1216        "some-mcp-server": {
1217            "source": "custom",
1218            "command": "npx",
1219            "args": [
1220                "-y",
1221                "@supabase/mcp-server-supabase@latest",
1222                "--read-only",
1223                "--project-ref=<project-ref>"
1224            ],
1225            "env": {
1226                "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1227            }
1228        }
1229    }
1230}"#,
1231            ),
1232        );
1233
1234        // Test with additional keys in server object
1235        assert_migrate_settings(
1236            r#"{
1237    "context_servers": {
1238        "server-with-extras": {
1239            "source": "custom",
1240            "command": {
1241                "path": "/usr/bin/node",
1242                "args": ["server.js"]
1243            },
1244            "settings": {}
1245        }
1246    }
1247}"#,
1248            Some(
1249                r#"{
1250    "context_servers": {
1251        "server-with-extras": {
1252            "source": "custom",
1253            "command": "/usr/bin/node",
1254            "args": ["server.js"],
1255            "settings": {}
1256        }
1257    }
1258}"#,
1259            ),
1260        );
1261
1262        // Test command without args or env
1263        assert_migrate_settings(
1264            r#"{
1265    "context_servers": {
1266        "simple-server": {
1267            "source": "custom",
1268            "command": {
1269                "path": "simple-mcp-server"
1270            }
1271        }
1272    }
1273}"#,
1274            Some(
1275                r#"{
1276    "context_servers": {
1277        "simple-server": {
1278            "source": "custom",
1279            "command": "simple-mcp-server"
1280        }
1281    }
1282}"#,
1283            ),
1284        );
1285    }
1286
1287    #[test]
1288    fn test_flatten_code_action_formatters_basic_array() {
1289        assert_migrate_settings(
1290            &r#"{
1291                "formatter": [
1292                  {
1293                      "code_actions": {
1294                          "included-1": true,
1295                          "included-2": true,
1296                          "excluded": false,
1297                      }
1298                  }
1299                ]
1300            }"#
1301            .unindent(),
1302            Some(
1303                &r#"{
1304                "formatter": [
1305                  { "code_action": "included-1" },
1306                  { "code_action": "included-2" }
1307                ]
1308            }"#
1309                .unindent(),
1310            ),
1311        );
1312    }
1313
1314    #[test]
1315    fn test_flatten_code_action_formatters_basic_object() {
1316        assert_migrate_settings(
1317            &r#"{
1318                "formatter": {
1319                    "code_actions": {
1320                        "included-1": true,
1321                        "excluded": false,
1322                        "included-2": true
1323                    }
1324                }
1325            }"#
1326            .unindent(),
1327            Some(
1328                &r#"{
1329                    "formatter": [
1330                      { "code_action": "included-1" },
1331                      { "code_action": "included-2" }
1332                    ]
1333                }"#
1334                .unindent(),
1335            ),
1336        );
1337    }
1338
1339    #[test]
1340    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1341        assert_migrate_settings(
1342            r#"{
1343                "formatter": [
1344                  {
1345                      "code_actions": {
1346                          "included-1": true,
1347                          "included-2": true,
1348                          "excluded": false,
1349                      }
1350                  },
1351                  {
1352                    "language_server": "ruff"
1353                  },
1354                  {
1355                      "code_actions": {
1356                          "excluded": false,
1357                          "excluded-2": false,
1358                      }
1359                  }
1360                  // some comment
1361                  ,
1362                  {
1363                      "code_actions": {
1364                        "excluded": false,
1365                        "included-3": true,
1366                        "included-4": true,
1367                      }
1368                  },
1369                ]
1370            }"#,
1371            Some(
1372                r#"{
1373                "formatter": [
1374                  { "code_action": "included-1" },
1375                  { "code_action": "included-2" },
1376                  {
1377                    "language_server": "ruff"
1378                  },
1379                  { "code_action": "included-3" },
1380                  { "code_action": "included-4" },
1381                ]
1382            }"#,
1383            ),
1384        );
1385    }
1386
1387    #[test]
1388    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1389        assert_migrate_settings(
1390            &r#"{
1391                "languages": {
1392                    "Rust": {
1393                        "formatter": [
1394                          {
1395                              "code_actions": {
1396                                  "included-1": true,
1397                                  "included-2": true,
1398                                  "excluded": false,
1399                              }
1400                          },
1401                          {
1402                              "language_server": "ruff"
1403                          },
1404                          {
1405                              "code_actions": {
1406                                  "excluded": false,
1407                                  "excluded-2": false,
1408                              }
1409                          }
1410                          // some comment
1411                          ,
1412                          {
1413                              "code_actions": {
1414                                  "excluded": false,
1415                                  "included-3": true,
1416                                  "included-4": true,
1417                              }
1418                          },
1419                        ]
1420                    }
1421                }
1422            }"#
1423            .unindent(),
1424            Some(
1425                &r#"{
1426                    "languages": {
1427                        "Rust": {
1428                            "formatter": [
1429                              { "code_action": "included-1" },
1430                              { "code_action": "included-2" },
1431                              {
1432                                  "language_server": "ruff"
1433                              },
1434                              { "code_action": "included-3" },
1435                              { "code_action": "included-4" },
1436                            ]
1437                        }
1438                    }
1439                }"#
1440                .unindent(),
1441            ),
1442        );
1443    }
1444
1445    #[test]
1446    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1447     {
1448        assert_migrate_settings(
1449            &r#"{
1450                "formatter": {
1451                    "code_actions": {
1452                        "default-1": true,
1453                        "default-2": true,
1454                        "default-3": true,
1455                        "default-4": true,
1456                    }
1457                }
1458                "languages": {
1459                    "Rust": {
1460                        "formatter": [
1461                          {
1462                              "code_actions": {
1463                                  "included-1": true,
1464                                  "included-2": true,
1465                                  "excluded": false,
1466                              }
1467                          },
1468                          {
1469                              "language_server": "ruff"
1470                          },
1471                          {
1472                              "code_actions": {
1473                                  "excluded": false,
1474                                  "excluded-2": false,
1475                              }
1476                          }
1477                          // some comment
1478                          ,
1479                          {
1480                              "code_actions": {
1481                                  "excluded": false,
1482                                  "included-3": true,
1483                                  "included-4": true,
1484                              }
1485                          },
1486                        ]
1487                    },
1488                    "Python": {
1489                        "formatter": [
1490                          {
1491                              "language_server": "ruff"
1492                          },
1493                          {
1494                              "code_actions": {
1495                                  "excluded": false,
1496                                  "excluded-2": false,
1497                              }
1498                          }
1499                          // some comment
1500                          ,
1501                          {
1502                              "code_actions": {
1503                                  "excluded": false,
1504                                  "included-3": true,
1505                                  "included-4": true,
1506                              }
1507                          },
1508                        ]
1509                    }
1510                }
1511            }"#
1512            .unindent(),
1513            Some(
1514                &r#"{
1515                    "formatter": [
1516                      { "code_action": "default-1" },
1517                      { "code_action": "default-2" },
1518                      { "code_action": "default-3" },
1519                      { "code_action": "default-4" }
1520                    ]
1521                    "languages": {
1522                        "Rust": {
1523                            "formatter": [
1524                              { "code_action": "included-1" },
1525                              { "code_action": "included-2" },
1526                              {
1527                                  "language_server": "ruff"
1528                              },
1529                              { "code_action": "included-3" },
1530                              { "code_action": "included-4" },
1531                            ]
1532                        },
1533                        "Python": {
1534                            "formatter": [
1535                              {
1536                                  "language_server": "ruff"
1537                              },
1538                              { "code_action": "included-3" },
1539                              { "code_action": "included-4" },
1540                            ]
1541                        }
1542                    }
1543                }"#
1544                .unindent(),
1545            ),
1546        );
1547    }
1548
1549    #[test]
1550    fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1551        assert_migrate_settings(
1552            &r#"{
1553                "formatter": {
1554                    "code_actions": {
1555                        "default-1": true,
1556                        "default-2": true,
1557                        "default-3": true,
1558                        "default-4": true,
1559                    }
1560                },
1561                "format_on_save": [
1562                  {
1563                      "code_actions": {
1564                          "included-1": true,
1565                          "included-2": true,
1566                          "excluded": false,
1567                      }
1568                  },
1569                  {
1570                      "language_server": "ruff"
1571                  },
1572                  {
1573                      "code_actions": {
1574                          "excluded": false,
1575                          "excluded-2": false,
1576                      }
1577                  }
1578                  // some comment
1579                  ,
1580                  {
1581                      "code_actions": {
1582                          "excluded": false,
1583                          "included-3": true,
1584                          "included-4": true,
1585                      }
1586                  },
1587                ],
1588                "languages": {
1589                    "Rust": {
1590                        "format_on_save": "prettier",
1591                        "formatter": [
1592                          {
1593                              "code_actions": {
1594                                  "included-1": true,
1595                                  "included-2": true,
1596                                  "excluded": false,
1597                              }
1598                          },
1599                          {
1600                              "language_server": "ruff"
1601                          },
1602                          {
1603                              "code_actions": {
1604                                  "excluded": false,
1605                                  "excluded-2": false,
1606                              }
1607                          }
1608                          // some comment
1609                          ,
1610                          {
1611                              "code_actions": {
1612                                  "excluded": false,
1613                                  "included-3": true,
1614                                  "included-4": true,
1615                              }
1616                          },
1617                        ]
1618                    },
1619                    "Python": {
1620                        "format_on_save": {
1621                            "code_actions": {
1622                                "on-save-1": true,
1623                                "on-save-2": true,
1624                            }
1625                        },
1626                        "formatter": [
1627                          {
1628                              "language_server": "ruff"
1629                          },
1630                          {
1631                              "code_actions": {
1632                                  "excluded": false,
1633                                  "excluded-2": false,
1634                              }
1635                          }
1636                          // some comment
1637                          ,
1638                          {
1639                              "code_actions": {
1640                                  "excluded": false,
1641                                  "included-3": true,
1642                                  "included-4": true,
1643                              }
1644                          },
1645                        ]
1646                    }
1647                }
1648            }"#
1649            .unindent(),
1650            Some(
1651                &r#"{
1652                    "formatter": [
1653                      { "code_action": "default-1" },
1654                      { "code_action": "default-2" },
1655                      { "code_action": "default-3" },
1656                      { "code_action": "default-4" }
1657                    ],
1658                    "format_on_save": [
1659                      { "code_action": "included-1" },
1660                      { "code_action": "included-2" },
1661                      {
1662                          "language_server": "ruff"
1663                      },
1664                      { "code_action": "included-3" },
1665                      { "code_action": "included-4" },
1666                    ],
1667                    "languages": {
1668                        "Rust": {
1669                            "format_on_save": "prettier",
1670                            "formatter": [
1671                              { "code_action": "included-1" },
1672                              { "code_action": "included-2" },
1673                              {
1674                                  "language_server": "ruff"
1675                              },
1676                              { "code_action": "included-3" },
1677                              { "code_action": "included-4" },
1678                            ]
1679                        },
1680                        "Python": {
1681                            "format_on_save": [
1682                              { "code_action": "on-save-1" },
1683                              { "code_action": "on-save-2" }
1684                            ],
1685                            "formatter": [
1686                              {
1687                                  "language_server": "ruff"
1688                              },
1689                              { "code_action": "included-3" },
1690                              { "code_action": "included-4" },
1691                            ]
1692                        }
1693                    }
1694                }"#
1695                .unindent(),
1696            ),
1697        );
1698    }
1699}