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