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        // Platform key: settings nested inside "linux" should be migrated
2224        assert_migrate_settings_with_migrations(
2225            &[MigrationType::Json(
2226                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2227            )],
2228            &r#"
2229            {
2230                "linux": {
2231                    "file_finder": {
2232                        "include_ignored": true
2233                    }
2234                }
2235            }
2236            "#
2237            .unindent(),
2238            Some(
2239                &r#"
2240                {
2241                    "linux": {
2242                        "file_finder": {
2243                            "include_ignored": "all"
2244                        }
2245                    }
2246                }
2247                "#
2248                .unindent(),
2249            ),
2250        );
2251
2252        // Profile: settings nested inside profiles should be migrated
2253        assert_migrate_settings_with_migrations(
2254            &[MigrationType::Json(
2255                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2256            )],
2257            &r#"
2258            {
2259                "profiles": {
2260                    "work": {
2261                        "file_finder": {
2262                            "include_ignored": false
2263                        }
2264                    }
2265                }
2266            }
2267            "#
2268            .unindent(),
2269            Some(
2270                &r#"
2271                {
2272                    "profiles": {
2273                        "work": {
2274                            "file_finder": {
2275                                "include_ignored": "indexed"
2276                            }
2277                        }
2278                    }
2279                }
2280                "#
2281                .unindent(),
2282            ),
2283        );
2284    }
2285
2286    #[test]
2287    fn test_make_relative_line_numbers_an_enum() {
2288        assert_migrate_settings_with_migrations(
2289            &[MigrationType::Json(
2290                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2291            )],
2292            &r#"{ }"#.unindent(),
2293            None,
2294        );
2295
2296        assert_migrate_settings_with_migrations(
2297            &[MigrationType::Json(
2298                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2299            )],
2300            &r#"{
2301                "relative_line_numbers": true
2302            }"#
2303            .unindent(),
2304            Some(
2305                &r#"{
2306                    "relative_line_numbers": "enabled"
2307                }"#
2308                .unindent(),
2309            ),
2310        );
2311
2312        assert_migrate_settings_with_migrations(
2313            &[MigrationType::Json(
2314                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2315            )],
2316            &r#"{
2317                "relative_line_numbers": false
2318            }"#
2319            .unindent(),
2320            Some(
2321                &r#"{
2322                    "relative_line_numbers": "disabled"
2323                }"#
2324                .unindent(),
2325            ),
2326        );
2327
2328        // Platform key: settings nested inside "macos" should be migrated
2329        assert_migrate_settings_with_migrations(
2330            &[MigrationType::Json(
2331                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2332            )],
2333            &r#"
2334            {
2335                "macos": {
2336                    "relative_line_numbers": true
2337                }
2338            }
2339            "#
2340            .unindent(),
2341            Some(
2342                &r#"
2343                {
2344                    "macos": {
2345                        "relative_line_numbers": "enabled"
2346                    }
2347                }
2348                "#
2349                .unindent(),
2350            ),
2351        );
2352
2353        // Profile: settings nested inside profiles should be migrated
2354        assert_migrate_settings_with_migrations(
2355            &[MigrationType::Json(
2356                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2357            )],
2358            &r#"
2359            {
2360                "profiles": {
2361                    "dev": {
2362                        "relative_line_numbers": false
2363                    }
2364                }
2365            }
2366            "#
2367            .unindent(),
2368            Some(
2369                &r#"
2370                {
2371                    "profiles": {
2372                        "dev": {
2373                            "relative_line_numbers": "disabled"
2374                        }
2375                    }
2376                }
2377                "#
2378                .unindent(),
2379            ),
2380        );
2381    }
2382
2383    #[test]
2384    fn test_remove_context_server_source() {
2385        assert_migrate_settings(
2386            &r#"
2387            {
2388                "context_servers": {
2389                    "extension_server": {
2390                        "source": "extension",
2391                        "settings": {
2392                            "foo": "bar"
2393                        }
2394                    },
2395                    "custom_server": {
2396                        "source": "custom",
2397                        "command": "foo",
2398                        "args": ["bar"],
2399                        "env": {
2400                            "FOO": "BAR"
2401                        }
2402                    },
2403                }
2404            }
2405            "#
2406            .unindent(),
2407            Some(
2408                &r#"
2409                {
2410                    "context_servers": {
2411                        "extension_server": {
2412                            "settings": {
2413                                "foo": "bar"
2414                            }
2415                        },
2416                        "custom_server": {
2417                            "command": "foo",
2418                            "args": ["bar"],
2419                            "env": {
2420                                "FOO": "BAR"
2421                            }
2422                        },
2423                    }
2424                }
2425                "#
2426                .unindent(),
2427            ),
2428        );
2429
2430        // Platform key: settings nested inside "linux" should be migrated
2431        assert_migrate_settings_with_migrations(
2432            &[MigrationType::Json(
2433                migrations::m_2025_11_25::remove_context_server_source,
2434            )],
2435            &r#"
2436            {
2437                "linux": {
2438                    "context_servers": {
2439                        "my_server": {
2440                            "source": "extension",
2441                            "settings": {
2442                                "key": "value"
2443                            }
2444                        }
2445                    }
2446                }
2447            }
2448            "#
2449            .unindent(),
2450            Some(
2451                &r#"
2452                {
2453                    "linux": {
2454                        "context_servers": {
2455                            "my_server": {
2456                                "settings": {
2457                                    "key": "value"
2458                                }
2459                            }
2460                        }
2461                    }
2462                }
2463                "#
2464                .unindent(),
2465            ),
2466        );
2467
2468        // Profile: settings nested inside profiles should be migrated
2469        assert_migrate_settings_with_migrations(
2470            &[MigrationType::Json(
2471                migrations::m_2025_11_25::remove_context_server_source,
2472            )],
2473            &r#"
2474            {
2475                "profiles": {
2476                    "work": {
2477                        "context_servers": {
2478                            "my_server": {
2479                                "source": "custom",
2480                                "command": "foo",
2481                                "args": ["bar"]
2482                            }
2483                        }
2484                    }
2485                }
2486            }
2487            "#
2488            .unindent(),
2489            Some(
2490                &r#"
2491                {
2492                    "profiles": {
2493                        "work": {
2494                            "context_servers": {
2495                                "my_server": {
2496                                    "command": "foo",
2497                                    "args": ["bar"]
2498                                }
2499                            }
2500                        }
2501                    }
2502                }
2503                "#
2504                .unindent(),
2505            ),
2506        );
2507    }
2508
2509    #[test]
2510    fn test_project_panel_open_file_on_paste_migration() {
2511        assert_migrate_settings(
2512            &r#"
2513            {
2514                "project_panel": {
2515                    "open_file_on_paste": true
2516                }
2517            }
2518            "#
2519            .unindent(),
2520            Some(
2521                &r#"
2522                {
2523                    "project_panel": {
2524                        "auto_open": { "on_paste": true }
2525                    }
2526                }
2527                "#
2528                .unindent(),
2529            ),
2530        );
2531
2532        assert_migrate_settings(
2533            &r#"
2534            {
2535                "project_panel": {
2536                    "open_file_on_paste": false
2537                }
2538            }
2539            "#
2540            .unindent(),
2541            Some(
2542                &r#"
2543                {
2544                    "project_panel": {
2545                        "auto_open": { "on_paste": false }
2546                    }
2547                }
2548                "#
2549                .unindent(),
2550            ),
2551        );
2552    }
2553
2554    #[test]
2555    fn test_enable_preview_from_code_navigation_migration() {
2556        assert_migrate_settings(
2557            &r#"
2558            {
2559                "other_setting_1": 1,
2560                "preview_tabs": {
2561                    "other_setting_2": 2,
2562                    "enable_preview_from_code_navigation": false
2563                }
2564            }
2565            "#
2566            .unindent(),
2567            Some(
2568                &r#"
2569                {
2570                    "other_setting_1": 1,
2571                    "preview_tabs": {
2572                        "other_setting_2": 2,
2573                        "enable_keep_preview_on_code_navigation": false
2574                    }
2575                }
2576                "#
2577                .unindent(),
2578            ),
2579        );
2580
2581        assert_migrate_settings(
2582            &r#"
2583            {
2584                "other_setting_1": 1,
2585                "preview_tabs": {
2586                    "other_setting_2": 2,
2587                    "enable_preview_from_code_navigation": true
2588                }
2589            }
2590            "#
2591            .unindent(),
2592            Some(
2593                &r#"
2594                {
2595                    "other_setting_1": 1,
2596                    "preview_tabs": {
2597                        "other_setting_2": 2,
2598                        "enable_keep_preview_on_code_navigation": true
2599                    }
2600                }
2601                "#
2602                .unindent(),
2603            ),
2604        );
2605    }
2606
2607    #[test]
2608    fn test_move_edit_prediction_provider_to_edit_predictions() {
2609        assert_migrate_settings_with_migrations(
2610            &[MigrationType::Json(
2611                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2612            )],
2613            &r#"{ }"#.unindent(),
2614            None,
2615        );
2616
2617        assert_migrate_settings_with_migrations(
2618            &[MigrationType::Json(
2619                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2620            )],
2621            &r#"
2622            {
2623                "features": {
2624                    "edit_prediction_provider": "copilot"
2625                }
2626            }
2627            "#
2628            .unindent(),
2629            Some(
2630                &r#"
2631                {
2632                    "edit_predictions": {
2633                        "provider": "copilot"
2634                    }
2635                }
2636                "#
2637                .unindent(),
2638            ),
2639        );
2640
2641        assert_migrate_settings_with_migrations(
2642            &[MigrationType::Json(
2643                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2644            )],
2645            &r#"
2646            {
2647                "features": {
2648                    "edit_prediction_provider": "zed"
2649                },
2650                "edit_predictions": {
2651                    "mode": "eager"
2652                }
2653            }
2654            "#
2655            .unindent(),
2656            Some(
2657                &r#"
2658                {
2659                    "edit_predictions": {
2660                        "provider": "zed",
2661                        "mode": "eager"
2662                    }
2663                }
2664                "#
2665                .unindent(),
2666            ),
2667        );
2668
2669        assert_migrate_settings_with_migrations(
2670            &[MigrationType::Json(
2671                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2672            )],
2673            &r#"
2674            {
2675                "features": {
2676                    "edit_prediction_provider": "supermaven"
2677                },
2678                "edit_predictions": {
2679                    "provider": "copilot"
2680                }
2681            }
2682            "#
2683            .unindent(),
2684            Some(
2685                &r#"
2686                {
2687                    "edit_predictions": {
2688                        "provider": "copilot"
2689                    }
2690                }
2691                "#
2692                .unindent(),
2693            ),
2694        );
2695
2696        assert_migrate_settings_with_migrations(
2697            &[MigrationType::Json(
2698                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2699            )],
2700            &r#"
2701            {
2702                "edit_predictions": {
2703                    "provider": "zed"
2704                }
2705            }
2706            "#
2707            .unindent(),
2708            None,
2709        );
2710
2711        // Platform key: settings nested inside "macos" should be migrated
2712        assert_migrate_settings_with_migrations(
2713            &[MigrationType::Json(
2714                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2715            )],
2716            &r#"
2717            {
2718                "macos": {
2719                    "features": {
2720                        "edit_prediction_provider": "copilot"
2721                    }
2722                }
2723            }
2724            "#
2725            .unindent(),
2726            Some(
2727                &r#"
2728                {
2729                    "macos": {
2730                        "edit_predictions": {
2731                            "provider": "copilot"
2732                        }
2733                    }
2734                }
2735                "#
2736                .unindent(),
2737            ),
2738        );
2739
2740        // Profile: settings nested inside profiles should be migrated
2741        assert_migrate_settings_with_migrations(
2742            &[MigrationType::Json(
2743                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2744            )],
2745            &r#"
2746            {
2747                "profiles": {
2748                    "work": {
2749                        "features": {
2750                            "edit_prediction_provider": "copilot"
2751                        }
2752                    }
2753                }
2754            }
2755            "#
2756            .unindent(),
2757            Some(
2758                &r#"
2759                {
2760                    "profiles": {
2761                        "work": {
2762                            "edit_predictions": {
2763                                "provider": "copilot"
2764                            }
2765                        }
2766                    }
2767                }
2768                "#
2769                .unindent(),
2770            ),
2771        );
2772
2773        // Combined: root + platform + profile should all be migrated simultaneously
2774        assert_migrate_settings_with_migrations(
2775            &[MigrationType::Json(
2776                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2777            )],
2778            &r#"
2779            {
2780                "features": {
2781                    "edit_prediction_provider": "copilot"
2782                },
2783                "macos": {
2784                    "features": {
2785                        "edit_prediction_provider": "zed"
2786                    }
2787                },
2788                "profiles": {
2789                    "work": {
2790                        "features": {
2791                            "edit_prediction_provider": "supermaven"
2792                        }
2793                    }
2794                }
2795            }
2796            "#
2797            .unindent(),
2798            Some(
2799                &r#"
2800                {
2801                    "edit_predictions": {
2802                        "provider": "copilot"
2803                    },
2804                    "macos": {
2805                        "edit_predictions": {
2806                            "provider": "zed"
2807                        }
2808                    },
2809                    "profiles": {
2810                        "work": {
2811                            "edit_predictions": {
2812                                "provider": "supermaven"
2813                            }
2814                        }
2815                    }
2816                }
2817                "#
2818                .unindent(),
2819            ),
2820        );
2821    }
2822
2823    #[test]
2824    fn test_migrate_experimental_sweep_mercury() {
2825        assert_migrate_settings_with_migrations(
2826            &[MigrationType::Json(
2827                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2828            )],
2829            &r#"{ }"#.unindent(),
2830            None,
2831        );
2832
2833        assert_migrate_settings_with_migrations(
2834            &[MigrationType::Json(
2835                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2836            )],
2837            &r#"
2838            {
2839                "edit_predictions": {
2840                    "provider": {
2841                        "experimental": "sweep"
2842                    }
2843                }
2844            }
2845            "#
2846            .unindent(),
2847            Some(
2848                &r#"
2849                {
2850                    "edit_predictions": {
2851                        "provider": "sweep"
2852                    }
2853                }
2854                "#
2855                .unindent(),
2856            ),
2857        );
2858
2859        assert_migrate_settings_with_migrations(
2860            &[MigrationType::Json(
2861                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2862            )],
2863            &r#"
2864            {
2865                "edit_predictions": {
2866                    "provider": {
2867                        "experimental": "mercury"
2868                    }
2869                }
2870            }
2871            "#
2872            .unindent(),
2873            Some(
2874                &r#"
2875                {
2876                    "edit_predictions": {
2877                        "provider": "mercury"
2878                    }
2879                }
2880                "#
2881                .unindent(),
2882            ),
2883        );
2884
2885        assert_migrate_settings_with_migrations(
2886            &[MigrationType::Json(
2887                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2888            )],
2889            &r#"
2890            {
2891                "features": {
2892                    "edit_prediction_provider": {
2893                        "experimental": "sweep"
2894                    }
2895                }
2896            }
2897            "#
2898            .unindent(),
2899            Some(
2900                &r#"
2901                {
2902                    "features": {
2903                        "edit_prediction_provider": "sweep"
2904                    }
2905                }
2906                "#
2907                .unindent(),
2908            ),
2909        );
2910
2911        assert_migrate_settings_with_migrations(
2912            &[MigrationType::Json(
2913                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2914            )],
2915            &r#"
2916            {
2917                "edit_predictions": {
2918                    "provider": "zed"
2919                }
2920            }
2921            "#
2922            .unindent(),
2923            None,
2924        );
2925
2926        assert_migrate_settings_with_migrations(
2927            &[MigrationType::Json(
2928                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2929            )],
2930            &r#"
2931            {
2932                "edit_predictions": {
2933                    "provider": {
2934                        "experimental": "zeta2"
2935                    }
2936                }
2937            }
2938            "#
2939            .unindent(),
2940            None,
2941        );
2942
2943        // Platform key: settings nested inside "linux" should be migrated
2944        assert_migrate_settings_with_migrations(
2945            &[MigrationType::Json(
2946                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2947            )],
2948            &r#"
2949            {
2950                "linux": {
2951                    "edit_predictions": {
2952                        "provider": {
2953                            "experimental": "sweep"
2954                        }
2955                    }
2956                }
2957            }
2958            "#
2959            .unindent(),
2960            Some(
2961                &r#"
2962                {
2963                    "linux": {
2964                        "edit_predictions": {
2965                            "provider": "sweep"
2966                        }
2967                    }
2968                }
2969                "#
2970                .unindent(),
2971            ),
2972        );
2973
2974        // Profile: settings nested inside profiles should be migrated
2975        assert_migrate_settings_with_migrations(
2976            &[MigrationType::Json(
2977                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2978            )],
2979            &r#"
2980            {
2981                "profiles": {
2982                    "dev": {
2983                        "edit_predictions": {
2984                            "provider": {
2985                                "experimental": "mercury"
2986                            }
2987                        }
2988                    }
2989                }
2990            }
2991            "#
2992            .unindent(),
2993            Some(
2994                &r#"
2995                {
2996                    "profiles": {
2997                        "dev": {
2998                            "edit_predictions": {
2999                                "provider": "mercury"
3000                            }
3001                        }
3002                    }
3003                }
3004                "#
3005                .unindent(),
3006            ),
3007        );
3008
3009        // Combined: root + platform + profile should all be migrated simultaneously
3010        assert_migrate_settings_with_migrations(
3011            &[MigrationType::Json(
3012                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3013            )],
3014            &r#"
3015            {
3016                "edit_predictions": {
3017                    "provider": {
3018                        "experimental": "sweep"
3019                    }
3020                },
3021                "linux": {
3022                    "edit_predictions": {
3023                        "provider": {
3024                            "experimental": "mercury"
3025                        }
3026                    }
3027                },
3028                "profiles": {
3029                    "dev": {
3030                        "edit_predictions": {
3031                            "provider": {
3032                                "experimental": "sweep"
3033                            }
3034                        }
3035                    }
3036                }
3037            }
3038            "#
3039            .unindent(),
3040            Some(
3041                &r#"
3042                {
3043                    "edit_predictions": {
3044                        "provider": "sweep"
3045                    },
3046                    "linux": {
3047                        "edit_predictions": {
3048                            "provider": "mercury"
3049                        }
3050                    },
3051                    "profiles": {
3052                        "dev": {
3053                            "edit_predictions": {
3054                                "provider": "sweep"
3055                            }
3056                        }
3057                    }
3058                }
3059                "#
3060                .unindent(),
3061            ),
3062        );
3063    }
3064}