migrator.rs

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