migrator.rs

   1use anyhow::{Context, Result};
   2use collections::HashMap;
   3use convert_case::{Case, Casing};
   4use std::{cmp::Reverse, ops::Range, sync::LazyLock};
   5use streaming_iterator::StreamingIterator;
   6use tree_sitter::{Query, QueryMatch};
   7
   8fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
   9    let mut parser = tree_sitter::Parser::new();
  10    parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
  11    let syntax_tree = parser
  12        .parse(&text, None)
  13        .context("failed to parse settings")?;
  14
  15    let mut cursor = tree_sitter::QueryCursor::new();
  16    let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
  17
  18    let mut edits = vec![];
  19    while let Some(mat) = matches.next() {
  20        if let Some((_, callback)) = patterns.get(mat.pattern_index) {
  21            edits.extend(callback(&text, &mat, query));
  22        }
  23    }
  24
  25    edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
  26    edits.dedup_by(|(range_b, _), (range_a, _)| {
  27        range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
  28    });
  29
  30    if edits.is_empty() {
  31        Ok(None)
  32    } else {
  33        let mut new_text = text.to_string();
  34        for (range, replacement) in edits.iter().rev() {
  35            new_text.replace_range(range.clone(), replacement);
  36        }
  37        if new_text == text {
  38            log::error!(
  39                "Edits computed for configuration migration do not cause a change: {:?}",
  40                edits
  41            );
  42            Ok(None)
  43        } else {
  44            Ok(Some(new_text))
  45        }
  46    }
  47}
  48
  49pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
  50    let transformed_text = migrate(
  51        text,
  52        KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS,
  53        &KEYMAP_MIGRATION_TRANSFORMATION_QUERY,
  54    )?;
  55    let replacement_text = migrate(
  56        &transformed_text.as_ref().unwrap_or(&text.to_string()),
  57        KEYMAP_MIGRATION_REPLACEMENT_PATTERNS,
  58        &KEYMAP_MIGRATION_REPLACEMENT_QUERY,
  59    )?;
  60    Ok(replacement_text.or(transformed_text))
  61}
  62
  63pub fn migrate_settings(text: &str) -> Result<Option<String>> {
  64    migrate(
  65        &text,
  66        SETTINGS_MIGRATION_PATTERNS,
  67        &SETTINGS_MIGRATION_QUERY,
  68    )
  69}
  70
  71pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
  72    migrate(
  73        &text,
  74        &[(
  75            SETTINGS_NESTED_KEY_VALUE_PATTERN,
  76            replace_edit_prediction_provider_setting,
  77        )],
  78        &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
  79    )
  80}
  81
  82type MigrationPatterns = &'static [(
  83    &'static str,
  84    fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
  85)];
  86
  87const KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS: MigrationPatterns = &[
  88    (ACTION_ARRAY_PATTERN, replace_array_with_single_string),
  89    (
  90        ACTION_ARGUMENT_OBJECT_PATTERN,
  91        replace_action_argument_object_with_single_value,
  92    ),
  93    (ACTION_STRING_PATTERN, rename_string_action),
  94    (CONTEXT_PREDICATE_PATTERN, rename_context_key),
  95    (
  96        ACTION_STRING_OF_ARRAY_PATTERN,
  97        replace_first_string_of_array,
  98    ),
  99];
 100
 101static KEYMAP_MIGRATION_TRANSFORMATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 102    Query::new(
 103        &tree_sitter_json::LANGUAGE.into(),
 104        &KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS
 105            .iter()
 106            .map(|pattern| pattern.0)
 107            .collect::<String>(),
 108    )
 109    .unwrap()
 110});
 111
 112const ACTION_ARRAY_PATTERN: &str = r#"(document
 113    (array
 114   	    (object
 115            (pair
 116                key: (string (string_content) @name)
 117                value: (
 118                    (object
 119                        (pair
 120                            key: (string)
 121                            value: ((array
 122                                . (string (string_content) @action_name)
 123                                . (string (string_content) @argument)
 124                                .)) @array
 125                        )
 126                    )
 127                )
 128            )
 129        )
 130    )
 131    (#eq? @name "bindings")
 132)"#;
 133
 134fn replace_array_with_single_string(
 135    contents: &str,
 136    mat: &QueryMatch,
 137    query: &Query,
 138) -> Option<(Range<usize>, String)> {
 139    let array_ix = query.capture_index_for_name("array")?;
 140    let action_name_ix = query.capture_index_for_name("action_name")?;
 141    let argument_ix = query.capture_index_for_name("argument")?;
 142
 143    let action_name = contents.get(
 144        mat.nodes_for_capture_index(action_name_ix)
 145            .next()?
 146            .byte_range(),
 147    )?;
 148    let argument = contents.get(
 149        mat.nodes_for_capture_index(argument_ix)
 150            .next()?
 151            .byte_range(),
 152    )?;
 153
 154    let replacement = TRANSFORM_ARRAY.get(&(action_name, argument))?;
 155    let replacement_as_string = format!("\"{replacement}\"");
 156    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
 157
 158    Some((range_to_replace, replacement_as_string))
 159}
 160
 161static TRANSFORM_ARRAY: LazyLock<HashMap<(&str, &str), &str>> = LazyLock::new(|| {
 162    HashMap::from_iter([
 163        // activate
 164        (
 165            ("workspace::ActivatePaneInDirection", "Up"),
 166            "workspace::ActivatePaneUp",
 167        ),
 168        (
 169            ("workspace::ActivatePaneInDirection", "Down"),
 170            "workspace::ActivatePaneDown",
 171        ),
 172        (
 173            ("workspace::ActivatePaneInDirection", "Left"),
 174            "workspace::ActivatePaneLeft",
 175        ),
 176        (
 177            ("workspace::ActivatePaneInDirection", "Right"),
 178            "workspace::ActivatePaneRight",
 179        ),
 180        // swap
 181        (
 182            ("workspace::SwapPaneInDirection", "Up"),
 183            "workspace::SwapPaneUp",
 184        ),
 185        (
 186            ("workspace::SwapPaneInDirection", "Down"),
 187            "workspace::SwapPaneDown",
 188        ),
 189        (
 190            ("workspace::SwapPaneInDirection", "Left"),
 191            "workspace::SwapPaneLeft",
 192        ),
 193        (
 194            ("workspace::SwapPaneInDirection", "Right"),
 195            "workspace::SwapPaneRight",
 196        ),
 197        // menu
 198        (
 199            ("app_menu::NavigateApplicationMenuInDirection", "Left"),
 200            "app_menu::ActivateMenuLeft",
 201        ),
 202        (
 203            ("app_menu::NavigateApplicationMenuInDirection", "Right"),
 204            "app_menu::ActivateMenuRight",
 205        ),
 206        // vim push
 207        (("vim::PushOperator", "Change"), "vim::PushChange"),
 208        (("vim::PushOperator", "Delete"), "vim::PushDelete"),
 209        (("vim::PushOperator", "Yank"), "vim::PushYank"),
 210        (("vim::PushOperator", "Replace"), "vim::PushReplace"),
 211        (
 212            ("vim::PushOperator", "DeleteSurrounds"),
 213            "vim::PushDeleteSurrounds",
 214        ),
 215        (("vim::PushOperator", "Mark"), "vim::PushMark"),
 216        (("vim::PushOperator", "Indent"), "vim::PushIndent"),
 217        (("vim::PushOperator", "Outdent"), "vim::PushOutdent"),
 218        (("vim::PushOperator", "AutoIndent"), "vim::PushAutoIndent"),
 219        (("vim::PushOperator", "Rewrap"), "vim::PushRewrap"),
 220        (
 221            ("vim::PushOperator", "ShellCommand"),
 222            "vim::PushShellCommand",
 223        ),
 224        (("vim::PushOperator", "Lowercase"), "vim::PushLowercase"),
 225        (("vim::PushOperator", "Uppercase"), "vim::PushUppercase"),
 226        (
 227            ("vim::PushOperator", "OppositeCase"),
 228            "vim::PushOppositeCase",
 229        ),
 230        (("vim::PushOperator", "Register"), "vim::PushRegister"),
 231        (
 232            ("vim::PushOperator", "RecordRegister"),
 233            "vim::PushRecordRegister",
 234        ),
 235        (
 236            ("vim::PushOperator", "ReplayRegister"),
 237            "vim::PushReplayRegister",
 238        ),
 239        (
 240            ("vim::PushOperator", "ReplaceWithRegister"),
 241            "vim::PushReplaceWithRegister",
 242        ),
 243        (
 244            ("vim::PushOperator", "ToggleComments"),
 245            "vim::PushToggleComments",
 246        ),
 247        // vim switch
 248        (("vim::SwitchMode", "Normal"), "vim::SwitchToNormalMode"),
 249        (("vim::SwitchMode", "Insert"), "vim::SwitchToInsertMode"),
 250        (("vim::SwitchMode", "Replace"), "vim::SwitchToReplaceMode"),
 251        (("vim::SwitchMode", "Visual"), "vim::SwitchToVisualMode"),
 252        (
 253            ("vim::SwitchMode", "VisualLine"),
 254            "vim::SwitchToVisualLineMode",
 255        ),
 256        (
 257            ("vim::SwitchMode", "VisualBlock"),
 258            "vim::SwitchToVisualBlockMode",
 259        ),
 260        (
 261            ("vim::SwitchMode", "HelixNormal"),
 262            "vim::SwitchToHelixNormalMode",
 263        ),
 264        // vim resize
 265        (("vim::ResizePane", "Widen"), "vim::ResizePaneRight"),
 266        (("vim::ResizePane", "Narrow"), "vim::ResizePaneLeft"),
 267        (("vim::ResizePane", "Shorten"), "vim::ResizePaneDown"),
 268        (("vim::ResizePane", "Lengthen"), "vim::ResizePaneUp"),
 269    ])
 270});
 271
 272const ACTION_STRING_OF_ARRAY_PATTERN: &str = r#"(document
 273    (array
 274   	    (object
 275            (pair
 276                key: (string (string_content) @name)
 277                value: (
 278                    (object
 279                        (pair
 280                            key: (string)
 281                            value: ((array
 282                                . (string (string_content) @action_name)
 283                                )
 284                            )
 285                        )
 286                    )
 287                )
 288            )
 289        )
 290    )
 291    (#eq? @name "bindings")
 292)"#;
 293
 294// ["editor::GoToPrevHunk", { "center_cursor": true }] -> ["editor::GoToPreviousHunk", { "center_cursor": true }]
 295fn replace_first_string_of_array(
 296    contents: &str,
 297    mat: &QueryMatch,
 298    query: &Query,
 299) -> Option<(Range<usize>, String)> {
 300    let action_name_ix = query.capture_index_for_name("action_name")?;
 301    let action_name = contents.get(
 302        mat.nodes_for_capture_index(action_name_ix)
 303            .next()?
 304            .byte_range(),
 305    )?;
 306    let replacement = STRING_OF_ARRAY_REPLACE.get(action_name)?;
 307    let range_to_replace = mat
 308        .nodes_for_capture_index(action_name_ix)
 309        .next()?
 310        .byte_range();
 311    Some((range_to_replace, replacement.to_string()))
 312}
 313
 314static STRING_OF_ARRAY_REPLACE: LazyLock<HashMap<&str, &str>> =
 315    LazyLock::new(|| HashMap::from_iter([("editor::GoToPrevHunk", "editor::GoToPreviousHunk")]));
 316
 317const ACTION_ARGUMENT_OBJECT_PATTERN: &str = r#"(document
 318    (array
 319        (object
 320            (pair
 321                key: (string (string_content) @name)
 322                value: (
 323                    (object
 324                        (pair
 325                            key: (string)
 326                            value: ((array
 327                                . (string (string_content) @action_name)
 328                                . (object
 329                                    (pair
 330                                    key: (string (string_content) @action_key)
 331                                    value: (_)  @argument))
 332                                . ) @array
 333                            ))
 334                        )
 335                    )
 336                )
 337            )
 338        )
 339        (#eq? @name "bindings")
 340)"#;
 341
 342/// [ "editor::FoldAtLevel", { "level": 1 } ] -> [ "editor::FoldAtLevel", 1 ]
 343fn replace_action_argument_object_with_single_value(
 344    contents: &str,
 345    mat: &QueryMatch,
 346    query: &Query,
 347) -> Option<(Range<usize>, String)> {
 348    let array_ix = query.capture_index_for_name("array")?;
 349    let action_name_ix = query.capture_index_for_name("action_name")?;
 350    let action_key_ix = query.capture_index_for_name("action_key")?;
 351    let argument_ix = query.capture_index_for_name("argument")?;
 352
 353    let action_name = contents.get(
 354        mat.nodes_for_capture_index(action_name_ix)
 355            .next()?
 356            .byte_range(),
 357    )?;
 358    let action_key = contents.get(
 359        mat.nodes_for_capture_index(action_key_ix)
 360            .next()?
 361            .byte_range(),
 362    )?;
 363    let argument = contents.get(
 364        mat.nodes_for_capture_index(argument_ix)
 365            .next()?
 366            .byte_range(),
 367    )?;
 368
 369    let new_action_name = UNWRAP_OBJECTS.get(&action_name)?.get(&action_key)?;
 370
 371    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
 372    let replacement = format!("[\"{}\", {}]", new_action_name, argument);
 373    Some((range_to_replace, replacement))
 374}
 375
 376/// "ctrl-k ctrl-1": [ "editor::PushOperator", { "Object": {} } ] -> [ "editor::vim::PushObject", {} ]
 377static UNWRAP_OBJECTS: LazyLock<HashMap<&str, HashMap<&str, &str>>> = LazyLock::new(|| {
 378    HashMap::from_iter([
 379        (
 380            "editor::FoldAtLevel",
 381            HashMap::from_iter([("level", "editor::FoldAtLevel")]),
 382        ),
 383        (
 384            "vim::PushOperator",
 385            HashMap::from_iter([
 386                ("Object", "vim::PushObject"),
 387                ("FindForward", "vim::PushFindForward"),
 388                ("FindBackward", "vim::PushFindBackward"),
 389                ("Sneak", "vim::PushSneak"),
 390                ("SneakBackward", "vim::PushSneakBackward"),
 391                ("AddSurrounds", "vim::PushAddSurrounds"),
 392                ("ChangeSurrounds", "vim::PushChangeSurrounds"),
 393                ("Jump", "vim::PushJump"),
 394                ("Digraph", "vim::PushDigraph"),
 395                ("Literal", "vim::PushLiteral"),
 396            ]),
 397        ),
 398    ])
 399});
 400
 401const KEYMAP_MIGRATION_REPLACEMENT_PATTERNS: MigrationPatterns = &[(
 402    ACTION_ARGUMENT_SNAKE_CASE_PATTERN,
 403    action_argument_snake_case,
 404)];
 405
 406static KEYMAP_MIGRATION_REPLACEMENT_QUERY: LazyLock<Query> = LazyLock::new(|| {
 407    Query::new(
 408        &tree_sitter_json::LANGUAGE.into(),
 409        &KEYMAP_MIGRATION_REPLACEMENT_PATTERNS
 410            .iter()
 411            .map(|pattern| pattern.0)
 412            .collect::<String>(),
 413    )
 414    .unwrap()
 415});
 416
 417const ACTION_STRING_PATTERN: &str = r#"(document
 418    (array
 419        (object
 420            (pair
 421                key: (string (string_content) @name)
 422                value: (
 423                    (object
 424                        (pair
 425                            key: (string)
 426                            value: (string (string_content) @action_name)
 427                        )
 428                    )
 429                )
 430            )
 431        )
 432    )
 433    (#eq? @name "bindings")
 434)"#;
 435
 436fn rename_string_action(
 437    contents: &str,
 438    mat: &QueryMatch,
 439    query: &Query,
 440) -> Option<(Range<usize>, String)> {
 441    let action_name_ix = query.capture_index_for_name("action_name")?;
 442    let action_name_range = mat
 443        .nodes_for_capture_index(action_name_ix)
 444        .next()?
 445        .byte_range();
 446    let action_name = contents.get(action_name_range.clone())?;
 447    let new_action_name = STRING_REPLACE.get(&action_name)?;
 448    Some((action_name_range, new_action_name.to_string()))
 449}
 450
 451/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
 452static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
 453    HashMap::from_iter([
 454        (
 455            "inline_completion::ToggleMenu",
 456            "edit_prediction::ToggleMenu",
 457        ),
 458        ("editor::NextInlineCompletion", "editor::NextEditPrediction"),
 459        (
 460            "editor::PreviousInlineCompletion",
 461            "editor::PreviousEditPrediction",
 462        ),
 463        (
 464            "editor::AcceptPartialInlineCompletion",
 465            "editor::AcceptPartialEditPrediction",
 466        ),
 467        ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"),
 468        (
 469            "editor::AcceptInlineCompletion",
 470            "editor::AcceptEditPrediction",
 471        ),
 472        (
 473            "editor::ToggleInlineCompletions",
 474            "editor::ToggleEditPrediction",
 475        ),
 476        (
 477            "editor::GoToPrevDiagnostic",
 478            "editor::GoToPreviousDiagnostic",
 479        ),
 480        ("editor::ContextMenuPrev", "editor::ContextMenuPrevious"),
 481        ("search::SelectPrevMatch", "search::SelectPreviousMatch"),
 482        ("file_finder::SelectPrev", "file_finder::SelectPrevious"),
 483        ("menu::SelectPrev", "menu::SelectPrevious"),
 484        ("editor::TabPrev", "editor::Backtab"),
 485    ])
 486});
 487
 488const CONTEXT_PREDICATE_PATTERN: &str = r#"(document
 489    (array
 490        (object
 491            (pair
 492                key: (string (string_content) @name)
 493                value: (string (string_content) @context_predicate)
 494            )
 495        )
 496    )
 497    (#eq? @name "context")
 498)"#;
 499
 500fn rename_context_key(
 501    contents: &str,
 502    mat: &QueryMatch,
 503    query: &Query,
 504) -> Option<(Range<usize>, String)> {
 505    let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
 506    let context_predicate_range = mat
 507        .nodes_for_capture_index(context_predicate_ix)
 508        .next()?
 509        .byte_range();
 510    let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
 511    let mut new_predicate = old_predicate.to_string();
 512    for (old_key, new_key) in CONTEXT_REPLACE.iter() {
 513        new_predicate = new_predicate.replace(old_key, new_key);
 514    }
 515    if new_predicate != old_predicate {
 516        Some((context_predicate_range, new_predicate.to_string()))
 517    } else {
 518        None
 519    }
 520}
 521
 522/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
 523pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
 524    HashMap::from_iter([
 525        ("inline_completion", "edit_prediction"),
 526        (
 527            "inline_completion_requires_modifier",
 528            "edit_prediction_requires_modifier",
 529        ),
 530    ])
 531});
 532
 533const ACTION_ARGUMENT_SNAKE_CASE_PATTERN: &str = r#"(document
 534    (array
 535        (object
 536            (pair
 537                key: (string (string_content) @name)
 538                value: (
 539                    (object
 540                        (pair
 541                            key: (string)
 542                            value: ((array
 543                                . (string (string_content) @action_name)
 544                                . (object
 545                                    (pair
 546                                    key: (string (string_content) @argument_key)
 547                                    value: (_)  @argument_value))
 548                                . ) @array
 549                            ))
 550                        )
 551                    )
 552                )
 553            )
 554        )
 555    (#eq? @name "bindings")
 556)"#;
 557
 558fn to_snake_case(text: &str) -> String {
 559    text.to_case(Case::Snake)
 560}
 561
 562fn action_argument_snake_case(
 563    contents: &str,
 564    mat: &QueryMatch,
 565    query: &Query,
 566) -> Option<(Range<usize>, String)> {
 567    let array_ix = query.capture_index_for_name("array")?;
 568    let action_name_ix = query.capture_index_for_name("action_name")?;
 569    let argument_key_ix = query.capture_index_for_name("argument_key")?;
 570    let argument_value_ix = query.capture_index_for_name("argument_value")?;
 571    let action_name = contents.get(
 572        mat.nodes_for_capture_index(action_name_ix)
 573            .next()?
 574            .byte_range(),
 575    )?;
 576
 577    let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?;
 578    let argument_key = contents.get(
 579        mat.nodes_for_capture_index(argument_key_ix)
 580            .next()?
 581            .byte_range(),
 582    )?;
 583
 584    if argument_key != *replacement_key {
 585        return None;
 586    }
 587
 588    let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?;
 589    let argument_value = contents.get(argument_value_node.byte_range())?;
 590
 591    let new_key = to_snake_case(argument_key);
 592    let new_value = if argument_value_node.kind() == "string" {
 593        format!("\"{}\"", to_snake_case(argument_value.trim_matches('"')))
 594    } else {
 595        argument_value.to_string()
 596    };
 597
 598    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
 599    let replacement = format!(
 600        "[\"{}\", {{ \"{}\": {} }}]",
 601        action_name, new_key, new_value
 602    );
 603
 604    Some((range_to_replace, replacement))
 605}
 606
 607pub static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock<HashMap<&str, &str>> =
 608    LazyLock::new(|| {
 609        HashMap::from_iter([
 610            ("vim::NextWordStart", "ignorePunctuation"),
 611            ("vim::NextWordEnd", "ignorePunctuation"),
 612            ("vim::PreviousWordStart", "ignorePunctuation"),
 613            ("vim::PreviousWordEnd", "ignorePunctuation"),
 614            ("vim::MoveToNext", "partialWord"),
 615            ("vim::MoveToPrev", "partialWord"),
 616            ("vim::Down", "displayLines"),
 617            ("vim::Up", "displayLines"),
 618            ("vim::EndOfLine", "displayLines"),
 619            ("vim::StartOfLine", "displayLines"),
 620            ("vim::FirstNonWhitespace", "displayLines"),
 621            ("pane::CloseActiveItem", "saveIntent"),
 622            ("vim::Paste", "preserveClipboard"),
 623            ("vim::Word", "ignorePunctuation"),
 624            ("vim::Subword", "ignorePunctuation"),
 625            ("vim::IndentObj", "includeBelow"),
 626        ])
 627    });
 628
 629const SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[
 630    (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name),
 631    (
 632        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 633        replace_edit_prediction_provider_setting,
 634    ),
 635    (
 636        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 637        replace_tab_close_button_setting_key,
 638    ),
 639    (
 640        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 641        replace_tab_close_button_setting_value,
 642    ),
 643    (
 644        SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
 645        replace_setting_in_languages,
 646    ),
 647];
 648
 649static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 650    Query::new(
 651        &tree_sitter_json::LANGUAGE.into(),
 652        &SETTINGS_MIGRATION_PATTERNS
 653            .iter()
 654            .map(|pattern| pattern.0)
 655            .collect::<String>(),
 656    )
 657    .unwrap()
 658});
 659
 660static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 661    Query::new(
 662        &tree_sitter_json::LANGUAGE.into(),
 663        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 664    )
 665    .unwrap()
 666});
 667
 668const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
 669    (object
 670        (pair
 671            key: (string (string_content) @name)
 672            value: (_)
 673        )
 674    )
 675)"#;
 676
 677fn replace_setting_name(
 678    contents: &str,
 679    mat: &QueryMatch,
 680    query: &Query,
 681) -> Option<(Range<usize>, String)> {
 682    let setting_capture_ix = query.capture_index_for_name("name")?;
 683    let setting_name_range = mat
 684        .nodes_for_capture_index(setting_capture_ix)
 685        .next()?
 686        .byte_range();
 687    let setting_name = contents.get(setting_name_range.clone())?;
 688    let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
 689    Some((setting_name_range, new_setting_name.to_string()))
 690}
 691
 692pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
 693    LazyLock::new(|| {
 694        HashMap::from_iter([
 695            (
 696                "show_inline_completions_in_menu",
 697                "show_edit_predictions_in_menu",
 698            ),
 699            ("show_inline_completions", "show_edit_predictions"),
 700            (
 701                "inline_completions_disabled_in",
 702                "edit_predictions_disabled_in",
 703            ),
 704            ("inline_completions", "edit_predictions"),
 705        ])
 706    });
 707
 708const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#"
 709(object
 710  (pair
 711    key: (string (string_content) @parent_key)
 712    value: (object
 713        (pair
 714            key: (string (string_content) @setting_name)
 715            value: (_) @setting_value
 716        )
 717    )
 718  )
 719)
 720"#;
 721
 722fn replace_edit_prediction_provider_setting(
 723    contents: &str,
 724    mat: &QueryMatch,
 725    query: &Query,
 726) -> Option<(Range<usize>, String)> {
 727    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 728    let parent_object_range = mat
 729        .nodes_for_capture_index(parent_object_capture_ix)
 730        .next()?
 731        .byte_range();
 732    let parent_object_name = contents.get(parent_object_range.clone())?;
 733
 734    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 735    let setting_range = mat
 736        .nodes_for_capture_index(setting_name_ix)
 737        .next()?
 738        .byte_range();
 739    let setting_name = contents.get(setting_range.clone())?;
 740
 741    if parent_object_name == "features" && setting_name == "inline_completion_provider" {
 742        return Some((setting_range, "edit_prediction_provider".into()));
 743    }
 744
 745    None
 746}
 747
 748fn replace_tab_close_button_setting_key(
 749    contents: &str,
 750    mat: &QueryMatch,
 751    query: &Query,
 752) -> Option<(Range<usize>, String)> {
 753    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 754    let parent_object_range = mat
 755        .nodes_for_capture_index(parent_object_capture_ix)
 756        .next()?
 757        .byte_range();
 758    let parent_object_name = contents.get(parent_object_range.clone())?;
 759
 760    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 761    let setting_range = mat
 762        .nodes_for_capture_index(setting_name_ix)
 763        .next()?
 764        .byte_range();
 765    let setting_name = contents.get(setting_range.clone())?;
 766
 767    if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
 768        return Some((setting_range, "show_close_button".into()));
 769    }
 770
 771    None
 772}
 773
 774fn replace_tab_close_button_setting_value(
 775    contents: &str,
 776    mat: &QueryMatch,
 777    query: &Query,
 778) -> Option<(Range<usize>, String)> {
 779    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 780    let parent_object_range = mat
 781        .nodes_for_capture_index(parent_object_capture_ix)
 782        .next()?
 783        .byte_range();
 784    let parent_object_name = contents.get(parent_object_range.clone())?;
 785
 786    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 787    let setting_name_range = mat
 788        .nodes_for_capture_index(setting_name_ix)
 789        .next()?
 790        .byte_range();
 791    let setting_name = contents.get(setting_name_range.clone())?;
 792
 793    let setting_value_ix = query.capture_index_for_name("setting_value")?;
 794    let setting_value_range = mat
 795        .nodes_for_capture_index(setting_value_ix)
 796        .next()?
 797        .byte_range();
 798    let setting_value = contents.get(setting_value_range.clone())?;
 799
 800    if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
 801        match setting_value {
 802            "true" => {
 803                return Some((setting_value_range, "\"always\"".to_string()));
 804            }
 805            "false" => {
 806                return Some((setting_value_range, "\"hover\"".to_string()));
 807            }
 808            _ => {}
 809        }
 810    }
 811
 812    None
 813}
 814
 815const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
 816(object
 817  (pair
 818    key: (string (string_content) @languages)
 819    value: (object
 820    (pair
 821        key: (string)
 822        value: (object
 823            (pair
 824                key: (string (string_content) @setting_name)
 825                value: (_) @value
 826            )
 827        )
 828    ))
 829  )
 830)
 831(#eq? @languages "languages")
 832"#;
 833
 834fn replace_setting_in_languages(
 835    contents: &str,
 836    mat: &QueryMatch,
 837    query: &Query,
 838) -> Option<(Range<usize>, String)> {
 839    let setting_capture_ix = query.capture_index_for_name("setting_name")?;
 840    let setting_name_range = mat
 841        .nodes_for_capture_index(setting_capture_ix)
 842        .next()?
 843        .byte_range();
 844    let setting_name = contents.get(setting_name_range.clone())?;
 845    let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
 846
 847    Some((setting_name_range, new_setting_name.to_string()))
 848}
 849
 850static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
 851    LazyLock::new(|| {
 852        HashMap::from_iter([
 853            ("show_inline_completions", "show_edit_predictions"),
 854            (
 855                "inline_completions_disabled_in",
 856                "edit_predictions_disabled_in",
 857            ),
 858        ])
 859    });
 860
 861#[cfg(test)]
 862mod tests {
 863    use super::*;
 864
 865    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 866        let migrated = migrate_keymap(&input).unwrap();
 867        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 868    }
 869
 870    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 871        let migrated = migrate_settings(&input).unwrap();
 872        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 873    }
 874
 875    #[test]
 876    fn test_replace_array_with_single_string() {
 877        assert_migrate_keymap(
 878            r#"
 879            [
 880                {
 881                    "bindings": {
 882                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 883                    }
 884                }
 885            ]
 886            "#,
 887            Some(
 888                r#"
 889            [
 890                {
 891                    "bindings": {
 892                        "cmd-1": "workspace::ActivatePaneUp"
 893                    }
 894                }
 895            ]
 896            "#,
 897            ),
 898        )
 899    }
 900
 901    #[test]
 902    fn test_replace_action_argument_object_with_single_value() {
 903        assert_migrate_keymap(
 904            r#"
 905            [
 906                {
 907                    "bindings": {
 908                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 909                    }
 910                }
 911            ]
 912            "#,
 913            Some(
 914                r#"
 915            [
 916                {
 917                    "bindings": {
 918                        "cmd-1": ["editor::FoldAtLevel", 1]
 919                    }
 920                }
 921            ]
 922            "#,
 923            ),
 924        )
 925    }
 926
 927    #[test]
 928    fn test_replace_action_argument_object_with_single_value_2() {
 929        assert_migrate_keymap(
 930            r#"
 931            [
 932                {
 933                    "bindings": {
 934                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 935                    }
 936                }
 937            ]
 938            "#,
 939            Some(
 940                r#"
 941            [
 942                {
 943                    "bindings": {
 944                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 945                    }
 946                }
 947            ]
 948            "#,
 949            ),
 950        )
 951    }
 952
 953    #[test]
 954    fn test_rename_string_action() {
 955        assert_migrate_keymap(
 956            r#"
 957                [
 958                    {
 959                        "bindings": {
 960                            "cmd-1": "inline_completion::ToggleMenu"
 961                        }
 962                    }
 963                ]
 964            "#,
 965            Some(
 966                r#"
 967                [
 968                    {
 969                        "bindings": {
 970                            "cmd-1": "edit_prediction::ToggleMenu"
 971                        }
 972                    }
 973                ]
 974            "#,
 975            ),
 976        )
 977    }
 978
 979    #[test]
 980    fn test_rename_context_key() {
 981        assert_migrate_keymap(
 982            r#"
 983                [
 984                    {
 985                        "context": "Editor && inline_completion && !showing_completions"
 986                    }
 987                ]
 988            "#,
 989            Some(
 990                r#"
 991                [
 992                    {
 993                        "context": "Editor && edit_prediction && !showing_completions"
 994                    }
 995                ]
 996            "#,
 997            ),
 998        )
 999    }
