migrator.rs

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