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