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