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