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