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    run_migrations(text, migrations)
 169}
 170
 171pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
 172    migrate(
 173        text,
 174        &[(
 175            SETTINGS_NESTED_KEY_VALUE_PATTERN,
 176            migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
 177        )],
 178        &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
 179    )
 180}
 181
 182pub type MigrationPatterns = &'static [(
 183    &'static str,
 184    fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
 185)];
 186
 187macro_rules! define_query {
 188    ($var_name:ident, $patterns_path:path) => {
 189        static $var_name: LazyLock<Query> = LazyLock::new(|| {
 190            Query::new(
 191                &tree_sitter_json::LANGUAGE.into(),
 192                &$patterns_path
 193                    .iter()
 194                    .map(|pattern| pattern.0)
 195                    .collect::<String>(),
 196            )
 197            .unwrap()
 198        });
 199    };
 200}
 201
 202// keymap
 203define_query!(
 204    KEYMAP_QUERY_2025_01_29,
 205    migrations::m_2025_01_29::KEYMAP_PATTERNS
 206);
 207define_query!(
 208    KEYMAP_QUERY_2025_01_30,
 209    migrations::m_2025_01_30::KEYMAP_PATTERNS
 210);
 211define_query!(
 212    KEYMAP_QUERY_2025_03_03,
 213    migrations::m_2025_03_03::KEYMAP_PATTERNS
 214);
 215define_query!(
 216    KEYMAP_QUERY_2025_03_06,
 217    migrations::m_2025_03_06::KEYMAP_PATTERNS
 218);
 219define_query!(
 220    KEYMAP_QUERY_2025_04_15,
 221    migrations::m_2025_04_15::KEYMAP_PATTERNS
 222);
 223
 224// settings
 225define_query!(
 226    SETTINGS_QUERY_2025_01_02,
 227    migrations::m_2025_01_02::SETTINGS_PATTERNS
 228);
 229define_query!(
 230    SETTINGS_QUERY_2025_01_29,
 231    migrations::m_2025_01_29::SETTINGS_PATTERNS
 232);
 233define_query!(
 234    SETTINGS_QUERY_2025_01_30,
 235    migrations::m_2025_01_30::SETTINGS_PATTERNS
 236);
 237define_query!(
 238    SETTINGS_QUERY_2025_03_29,
 239    migrations::m_2025_03_29::SETTINGS_PATTERNS
 240);
 241define_query!(
 242    SETTINGS_QUERY_2025_04_15,
 243    migrations::m_2025_04_15::SETTINGS_PATTERNS
 244);
 245define_query!(
 246    SETTINGS_QUERY_2025_04_21,
 247    migrations::m_2025_04_21::SETTINGS_PATTERNS
 248);
 249define_query!(
 250    SETTINGS_QUERY_2025_04_23,
 251    migrations::m_2025_04_23::SETTINGS_PATTERNS
 252);
 253define_query!(
 254    SETTINGS_QUERY_2025_05_05,
 255    migrations::m_2025_05_05::SETTINGS_PATTERNS
 256);
 257define_query!(
 258    SETTINGS_QUERY_2025_05_08,
 259    migrations::m_2025_05_08::SETTINGS_PATTERNS
 260);
 261define_query!(
 262    SETTINGS_QUERY_2025_05_29,
 263    migrations::m_2025_05_29::SETTINGS_PATTERNS
 264);
 265define_query!(
 266    SETTINGS_QUERY_2025_06_16,
 267    migrations::m_2025_06_16::SETTINGS_PATTERNS
 268);
 269define_query!(
 270    SETTINGS_QUERY_2025_06_25,
 271    migrations::m_2025_06_25::SETTINGS_PATTERNS
 272);
 273define_query!(
 274    SETTINGS_QUERY_2025_06_27,
 275    migrations::m_2025_06_27::SETTINGS_PATTERNS
 276);
 277define_query!(
 278    SETTINGS_QUERY_2025_07_08,
 279    migrations::m_2025_07_08::SETTINGS_PATTERNS
 280);
 281
 282// custom query
 283static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 284    Query::new(
 285        &tree_sitter_json::LANGUAGE.into(),
 286        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 287    )
 288    .unwrap()
 289});
 290
 291#[cfg(test)]
 292mod tests {
 293    use super::*;
 294
 295    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 296        let migrated = migrate_keymap(input).unwrap();
 297        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 298    }
 299
 300    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 301        let migrated = migrate_settings(input).unwrap();
 302        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 303    }
 304
 305    fn assert_migrate_settings_with_migrations(
 306        migrations: &[(MigrationPatterns, &Query)],
 307        input: &str,
 308        output: Option<&str>,
 309    ) {
 310        let migrated = run_migrations(input, migrations).unwrap();
 311        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 312    }
 313
 314    #[test]
 315    fn test_replace_array_with_single_string() {
 316        assert_migrate_keymap(
 317            r#"
 318            [
 319                {
 320                    "bindings": {
 321                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 322                    }
 323                }
 324            ]
 325            "#,
 326            Some(
 327                r#"
 328            [
 329                {
 330                    "bindings": {
 331                        "cmd-1": "workspace::ActivatePaneUp"
 332                    }
 333                }
 334            ]
 335            "#,
 336            ),
 337        )
 338    }
 339
 340    #[test]
 341    fn test_replace_action_argument_object_with_single_value() {
 342        assert_migrate_keymap(
 343            r#"
 344            [
 345                {
 346                    "bindings": {
 347                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 348                    }
 349                }
 350            ]
 351            "#,
 352            Some(
 353                r#"
 354            [
 355                {
 356                    "bindings": {
 357                        "cmd-1": ["editor::FoldAtLevel", 1]
 358                    }
 359                }
 360            ]
 361            "#,
 362            ),
 363        )
 364    }
 365
 366    #[test]
 367    fn test_replace_action_argument_object_with_single_value_2() {
 368        assert_migrate_keymap(
 369            r#"
 370            [
 371                {
 372                    "bindings": {
 373                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 374                    }
 375                }
 376            ]
 377            "#,
 378            Some(
 379                r#"
 380            [
 381                {
 382                    "bindings": {
 383                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 384                    }
 385                }
 386            ]
 387            "#,
 388            ),
 389        )
 390    }
 391
 392    #[test]
 393    fn test_rename_string_action() {
 394        assert_migrate_keymap(
 395            r#"
 396                [
 397                    {
 398                        "bindings": {
 399                            "cmd-1": "inline_completion::ToggleMenu"
 400                        }
 401                    }
 402                ]
 403            "#,
 404            Some(
 405                r#"
 406                [
 407                    {
 408                        "bindings": {
 409                            "cmd-1": "edit_prediction::ToggleMenu"
 410                        }
 411                    }
 412                ]
 413            "#,
 414            ),
 415        )
 416    }
 417
 418    #[test]
 419    fn test_rename_context_key() {
 420        assert_migrate_keymap(
 421            r#"
 422                [
 423                    {
 424                        "context": "Editor && inline_completion && !showing_completions"
 425                    }
 426                ]
 427            "#,
 428            Some(
 429                r#"
 430                [
 431                    {
 432                        "context": "Editor && edit_prediction && !showing_completions"
 433                    }
 434                ]
 435            "#,
 436            ),
 437        )
 438    }
 439
 440    #[test]
 441    fn test_incremental_migrations() {
 442        // Here string transforms to array internally. Then, that array transforms back to string.
 443        assert_migrate_keymap(
 444            r#"
 445                [
 446                    {
 447                        "bindings": {
 448                            "ctrl-q": "editor::GoToHunk", // should remain same
 449                            "ctrl-w": "editor::GoToPrevHunk", // should rename
 450                            "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
 451                            "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
 452                        }
 453                    }
 454                ]
 455            "#,
 456            Some(
 457                r#"
 458                [
 459                    {
 460                        "bindings": {
 461                            "ctrl-q": "editor::GoToHunk", // should remain same
 462                            "ctrl-w": "editor::GoToPreviousHunk", // should rename
 463                            "ctrl-q": "editor::GoToHunk", // should transform
 464                            "ctrl-w": "editor::GoToPreviousHunk" // should transform
 465                        }
 466                    }
 467                ]
 468            "#,
 469            ),
 470        )
 471    }
 472
 473    #[test]
 474    fn test_action_argument_snake_case() {
 475        // First performs transformations, then replacements
 476        assert_migrate_keymap(
 477            r#"
 478            [
 479                {
 480                    "bindings": {
 481                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
 482                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
 483                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
 484                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 485                    }
 486                }
 487            ]
 488            "#,
 489            Some(
 490                r#"
 491            [
 492                {
 493                    "bindings": {
 494                        "cmd-1": ["vim::PushObject", { "around": false }],
 495                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
 496                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
 497                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 498                    }
 499                }
 500            ]
 501            "#,
 502            ),
 503        )
 504    }
 505
 506    #[test]
 507    fn test_replace_setting_name() {
 508        assert_migrate_settings(
 509            r#"
 510                {
 511                    "show_inline_completions_in_menu": true,
 512                    "show_inline_completions": true,
 513                    "inline_completions_disabled_in": ["string"],
 514                    "inline_completions": { "some" : "value" }
 515                }
 516            "#,
 517            Some(
 518                r#"
 519                {
 520                    "show_edit_predictions_in_menu": true,
 521                    "show_edit_predictions": true,
 522                    "edit_predictions_disabled_in": ["string"],
 523                    "edit_predictions": { "some" : "value" }
 524                }
 525            "#,
 526            ),
 527        )
 528    }
 529
 530    #[test]
 531    fn test_nested_string_replace_for_settings() {
 532        assert_migrate_settings(
 533            r#"
 534                {
 535                    "features": {
 536                        "inline_completion_provider": "zed"
 537                    },
 538                }
 539            "#,
 540            Some(
 541                r#"
 542                {
 543                    "features": {
 544                        "edit_prediction_provider": "zed"
 545                    },
 546                }
 547            "#,
 548            ),
 549        )
 550    }
 551
 552    #[test]
 553    fn test_replace_settings_in_languages() {
 554        assert_migrate_settings(
 555            r#"
 556                {
 557                    "languages": {
 558                        "Astro": {
 559                            "show_inline_completions": true
 560                        }
 561                    }
 562                }
 563            "#,
 564            Some(
 565                r#"
 566                {
 567                    "languages": {
 568                        "Astro": {
 569                            "show_edit_predictions": true
 570                        }
 571                    }
 572                }
 573            "#,
 574            ),
 575        )
 576    }
 577
 578    #[test]
 579    fn test_replace_settings_value() {
 580        assert_migrate_settings(
 581            r#"
 582                {
 583                    "scrollbar": {
 584                        "diagnostics": true
 585                    },
 586                    "chat_panel": {
 587                        "button": true
 588                    }
 589                }
 590            "#,
 591            Some(
 592                r#"
 593                {
 594                    "scrollbar": {
 595                        "diagnostics": "all"
 596                    },
 597                    "chat_panel": {
 598                        "button": "always"
 599                    }
 600                }
 601            "#,
 602            ),
 603        )
 604    }
 605
 606    #[test]
 607    fn test_replace_settings_name_and_value() {
 608        assert_migrate_settings(
 609            r#"
 610                {
 611                    "tabs": {
 612                        "always_show_close_button": true
 613                    }
 614                }
 615            "#,
 616            Some(
 617                r#"
 618                {
 619                    "tabs": {
 620                        "show_close_button": "always"
 621                    }
 622                }
 623            "#,
 624            ),
 625        )
 626    }
 627
 628    #[test]
 629    fn test_replace_bash_with_terminal_in_profiles() {
 630        assert_migrate_settings(
 631            r#"
 632                {
 633                    "assistant": {
 634                        "profiles": {
 635                            "custom": {
 636                                "name": "Custom",
 637                                "tools": {
 638                                    "bash": true,
 639                                    "diagnostics": true
 640                                }
 641                            }
 642                        }
 643                    }
 644                }
 645            "#,
 646            Some(
 647                r#"
 648                {
 649                    "agent": {
 650                        "profiles": {
 651                            "custom": {
 652                                "name": "Custom",
 653                                "tools": {
 654                                    "terminal": true,
 655                                    "diagnostics": true
 656                                }
 657                            }
 658                        }
 659                    }
 660                }
 661            "#,
 662            ),
 663        )
 664    }
 665
 666    #[test]
 667    fn test_replace_bash_false_with_terminal_in_profiles() {
 668        assert_migrate_settings(
 669            r#"
 670                {
 671                    "assistant": {
 672                        "profiles": {
 673                            "custom": {
 674                                "name": "Custom",
 675                                "tools": {
 676                                    "bash": false,
 677                                    "diagnostics": true
 678                                }
 679                            }
 680                        }
 681                    }
 682                }
 683            "#,
 684            Some(
 685                r#"
 686                {
 687                    "agent": {
 688                        "profiles": {
 689                            "custom": {
 690                                "name": "Custom",
 691                                "tools": {
 692                                    "terminal": false,
 693                                    "diagnostics": true
 694                                }
 695                            }
 696                        }
 697                    }
 698                }
 699            "#,
 700            ),
 701        )
 702    }
 703
 704    #[test]
 705    fn test_no_bash_in_profiles() {
 706        assert_migrate_settings(
 707            r#"
 708                {
 709                    "assistant": {
 710                        "profiles": {
 711                            "custom": {
 712                                "name": "Custom",
 713                                "tools": {
 714                                    "diagnostics": true,
 715                                    "find_path": true,
 716                                    "read_file": true
 717                                }
 718                            }
 719                        }
 720                    }
 721                }
 722            "#,
 723            Some(
 724                r#"
 725                {
 726                    "agent": {
 727                        "profiles": {
 728                            "custom": {
 729                                "name": "Custom",
 730                                "tools": {
 731                                    "diagnostics": true,
 732                                    "find_path": true,
 733                                    "read_file": true
 734                                }
 735                            }
 736                        }
 737                    }
 738                }
 739            "#,
 740            ),
 741        )
 742    }
 743
 744    #[test]
 745    fn test_rename_path_search_to_find_path() {
 746        assert_migrate_settings(
 747            r#"
 748                {
 749                    "assistant": {
 750                        "profiles": {
 751                            "default": {
 752                                "tools": {
 753                                    "path_search": true,
 754                                    "read_file": true
 755                                }
 756                            }
 757                        }
 758                    }
 759                }
 760            "#,
 761            Some(
 762                r#"
 763                {
 764                    "agent": {
 765                        "profiles": {
 766                            "default": {
 767                                "tools": {
 768                                    "find_path": true,
 769                                    "read_file": true
 770                                }
 771                            }
 772                        }
 773                    }
 774                }
 775            "#,
 776            ),
 777        );
 778    }
 779
 780    #[test]
 781    fn test_rename_assistant() {
 782        assert_migrate_settings(
 783            r#"{
 784                "assistant": {
 785                    "foo": "bar"
 786                },
 787                "edit_predictions": {
 788                    "enabled_in_assistant": false,
 789                }
 790            }"#,
 791            Some(
 792                r#"{
 793                "agent": {
 794                    "foo": "bar"
 795                },
 796                "edit_predictions": {
 797                    "enabled_in_text_threads": false,
 798                }
 799            }"#,
 800            ),
 801        );
 802    }
 803
 804    #[test]
 805    fn test_comment_duplicated_agent() {
 806        assert_migrate_settings(
 807            r#"{
 808                "agent": {
 809                    "name": "assistant-1",
 810                "model": "gpt-4", // weird formatting
 811                    "utf8": "привіт"
 812                },
 813                "something": "else",
 814                "agent": {
 815                    "name": "assistant-2",
 816                    "model": "gemini-pro"
 817                }
 818            }
 819        "#,
 820            Some(
 821                r#"{
 822                /* Duplicated key auto-commented: "agent": {
 823                    "name": "assistant-1",
 824                "model": "gpt-4", // weird formatting
 825                    "utf8": "привіт"
 826                }, */
 827                "something": "else",
 828                "agent": {
 829                    "name": "assistant-2",
 830                    "model": "gemini-pro"
 831                }
 832            }
 833        "#,
 834            ),
 835        );
 836    }
 837
 838    #[test]
 839    fn test_preferred_completion_mode_migration() {
 840        assert_migrate_settings(
 841            r#"{
 842                "agent": {
 843                    "preferred_completion_mode": "max",
 844                    "enabled": true
 845                }
 846            }"#,
 847            Some(
 848                r#"{
 849                "agent": {
 850                    "preferred_completion_mode": "burn",
 851                    "enabled": true
 852                }
 853            }"#,
 854            ),
 855        );
 856
 857        assert_migrate_settings(
 858            r#"{
 859                "agent": {
 860                    "preferred_completion_mode": "normal",
 861                    "enabled": true
 862                }
 863            }"#,
 864            None,
 865        );
 866
 867        assert_migrate_settings(
 868            r#"{
 869                "agent": {
 870                    "preferred_completion_mode": "burn",
 871                    "enabled": true
 872                }
 873            }"#,
 874            None,
 875        );
 876
 877        assert_migrate_settings(
 878            r#"{
 879                "other_section": {
 880                    "preferred_completion_mode": "max"
 881                },
 882                "agent": {
 883                    "preferred_completion_mode": "max"
 884                }
 885            }"#,
 886            Some(
 887                r#"{
 888                "other_section": {
 889                    "preferred_completion_mode": "max"
 890                },
 891                "agent": {
 892                    "preferred_completion_mode": "burn"
 893                }
 894            }"#,
 895            ),
 896        );
 897    }
 898
 899    #[test]
 900    fn test_mcp_settings_migration() {
 901        assert_migrate_settings_with_migrations(
 902            &[(
 903                migrations::m_2025_06_16::SETTINGS_PATTERNS,
 904                &SETTINGS_QUERY_2025_06_16,
 905            )],
 906            r#"{
 907    "context_servers": {
 908        "empty_server": {},
 909        "extension_server": {
 910            "settings": {
 911                "foo": "bar"
 912            }
 913        },
 914        "custom_server": {
 915            "command": {
 916                "path": "foo",
 917                "args": ["bar"],
 918                "env": {
 919                    "FOO": "BAR"
 920                }
 921            }
 922        },
 923        "invalid_server": {
 924            "command": {
 925                "path": "foo",
 926                "args": ["bar"],
 927                "env": {
 928                    "FOO": "BAR"
 929                }
 930            },
 931            "settings": {
 932                "foo": "bar"
 933            }
 934        },
 935        "empty_server2": {},
 936        "extension_server2": {
 937            "foo": "bar",
 938            "settings": {
 939                "foo": "bar"
 940            },
 941            "bar": "foo"
 942        },
 943        "custom_server2": {
 944            "foo": "bar",
 945            "command": {
 946                "path": "foo",
 947                "args": ["bar"],
 948                "env": {
 949                    "FOO": "BAR"
 950                }
 951            },
 952            "bar": "foo"
 953        },
 954        "invalid_server2": {
 955            "foo": "bar",
 956            "command": {
 957                "path": "foo",
 958                "args": ["bar"],
 959                "env": {
 960                    "FOO": "BAR"
 961                }
 962            },
 963            "bar": "foo",
 964            "settings": {
 965                "foo": "bar"
 966            }
 967        }
 968    }
 969}"#,
 970            Some(
 971                r#"{
 972    "context_servers": {
 973        "empty_server": {
 974            "source": "extension",
 975            "settings": {}
 976        },
 977        "extension_server": {
 978            "source": "extension",
 979            "settings": {
 980                "foo": "bar"
 981            }
 982        },
 983        "custom_server": {
 984            "source": "custom",
 985            "command": {
 986                "path": "foo",
 987                "args": ["bar"],
 988                "env": {
 989                    "FOO": "BAR"
 990                }
 991            }
 992        },
 993        "invalid_server": {
 994            "source": "custom",
 995            "command": {
 996                "path": "foo",
 997                "args": ["bar"],
 998                "env": {
 999                    "FOO": "BAR"
1000                }
1001            },
1002            "settings": {
1003                "foo": "bar"
1004            }
1005        },
1006        "empty_server2": {
1007            "source": "extension",
1008            "settings": {}
1009        },
1010        "extension_server2": {
1011            "source": "extension",
1012            "foo": "bar",
1013            "settings": {
1014                "foo": "bar"
1015            },
1016            "bar": "foo"
1017        },
1018        "custom_server2": {
1019            "source": "custom",
1020            "foo": "bar",
1021            "command": {
1022                "path": "foo",
1023                "args": ["bar"],
1024                "env": {
1025                    "FOO": "BAR"
1026                }
1027            },
1028            "bar": "foo"
1029        },
1030        "invalid_server2": {
1031            "source": "custom",
1032            "foo": "bar",
1033            "command": {
1034                "path": "foo",
1035                "args": ["bar"],
1036                "env": {
1037                    "FOO": "BAR"
1038                }
1039            },
1040            "bar": "foo",
1041            "settings": {
1042                "foo": "bar"
1043            }
1044        }
1045    }
1046}"#,
1047            ),
1048        );
1049    }
1050
1051    #[test]
1052    fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1053        let settings = r#"{
1054    "context_servers": {
1055        "empty_server": {
1056            "source": "extension",
1057            "settings": {}
1058        },
1059        "extension_server": {
1060            "source": "extension",
1061            "settings": {
1062                "foo": "bar"
1063            }
1064        },
1065        "custom_server": {
1066            "source": "custom",
1067            "command": {
1068                "path": "foo",
1069                "args": ["bar"],
1070                "env": {
1071                    "FOO": "BAR"
1072                }
1073            }
1074        },
1075        "invalid_server": {
1076            "source": "custom",
1077            "command": {
1078                "path": "foo",
1079                "args": ["bar"],
1080                "env": {
1081                    "FOO": "BAR"
1082                }
1083            },
1084            "settings": {
1085                "foo": "bar"
1086            }
1087        }
1088    }
1089}"#;
1090        assert_migrate_settings_with_migrations(
1091            &[(
1092                migrations::m_2025_06_16::SETTINGS_PATTERNS,
1093                &SETTINGS_QUERY_2025_06_16,
1094            )],
1095            settings,
1096            None,
1097        );
1098    }
1099
1100    #[test]
1101    fn test_remove_version_fields() {
1102        assert_migrate_settings(
1103            r#"{
1104    "language_models": {
1105        "anthropic": {
1106            "version": "1",
1107            "api_url": "https://api.anthropic.com"
1108        },
1109        "openai": {
1110            "version": "1",
1111            "api_url": "https://api.openai.com/v1"
1112        }
1113    },
1114    "agent": {
1115        "version": "2",
1116        "enabled": true,
1117        "preferred_completion_mode": "normal",
1118        "button": true,
1119        "dock": "right",
1120        "default_width": 640,
1121        "default_height": 320,
1122        "default_model": {
1123            "provider": "zed.dev",
1124            "model": "claude-sonnet-4"
1125        }
1126    }
1127}"#,
1128            Some(
1129                r#"{
1130    "language_models": {
1131        "anthropic": {
1132            "api_url": "https://api.anthropic.com"
1133        },
1134        "openai": {
1135            "api_url": "https://api.openai.com/v1"
1136        }
1137    },
1138    "agent": {
1139        "enabled": true,
1140        "preferred_completion_mode": "normal",
1141        "button": true,
1142        "dock": "right",
1143        "default_width": 640,
1144        "default_height": 320,
1145        "default_model": {
1146            "provider": "zed.dev",
1147            "model": "claude-sonnet-4"
1148        }
1149    }
1150}"#,
1151            ),
1152        );
1153
1154        // Test that version fields in other contexts are not removed
1155        assert_migrate_settings(
1156            r#"{
1157    "language_models": {
1158        "other_provider": {
1159            "version": "1",
1160            "api_url": "https://api.example.com"
1161        }
1162    },
1163    "other_section": {
1164        "version": "1"
1165    }
1166}"#,
1167            None,
1168        );
1169    }
1170
1171    #[test]
1172    fn test_flatten_context_server_command() {
1173        assert_migrate_settings(
1174            r#"{
1175    "context_servers": {
1176        "some-mcp-server": {
1177            "source": "custom",
1178            "command": {
1179                "path": "npx",
1180                "args": [
1181                    "-y",
1182                    "@supabase/mcp-server-supabase@latest",
1183                    "--read-only",
1184                    "--project-ref=<project-ref>"
1185                ],
1186                "env": {
1187                    "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1188                }
1189            }
1190        }
1191    }
1192}"#,
1193            Some(
1194                r#"{
1195    "context_servers": {
1196        "some-mcp-server": {
1197            "source": "custom",
1198            "command": "npx",
1199            "args": [
1200                "-y",
1201                "@supabase/mcp-server-supabase@latest",
1202                "--read-only",
1203                "--project-ref=<project-ref>"
1204            ],
1205            "env": {
1206                "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1207            }
1208        }
1209    }
1210}"#,
1211            ),
1212        );
1213
1214        // Test with additional keys in server object
1215        assert_migrate_settings(
1216            r#"{
1217    "context_servers": {
1218        "server-with-extras": {
1219            "source": "custom",
1220            "command": {
1221                "path": "/usr/bin/node",
1222                "args": ["server.js"]
1223            },
1224            "settings": {}
1225        }
1226    }
1227}"#,
1228            Some(
1229                r#"{
1230    "context_servers": {
1231        "server-with-extras": {
1232            "source": "custom",
1233            "command": "/usr/bin/node",
1234            "args": ["server.js"],
1235            "settings": {}
1236        }
1237    }
1238}"#,
1239            ),
1240        );
1241
1242        // Test command without args or env
1243        assert_migrate_settings(
1244            r#"{
1245    "context_servers": {
1246        "simple-server": {
1247            "source": "custom",
1248            "command": {
1249                "path": "simple-mcp-server"
1250            }
1251        }
1252    }
1253}"#,
1254            Some(
1255                r#"{
1256    "context_servers": {
1257        "simple-server": {
1258            "source": "custom",
1259            "command": "simple-mcp-server"
1260        }
1261    }
1262}"#,
1263            ),
1264        );
1265    }
1266}