migrator.rs

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