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                        "auto"
2015                    ]
2016                }
2017                "#
2018                .unindent(),
2019            ),
2020        );
2021    }
2022
2023    #[test]
2024    fn test_code_actions_on_format_migration_filters_false_values() {
2025        assert_migrate_settings_with_migrations(
2026            &[MigrationType::Json(
2027                migrations::m_2025_10_10::remove_code_actions_on_format,
2028            )],
2029            &r#"{
2030        "code_actions_on_format": {
2031          "a": true,
2032          "b": false,
2033          "c": true
2034        }
2035      }"#
2036            .unindent(),
2037            Some(
2038                &r#"{
2039          "formatter": [
2040            {
2041              "code_action": "a"
2042            },
2043            {
2044              "code_action": "c"
2045            },
2046            "auto"
2047          ]
2048        }
2049        "#
2050                .unindent(),
2051            ),
2052        );
2053    }
2054
2055    #[test]
2056    fn test_code_actions_on_format_migration_with_existing_formatter_object() {
2057        assert_migrate_settings_with_migrations(
2058            &[MigrationType::Json(
2059                migrations::m_2025_10_10::remove_code_actions_on_format,
2060            )],
2061            &r#"{
2062              "formatter": "prettier",
2063              "code_actions_on_format": {
2064                "source.organizeImports": true
2065              }
2066            }"#
2067            .unindent(),
2068            Some(
2069                &r#"{
2070                  "formatter": [
2071                    {
2072                      "code_action": "source.organizeImports"
2073                    },
2074                    "prettier"
2075                  ]
2076                }"#
2077                .unindent(),
2078            ),
2079        );
2080    }
2081
2082    #[test]
2083    fn test_code_actions_on_format_migration_with_existing_formatter_array() {
2084        assert_migrate_settings_with_migrations(
2085            &[MigrationType::Json(
2086                migrations::m_2025_10_10::remove_code_actions_on_format,
2087            )],
2088            &r#"{
2089              "formatter": ["prettier", {"language_server": "eslint"}],
2090              "code_actions_on_format": {
2091                "source.organizeImports": true,
2092                "source.fixAll": true
2093              }
2094            }"#
2095            .unindent(),
2096            Some(
2097                &r#"{
2098                  "formatter": [
2099                    {
2100                      "code_action": "source.organizeImports"
2101                    },
2102                    {
2103                      "code_action": "source.fixAll"
2104                    },
2105                    "prettier",
2106                    {
2107                      "language_server": "eslint"
2108                    }
2109                  ]
2110                }"#
2111                .unindent(),
2112            ),
2113        );
2114    }
2115
2116    #[test]
2117    fn test_code_actions_on_format_migration_in_languages() {
2118        assert_migrate_settings_with_migrations(
2119            &[MigrationType::Json(
2120                migrations::m_2025_10_10::remove_code_actions_on_format,
2121            )],
2122            &r#"{
2123                "languages": {
2124                    "JavaScript": {
2125                        "code_actions_on_format": {
2126                            "source.fixAll.eslint": true
2127                        }
2128                    },
2129                    "Go": {
2130                        "code_actions_on_format": {
2131                            "source.organizeImports": true
2132                        },
2133                    }
2134                }
2135            }"#
2136            .unindent(),
2137            Some(
2138                &r#"{
2139                    "languages": {
2140                        "JavaScript": {
2141                            "formatter": [
2142                                {
2143                                    "code_action": "source.fixAll.eslint"
2144                                },
2145                                "auto"
2146                            ]
2147                        },
2148                        "Go": {
2149                            "formatter": [
2150                                {
2151                                    "code_action": "source.organizeImports"
2152                                },
2153                                {
2154                                    "code_action": "source.organizeImports"
2155                                },
2156                                "language_server"
2157                            ]
2158                        }
2159                    }
2160                }"#
2161                .unindent(),
2162            ),
2163        );
2164    }
2165
2166    #[test]
2167    fn test_code_actions_on_format_migration_in_languages_with_existing_formatter() {
2168        assert_migrate_settings_with_migrations(
2169            &[MigrationType::Json(
2170                migrations::m_2025_10_10::remove_code_actions_on_format,
2171            )],
2172            &r#"{
2173              "languages": {
2174                "JavaScript": {
2175                  "formatter": "prettier",
2176                  "code_actions_on_format": {
2177                    "source.fixAll.eslint": true,
2178                    "source.organizeImports": false
2179                  }
2180                }
2181              }
2182            }"#
2183            .unindent(),
2184            Some(
2185                &r#"{
2186                  "languages": {
2187                    "JavaScript": {
2188                      "formatter": [
2189                        {
2190                          "code_action": "source.fixAll.eslint"
2191                        },
2192                        "prettier"
2193                      ]
2194                    }
2195                  }
2196                }"#
2197                .unindent(),
2198            ),
2199        );
2200    }
2201
2202    #[test]
2203    fn test_code_actions_on_format_migration_mixed_global_and_languages() {
2204        assert_migrate_settings_with_migrations(
2205            &[MigrationType::Json(
2206                migrations::m_2025_10_10::remove_code_actions_on_format,
2207            )],
2208            &r#"{
2209              "formatter": "prettier",
2210              "code_actions_on_format": {
2211                "source.fixAll": true
2212              },
2213              "languages": {
2214                "Rust": {
2215                  "formatter": "rust-analyzer",
2216                  "code_actions_on_format": {
2217                    "source.organizeImports": true
2218                  }
2219                },
2220                "Python": {
2221                  "code_actions_on_format": {
2222                    "source.organizeImports": true,
2223                    "source.fixAll": false
2224                  }
2225                }
2226              }
2227            }"#
2228            .unindent(),
2229            Some(
2230                &r#"{
2231                  "formatter": [
2232                    {
2233                      "code_action": "source.fixAll"
2234                    },
2235                    "prettier"
2236                  ],
2237                  "languages": {
2238                    "Rust": {
2239                      "formatter": [
2240                        {
2241                          "code_action": "source.organizeImports"
2242                        },
2243                        {
2244                          "code_action": "source.fixAll"
2245                        },
2246                        "rust-analyzer"
2247                      ]
2248                    },
2249                    "Python": {
2250                      "formatter": [
2251                        {
2252                          "code_action": "source.organizeImports"
2253                        },
2254                        {
2255                          "code_action": "source.fixAll"
2256                        },
2257                        {
2258                          "code_action": "source.fixAll"
2259                        },
2260                        "prettier"
2261                      ]
2262                    }
2263                  }
2264                }"#
2265                .unindent(),
2266            ),
2267        );
2268    }
2269
2270    #[test]
2271    fn test_code_actions_on_format_inserts_default_formatters() {
2272        assert_migrate_settings_with_migrations(
2273            &[MigrationType::Json(
2274                migrations::m_2025_10_10::remove_code_actions_on_format,
2275            )],
2276            &r#"{
2277            "code_actions_on_format": {
2278                "source.organizeImports": false,
2279                "source.fixAll.eslint": true
2280            }
2281        }"#
2282            .unindent(),
2283            Some(
2284                &r#"
2285        {
2286            "formatter": [
2287                {
2288                    "code_action": "source.fixAll.eslint"
2289                },
2290                "auto"
2291            ]
2292        }
2293        "#
2294                .unindent(),
2295            ),
2296        )
2297    }
2298
2299    #[test]
2300    fn test_code_actions_on_format_no_migration_when_not_present() {
2301        assert_migrate_settings_with_migrations(
2302            &[MigrationType::Json(
2303                migrations::m_2025_10_10::remove_code_actions_on_format,
2304            )],
2305            &r#"{
2306              "formatter": ["prettier"]
2307            }"#
2308            .unindent(),
2309            None,
2310        );
2311    }
2312
2313    #[test]
2314    fn test_code_actions_on_format_migration_all_false_values() {
2315        assert_migrate_settings_with_migrations(
2316            &[MigrationType::Json(
2317                migrations::m_2025_10_10::remove_code_actions_on_format,
2318            )],
2319            &r#"{
2320                "code_actions_on_format": {
2321                    "a": false,
2322                    "b": false
2323                },
2324                "formatter": "prettier"
2325            }"#
2326            .unindent(),
2327            Some(
2328                &r#"{
2329                    "formatter": "prettier"
2330                }"#
2331                .unindent(),
2332            ),
2333        );
2334    }
2335
2336    #[test]
2337    fn test_code_action_formatters_issue() {
2338        assert_migrate_settings_with_migrations(
2339            &[MigrationType::Json(
2340                migrations::m_2025_10_01::flatten_code_actions_formatters,
2341            )],
2342            &r#"
2343    {
2344      "languages": {
2345        "Python": {
2346          "language_servers": ["ruff"],
2347          "format_on_save": "on",
2348          "formatter": [
2349            {
2350              "code_actions": {
2351                // Fix all auto-fixable lint violations
2352                "source.fixAll.ruff": true,
2353                // Organize imports
2354                "source.organizeImports.ruff": true
2355              }
2356            }
2357          ]
2358        }
2359      }
2360    }"#
2361            .unindent(),
2362            Some(
2363                &r#"
2364    {
2365      "languages": {
2366        "Python": {
2367          "language_servers": ["ruff"],
2368          "format_on_save": "on",
2369          "formatter": [
2370            {
2371              "code_action": "source.fixAll.ruff"
2372            },
2373            {
2374              "code_action": "source.organizeImports.ruff"
2375            }
2376          ]
2377        }
2378      }
2379    }"#
2380                .unindent(),
2381            ),
2382        );
2383    }
2384}