migrator.rs

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