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