migrator.rs

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