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 settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text};
  19use std::{cmp::Reverse, ops::Range, sync::LazyLock};
  20use streaming_iterator::StreamingIterator;
  21use tree_sitter::{Query, QueryMatch};
  22
  23use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
  24
  25mod migrations;
  26mod patterns;
  27
  28fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
  29    let mut parser = tree_sitter::Parser::new();
  30    parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
  31    let syntax_tree = parser
  32        .parse(text, None)
  33        .context("failed to parse settings")?;
  34
  35    let mut cursor = tree_sitter::QueryCursor::new();
  36    let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
  37
  38    let mut edits = vec![];
  39    while let Some(mat) = matches.next() {
  40        if let Some((_, callback)) = patterns.get(mat.pattern_index) {
  41            edits.extend(callback(text, mat, query));
  42        }
  43    }
  44
  45    edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
  46    edits.dedup_by(|(range_b, _), (range_a, _)| {
  47        range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
  48    });
  49
  50    if edits.is_empty() {
  51        Ok(None)
  52    } else {
  53        let mut new_text = text.to_string();
  54        for (range, replacement) in edits.iter().rev() {
  55            new_text.replace_range(range.clone(), replacement);
  56        }
  57        if new_text == text {
  58            log::error!(
  59                "Edits computed for configuration migration do not cause a change: {:?}",
  60                edits
  61            );
  62            Ok(None)
  63        } else {
  64            Ok(Some(new_text))
  65        }
  66    }
  67}
  68
  69/// Runs the provided migrations on the given text.
  70/// Will automatically return `Ok(None)` if there's no content to migrate.
  71fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<String>> {
  72    if text.is_empty() {
  73        return Ok(None);
  74    }
  75
  76    let mut current_text = text.to_string();
  77    let mut result: Option<String> = None;
  78    let json_indent_size = infer_json_indent_size(&current_text);
  79    for migration in migrations.iter() {
  80        let migrated_text = match migration {
  81            MigrationType::TreeSitter(patterns, query) => migrate(&current_text, patterns, query)?,
  82            MigrationType::Json(callback) => {
  83                if current_text.trim().is_empty() {
  84                    return Ok(None);
  85                }
  86                let old_content: serde_json_lenient::Value =
  87                    parse_json_with_comments(&current_text)?;
  88                let old_value = serde_json::to_value(&old_content).unwrap();
  89                let mut new_value = old_value.clone();
  90                callback(&mut new_value)?;
  91                if new_value != old_value {
  92                    let mut current = current_text.clone();
  93                    let mut edits = vec![];
  94                    update_value_in_json_text(
  95                        &mut current,
  96                        &mut vec![],
  97                        json_indent_size,
  98                        &old_value,
  99                        &new_value,
 100                        &mut edits,
 101                    );
 102                    let mut migrated_text = current_text.clone();
 103                    for (range, replacement) in edits.into_iter() {
 104                        migrated_text.replace_range(range, &replacement);
 105                    }
 106                    Some(migrated_text)
 107                } else {
 108                    None
 109                }
 110            }
 111        };
 112        if let Some(migrated_text) = migrated_text {
 113            current_text = migrated_text.clone();
 114            result = Some(migrated_text);
 115        }
 116    }
 117    Ok(result.filter(|new_text| text != new_text))
 118}
 119
 120pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
 121    let migrations: &[MigrationType] = &[
 122        MigrationType::TreeSitter(
 123            migrations::m_2025_01_29::KEYMAP_PATTERNS,
 124            &KEYMAP_QUERY_2025_01_29,
 125        ),
 126        MigrationType::TreeSitter(
 127            migrations::m_2025_01_30::KEYMAP_PATTERNS,
 128            &KEYMAP_QUERY_2025_01_30,
 129        ),
 130        MigrationType::TreeSitter(
 131            migrations::m_2025_03_03::KEYMAP_PATTERNS,
 132            &KEYMAP_QUERY_2025_03_03,
 133        ),
 134        MigrationType::TreeSitter(
 135            migrations::m_2025_03_06::KEYMAP_PATTERNS,
 136            &KEYMAP_QUERY_2025_03_06,
 137        ),
 138        MigrationType::TreeSitter(
 139            migrations::m_2025_04_15::KEYMAP_PATTERNS,
 140            &KEYMAP_QUERY_2025_04_15,
 141        ),
 142        MigrationType::TreeSitter(
 143            migrations::m_2025_12_08::KEYMAP_PATTERNS,
 144            &KEYMAP_QUERY_2025_12_08,
 145        ),
 146    ];
 147    run_migrations(text, migrations)
 148}
 149
 150enum MigrationType<'a> {
 151    TreeSitter(MigrationPatterns, &'a Query),
 152    Json(fn(&mut serde_json::Value) -> Result<()>),
 153}
 154
 155pub fn migrate_settings(text: &str) -> Result<Option<String>> {
 156    let migrations: &[MigrationType] = &[
 157        MigrationType::TreeSitter(
 158            migrations::m_2025_01_02::SETTINGS_PATTERNS,
 159            &SETTINGS_QUERY_2025_01_02,
 160        ),
 161        MigrationType::TreeSitter(
 162            migrations::m_2025_01_29::SETTINGS_PATTERNS,
 163            &SETTINGS_QUERY_2025_01_29,
 164        ),
 165        MigrationType::TreeSitter(
 166            migrations::m_2025_01_30::SETTINGS_PATTERNS,
 167            &SETTINGS_QUERY_2025_01_30,
 168        ),
 169        MigrationType::TreeSitter(
 170            migrations::m_2025_03_29::SETTINGS_PATTERNS,
 171            &SETTINGS_QUERY_2025_03_29,
 172        ),
 173        MigrationType::TreeSitter(
 174            migrations::m_2025_04_15::SETTINGS_PATTERNS,
 175            &SETTINGS_QUERY_2025_04_15,
 176        ),
 177        MigrationType::TreeSitter(
 178            migrations::m_2025_04_21::SETTINGS_PATTERNS,
 179            &SETTINGS_QUERY_2025_04_21,
 180        ),
 181        MigrationType::TreeSitter(
 182            migrations::m_2025_04_23::SETTINGS_PATTERNS,
 183            &SETTINGS_QUERY_2025_04_23,
 184        ),
 185        MigrationType::TreeSitter(
 186            migrations::m_2025_05_05::SETTINGS_PATTERNS,
 187            &SETTINGS_QUERY_2025_05_05,
 188        ),
 189        MigrationType::TreeSitter(
 190            migrations::m_2025_05_08::SETTINGS_PATTERNS,
 191            &SETTINGS_QUERY_2025_05_08,
 192        ),
 193        MigrationType::TreeSitter(
 194            migrations::m_2025_06_16::SETTINGS_PATTERNS,
 195            &SETTINGS_QUERY_2025_06_16,
 196        ),
 197        MigrationType::TreeSitter(
 198            migrations::m_2025_06_25::SETTINGS_PATTERNS,
 199            &SETTINGS_QUERY_2025_06_25,
 200        ),
 201        MigrationType::TreeSitter(
 202            migrations::m_2025_06_27::SETTINGS_PATTERNS,
 203            &SETTINGS_QUERY_2025_06_27,
 204        ),
 205        MigrationType::TreeSitter(
 206            migrations::m_2025_07_08::SETTINGS_PATTERNS,
 207            &SETTINGS_QUERY_2025_07_08,
 208        ),
 209        MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters),
 210        MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save),
 211        MigrationType::TreeSitter(
 212            migrations::m_2025_10_03::SETTINGS_PATTERNS,
 213            &SETTINGS_QUERY_2025_10_03,
 214        ),
 215        MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
 216        MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum),
 217        MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum),
 218        MigrationType::TreeSitter(
 219            migrations::m_2025_11_12::SETTINGS_PATTERNS,
 220            &SETTINGS_QUERY_2025_11_12,
 221        ),
 222        MigrationType::TreeSitter(
 223            migrations::m_2025_12_01::SETTINGS_PATTERNS,
 224            &SETTINGS_QUERY_2025_12_01,
 225        ),
 226        MigrationType::TreeSitter(
 227            migrations::m_2025_11_20::SETTINGS_PATTERNS,
 228            &SETTINGS_QUERY_2025_11_20,
 229        ),
 230        MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source),
 231        MigrationType::TreeSitter(
 232            migrations::m_2025_12_15::SETTINGS_PATTERNS,
 233            &SETTINGS_QUERY_2025_12_15,
 234        ),
 235        MigrationType::Json(
 236            migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
 237        ),
 238        MigrationType::Json(migrations::m_2026_02_03::migrate_experimental_sweep_mercury),
 239    ];
 240    run_migrations(text, migrations)
 241}
 242
 243pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
 244    migrate(
 245        text,
 246        &[(
 247            SETTINGS_NESTED_KEY_VALUE_PATTERN,
 248            migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
 249        )],
 250        &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
 251    )
 252}
 253
 254pub type MigrationPatterns = &'static [(
 255    &'static str,
 256    fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
 257)];
 258
 259macro_rules! define_query {
 260    ($var_name:ident, $patterns_path:path) => {
 261        static $var_name: LazyLock<Query> = LazyLock::new(|| {
 262            Query::new(
 263                &tree_sitter_json::LANGUAGE.into(),
 264                &$patterns_path
 265                    .iter()
 266                    .map(|pattern| pattern.0)
 267                    .collect::<String>(),
 268            )
 269            .unwrap()
 270        });
 271    };
 272}
 273
 274// keymap
 275define_query!(
 276    KEYMAP_QUERY_2025_01_29,
 277    migrations::m_2025_01_29::KEYMAP_PATTERNS
 278);
 279define_query!(
 280    KEYMAP_QUERY_2025_01_30,
 281    migrations::m_2025_01_30::KEYMAP_PATTERNS
 282);
 283define_query!(
 284    KEYMAP_QUERY_2025_03_03,
 285    migrations::m_2025_03_03::KEYMAP_PATTERNS
 286);
 287define_query!(
 288    KEYMAP_QUERY_2025_03_06,
 289    migrations::m_2025_03_06::KEYMAP_PATTERNS
 290);
 291define_query!(
 292    KEYMAP_QUERY_2025_04_15,
 293    migrations::m_2025_04_15::KEYMAP_PATTERNS
 294);
 295
 296// settings
 297define_query!(
 298    SETTINGS_QUERY_2025_01_02,
 299    migrations::m_2025_01_02::SETTINGS_PATTERNS
 300);
 301define_query!(
 302    SETTINGS_QUERY_2025_01_29,
 303    migrations::m_2025_01_29::SETTINGS_PATTERNS
 304);
 305define_query!(
 306    SETTINGS_QUERY_2025_01_30,
 307    migrations::m_2025_01_30::SETTINGS_PATTERNS
 308);
 309define_query!(
 310    SETTINGS_QUERY_2025_03_29,
 311    migrations::m_2025_03_29::SETTINGS_PATTERNS
 312);
 313define_query!(
 314    SETTINGS_QUERY_2025_04_15,
 315    migrations::m_2025_04_15::SETTINGS_PATTERNS
 316);
 317define_query!(
 318    SETTINGS_QUERY_2025_04_21,
 319    migrations::m_2025_04_21::SETTINGS_PATTERNS
 320);
 321define_query!(
 322    SETTINGS_QUERY_2025_04_23,
 323    migrations::m_2025_04_23::SETTINGS_PATTERNS
 324);
 325define_query!(
 326    SETTINGS_QUERY_2025_05_05,
 327    migrations::m_2025_05_05::SETTINGS_PATTERNS
 328);
 329define_query!(
 330    SETTINGS_QUERY_2025_05_08,
 331    migrations::m_2025_05_08::SETTINGS_PATTERNS
 332);
 333define_query!(
 334    SETTINGS_QUERY_2025_06_16,
 335    migrations::m_2025_06_16::SETTINGS_PATTERNS
 336);
 337define_query!(
 338    SETTINGS_QUERY_2025_06_25,
 339    migrations::m_2025_06_25::SETTINGS_PATTERNS
 340);
 341define_query!(
 342    SETTINGS_QUERY_2025_06_27,
 343    migrations::m_2025_06_27::SETTINGS_PATTERNS
 344);
 345define_query!(
 346    SETTINGS_QUERY_2025_07_08,
 347    migrations::m_2025_07_08::SETTINGS_PATTERNS
 348);
 349define_query!(
 350    SETTINGS_QUERY_2025_10_03,
 351    migrations::m_2025_10_03::SETTINGS_PATTERNS
 352);
 353define_query!(
 354    SETTINGS_QUERY_2025_11_12,
 355    migrations::m_2025_11_12::SETTINGS_PATTERNS
 356);
 357define_query!(
 358    SETTINGS_QUERY_2025_12_01,
 359    migrations::m_2025_12_01::SETTINGS_PATTERNS
 360);
 361define_query!(
 362    SETTINGS_QUERY_2025_11_20,
 363    migrations::m_2025_11_20::SETTINGS_PATTERNS
 364);
 365define_query!(
 366    KEYMAP_QUERY_2025_12_08,
 367    migrations::m_2025_12_08::KEYMAP_PATTERNS
 368);
 369define_query!(
 370    SETTINGS_QUERY_2025_12_15,
 371    migrations::m_2025_12_15::SETTINGS_PATTERNS
 372);
 373
 374// custom query
 375static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 376    Query::new(
 377        &tree_sitter_json::LANGUAGE.into(),
 378        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 379    )
 380    .unwrap()
 381});
 382
 383#[cfg(test)]
 384mod tests {
 385    use super::*;
 386    use unindent::Unindent as _;
 387
 388    #[track_caller]
 389    fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
 390        match (&migrated, &expected) {
 391            (Some(migrated), Some(expected)) => {
 392                pretty_assertions::assert_str_eq!(expected, migrated);
 393            }
 394            _ => {
 395                pretty_assertions::assert_eq!(migrated.as_deref(), expected);
 396            }
 397        }
 398    }
 399
 400    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 401        let migrated = migrate_keymap(input).unwrap();
 402        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 403    }
 404
 405    #[track_caller]
 406    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 407        let migrated = migrate_settings(input).unwrap();
 408        assert_migrated_correctly(migrated.clone(), output);
 409
 410        // expect that rerunning the migration does not result in another migration
 411        if let Some(migrated) = migrated {
 412            let rerun = migrate_settings(&migrated).unwrap();
 413            assert_migrated_correctly(rerun, None);
 414        }
 415    }
 416
 417    #[track_caller]
 418    fn assert_migrate_settings_with_migrations(
 419        migrations: &[MigrationType],
 420        input: &str,
 421        output: Option<&str>,
 422    ) {
 423        let migrated = run_migrations(input, migrations).unwrap();
 424        assert_migrated_correctly(migrated.clone(), output);
 425
 426        // expect that rerunning the migration does not result in another migration
 427        if let Some(migrated) = migrated {
 428            let rerun = run_migrations(&migrated, migrations).unwrap();
 429            assert_migrated_correctly(rerun, None);
 430        }
 431    }
 432
 433    #[test]
 434    fn test_empty_content() {
 435        assert_migrate_settings("", None)
 436    }
 437
 438    #[test]
 439    fn test_replace_array_with_single_string() {
 440        assert_migrate_keymap(
 441            r#"
 442            [
 443                {
 444                    "bindings": {
 445                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 446                    }
 447                }
 448            ]
 449            "#,
 450            Some(
 451                r#"
 452            [
 453                {
 454                    "bindings": {
 455                        "cmd-1": "workspace::ActivatePaneUp"
 456                    }
 457                }
 458            ]
 459            "#,
 460            ),
 461        )
 462    }
 463
 464    #[test]
 465    fn test_replace_action_argument_object_with_single_value() {
 466        assert_migrate_keymap(
 467            r#"
 468            [
 469                {
 470                    "bindings": {
 471                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 472                    }
 473                }
 474            ]
 475            "#,
 476            Some(
 477                r#"
 478            [
 479                {
 480                    "bindings": {
 481                        "cmd-1": ["editor::FoldAtLevel", 1]
 482                    }
 483                }
 484            ]
 485            "#,
 486            ),
 487        )
 488    }
 489
 490    #[test]
 491    fn test_replace_action_argument_object_with_single_value_2() {
 492        assert_migrate_keymap(
 493            r#"
 494            [
 495                {
 496                    "bindings": {
 497                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 498                    }
 499                }
 500            ]
 501            "#,
 502            Some(
 503                r#"
 504            [
 505                {
 506                    "bindings": {
 507                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 508                    }
 509                }
 510            ]
 511            "#,
 512            ),
 513        )
 514    }
 515
 516    #[test]
 517    fn test_rename_string_action() {
 518        assert_migrate_keymap(
 519            r#"
 520                [
 521                    {
 522                        "bindings": {
 523                            "cmd-1": "inline_completion::ToggleMenu"
 524                        }
 525                    }
 526                ]
 527            "#,
 528            Some(
 529                r#"
 530                [
 531                    {
 532                        "bindings": {
 533                            "cmd-1": "edit_prediction::ToggleMenu"
 534                        }
 535                    }
 536                ]
 537            "#,
 538            ),
 539        )
 540    }
 541
 542    #[test]
 543    fn test_rename_context_key() {
 544        assert_migrate_keymap(
 545            r#"
 546                [
 547                    {
 548                        "context": "Editor && inline_completion && !showing_completions"
 549                    }
 550                ]
 551            "#,
 552            Some(
 553                r#"
 554                [
 555                    {
 556                        "context": "Editor && edit_prediction && !showing_completions"
 557                    }
 558                ]
 559            "#,
 560            ),
 561        )
 562    }
 563
 564    #[test]
 565    fn test_incremental_migrations() {
 566        // Here string transforms to array internally. Then, that array transforms back to string.
 567        assert_migrate_keymap(
 568            r#"
 569                [
 570                    {
 571                        "bindings": {
 572                            "ctrl-q": "editor::GoToHunk", // should remain same
 573                            "ctrl-w": "editor::GoToPrevHunk", // should rename
 574                            "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
 575                            "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
 576                        }
 577                    }
 578                ]
 579            "#,
 580            Some(
 581                r#"
 582                [
 583                    {
 584                        "bindings": {
 585                            "ctrl-q": "editor::GoToHunk", // should remain same
 586                            "ctrl-w": "editor::GoToPreviousHunk", // should rename
 587                            "ctrl-q": "editor::GoToHunk", // should transform
 588                            "ctrl-w": "editor::GoToPreviousHunk" // should transform
 589                        }
 590                    }
 591                ]
 592            "#,
 593            ),
 594        )
 595    }
 596
 597    #[test]
 598    fn test_action_argument_snake_case() {
 599        // First performs transformations, then replacements
 600        assert_migrate_keymap(
 601            r#"
 602            [
 603                {
 604                    "bindings": {
 605                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
 606                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
 607                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
 608                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 609                    }
 610                }
 611            ]
 612            "#,
 613            Some(
 614                r#"
 615            [
 616                {
 617                    "bindings": {
 618                        "cmd-1": ["vim::PushObject", { "around": false }],
 619                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
 620                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
 621                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 622                    }
 623                }
 624            ]
 625            "#,
 626            ),
 627        )
 628    }
 629
 630    #[test]
 631    fn test_replace_setting_name() {
 632        assert_migrate_settings(
 633            r#"
 634                {
 635                    "show_inline_completions_in_menu": true,
 636                    "show_inline_completions": true,
 637                    "inline_completions_disabled_in": ["string"],
 638                    "inline_completions": { "some" : "value" }
 639                }
 640            "#,
 641            Some(
 642                r#"
 643                {
 644                    "show_edit_predictions_in_menu": true,
 645                    "show_edit_predictions": true,
 646                    "edit_predictions_disabled_in": ["string"],
 647                    "edit_predictions": { "some" : "value" }
 648                }
 649            "#,
 650            ),
 651        )
 652    }
 653
 654    #[test]
 655    fn test_nested_string_replace_for_settings() {
 656        assert_migrate_settings(
 657            &r#"
 658            {
 659                "features": {
 660                    "inline_completion_provider": "zed"
 661                },
 662            }
 663            "#
 664            .unindent(),
 665            Some(
 666                &r#"
 667                {
 668                    "edit_predictions": {
 669                        "provider": "zed"
 670                    }
 671                }
 672                "#
 673                .unindent(),
 674            ),
 675        )
 676    }
 677
 678    #[test]
 679    fn test_replace_settings_in_languages() {
 680        assert_migrate_settings(
 681            r#"
 682                {
 683                    "languages": {
 684                        "Astro": {
 685                            "show_inline_completions": true
 686                        }
 687                    }
 688                }
 689            "#,
 690            Some(
 691                r#"
 692                {
 693                    "languages": {
 694                        "Astro": {
 695                            "show_edit_predictions": true
 696                        }
 697                    }
 698                }
 699            "#,
 700            ),
 701        )
 702    }
 703
 704    #[test]
 705    fn test_replace_settings_value() {
 706        assert_migrate_settings(
 707            r#"
 708                {
 709                    "scrollbar": {
 710                        "diagnostics": true
 711                    },
 712                    "chat_panel": {
 713                        "button": true
 714                    }
 715                }
 716            "#,
 717            Some(
 718                r#"
 719                {
 720                    "scrollbar": {
 721                        "diagnostics": "all"
 722                    },
 723                    "chat_panel": {
 724                        "button": "always"
 725                    }
 726                }
 727            "#,
 728            ),
 729        )
 730    }
 731
 732    #[test]
 733    fn test_replace_settings_name_and_value() {
 734        assert_migrate_settings(
 735            r#"
 736                {
 737                    "tabs": {
 738                        "always_show_close_button": true
 739                    }
 740                }
 741            "#,
 742            Some(
 743                r#"
 744                {
 745                    "tabs": {
 746                        "show_close_button": "always"
 747                    }
 748                }
 749            "#,
 750            ),
 751        )
 752    }
 753
 754    #[test]
 755    fn test_replace_bash_with_terminal_in_profiles() {
 756        assert_migrate_settings(
 757            r#"
 758                {
 759                    "assistant": {
 760                        "profiles": {
 761                            "custom": {
 762                                "name": "Custom",
 763                                "tools": {
 764                                    "bash": true,
 765                                    "diagnostics": true
 766                                }
 767                            }
 768                        }
 769                    }
 770                }
 771            "#,
 772            Some(
 773                r#"
 774                {
 775                    "agent": {
 776                        "profiles": {
 777                            "custom": {
 778                                "name": "Custom",
 779                                "tools": {
 780                                    "terminal": true,
 781                                    "diagnostics": true
 782                                }
 783                            }
 784                        }
 785                    }
 786                }
 787            "#,
 788            ),
 789        )
 790    }
 791
 792    #[test]
 793    fn test_replace_bash_false_with_terminal_in_profiles() {
 794        assert_migrate_settings(
 795            r#"
 796                {
 797                    "assistant": {
 798                        "profiles": {
 799                            "custom": {
 800                                "name": "Custom",
 801                                "tools": {
 802                                    "bash": false,
 803                                    "diagnostics": true
 804                                }
 805                            }
 806                        }
 807                    }
 808                }
 809            "#,
 810            Some(
 811                r#"
 812                {
 813                    "agent": {
 814                        "profiles": {
 815                            "custom": {
 816                                "name": "Custom",
 817                                "tools": {
 818                                    "terminal": false,
 819                                    "diagnostics": true
 820                                }
 821                            }
 822                        }
 823                    }
 824                }
 825            "#,
 826            ),
 827        )
 828    }
 829
 830    #[test]
 831    fn test_no_bash_in_profiles() {
 832        assert_migrate_settings(
 833            r#"
 834                {
 835                    "assistant": {
 836                        "profiles": {
 837                            "custom": {
 838                                "name": "Custom",
 839                                "tools": {
 840                                    "diagnostics": true,
 841                                    "find_path": true,
 842                                    "read_file": true
 843                                }
 844                            }
 845                        }
 846                    }
 847                }
 848            "#,
 849            Some(
 850                r#"
 851                {
 852                    "agent": {
 853                        "profiles": {
 854                            "custom": {
 855                                "name": "Custom",
 856                                "tools": {
 857                                    "diagnostics": true,
 858                                    "find_path": true,
 859                                    "read_file": true
 860                                }
 861                            }
 862                        }
 863                    }
 864                }
 865            "#,
 866            ),
 867        )
 868    }
 869
 870    #[test]
 871    fn test_rename_path_search_to_find_path() {
 872        assert_migrate_settings(
 873            r#"
 874                {
 875                    "assistant": {
 876                        "profiles": {
 877                            "default": {
 878                                "tools": {
 879                                    "path_search": true,
 880                                    "read_file": true
 881                                }
 882                            }
 883                        }
 884                    }
 885                }
 886            "#,
 887            Some(
 888                r#"
 889                {
 890                    "agent": {
 891                        "profiles": {
 892                            "default": {
 893                                "tools": {
 894                                    "find_path": true,
 895                                    "read_file": true
 896                                }
 897                            }
 898                        }
 899                    }
 900                }
 901            "#,
 902            ),
 903        );
 904    }
 905
 906    #[test]
 907    fn test_rename_assistant() {
 908        assert_migrate_settings(
 909            r#"{
 910                "assistant": {
 911                    "foo": "bar"
 912                },
 913                "edit_predictions": {
 914                    "enabled_in_assistant": false,
 915                }
 916            }"#,
 917            Some(
 918                r#"{
 919                "agent": {
 920                    "foo": "bar"
 921                },
 922                "edit_predictions": {
 923                    "enabled_in_text_threads": false,
 924                }
 925            }"#,
 926            ),
 927        );
 928    }
 929
 930    #[test]
 931    fn test_comment_duplicated_agent() {
 932        assert_migrate_settings(
 933            r#"{
 934                "agent": {
 935                    "name": "assistant-1",
 936                "model": "gpt-4", // weird formatting
 937                    "utf8": "привіт"
 938                },
 939                "something": "else",
 940                "agent": {
 941                    "name": "assistant-2",
 942                    "model": "gemini-pro"
 943                }
 944            }
 945        "#,
 946            Some(
 947                r#"{
 948                /* Duplicated key auto-commented: "agent": {
 949                    "name": "assistant-1",
 950                "model": "gpt-4", // weird formatting
 951                    "utf8": "привіт"
 952                }, */
 953                "something": "else",
 954                "agent": {
 955                    "name": "assistant-2",
 956                    "model": "gemini-pro"
 957                }
 958            }
 959        "#,
 960            ),
 961        );
 962    }
 963
 964    #[test]
 965    fn test_mcp_settings_migration() {
 966        assert_migrate_settings_with_migrations(
 967            &[MigrationType::TreeSitter(
 968                migrations::m_2025_06_16::SETTINGS_PATTERNS,
 969                &SETTINGS_QUERY_2025_06_16,
 970            )],
 971            r#"{
 972    "context_servers": {
 973        "empty_server": {},
 974        "extension_server": {
 975            "settings": {
 976                "foo": "bar"
 977            }
 978        },
 979        "custom_server": {
 980            "command": {
 981                "path": "foo",
 982                "args": ["bar"],
 983                "env": {
 984                    "FOO": "BAR"
 985                }
 986            }
 987        },
 988        "invalid_server": {
 989            "command": {
 990                "path": "foo",
 991                "args": ["bar"],
 992                "env": {
 993                    "FOO": "BAR"
 994                }
 995            },
 996            "settings": {
 997                "foo": "bar"
 998            }
 999        },
1000        "empty_server2": {},
1001        "extension_server2": {
1002            "foo": "bar",
1003            "settings": {
1004                "foo": "bar"
1005            },
1006            "bar": "foo"
1007        },
1008        "custom_server2": {
1009            "foo": "bar",
1010            "command": {
1011                "path": "foo",
1012                "args": ["bar"],
1013                "env": {
1014                    "FOO": "BAR"
1015                }
1016            },
1017            "bar": "foo"
1018        },
1019        "invalid_server2": {
1020            "foo": "bar",
1021            "command": {
1022                "path": "foo",
1023                "args": ["bar"],
1024                "env": {
1025                    "FOO": "BAR"
1026                }
1027            },
1028            "bar": "foo",
1029            "settings": {
1030                "foo": "bar"
1031            }
1032        }
1033    }
1034}"#,
1035            Some(
1036                r#"{
1037    "context_servers": {
1038        "empty_server": {
1039            "source": "extension",
1040            "settings": {}
1041        },
1042        "extension_server": {
1043            "source": "extension",
1044            "settings": {
1045                "foo": "bar"
1046            }
1047        },
1048        "custom_server": {
1049            "source": "custom",
1050            "command": {
1051                "path": "foo",
1052                "args": ["bar"],
1053                "env": {
1054                    "FOO": "BAR"
1055                }
1056            }
1057        },
1058        "invalid_server": {
1059            "source": "custom",
1060            "command": {
1061                "path": "foo",
1062                "args": ["bar"],
1063                "env": {
1064                    "FOO": "BAR"
1065                }
1066            },
1067            "settings": {
1068                "foo": "bar"
1069            }
1070        },
1071        "empty_server2": {
1072            "source": "extension",
1073            "settings": {}
1074        },
1075        "extension_server2": {
1076            "source": "extension",
1077            "foo": "bar",
1078            "settings": {
1079                "foo": "bar"
1080            },
1081            "bar": "foo"
1082        },
1083        "custom_server2": {
1084            "source": "custom",
1085            "foo": "bar",
1086            "command": {
1087                "path": "foo",
1088                "args": ["bar"],
1089                "env": {
1090                    "FOO": "BAR"
1091                }
1092            },
1093            "bar": "foo"
1094        },
1095        "invalid_server2": {
1096            "source": "custom",
1097            "foo": "bar",
1098            "command": {
1099                "path": "foo",
1100                "args": ["bar"],
1101                "env": {
1102                    "FOO": "BAR"
1103                }
1104            },
1105            "bar": "foo",
1106            "settings": {
1107                "foo": "bar"
1108            }
1109        }
1110    }
1111}"#,
1112            ),
1113        );
1114    }
1115
1116    #[test]
1117    fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1118        let settings = r#"{
1119    "context_servers": {
1120        "empty_server": {
1121            "source": "extension",
1122            "settings": {}
1123        },
1124        "extension_server": {
1125            "source": "extension",
1126            "settings": {
1127                "foo": "bar"
1128            }
1129        },
1130        "custom_server": {
1131            "source": "custom",
1132            "command": {
1133                "path": "foo",
1134                "args": ["bar"],
1135                "env": {
1136                    "FOO": "BAR"
1137                }
1138            }
1139        },
1140        "invalid_server": {
1141            "source": "custom",
1142            "command": {
1143                "path": "foo",
1144                "args": ["bar"],
1145                "env": {
1146                    "FOO": "BAR"
1147                }
1148            },
1149            "settings": {
1150                "foo": "bar"
1151            }
1152        }
1153    }
1154}"#;
1155        assert_migrate_settings_with_migrations(
1156            &[MigrationType::TreeSitter(
1157                migrations::m_2025_06_16::SETTINGS_PATTERNS,
1158                &SETTINGS_QUERY_2025_06_16,
1159            )],
1160            settings,
1161            None,
1162        );
1163    }
1164
1165    #[test]
1166    fn test_custom_agent_server_settings_migration() {
1167        assert_migrate_settings_with_migrations(
1168            &[MigrationType::TreeSitter(
1169                migrations::m_2025_11_20::SETTINGS_PATTERNS,
1170                &SETTINGS_QUERY_2025_11_20,
1171            )],
1172            r#"{
1173    "agent_servers": {
1174        "gemini": {
1175            "default_model": "gemini-1.5-pro"
1176        },
1177        "claude": {},
1178        "codex": {},
1179        "my-custom-agent": {
1180            "command": "/path/to/agent",
1181            "args": ["--foo"],
1182            "default_model": "my-model"
1183        },
1184        "already-migrated-agent": {
1185            "type": "custom",
1186            "command": "/path/to/agent"
1187        },
1188        "future-extension-agent": {
1189            "type": "extension",
1190            "default_model": "ext-model"
1191        }
1192    }
1193}"#,
1194            Some(
1195                r#"{
1196    "agent_servers": {
1197        "gemini": {
1198            "default_model": "gemini-1.5-pro"
1199        },
1200        "claude": {},
1201        "codex": {},
1202        "my-custom-agent": {
1203            "type": "custom",
1204            "command": "/path/to/agent",
1205            "args": ["--foo"],
1206            "default_model": "my-model"
1207        },
1208        "already-migrated-agent": {
1209            "type": "custom",
1210            "command": "/path/to/agent"
1211        },
1212        "future-extension-agent": {
1213            "type": "extension",
1214            "default_model": "ext-model"
1215        }
1216    }
1217}"#,
1218            ),
1219        );
1220    }
1221
1222    #[test]
1223    fn test_remove_version_fields() {
1224        assert_migrate_settings(
1225            r#"{
1226    "language_models": {
1227        "anthropic": {
1228            "version": "1",
1229            "api_url": "https://api.anthropic.com"
1230        },
1231        "openai": {
1232            "version": "1",
1233            "api_url": "https://api.openai.com/v1"
1234        }
1235    },
1236    "agent": {
1237        "version": "2",
1238        "enabled": true,
1239        "button": true,
1240        "dock": "right",
1241        "default_width": 640,
1242        "default_height": 320,
1243        "default_model": {
1244            "provider": "zed.dev",
1245            "model": "claude-sonnet-4"
1246        }
1247    }
1248}"#,
1249            Some(
1250                r#"{
1251    "language_models": {
1252        "anthropic": {
1253            "api_url": "https://api.anthropic.com"
1254        },
1255        "openai": {
1256            "api_url": "https://api.openai.com/v1"
1257        }
1258    },
1259    "agent": {
1260        "enabled": true,
1261        "button": true,
1262        "dock": "right",
1263        "default_width": 640,
1264        "default_height": 320,
1265        "default_model": {
1266            "provider": "zed.dev",
1267            "model": "claude-sonnet-4"
1268        }
1269    }
1270}"#,
1271            ),
1272        );
1273
1274        // Test that version fields in other contexts are not removed
1275        assert_migrate_settings(
1276            r#"{
1277    "language_models": {
1278        "other_provider": {
1279            "version": "1",
1280            "api_url": "https://api.example.com"
1281        }
1282    },
1283    "other_section": {
1284        "version": "1"
1285    }
1286}"#,
1287            None,
1288        );
1289    }
1290
1291    #[test]
1292    fn test_flatten_context_server_command() {
1293        assert_migrate_settings(
1294            r#"{
1295    "context_servers": {
1296        "some-mcp-server": {
1297            "command": {
1298                "path": "npx",
1299                "args": [
1300                    "-y",
1301                    "@supabase/mcp-server-supabase@latest",
1302                    "--read-only",
1303                    "--project-ref=<project-ref>"
1304                ],
1305                "env": {
1306                    "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1307                }
1308            }
1309        }
1310    }
1311}"#,
1312            Some(
1313                r#"{
1314    "context_servers": {
1315        "some-mcp-server": {
1316            "command": "npx",
1317            "args": [
1318                "-y",
1319                "@supabase/mcp-server-supabase@latest",
1320                "--read-only",
1321                "--project-ref=<project-ref>"
1322            ],
1323            "env": {
1324                "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1325            }
1326        }
1327    }
1328}"#,
1329            ),
1330        );
1331
1332        // Test with additional keys in server object
1333        assert_migrate_settings(
1334            r#"{
1335    "context_servers": {
1336        "server-with-extras": {
1337            "command": {
1338                "path": "/usr/bin/node",
1339                "args": ["server.js"]
1340            },
1341            "settings": {}
1342        }
1343    }
1344}"#,
1345            Some(
1346                r#"{
1347    "context_servers": {
1348        "server-with-extras": {
1349            "command": "/usr/bin/node",
1350            "args": ["server.js"],
1351            "settings": {}
1352        }
1353    }
1354}"#,
1355            ),
1356        );
1357
1358        // Test command without args or env
1359        assert_migrate_settings(
1360            r#"{
1361    "context_servers": {
1362        "simple-server": {
1363            "command": {
1364                "path": "simple-mcp-server"
1365            }
1366        }
1367    }
1368}"#,
1369            Some(
1370                r#"{
1371    "context_servers": {
1372        "simple-server": {
1373            "command": "simple-mcp-server"
1374        }
1375    }
1376}"#,
1377            ),
1378        );
1379    }
1380
1381    #[test]
1382    fn test_flatten_code_action_formatters_basic_array() {
1383        assert_migrate_settings_with_migrations(
1384            &[MigrationType::Json(
1385                migrations::m_2025_10_01::flatten_code_actions_formatters,
1386            )],
1387            &r#"{
1388        "formatter": [
1389          {
1390            "code_actions": {
1391              "included-1": true,
1392              "included-2": true,
1393              "excluded": false,
1394            }
1395          }
1396        ]
1397      }"#
1398            .unindent(),
1399            Some(
1400                &r#"{
1401        "formatter": [
1402          {
1403            "code_action": "included-1"
1404          },
1405          {
1406            "code_action": "included-2"
1407          }
1408        ]
1409      }"#
1410                .unindent(),
1411            ),
1412        );
1413    }
1414
1415    #[test]
1416    fn test_flatten_code_action_formatters_basic_object() {
1417        assert_migrate_settings_with_migrations(
1418            &[MigrationType::Json(
1419                migrations::m_2025_10_01::flatten_code_actions_formatters,
1420            )],
1421            &r#"{
1422        "formatter": {
1423          "code_actions": {
1424            "included-1": true,
1425            "excluded": false,
1426            "included-2": true
1427          }
1428        }
1429      }"#
1430            .unindent(),
1431            Some(
1432                &r#"{
1433                  "formatter": [
1434                    {
1435                      "code_action": "included-1"
1436                    },
1437                    {
1438                      "code_action": "included-2"
1439                    }
1440                  ]
1441                }"#
1442                .unindent(),
1443            ),
1444        );
1445    }
1446
1447    #[test]
1448    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1449        assert_migrate_settings(
1450            &r#"{
1451          "formatter": [
1452            {
1453               "code_actions": {
1454                  "included-1": true,
1455                  "included-2": true,
1456                  "excluded": false,
1457               }
1458            },
1459            {
1460              "language_server": "ruff"
1461            },
1462            {
1463               "code_actions": {
1464                  "excluded": false,
1465                  "excluded-2": false,
1466               }
1467            }
1468            // some comment
1469            ,
1470            {
1471               "code_actions": {
1472                "excluded": false,
1473                "included-3": true,
1474                "included-4": true,
1475               }
1476            },
1477          ]
1478        }"#
1479            .unindent(),
1480            Some(
1481                &r#"{
1482        "formatter": [
1483          {
1484            "code_action": "included-1"
1485          },
1486          {
1487            "code_action": "included-2"
1488          },
1489          {
1490            "language_server": "ruff"
1491          },
1492          {
1493            "code_action": "included-3"
1494          },
1495          {
1496            "code_action": "included-4"
1497          }
1498        ]
1499      }"#
1500                .unindent(),
1501            ),
1502        );
1503    }
1504
1505    #[test]
1506    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1507        assert_migrate_settings(
1508            &r#"{
1509        "languages": {
1510          "Rust": {
1511            "formatter": [
1512              {
1513                "code_actions": {
1514                  "included-1": true,
1515                  "included-2": true,
1516                  "excluded": false,
1517                }
1518              },
1519              {
1520                "language_server": "ruff"
1521              },
1522              {
1523                "code_actions": {
1524                  "excluded": false,
1525                  "excluded-2": false,
1526                }
1527              }
1528              // some comment
1529              ,
1530              {
1531                "code_actions": {
1532                  "excluded": false,
1533                  "included-3": true,
1534                  "included-4": true,
1535                }
1536              },
1537            ]
1538          }
1539        }
1540      }"#
1541            .unindent(),
1542            Some(
1543                &r#"{
1544          "languages": {
1545            "Rust": {
1546              "formatter": [
1547                {
1548                  "code_action": "included-1"
1549                },
1550                {
1551                  "code_action": "included-2"
1552                },
1553                {
1554                  "language_server": "ruff"
1555                },
1556                {
1557                  "code_action": "included-3"
1558                },
1559                {
1560                  "code_action": "included-4"
1561                }
1562              ]
1563            }
1564          }
1565        }"#
1566                .unindent(),
1567            ),
1568        );
1569    }
1570
1571    #[test]
1572    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1573     {
1574        assert_migrate_settings_with_migrations(
1575            &[MigrationType::Json(
1576                migrations::m_2025_10_01::flatten_code_actions_formatters,
1577            )],
1578            &r#"{
1579        "formatter": {
1580          "code_actions": {
1581            "default-1": true,
1582            "default-2": true,
1583            "default-3": true,
1584            "default-4": true,
1585          }
1586        },
1587        "languages": {
1588          "Rust": {
1589            "formatter": [
1590              {
1591                "code_actions": {
1592                  "included-1": true,
1593                  "included-2": true,
1594                  "excluded": false,
1595                }
1596              },
1597              {
1598                "language_server": "ruff"
1599              },
1600              {
1601                "code_actions": {
1602                  "excluded": false,
1603                  "excluded-2": false,
1604                }
1605              }
1606              // some comment
1607              ,
1608              {
1609                "code_actions": {
1610                  "excluded": false,
1611                  "included-3": true,
1612                  "included-4": true,
1613                }
1614              },
1615            ]
1616          },
1617          "Python": {
1618            "formatter": [
1619              {
1620                "language_server": "ruff"
1621              },
1622              {
1623                "code_actions": {
1624                  "excluded": false,
1625                  "excluded-2": false,
1626                }
1627              }
1628              // some comment
1629              ,
1630              {
1631                "code_actions": {
1632                  "excluded": false,
1633                  "included-3": true,
1634                  "included-4": true,
1635                }
1636              },
1637            ]
1638          }
1639        }
1640      }"#
1641            .unindent(),
1642            Some(
1643                &r#"{
1644          "formatter": [
1645            {
1646              "code_action": "default-1"
1647            },
1648            {
1649              "code_action": "default-2"
1650            },
1651            {
1652              "code_action": "default-3"
1653            },
1654            {
1655              "code_action": "default-4"
1656            }
1657          ],
1658          "languages": {
1659            "Rust": {
1660              "formatter": [
1661                {
1662                  "code_action": "included-1"
1663                },
1664                {
1665                  "code_action": "included-2"
1666                },
1667                {
1668                  "language_server": "ruff"
1669                },
1670                {
1671                  "code_action": "included-3"
1672                },
1673                {
1674                  "code_action": "included-4"
1675                }
1676              ]
1677            },
1678            "Python": {
1679              "formatter": [
1680                {
1681                  "language_server": "ruff"
1682                },
1683                {
1684                  "code_action": "included-3"
1685                },
1686                {
1687                  "code_action": "included-4"
1688                }
1689              ]
1690            }
1691          }
1692        }"#
1693                .unindent(),
1694            ),
1695        );
1696    }
1697
1698    #[test]
1699    fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1700        assert_migrate_settings_with_migrations(
1701            &[MigrationType::Json(
1702                migrations::m_2025_10_01::flatten_code_actions_formatters,
1703            )],
1704            &r#"{
1705        "formatter": {
1706          "code_actions": {
1707            "default-1": true,
1708            "default-2": true,
1709            "default-3": true,
1710            "default-4": true,
1711          }
1712        },
1713        "format_on_save": [
1714          {
1715            "code_actions": {
1716              "included-1": true,
1717              "included-2": true,
1718              "excluded": false,
1719            }
1720          },
1721          {
1722            "language_server": "ruff"
1723          },
1724          {
1725            "code_actions": {
1726              "excluded": false,
1727              "excluded-2": false,
1728            }
1729          }
1730          // some comment
1731          ,
1732          {
1733            "code_actions": {
1734              "excluded": false,
1735              "included-3": true,
1736              "included-4": true,
1737            }
1738          },
1739        ],
1740        "languages": {
1741          "Rust": {
1742            "format_on_save": "prettier",
1743            "formatter": [
1744              {
1745                "code_actions": {
1746                  "included-1": true,
1747                  "included-2": true,
1748                  "excluded": false,
1749                }
1750              },
1751              {
1752                "language_server": "ruff"
1753              },
1754              {
1755                "code_actions": {
1756                  "excluded": false,
1757                  "excluded-2": false,
1758                }
1759              }
1760              // some comment
1761              ,
1762              {
1763                "code_actions": {
1764                  "excluded": false,
1765                  "included-3": true,
1766                  "included-4": true,
1767                }
1768              },
1769            ]
1770          },
1771          "Python": {
1772            "format_on_save": {
1773              "code_actions": {
1774                "on-save-1": true,
1775                "on-save-2": true,
1776              }
1777            },
1778            "formatter": [
1779              {
1780                "language_server": "ruff"
1781              },
1782              {
1783                "code_actions": {
1784                  "excluded": false,
1785                  "excluded-2": false,
1786                }
1787              }
1788              // some comment
1789              ,
1790              {
1791                "code_actions": {
1792                  "excluded": false,
1793                  "included-3": true,
1794                  "included-4": true,
1795                }
1796              },
1797            ]
1798          }
1799        }
1800      }"#
1801            .unindent(),
1802            Some(
1803                &r#"
1804        {
1805          "formatter": [
1806            {
1807              "code_action": "default-1"
1808            },
1809            {
1810              "code_action": "default-2"
1811            },
1812            {
1813              "code_action": "default-3"
1814            },
1815            {
1816              "code_action": "default-4"
1817            }
1818          ],
1819          "format_on_save": [
1820            {
1821              "code_action": "included-1"
1822            },
1823            {
1824              "code_action": "included-2"
1825            },
1826            {
1827              "language_server": "ruff"
1828            },
1829            {
1830              "code_action": "included-3"
1831            },
1832            {
1833              "code_action": "included-4"
1834            }
1835          ],
1836          "languages": {
1837            "Rust": {
1838              "format_on_save": "prettier",
1839              "formatter": [
1840                {
1841                  "code_action": "included-1"
1842                },
1843                {
1844                  "code_action": "included-2"
1845                },
1846                {
1847                  "language_server": "ruff"
1848                },
1849                {
1850                  "code_action": "included-3"
1851                },
1852                {
1853                  "code_action": "included-4"
1854                }
1855              ]
1856            },
1857            "Python": {
1858              "format_on_save": [
1859                {
1860                  "code_action": "on-save-1"
1861                },
1862                {
1863                  "code_action": "on-save-2"
1864                }
1865              ],
1866              "formatter": [
1867                {
1868                  "language_server": "ruff"
1869                },
1870                {
1871                  "code_action": "included-3"
1872                },
1873                {
1874                  "code_action": "included-4"
1875                }
1876              ]
1877            }
1878          }
1879        }"#
1880                .unindent(),
1881            ),
1882        );
1883    }
1884
1885    #[test]
1886    fn test_format_on_save_formatter_migration_basic() {
1887        assert_migrate_settings_with_migrations(
1888            &[MigrationType::Json(
1889                migrations::m_2025_10_02::remove_formatters_on_save,
1890            )],
1891            &r#"{
1892                  "format_on_save": "prettier"
1893              }"#
1894            .unindent(),
1895            Some(
1896                &r#"{
1897                      "formatter": "prettier",
1898                      "format_on_save": "on"
1899                  }"#
1900                .unindent(),
1901            ),
1902        );
1903    }
1904
1905    #[test]
1906    fn test_format_on_save_formatter_migration_array() {
1907        assert_migrate_settings_with_migrations(
1908            &[MigrationType::Json(
1909                migrations::m_2025_10_02::remove_formatters_on_save,
1910            )],
1911            &r#"{
1912                "format_on_save": ["prettier", {"language_server": "eslint"}]
1913            }"#
1914            .unindent(),
1915            Some(
1916                &r#"{
1917                    "formatter": [
1918                        "prettier",
1919                        {
1920                            "language_server": "eslint"
1921                        }
1922                    ],
1923                    "format_on_save": "on"
1924                }"#
1925                .unindent(),
1926            ),
1927        );
1928    }
1929
1930    #[test]
1931    fn test_format_on_save_on_off_unchanged() {
1932        assert_migrate_settings_with_migrations(
1933            &[MigrationType::Json(
1934                migrations::m_2025_10_02::remove_formatters_on_save,
1935            )],
1936            &r#"{
1937                "format_on_save": "on"
1938            }"#
1939            .unindent(),
1940            None,
1941        );
1942
1943        assert_migrate_settings_with_migrations(
1944            &[MigrationType::Json(
1945                migrations::m_2025_10_02::remove_formatters_on_save,
1946            )],
1947            &r#"{
1948                "format_on_save": "off"
1949            }"#
1950            .unindent(),
1951            None,
1952        );
1953    }
1954
1955    #[test]
1956    fn test_format_on_save_formatter_migration_in_languages() {
1957        assert_migrate_settings_with_migrations(
1958            &[MigrationType::Json(
1959                migrations::m_2025_10_02::remove_formatters_on_save,
1960            )],
1961            &r#"{
1962                "languages": {
1963                    "Rust": {
1964                        "format_on_save": "rust-analyzer"
1965                    },
1966                    "Python": {
1967                        "format_on_save": ["ruff", "black"]
1968                    }
1969                }
1970            }"#
1971            .unindent(),
1972            Some(
1973                &r#"{
1974                    "languages": {
1975                        "Rust": {
1976                            "formatter": "rust-analyzer",
1977                            "format_on_save": "on"
1978                        },
1979                        "Python": {
1980                            "formatter": [
1981                                "ruff",
1982                                "black"
1983                            ],
1984                            "format_on_save": "on"
1985                        }
1986                    }
1987                }"#
1988                .unindent(),
1989            ),
1990        );
1991    }
1992
1993    #[test]
1994    fn test_format_on_save_formatter_migration_mixed_global_and_languages() {
1995        assert_migrate_settings_with_migrations(
1996            &[MigrationType::Json(
1997                migrations::m_2025_10_02::remove_formatters_on_save,
1998            )],
1999            &r#"{
2000                "format_on_save": "prettier",
2001                "languages": {
2002                    "Rust": {
2003                        "format_on_save": "rust-analyzer"
2004                    },
2005                    "Python": {
2006                        "format_on_save": "on"
2007                    }
2008                }
2009            }"#
2010            .unindent(),
2011            Some(
2012                &r#"{
2013                    "formatter": "prettier",
2014                    "format_on_save": "on",
2015                    "languages": {
2016                        "Rust": {
2017                            "formatter": "rust-analyzer",
2018                            "format_on_save": "on"
2019                        },
2020                        "Python": {
2021                            "format_on_save": "on"
2022                        }
2023                    }
2024                }"#
2025                .unindent(),
2026            ),
2027        );
2028    }
2029
2030    #[test]
2031    fn test_format_on_save_no_migration_when_no_format_on_save() {
2032        assert_migrate_settings_with_migrations(
2033            &[MigrationType::Json(
2034                migrations::m_2025_10_02::remove_formatters_on_save,
2035            )],
2036            &r#"{
2037                "formatter": ["prettier"]
2038            }"#
2039            .unindent(),
2040            None,
2041        );
2042    }
2043
2044    #[test]
2045    fn test_restore_code_actions_on_format() {
2046        assert_migrate_settings_with_migrations(
2047            &[MigrationType::Json(
2048                migrations::m_2025_10_16::restore_code_actions_on_format,
2049            )],
2050            &r#"{
2051                "formatter": {
2052                    "code_action": "foo"
2053                }
2054            }"#
2055            .unindent(),
2056            Some(
2057                &r#"{
2058                    "code_actions_on_format": {
2059                        "foo": true
2060                    },
2061                    "formatter": []
2062                }"#
2063                .unindent(),
2064            ),
2065        );
2066
2067        assert_migrate_settings_with_migrations(
2068            &[MigrationType::Json(
2069                migrations::m_2025_10_16::restore_code_actions_on_format,
2070            )],
2071            &r#"{
2072                "formatter": [
2073                    { "code_action": "foo" },
2074                    "auto"
2075                ]
2076            }"#
2077            .unindent(),
2078            None,
2079        );
2080
2081        assert_migrate_settings_with_migrations(
2082            &[MigrationType::Json(
2083                migrations::m_2025_10_16::restore_code_actions_on_format,
2084            )],
2085            &r#"{
2086                "formatter": {
2087                    "code_action": "foo"
2088                },
2089                "code_actions_on_format": {
2090                    "bar": true,
2091                    "baz": false
2092                }
2093            }"#
2094            .unindent(),
2095            Some(
2096                &r#"{
2097                    "formatter": [],
2098                    "code_actions_on_format": {
2099                        "foo": true,
2100                        "bar": true,
2101                        "baz": false
2102                    }
2103                }"#
2104                .unindent(),
2105            ),
2106        );
2107
2108        assert_migrate_settings_with_migrations(
2109            &[MigrationType::Json(
2110                migrations::m_2025_10_16::restore_code_actions_on_format,
2111            )],
2112            &r#"{
2113                "formatter": [
2114                    { "code_action": "foo" },
2115                    { "code_action": "qux" },
2116                ],
2117                "code_actions_on_format": {
2118                    "bar": true,
2119                    "baz": false
2120                }
2121            }"#
2122            .unindent(),
2123            Some(
2124                &r#"{
2125                    "formatter": [],
2126                    "code_actions_on_format": {
2127                        "foo": true,
2128                        "qux": true,
2129                        "bar": true,
2130                        "baz": false
2131                    }
2132                }"#
2133                .unindent(),
2134            ),
2135        );
2136
2137        assert_migrate_settings_with_migrations(
2138            &[MigrationType::Json(
2139                migrations::m_2025_10_16::restore_code_actions_on_format,
2140            )],
2141            &r#"{
2142                "formatter": [],
2143                "code_actions_on_format": {
2144                    "bar": true,
2145                    "baz": false
2146                }
2147            }"#
2148            .unindent(),
2149            None,
2150        );
2151    }
2152
2153    #[test]
2154    fn test_make_file_finder_include_ignored_an_enum() {
2155        assert_migrate_settings_with_migrations(
2156            &[MigrationType::Json(
2157                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2158            )],
2159            &r#"{ }"#.unindent(),
2160            None,
2161        );
2162
2163        assert_migrate_settings_with_migrations(
2164            &[MigrationType::Json(
2165                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2166            )],
2167            &r#"{
2168                "file_finder": {
2169                    "include_ignored": true
2170                }
2171            }"#
2172            .unindent(),
2173            Some(
2174                &r#"{
2175                    "file_finder": {
2176                        "include_ignored": "all"
2177                    }
2178                }"#
2179                .unindent(),
2180            ),
2181        );
2182
2183        assert_migrate_settings_with_migrations(
2184            &[MigrationType::Json(
2185                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2186            )],
2187            &r#"{
2188                "file_finder": {
2189                    "include_ignored": false
2190                }
2191            }"#
2192            .unindent(),
2193            Some(
2194                &r#"{
2195                    "file_finder": {
2196                        "include_ignored": "indexed"
2197                    }
2198                }"#
2199                .unindent(),
2200            ),
2201        );
2202
2203        assert_migrate_settings_with_migrations(
2204            &[MigrationType::Json(
2205                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2206            )],
2207            &r#"{
2208                "file_finder": {
2209                    "include_ignored": null
2210                }
2211            }"#
2212            .unindent(),
2213            Some(
2214                &r#"{
2215                    "file_finder": {
2216                        "include_ignored": "smart"
2217                    }
2218                }"#
2219                .unindent(),
2220            ),
2221        );
2222    }
2223
2224    #[test]
2225    fn test_remove_context_server_source() {
2226        assert_migrate_settings(
2227            &r#"
2228            {
2229                "context_servers": {
2230                    "extension_server": {
2231                        "source": "extension",
2232                        "settings": {
2233                            "foo": "bar"
2234                        }
2235                    },
2236                    "custom_server": {
2237                        "source": "custom",
2238                        "command": "foo",
2239                        "args": ["bar"],
2240                        "env": {
2241                            "FOO": "BAR"
2242                        }
2243                    },
2244                }
2245            }
2246            "#
2247            .unindent(),
2248            Some(
2249                &r#"
2250                {
2251                    "context_servers": {
2252                        "extension_server": {
2253                            "settings": {
2254                                "foo": "bar"
2255                            }
2256                        },
2257                        "custom_server": {
2258                            "command": "foo",
2259                            "args": ["bar"],
2260                            "env": {
2261                                "FOO": "BAR"
2262                            }
2263                        },
2264                    }
2265                }
2266                "#
2267                .unindent(),
2268            ),
2269        );
2270    }
2271
2272    #[test]
2273    fn test_project_panel_open_file_on_paste_migration() {
2274        assert_migrate_settings(
2275            &r#"
2276            {
2277                "project_panel": {
2278                    "open_file_on_paste": true
2279                }
2280            }
2281            "#
2282            .unindent(),
2283            Some(
2284                &r#"
2285                {
2286                    "project_panel": {
2287                        "auto_open": { "on_paste": true }
2288                    }
2289                }
2290                "#
2291                .unindent(),
2292            ),
2293        );
2294
2295        assert_migrate_settings(
2296            &r#"
2297            {
2298                "project_panel": {
2299                    "open_file_on_paste": false
2300                }
2301            }
2302            "#
2303            .unindent(),
2304            Some(
2305                &r#"
2306                {
2307                    "project_panel": {
2308                        "auto_open": { "on_paste": false }
2309                    }
2310                }
2311                "#
2312                .unindent(),
2313            ),
2314        );
2315    }
2316
2317    #[test]
2318    fn test_enable_preview_from_code_navigation_migration() {
2319        assert_migrate_settings(
2320            &r#"
2321            {
2322                "other_setting_1": 1,
2323                "preview_tabs": {
2324                    "other_setting_2": 2,
2325                    "enable_preview_from_code_navigation": false
2326                }
2327            }
2328            "#
2329            .unindent(),
2330            Some(
2331                &r#"
2332                {
2333                    "other_setting_1": 1,
2334                    "preview_tabs": {
2335                        "other_setting_2": 2,
2336                        "enable_keep_preview_on_code_navigation": false
2337                    }
2338                }
2339                "#
2340                .unindent(),
2341            ),
2342        );
2343
2344        assert_migrate_settings(
2345            &r#"
2346            {
2347                "other_setting_1": 1,
2348                "preview_tabs": {
2349                    "other_setting_2": 2,
2350                    "enable_preview_from_code_navigation": true
2351                }
2352            }
2353            "#
2354            .unindent(),
2355            Some(
2356                &r#"
2357                {
2358                    "other_setting_1": 1,
2359                    "preview_tabs": {
2360                        "other_setting_2": 2,
2361                        "enable_keep_preview_on_code_navigation": true
2362                    }
2363                }
2364                "#
2365                .unindent(),
2366            ),
2367        );
2368    }
2369
2370    #[test]
2371    fn test_move_edit_prediction_provider_to_edit_predictions() {
2372        assert_migrate_settings_with_migrations(
2373            &[MigrationType::Json(
2374                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2375            )],
2376            &r#"{ }"#.unindent(),
2377            None,
2378        );
2379
2380        assert_migrate_settings_with_migrations(
2381            &[MigrationType::Json(
2382                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2383            )],
2384            &r#"
2385            {
2386                "features": {
2387                    "edit_prediction_provider": "copilot"
2388                }
2389            }
2390            "#
2391            .unindent(),
2392            Some(
2393                &r#"
2394                {
2395                    "edit_predictions": {
2396                        "provider": "copilot"
2397                    }
2398                }
2399                "#
2400                .unindent(),
2401            ),
2402        );
2403
2404        assert_migrate_settings_with_migrations(
2405            &[MigrationType::Json(
2406                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2407            )],
2408            &r#"
2409            {
2410                "features": {
2411                    "edit_prediction_provider": "zed"
2412                },
2413                "edit_predictions": {
2414                    "mode": "eager"
2415                }
2416            }
2417            "#
2418            .unindent(),
2419            Some(
2420                &r#"
2421                {
2422                    "edit_predictions": {
2423                        "provider": "zed",
2424                        "mode": "eager"
2425                    }
2426                }
2427                "#
2428                .unindent(),
2429            ),
2430        );
2431
2432        assert_migrate_settings_with_migrations(
2433            &[MigrationType::Json(
2434                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2435            )],
2436            &r#"
2437            {
2438                "features": {
2439                    "edit_prediction_provider": "supermaven"
2440                },
2441                "edit_predictions": {
2442                    "provider": "copilot"
2443                }
2444            }
2445            "#
2446            .unindent(),
2447            Some(
2448                &r#"
2449                {
2450                    "edit_predictions": {
2451                        "provider": "copilot"
2452                    }
2453                }
2454                "#
2455                .unindent(),
2456            ),
2457        );
2458
2459        assert_migrate_settings_with_migrations(
2460            &[MigrationType::Json(
2461                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2462            )],
2463            &r#"
2464            {
2465                "edit_predictions": {
2466                    "provider": "zed"
2467                }
2468            }
2469            "#
2470            .unindent(),
2471            None,
2472        );
2473    }
2474
2475    #[test]
2476    fn test_migrate_experimental_sweep_mercury() {
2477        assert_migrate_settings_with_migrations(
2478            &[MigrationType::Json(
2479                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2480            )],
2481            &r#"{ }"#.unindent(),
2482            None,
2483        );
2484
2485        assert_migrate_settings_with_migrations(
2486            &[MigrationType::Json(
2487                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2488            )],
2489            &r#"
2490            {
2491                "edit_predictions": {
2492                    "provider": {
2493                        "experimental": "sweep"
2494                    }
2495                }
2496            }
2497            "#
2498            .unindent(),
2499            Some(
2500                &r#"
2501                {
2502                    "edit_predictions": {
2503                        "provider": "sweep"
2504                    }
2505                }
2506                "#
2507                .unindent(),
2508            ),
2509        );
2510
2511        assert_migrate_settings_with_migrations(
2512            &[MigrationType::Json(
2513                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2514            )],
2515            &r#"
2516            {
2517                "edit_predictions": {
2518                    "provider": {
2519                        "experimental": "mercury"
2520                    }
2521                }
2522            }
2523            "#
2524            .unindent(),
2525            Some(
2526                &r#"
2527                {
2528                    "edit_predictions": {
2529                        "provider": "mercury"
2530                    }
2531                }
2532                "#
2533                .unindent(),
2534            ),
2535        );
2536
2537        assert_migrate_settings_with_migrations(
2538            &[MigrationType::Json(
2539                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2540            )],
2541            &r#"
2542            {
2543                "features": {
2544                    "edit_prediction_provider": {
2545                        "experimental": "sweep"
2546                    }
2547                }
2548            }
2549            "#
2550            .unindent(),
2551            Some(
2552                &r#"
2553                {
2554                    "features": {
2555                        "edit_prediction_provider": "sweep"
2556                    }
2557                }
2558                "#
2559                .unindent(),
2560            ),
2561        );
2562
2563        assert_migrate_settings_with_migrations(
2564            &[MigrationType::Json(
2565                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2566            )],
2567            &r#"
2568            {
2569                "edit_predictions": {
2570                    "provider": "zed"
2571                }
2572            }
2573            "#
2574            .unindent(),
2575            None,
2576        );
2577
2578        assert_migrate_settings_with_migrations(
2579            &[MigrationType::Json(
2580                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2581            )],
2582            &r#"
2583            {
2584                "edit_predictions": {
2585                    "provider": {
2586                        "experimental": "zeta2"
2587                    }
2588                }
2589            }
2590            "#
2591            .unindent(),
2592            None,
2593        );
2594    }
2595}