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