1000
1001    #[test]
1002    fn test_string_of_array_replace() {
1003        assert_migrate_keymap(
1004            r#"
1005                [
1006                    {
1007                        "bindings": {
1008                            "ctrl-p": ["editor::GoToPrevHunk", { "center_cursor": true }],
1009                            "ctrl-q": ["editor::GoToPrevHunk"],
1010                            "ctrl-q": "editor::GoToPrevHunk", // should remain same
1011                        }
1012                    }
1013                ]
1014            "#,
1015            Some(
1016                r#"
1017                [
1018                    {
1019                        "bindings": {
1020                            "ctrl-p": ["editor::GoToPreviousHunk", { "center_cursor": true }],
1021                            "ctrl-q": ["editor::GoToPreviousHunk"],
1022                            "ctrl-q": "editor::GoToPrevHunk", // should remain same
1023                        }
1024                    }
1025                ]
1026            "#,
1027            ),
1028        )
1029    }
1030
1031    #[test]
1032    fn test_action_argument_snake_case() {
1033        // First performs transformations, then replacements
1034        assert_migrate_keymap(
1035            r#"
1036            [
1037                {
1038                    "bindings": {
1039                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
1040                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
1041                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
1042                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
1043                    }
1044                }
1045            ]
1046            "#,
1047            Some(
1048                r#"
1049            [
1050                {
1051                    "bindings": {
1052                        "cmd-1": ["vim::PushObject", { "around": false }],
1053                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
1054                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
1055                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
1056                    }
1057                }
1058            ]
1059            "#,
1060            ),
1061        )
1062    }
1063
1064    #[test]
1065    fn test_replace_setting_name() {
1066        assert_migrate_settings(
1067            r#"
1068                {
1069                    "show_inline_completions_in_menu": true,
1070                    "show_inline_completions": true,
1071                    "inline_completions_disabled_in": ["string"],
1072                    "inline_completions": { "some" : "value" }
1073                }
1074            "#,
1075            Some(
1076                r#"
1077                {
1078                    "show_edit_predictions_in_menu": true,
1079                    "show_edit_predictions": true,
1080                    "edit_predictions_disabled_in": ["string"],
1081                    "edit_predictions": { "some" : "value" }
1082                }
1083            "#,
1084            ),
1085        )
1086    }
1087
1088    #[test]
1089    fn test_nested_string_replace_for_settings() {
1090        assert_migrate_settings(
1091            r#"
1092                {
1093                    "features": {
1094                        "inline_completion_provider": "zed"
1095                    },
1096                }
1097            "#,
1098            Some(
1099                r#"
1100                {
1101                    "features": {
1102                        "edit_prediction_provider": "zed"
1103                    },
1104                }
1105            "#,
1106            ),
1107        )
1108    }
1109
1110    #[test]
1111    fn test_replace_settings_in_languages() {
1112        assert_migrate_settings(
1113            r#"
1114                {
1115                    "languages": {
1116                        "Astro": {
1117                            "show_inline_completions": true
1118                        }
1119                    }
1120                }
1121            "#,
1122            Some(
1123                r#"
1124                {
1125                    "languages": {
1126                        "Astro": {
1127                            "show_edit_predictions": true
1128                        }
1129                    }
1130                }
1131            "#,
1132            ),
1133        )
1134    }
1135}