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_06_16::SETTINGS_PATTERNS,
 195            &SETTINGS_QUERY_2025_06_16,
 196        ),
 197        MigrationType::TreeSitter(
 198            migrations::m_2025_06_25::SETTINGS_PATTERNS,
 199            &SETTINGS_QUERY_2025_06_25,
 200        ),
 201        MigrationType::TreeSitter(
 202            migrations::m_2025_06_27::SETTINGS_PATTERNS,
 203            &SETTINGS_QUERY_2025_06_27,
 204        ),
 205        MigrationType::TreeSitter(
 206            migrations::m_2025_07_08::SETTINGS_PATTERNS,
 207            &SETTINGS_QUERY_2025_07_08,
 208        ),
 209        MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters),
 210        MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save),
 211        MigrationType::TreeSitter(
 212            migrations::m_2025_10_03::SETTINGS_PATTERNS,
 213            &SETTINGS_QUERY_2025_10_03,
 214        ),
 215        MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
 216        MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum),
 217        MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum),
 218        MigrationType::TreeSitter(
 219            migrations::m_2025_11_12::SETTINGS_PATTERNS,
 220            &SETTINGS_QUERY_2025_11_12,
 221        ),
 222        MigrationType::TreeSitter(
 223            migrations::m_2025_12_01::SETTINGS_PATTERNS,
 224            &SETTINGS_QUERY_2025_12_01,
 225        ),
 226        MigrationType::TreeSitter(
 227            migrations::m_2025_11_20::SETTINGS_PATTERNS,
 228            &SETTINGS_QUERY_2025_11_20,
 229        ),
 230        MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source),
 231        MigrationType::TreeSitter(
 232            migrations::m_2025_12_15::SETTINGS_PATTERNS,
 233            &SETTINGS_QUERY_2025_12_15,
 234        ),
 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_06_16,
 331    migrations::m_2025_06_16::SETTINGS_PATTERNS
 332);
 333define_query!(
 334    SETTINGS_QUERY_2025_06_25,
 335    migrations::m_2025_06_25::SETTINGS_PATTERNS
 336);
 337define_query!(
 338    SETTINGS_QUERY_2025_06_27,
 339    migrations::m_2025_06_27::SETTINGS_PATTERNS
 340);
 341define_query!(
 342    SETTINGS_QUERY_2025_07_08,
 343    migrations::m_2025_07_08::SETTINGS_PATTERNS
 344);
 345define_query!(
 346    SETTINGS_QUERY_2025_10_03,
 347    migrations::m_2025_10_03::SETTINGS_PATTERNS
 348);
 349define_query!(
 350    SETTINGS_QUERY_2025_11_12,
 351    migrations::m_2025_11_12::SETTINGS_PATTERNS
 352);
 353define_query!(
 354    SETTINGS_QUERY_2025_12_01,
 355    migrations::m_2025_12_01::SETTINGS_PATTERNS
 356);
 357define_query!(
 358    SETTINGS_QUERY_2025_11_20,
 359    migrations::m_2025_11_20::SETTINGS_PATTERNS
 360);
 361define_query!(
 362    KEYMAP_QUERY_2025_12_08,
 363    migrations::m_2025_12_08::KEYMAP_PATTERNS
 364);
 365define_query!(
 366    SETTINGS_QUERY_2025_12_15,
 367    migrations::m_2025_12_15::SETTINGS_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_mcp_settings_migration() {
 960        assert_migrate_settings_with_migrations(
 961            &[MigrationType::TreeSitter(
 962                migrations::m_2025_06_16::SETTINGS_PATTERNS,
 963                &SETTINGS_QUERY_2025_06_16,
 964            )],
 965            r#"{
 966    "context_servers": {
 967        "empty_server": {},
 968        "extension_server": {
 969            "settings": {
 970                "foo": "bar"
 971            }
 972        },
 973        "custom_server": {
 974            "command": {
 975                "path": "foo",
 976                "args": ["bar"],
 977                "env": {
 978                    "FOO": "BAR"
 979                }
 980            }
 981        },
 982        "invalid_server": {
 983            "command": {
 984                "path": "foo",
 985                "args": ["bar"],
 986                "env": {
 987                    "FOO": "BAR"
 988                }
 989            },
 990            "settings": {
 991                "foo": "bar"
 992            }
 993        },
 994        "empty_server2": {},
 995        "extension_server2": {
 996            "foo": "bar",
 997            "settings": {
 998                "foo": "bar"
 999            },
1000            "bar": "foo"
1001        },
1002        "custom_server2": {
1003            "foo": "bar",
1004            "command": {
1005                "path": "foo",
1006                "args": ["bar"],
1007                "env": {
1008                    "FOO": "BAR"
1009                }
1010            },
1011            "bar": "foo"
1012        },
1013        "invalid_server2": {
1014            "foo": "bar",
1015            "command": {
1016                "path": "foo",
1017                "args": ["bar"],
1018                "env": {
1019                    "FOO": "BAR"
1020                }
1021            },
1022            "bar": "foo",
1023            "settings": {
1024                "foo": "bar"
1025            }
1026        }
1027    }
1028}"#,
1029            Some(
1030                r#"{
1031    "context_servers": {
1032        "empty_server": {
1033            "source": "extension",
1034            "settings": {}
1035        },
1036        "extension_server": {
1037            "source": "extension",
1038            "settings": {
1039                "foo": "bar"
1040            }
1041        },
1042        "custom_server": {
1043            "source": "custom",
1044            "command": {
1045                "path": "foo",
1046                "args": ["bar"],
1047                "env": {
1048                    "FOO": "BAR"
1049                }
1050            }
1051        },
1052        "invalid_server": {
1053            "source": "custom",
1054            "command": {
1055                "path": "foo",
1056                "args": ["bar"],
1057                "env": {
1058                    "FOO": "BAR"
1059                }
1060            },
1061            "settings": {
1062                "foo": "bar"
1063            }
1064        },
1065        "empty_server2": {
1066            "source": "extension",
1067            "settings": {}
1068        },
1069        "extension_server2": {
1070            "source": "extension",
1071            "foo": "bar",
1072            "settings": {
1073                "foo": "bar"
1074            },
1075            "bar": "foo"
1076        },
1077        "custom_server2": {
1078            "source": "custom",
1079            "foo": "bar",
1080            "command": {
1081                "path": "foo",
1082                "args": ["bar"],
1083                "env": {
1084                    "FOO": "BAR"
1085                }
1086            },
1087            "bar": "foo"
1088        },
1089        "invalid_server2": {
1090            "source": "custom",
1091            "foo": "bar",
1092            "command": {
1093                "path": "foo",
1094                "args": ["bar"],
1095                "env": {
1096                    "FOO": "BAR"
1097                }
1098            },
1099            "bar": "foo",
1100            "settings": {
1101                "foo": "bar"
1102            }
1103        }
1104    }
1105}"#,
1106            ),
1107        );
1108    }
1109
1110    #[test]
1111    fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1112        let settings = r#"{
1113    "context_servers": {
1114        "empty_server": {
1115            "source": "extension",
1116            "settings": {}
1117        },
1118        "extension_server": {
1119            "source": "extension",
1120            "settings": {
1121                "foo": "bar"
1122            }
1123        },
1124        "custom_server": {
1125            "source": "custom",
1126            "command": {
1127                "path": "foo",
1128                "args": ["bar"],
1129                "env": {
1130                    "FOO": "BAR"
1131                }
1132            }
1133        },
1134        "invalid_server": {
1135            "source": "custom",
1136            "command": {
1137                "path": "foo",
1138                "args": ["bar"],
1139                "env": {
1140                    "FOO": "BAR"
1141                }
1142            },
1143            "settings": {
1144                "foo": "bar"
1145            }
1146        }
1147    }
1148}"#;
1149        assert_migrate_settings_with_migrations(
1150            &[MigrationType::TreeSitter(
1151                migrations::m_2025_06_16::SETTINGS_PATTERNS,
1152                &SETTINGS_QUERY_2025_06_16,
1153            )],
1154            settings,
1155            None,
1156        );
1157    }
1158
1159    #[test]
1160    fn test_custom_agent_server_settings_migration() {
1161        assert_migrate_settings_with_migrations(
1162            &[MigrationType::TreeSitter(
1163                migrations::m_2025_11_20::SETTINGS_PATTERNS,
1164                &SETTINGS_QUERY_2025_11_20,
1165            )],
1166            r#"{
1167    "agent_servers": {
1168        "gemini": {
1169            "default_model": "gemini-1.5-pro"
1170        },
1171        "claude": {},
1172        "codex": {},
1173        "my-custom-agent": {
1174            "command": "/path/to/agent",
1175            "args": ["--foo"],
1176            "default_model": "my-model"
1177        },
1178        "already-migrated-agent": {
1179            "type": "custom",
1180            "command": "/path/to/agent"
1181        },
1182        "future-extension-agent": {
1183            "type": "extension",
1184            "default_model": "ext-model"
1185        }
1186    }
1187}"#,
1188            Some(
1189                r#"{
1190    "agent_servers": {
1191        "gemini": {
1192            "default_model": "gemini-1.5-pro"
1193        },
1194        "claude": {},
1195        "codex": {},
1196        "my-custom-agent": {
1197            "type": "custom",
1198            "command": "/path/to/agent",
1199            "args": ["--foo"],
1200            "default_model": "my-model"
1201        },
1202        "already-migrated-agent": {
1203            "type": "custom",
1204            "command": "/path/to/agent"
1205        },
1206        "future-extension-agent": {
1207            "type": "extension",
1208            "default_model": "ext-model"
1209        }
1210    }
1211}"#,
1212            ),
1213        );
1214    }
1215
1216    #[test]
1217    fn test_remove_version_fields() {
1218        assert_migrate_settings(
1219            r#"{
1220    "language_models": {
1221        "anthropic": {
1222            "version": "1",
1223            "api_url": "https://api.anthropic.com"
1224        },
1225        "openai": {
1226            "version": "1",
1227            "api_url": "https://api.openai.com/v1"
1228        }
1229    },
1230    "agent": {
1231        "version": "2",
1232        "enabled": true,
1233        "button": true,
1234        "dock": "right",
1235        "default_width": 640,
1236        "default_height": 320,
1237        "default_model": {
1238            "provider": "zed.dev",
1239            "model": "claude-sonnet-4"
1240        }
1241    }
1242}"#,
1243            Some(
1244                r#"{
1245    "language_models": {
1246        "anthropic": {
1247            "api_url": "https://api.anthropic.com"
1248        },
1249        "openai": {
1250            "api_url": "https://api.openai.com/v1"
1251        }
1252    },
1253    "agent": {
1254        "enabled": true,
1255        "button": true,
1256        "dock": "right",
1257        "default_width": 640,
1258        "default_height": 320,
1259        "default_model": {
1260            "provider": "zed.dev",
1261            "model": "claude-sonnet-4"
1262        }
1263    }
1264}"#,
1265            ),
1266        );
1267
1268        // Test that version fields in other contexts are not removed
1269        assert_migrate_settings(
1270            r#"{
1271    "language_models": {
1272        "other_provider": {
1273            "version": "1",
1274            "api_url": "https://api.example.com"
1275        }
1276    },
1277    "other_section": {
1278        "version": "1"
1279    }
1280}"#,
1281            None,
1282        );
1283    }
1284
1285    #[test]
1286    fn test_flatten_context_server_command() {
1287        assert_migrate_settings(
1288            r#"{
1289    "context_servers": {
1290        "some-mcp-server": {
1291            "command": {
1292                "path": "npx",
1293                "args": [
1294                    "-y",
1295                    "@supabase/mcp-server-supabase@latest",
1296                    "--read-only",
1297                    "--project-ref=<project-ref>"
1298                ],
1299                "env": {
1300                    "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1301                }
1302            }
1303        }
1304    }
1305}"#,
1306            Some(
1307                r#"{
1308    "context_servers": {
1309        "some-mcp-server": {
1310            "command": "npx",
1311            "args": [
1312                "-y",
1313                "@supabase/mcp-server-supabase@latest",
1314                "--read-only",
1315                "--project-ref=<project-ref>"
1316            ],
1317            "env": {
1318                "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1319            }
1320        }
1321    }
1322}"#,
1323            ),
1324        );
1325
1326        // Test with additional keys in server object
1327        assert_migrate_settings(
1328            r#"{
1329    "context_servers": {
1330        "server-with-extras": {
1331            "command": {
1332                "path": "/usr/bin/node",
1333                "args": ["server.js"]
1334            },
1335            "settings": {}
1336        }
1337    }
1338}"#,
1339            Some(
1340                r#"{
1341    "context_servers": {
1342        "server-with-extras": {
1343            "command": "/usr/bin/node",
1344            "args": ["server.js"],
1345            "settings": {}
1346        }
1347    }
1348}"#,
1349            ),
1350        );
1351
1352        // Test command without args or env
1353        assert_migrate_settings(
1354            r#"{
1355    "context_servers": {
1356        "simple-server": {
1357            "command": {
1358                "path": "simple-mcp-server"
1359            }
1360        }
1361    }
1362}"#,
1363            Some(
1364                r#"{
1365    "context_servers": {
1366        "simple-server": {
1367            "command": "simple-mcp-server"
1368        }
1369    }
1370}"#,
1371            ),
1372        );
1373    }
1374
1375    #[test]
1376    fn test_flatten_code_action_formatters_basic_array() {
1377        assert_migrate_settings_with_migrations(
1378            &[MigrationType::Json(
1379                migrations::m_2025_10_01::flatten_code_actions_formatters,
1380            )],
1381            &r#"{
1382        "formatter": [
1383          {
1384            "code_actions": {
1385              "included-1": true,
1386              "included-2": true,
1387              "excluded": false,
1388            }
1389          }
1390        ]
1391      }"#
1392            .unindent(),
1393            Some(
1394                &r#"{
1395        "formatter": [
1396          {
1397            "code_action": "included-1"
1398          },
1399          {
1400            "code_action": "included-2"
1401          }
1402        ]
1403      }"#
1404                .unindent(),
1405            ),
1406        );
1407    }
1408
1409    #[test]
1410    fn test_flatten_code_action_formatters_basic_object() {
1411        assert_migrate_settings_with_migrations(
1412            &[MigrationType::Json(
1413                migrations::m_2025_10_01::flatten_code_actions_formatters,
1414            )],
1415            &r#"{
1416        "formatter": {
1417          "code_actions": {
1418            "included-1": true,
1419            "excluded": false,
1420            "included-2": true
1421          }
1422        }
1423      }"#
1424            .unindent(),
1425            Some(
1426                &r#"{
1427                  "formatter": [
1428                    {
1429                      "code_action": "included-1"
1430                    },
1431                    {
1432                      "code_action": "included-2"
1433                    }
1434                  ]
1435                }"#
1436                .unindent(),
1437            ),
1438        );
1439    }
1440
1441    #[test]
1442    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1443        assert_migrate_settings(
1444            &r#"{
1445          "formatter": [
1446            {
1447               "code_actions": {
1448                  "included-1": true,
1449                  "included-2": true,
1450                  "excluded": false,
1451               }
1452            },
1453            {
1454              "language_server": "ruff"
1455            },
1456            {
1457               "code_actions": {
1458                  "excluded": false,
1459                  "excluded-2": false,
1460               }
1461            }
1462            // some comment
1463            ,
1464            {
1465               "code_actions": {
1466                "excluded": false,
1467                "included-3": true,
1468                "included-4": true,
1469               }
1470            },
1471          ]
1472        }"#
1473            .unindent(),
1474            Some(
1475                &r#"{
1476        "formatter": [
1477          {
1478            "code_action": "included-1"
1479          },
1480          {
1481            "code_action": "included-2"
1482          },
1483          {
1484            "language_server": "ruff"
1485          },
1486          {
1487            "code_action": "included-3"
1488          },
1489          {
1490            "code_action": "included-4"
1491          }
1492        ]
1493      }"#
1494                .unindent(),
1495            ),
1496        );
1497    }
1498
1499    #[test]
1500    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1501        assert_migrate_settings(
1502            &r#"{
1503        "languages": {
1504          "Rust": {
1505            "formatter": [
1506              {
1507                "code_actions": {
1508                  "included-1": true,
1509                  "included-2": true,
1510                  "excluded": false,
1511                }
1512              },
1513              {
1514                "language_server": "ruff"
1515              },
1516              {
1517                "code_actions": {
1518                  "excluded": false,
1519                  "excluded-2": false,
1520                }
1521              }
1522              // some comment
1523              ,
1524              {
1525                "code_actions": {
1526                  "excluded": false,
1527                  "included-3": true,
1528                  "included-4": true,
1529                }
1530              },
1531            ]
1532          }
1533        }
1534      }"#
1535            .unindent(),
1536            Some(
1537                &r#"{
1538          "languages": {
1539            "Rust": {
1540              "formatter": [
1541                {
1542                  "code_action": "included-1"
1543                },
1544                {
1545                  "code_action": "included-2"
1546                },
1547                {
1548                  "language_server": "ruff"
1549                },
1550                {
1551                  "code_action": "included-3"
1552                },
1553                {
1554                  "code_action": "included-4"
1555                }
1556              ]
1557            }
1558          }
1559        }"#
1560                .unindent(),
1561            ),
1562        );
1563    }
1564
1565    #[test]
1566    fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1567     {
1568        assert_migrate_settings_with_migrations(
1569            &[MigrationType::Json(
1570                migrations::m_2025_10_01::flatten_code_actions_formatters,
1571            )],
1572            &r#"{
1573        "formatter": {
1574          "code_actions": {
1575            "default-1": true,
1576            "default-2": true,
1577            "default-3": true,
1578            "default-4": true,
1579          }
1580        },
1581        "languages": {
1582          "Rust": {
1583            "formatter": [
1584              {
1585                "code_actions": {
1586                  "included-1": true,
1587                  "included-2": true,
1588                  "excluded": false,
1589                }
1590              },
1591              {
1592                "language_server": "ruff"
1593              },
1594              {
1595                "code_actions": {
1596                  "excluded": false,
1597                  "excluded-2": false,
1598                }
1599              }
1600              // some comment
1601              ,
1602              {
1603                "code_actions": {
1604                  "excluded": false,
1605                  "included-3": true,
1606                  "included-4": true,
1607                }
1608              },
1609            ]
1610          },
1611          "Python": {
1612            "formatter": [
1613              {
1614                "language_server": "ruff"
1615              },
1616              {
1617                "code_actions": {
1618                  "excluded": false,
1619                  "excluded-2": false,
1620                }
1621              }
1622              // some comment
1623              ,
1624              {
1625                "code_actions": {
1626                  "excluded": false,
1627                  "included-3": true,
1628                  "included-4": true,
1629                }
1630              },
1631            ]
1632          }
1633        }
1634      }"#
1635            .unindent(),
1636            Some(
1637                &r#"{
1638          "formatter": [
1639            {
1640              "code_action": "default-1"
1641            },
1642            {
1643              "code_action": "default-2"
1644            },
1645            {
1646              "code_action": "default-3"
1647            },
1648            {
1649              "code_action": "default-4"
1650            }
1651          ],
1652          "languages": {
1653            "Rust": {
1654              "formatter": [
1655                {
1656                  "code_action": "included-1"
1657                },
1658                {
1659                  "code_action": "included-2"
1660                },
1661                {
1662                  "language_server": "ruff"
1663                },
1664                {
1665                  "code_action": "included-3"
1666                },
1667                {
1668                  "code_action": "included-4"
1669                }
1670              ]
1671            },
1672            "Python": {
1673              "formatter": [
1674                {
1675                  "language_server": "ruff"
1676                },
1677                {
1678                  "code_action": "included-3"
1679                },
1680                {
1681                  "code_action": "included-4"
1682                }
1683              ]
1684            }
1685          }
1686        }"#
1687                .unindent(),
1688            ),
1689        );
1690    }
1691
1692    #[test]
1693    fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1694        assert_migrate_settings_with_migrations(
1695            &[MigrationType::Json(
1696                migrations::m_2025_10_01::flatten_code_actions_formatters,
1697            )],
1698            &r#"{
1699        "formatter": {
1700          "code_actions": {
1701            "default-1": true,
1702            "default-2": true,
1703            "default-3": true,
1704            "default-4": true,
1705          }
1706        },
1707        "format_on_save": [
1708          {
1709            "code_actions": {
1710              "included-1": true,
1711              "included-2": true,
1712              "excluded": false,
1713            }
1714          },
1715          {
1716            "language_server": "ruff"
1717          },
1718          {
1719            "code_actions": {
1720              "excluded": false,
1721              "excluded-2": false,
1722            }
1723          }
1724          // some comment
1725          ,
1726          {
1727            "code_actions": {
1728              "excluded": false,
1729              "included-3": true,
1730              "included-4": true,
1731            }
1732          },
1733        ],
1734        "languages": {
1735          "Rust": {
1736            "format_on_save": "prettier",
1737            "formatter": [
1738              {
1739                "code_actions": {
1740                  "included-1": true,
1741                  "included-2": true,
1742                  "excluded": false,
1743                }
1744              },
1745              {
1746                "language_server": "ruff"
1747              },
1748              {
1749                "code_actions": {
1750                  "excluded": false,
1751                  "excluded-2": false,
1752                }
1753              }
1754              // some comment
1755              ,
1756              {
1757                "code_actions": {
1758                  "excluded": false,
1759                  "included-3": true,
1760                  "included-4": true,
1761                }
1762              },
1763            ]
1764          },
1765          "Python": {
1766            "format_on_save": {
1767              "code_actions": {
1768                "on-save-1": true,
1769                "on-save-2": true,
1770              }
1771            },
1772            "formatter": [
1773              {
1774                "language_server": "ruff"
1775              },
1776              {
1777                "code_actions": {
1778                  "excluded": false,
1779                  "excluded-2": false,
1780                }
1781              }
1782              // some comment
1783              ,
1784              {
1785                "code_actions": {
1786                  "excluded": false,
1787                  "included-3": true,
1788                  "included-4": true,
1789                }
1790              },
1791            ]
1792          }
1793        }
1794      }"#
1795            .unindent(),
1796            Some(
1797                &r#"
1798        {
1799          "formatter": [
1800            {
1801              "code_action": "default-1"
1802            },
1803            {
1804              "code_action": "default-2"
1805            },
1806            {
1807              "code_action": "default-3"
1808            },
1809            {
1810              "code_action": "default-4"
1811            }
1812          ],
1813          "format_on_save": [
1814            {
1815              "code_action": "included-1"
1816            },
1817            {
1818              "code_action": "included-2"
1819            },
1820            {
1821              "language_server": "ruff"
1822            },
1823            {
1824              "code_action": "included-3"
1825            },
1826            {
1827              "code_action": "included-4"
1828            }
1829          ],
1830          "languages": {
1831            "Rust": {
1832              "format_on_save": "prettier",
1833              "formatter": [
1834                {
1835                  "code_action": "included-1"
1836                },
1837                {
1838                  "code_action": "included-2"
1839                },
1840                {
1841                  "language_server": "ruff"
1842                },
1843                {
1844                  "code_action": "included-3"
1845                },
1846                {
1847                  "code_action": "included-4"
1848                }
1849              ]
1850            },
1851            "Python": {
1852              "format_on_save": [
1853                {
1854                  "code_action": "on-save-1"
1855                },
1856                {
1857                  "code_action": "on-save-2"
1858                }
1859              ],
1860              "formatter": [
1861                {
1862                  "language_server": "ruff"
1863                },
1864                {
1865                  "code_action": "included-3"
1866                },
1867                {
1868                  "code_action": "included-4"
1869                }
1870              ]
1871            }
1872          }
1873        }"#
1874                .unindent(),
1875            ),
1876        );
1877    }
1878
1879    #[test]
1880    fn test_format_on_save_formatter_migration_basic() {
1881        assert_migrate_settings_with_migrations(
1882            &[MigrationType::Json(
1883                migrations::m_2025_10_02::remove_formatters_on_save,
1884            )],
1885            &r#"{
1886                  "format_on_save": "prettier"
1887              }"#
1888            .unindent(),
1889            Some(
1890                &r#"{
1891                      "formatter": "prettier",
1892                      "format_on_save": "on"
1893                  }"#
1894                .unindent(),
1895            ),
1896        );
1897    }
1898
1899    #[test]
1900    fn test_format_on_save_formatter_migration_array() {
1901        assert_migrate_settings_with_migrations(
1902            &[MigrationType::Json(
1903                migrations::m_2025_10_02::remove_formatters_on_save,
1904            )],
1905            &r#"{
1906                "format_on_save": ["prettier", {"language_server": "eslint"}]
1907            }"#
1908            .unindent(),
1909            Some(
1910                &r#"{
1911                    "formatter": [
1912                        "prettier",
1913                        {
1914                            "language_server": "eslint"
1915                        }
1916                    ],
1917                    "format_on_save": "on"
1918                }"#
1919                .unindent(),
1920            ),
1921        );
1922    }
1923
1924    #[test]
1925    fn test_format_on_save_on_off_unchanged() {
1926        assert_migrate_settings_with_migrations(
1927            &[MigrationType::Json(
1928                migrations::m_2025_10_02::remove_formatters_on_save,
1929            )],
1930            &r#"{
1931                "format_on_save": "on"
1932            }"#
1933            .unindent(),
1934            None,
1935        );
1936
1937        assert_migrate_settings_with_migrations(
1938            &[MigrationType::Json(
1939                migrations::m_2025_10_02::remove_formatters_on_save,
1940            )],
1941            &r#"{
1942                "format_on_save": "off"
1943            }"#
1944            .unindent(),
1945            None,
1946        );
1947    }
1948
1949    #[test]
1950    fn test_format_on_save_formatter_migration_in_languages() {
1951        assert_migrate_settings_with_migrations(
1952            &[MigrationType::Json(
1953                migrations::m_2025_10_02::remove_formatters_on_save,
1954            )],
1955            &r#"{
1956                "languages": {
1957                    "Rust": {
1958                        "format_on_save": "rust-analyzer"
1959                    },
1960                    "Python": {
1961                        "format_on_save": ["ruff", "black"]
1962                    }
1963                }
1964            }"#
1965            .unindent(),
1966            Some(
1967                &r#"{
1968                    "languages": {
1969                        "Rust": {
1970                            "formatter": "rust-analyzer",
1971                            "format_on_save": "on"
1972                        },
1973                        "Python": {
1974                            "formatter": [
1975                                "ruff",
1976                                "black"
1977                            ],
1978                            "format_on_save": "on"
1979                        }
1980                    }
1981                }"#
1982                .unindent(),
1983            ),
1984        );
1985    }
1986
1987    #[test]
1988    fn test_format_on_save_formatter_migration_mixed_global_and_languages() {
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": "prettier",
1995                "languages": {
1996                    "Rust": {
1997                        "format_on_save": "rust-analyzer"
1998                    },
1999                    "Python": {
2000                        "format_on_save": "on"
2001                    }
2002                }
2003            }"#
2004            .unindent(),
2005            Some(
2006                &r#"{
2007                    "formatter": "prettier",
2008                    "format_on_save": "on",
2009                    "languages": {
2010                        "Rust": {
2011                            "formatter": "rust-analyzer",
2012                            "format_on_save": "on"
2013                        },
2014                        "Python": {
2015                            "format_on_save": "on"
2016                        }
2017                    }
2018                }"#
2019                .unindent(),
2020            ),
2021        );
2022    }
2023
2024    #[test]
2025    fn test_format_on_save_no_migration_when_no_format_on_save() {
2026        assert_migrate_settings_with_migrations(
2027            &[MigrationType::Json(
2028                migrations::m_2025_10_02::remove_formatters_on_save,
2029            )],
2030            &r#"{
2031                "formatter": ["prettier"]
2032            }"#
2033            .unindent(),
2034            None,
2035        );
2036    }
2037
2038    #[test]
2039    fn test_restore_code_actions_on_format() {
2040        assert_migrate_settings_with_migrations(
2041            &[MigrationType::Json(
2042                migrations::m_2025_10_16::restore_code_actions_on_format,
2043            )],
2044            &r#"{
2045                "formatter": {
2046                    "code_action": "foo"
2047                }
2048            }"#
2049            .unindent(),
2050            Some(
2051                &r#"{
2052                    "code_actions_on_format": {
2053                        "foo": true
2054                    },
2055                    "formatter": []
2056                }"#
2057                .unindent(),
2058            ),
2059        );
2060
2061        assert_migrate_settings_with_migrations(
2062            &[MigrationType::Json(
2063                migrations::m_2025_10_16::restore_code_actions_on_format,
2064            )],
2065            &r#"{
2066                "formatter": [
2067                    { "code_action": "foo" },
2068                    "auto"
2069                ]
2070            }"#
2071            .unindent(),
2072            None,
2073        );
2074
2075        assert_migrate_settings_with_migrations(
2076            &[MigrationType::Json(
2077                migrations::m_2025_10_16::restore_code_actions_on_format,
2078            )],
2079            &r#"{
2080                "formatter": {
2081                    "code_action": "foo"
2082                },
2083                "code_actions_on_format": {
2084                    "bar": true,
2085                    "baz": false
2086                }
2087            }"#
2088            .unindent(),
2089            Some(
2090                &r#"{
2091                    "formatter": [],
2092                    "code_actions_on_format": {
2093                        "foo": true,
2094                        "bar": true,
2095                        "baz": false
2096                    }
2097                }"#
2098                .unindent(),
2099            ),
2100        );
2101
2102        assert_migrate_settings_with_migrations(
2103            &[MigrationType::Json(
2104                migrations::m_2025_10_16::restore_code_actions_on_format,
2105            )],
2106            &r#"{
2107                "formatter": [
2108                    { "code_action": "foo" },
2109                    { "code_action": "qux" },
2110                ],
2111                "code_actions_on_format": {
2112                    "bar": true,
2113                    "baz": false
2114                }
2115            }"#
2116            .unindent(),
2117            Some(
2118                &r#"{
2119                    "formatter": [],
2120                    "code_actions_on_format": {
2121                        "foo": true,
2122                        "qux": true,
2123                        "bar": true,
2124                        "baz": false
2125                    }
2126                }"#
2127                .unindent(),
2128            ),
2129        );
2130
2131        assert_migrate_settings_with_migrations(
2132            &[MigrationType::Json(
2133                migrations::m_2025_10_16::restore_code_actions_on_format,
2134            )],
2135            &r#"{
2136                "formatter": [],
2137                "code_actions_on_format": {
2138                    "bar": true,
2139                    "baz": false
2140                }
2141            }"#
2142            .unindent(),
2143            None,
2144        );
2145    }
2146
2147    #[test]
2148    fn test_make_file_finder_include_ignored_an_enum() {
2149        assert_migrate_settings_with_migrations(
2150            &[MigrationType::Json(
2151                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2152            )],
2153            &r#"{ }"#.unindent(),
2154            None,
2155        );
2156
2157        assert_migrate_settings_with_migrations(
2158            &[MigrationType::Json(
2159                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2160            )],
2161            &r#"{
2162                "file_finder": {
2163                    "include_ignored": true
2164                }
2165            }"#
2166            .unindent(),
2167            Some(
2168                &r#"{
2169                    "file_finder": {
2170                        "include_ignored": "all"
2171                    }
2172                }"#
2173                .unindent(),
2174            ),
2175        );
2176
2177        assert_migrate_settings_with_migrations(
2178            &[MigrationType::Json(
2179                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2180            )],
2181            &r#"{
2182                "file_finder": {
2183                    "include_ignored": false
2184                }
2185            }"#
2186            .unindent(),
2187            Some(
2188                &r#"{
2189                    "file_finder": {
2190                        "include_ignored": "indexed"
2191                    }
2192                }"#
2193                .unindent(),
2194            ),
2195        );
2196
2197        assert_migrate_settings_with_migrations(
2198            &[MigrationType::Json(
2199                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2200            )],
2201            &r#"{
2202                "file_finder": {
2203                    "include_ignored": null
2204                }
2205            }"#
2206            .unindent(),
2207            Some(
2208                &r#"{
2209                    "file_finder": {
2210                        "include_ignored": "smart"
2211                    }
2212                }"#
2213                .unindent(),
2214            ),
2215        );
2216    }
2217
2218    #[test]
2219    fn test_remove_context_server_source() {
2220        assert_migrate_settings(
2221            &r#"
2222            {
2223                "context_servers": {
2224                    "extension_server": {
2225                        "source": "extension",
2226                        "settings": {
2227                            "foo": "bar"
2228                        }
2229                    },
2230                    "custom_server": {
2231                        "source": "custom",
2232                        "command": "foo",
2233                        "args": ["bar"],
2234                        "env": {
2235                            "FOO": "BAR"
2236                        }
2237                    },
2238                }
2239            }
2240            "#
2241            .unindent(),
2242            Some(
2243                &r#"
2244                {
2245                    "context_servers": {
2246                        "extension_server": {
2247                            "settings": {
2248                                "foo": "bar"
2249                            }
2250                        },
2251                        "custom_server": {
2252                            "command": "foo",
2253                            "args": ["bar"],
2254                            "env": {
2255                                "FOO": "BAR"
2256                            }
2257                        },
2258                    }
2259                }
2260                "#
2261                .unindent(),
2262            ),
2263        );
2264    }
2265
2266    #[test]
2267    fn test_project_panel_open_file_on_paste_migration() {
2268        assert_migrate_settings(
2269            &r#"
2270            {
2271                "project_panel": {
2272                    "open_file_on_paste": true
2273                }
2274            }
2275            "#
2276            .unindent(),
2277            Some(
2278                &r#"
2279                {
2280                    "project_panel": {
2281                        "auto_open": { "on_paste": true }
2282                    }
2283                }
2284                "#
2285                .unindent(),
2286            ),
2287        );
2288
2289        assert_migrate_settings(
2290            &r#"
2291            {
2292                "project_panel": {
2293                    "open_file_on_paste": false
2294                }
2295            }
2296            "#
2297            .unindent(),
2298            Some(
2299                &r#"
2300                {
2301                    "project_panel": {
2302                        "auto_open": { "on_paste": false }
2303                    }
2304                }
2305                "#
2306                .unindent(),
2307            ),
2308        );
2309    }
2310
2311    #[test]
2312    fn test_enable_preview_from_code_navigation_migration() {
2313        assert_migrate_settings(
2314            &r#"
2315            {
2316                "other_setting_1": 1,
2317                "preview_tabs": {
2318                    "other_setting_2": 2,
2319                    "enable_preview_from_code_navigation": false
2320                }
2321            }
2322            "#
2323            .unindent(),
2324            Some(
2325                &r#"
2326                {
2327                    "other_setting_1": 1,
2328                    "preview_tabs": {
2329                        "other_setting_2": 2,
2330                        "enable_keep_preview_on_code_navigation": false
2331                    }
2332                }
2333                "#
2334                .unindent(),
2335            ),
2336        );
2337
2338        assert_migrate_settings(
2339            &r#"
2340            {
2341                "other_setting_1": 1,
2342                "preview_tabs": {
2343                    "other_setting_2": 2,
2344                    "enable_preview_from_code_navigation": true
2345                }
2346            }
2347            "#
2348            .unindent(),
2349            Some(
2350                &r#"
2351                {
2352                    "other_setting_1": 1,
2353                    "preview_tabs": {
2354                        "other_setting_2": 2,
2355                        "enable_keep_preview_on_code_navigation": true
2356                    }
2357                }
2358                "#
2359                .unindent(),
2360            ),
2361        );
2362    }
2363}