migrator.rs

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