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