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