migrator.rs

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