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