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_REPLACE_NESTED_KEY,
  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        ("pane::ActivatePrevItem", "pane::ActivatePreviousItem"),
 486        ("vim::MoveToPrev", "vim::MoveToPrevious"),
 487        ("vim::MoveToPrevMatch", "vim::MoveToPreviousMatch"),
 488    ])
 489});
 490
 491const CONTEXT_PREDICATE_PATTERN: &str = r#"(document
 492    (array
 493        (object
 494            (pair
 495                key: (string (string_content) @name)
 496                value: (string (string_content) @context_predicate)
 497            )
 498        )
 499    )
 500    (#eq? @name "context")
 501)"#;
 502
 503fn rename_context_key(
 504    contents: &str,
 505    mat: &QueryMatch,
 506    query: &Query,
 507) -> Option<(Range<usize>, String)> {
 508    let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
 509    let context_predicate_range = mat
 510        .nodes_for_capture_index(context_predicate_ix)
 511        .next()?
 512        .byte_range();
 513    let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
 514    let mut new_predicate = old_predicate.to_string();
 515    for (old_key, new_key) in CONTEXT_REPLACE.iter() {
 516        new_predicate = new_predicate.replace(old_key, new_key);
 517    }
 518    if new_predicate != old_predicate {
 519        Some((context_predicate_range, new_predicate.to_string()))
 520    } else {
 521        None
 522    }
 523}
 524
 525/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
 526pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
 527    HashMap::from_iter([
 528        ("inline_completion", "edit_prediction"),
 529        (
 530            "inline_completion_requires_modifier",
 531            "edit_prediction_requires_modifier",
 532        ),
 533    ])
 534});
 535
 536const ACTION_ARGUMENT_SNAKE_CASE_PATTERN: &str = r#"(document
 537    (array
 538        (object
 539            (pair
 540                key: (string (string_content) @name)
 541                value: (
 542                    (object
 543                        (pair
 544                            key: (string)
 545                            value: ((array
 546                                . (string (string_content) @action_name)
 547                                . (object
 548                                    (pair
 549                                    key: (string (string_content) @argument_key)
 550                                    value: (_)  @argument_value))
 551                                . ) @array
 552                            ))
 553                        )
 554                    )
 555                )
 556            )
 557        )
 558    (#eq? @name "bindings")
 559)"#;
 560
 561fn to_snake_case(text: &str) -> String {
 562    text.to_case(Case::Snake)
 563}
 564
 565fn action_argument_snake_case(
 566    contents: &str,
 567    mat: &QueryMatch,
 568    query: &Query,
 569) -> Option<(Range<usize>, String)> {
 570    let array_ix = query.capture_index_for_name("array")?;
 571    let action_name_ix = query.capture_index_for_name("action_name")?;
 572    let argument_key_ix = query.capture_index_for_name("argument_key")?;
 573    let argument_value_ix = query.capture_index_for_name("argument_value")?;
 574    let action_name = contents.get(
 575        mat.nodes_for_capture_index(action_name_ix)
 576            .next()?
 577            .byte_range(),
 578    )?;
 579
 580    let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?;
 581    let argument_key = contents.get(
 582        mat.nodes_for_capture_index(argument_key_ix)
 583            .next()?
 584            .byte_range(),
 585    )?;
 586
 587    if argument_key != *replacement_key {
 588        return None;
 589    }
 590
 591    let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?;
 592    let argument_value = contents.get(argument_value_node.byte_range())?;
 593
 594    let new_key = to_snake_case(argument_key);
 595    let new_value = if argument_value_node.kind() == "string" {
 596        format!("\"{}\"", to_snake_case(argument_value.trim_matches('"')))
 597    } else {
 598        argument_value.to_string()
 599    };
 600
 601    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
 602    let replacement = format!(
 603        "[\"{}\", {{ \"{}\": {} }}]",
 604        action_name, new_key, new_value
 605    );
 606
 607    Some((range_to_replace, replacement))
 608}
 609
 610pub static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock<HashMap<&str, &str>> =
 611    LazyLock::new(|| {
 612        HashMap::from_iter([
 613            ("vim::NextWordStart", "ignorePunctuation"),
 614            ("vim::NextWordEnd", "ignorePunctuation"),
 615            ("vim::PreviousWordStart", "ignorePunctuation"),
 616            ("vim::PreviousWordEnd", "ignorePunctuation"),
 617            ("vim::MoveToNext", "partialWord"),
 618            ("vim::MoveToPrev", "partialWord"),
 619            ("vim::Down", "displayLines"),
 620            ("vim::Up", "displayLines"),
 621            ("vim::EndOfLine", "displayLines"),
 622            ("vim::StartOfLine", "displayLines"),
 623            ("vim::FirstNonWhitespace", "displayLines"),
 624            ("pane::CloseActiveItem", "saveIntent"),
 625            ("vim::Paste", "preserveClipboard"),
 626            ("vim::Word", "ignorePunctuation"),
 627            ("vim::Subword", "ignorePunctuation"),
 628            ("vim::IndentObj", "includeBelow"),
 629        ])
 630    });
 631
 632const SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[
 633    (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name),
 634    (
 635        SETTINGS_REPLACE_NESTED_KEY,
 636        replace_edit_prediction_provider_setting,
 637    ),
 638    (
 639        SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
 640        replace_setting_in_languages,
 641    ),
 642];
 643
 644static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 645    Query::new(
 646        &tree_sitter_json::LANGUAGE.into(),
 647        &SETTINGS_MIGRATION_PATTERNS
 648            .iter()
 649            .map(|pattern| pattern.0)
 650            .collect::<String>(),
 651    )
 652    .unwrap()
 653});
 654
 655static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 656    Query::new(
 657        &tree_sitter_json::LANGUAGE.into(),
 658        SETTINGS_REPLACE_NESTED_KEY,
 659    )
 660    .unwrap()
 661});
 662
 663const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
 664    (object
 665        (pair
 666            key: (string (string_content) @name)
 667            value: (_)
 668        )
 669    )
 670)"#;
 671
 672fn replace_setting_name(
 673    contents: &str,
 674    mat: &QueryMatch,
 675    query: &Query,
 676) -> Option<(Range<usize>, String)> {
 677    let setting_capture_ix = query.capture_index_for_name("name")?;
 678    let setting_name_range = mat
 679        .nodes_for_capture_index(setting_capture_ix)
 680        .next()?
 681        .byte_range();
 682    let setting_name = contents.get(setting_name_range.clone())?;
 683    let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
 684    Some((setting_name_range, new_setting_name.to_string()))
 685}
 686
 687pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
 688    LazyLock::new(|| {
 689        HashMap::from_iter([
 690            (
 691                "show_inline_completions_in_menu",
 692                "show_edit_predictions_in_menu",
 693            ),
 694            ("show_inline_completions", "show_edit_predictions"),
 695            (
 696                "inline_completions_disabled_in",
 697                "edit_predictions_disabled_in",
 698            ),
 699            ("inline_completions", "edit_predictions"),
 700        ])
 701    });
 702
 703const SETTINGS_REPLACE_NESTED_KEY: &str = r#"
 704(object
 705  (pair
 706    key: (string (string_content) @parent_key)
 707    value: (object
 708        (pair
 709            key: (string (string_content) @setting_name)
 710            value: (_) @value
 711        )
 712    )
 713  )
 714)
 715"#;
 716
 717fn replace_edit_prediction_provider_setting(
 718    contents: &str,
 719    mat: &QueryMatch,
 720    query: &Query,
 721) -> Option<(Range<usize>, String)> {
 722    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 723    let parent_object_range = mat
 724        .nodes_for_capture_index(parent_object_capture_ix)
 725        .next()?
 726        .byte_range();
 727    let parent_object_name = contents.get(parent_object_range.clone())?;
 728
 729    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 730    let setting_range = mat
 731        .nodes_for_capture_index(setting_name_ix)
 732        .next()?
 733        .byte_range();
 734    let setting_name = contents.get(setting_range.clone())?;
 735
 736    if parent_object_name == "features" && setting_name == "inline_completion_provider" {
 737        return Some((setting_range, "edit_prediction_provider".into()));
 738    }
 739
 740    None
 741}
 742
 743const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
 744(object
 745  (pair
 746    key: (string (string_content) @languages)
 747    value: (object
 748    (pair
 749        key: (string)
 750        value: (object
 751            (pair
 752                key: (string (string_content) @setting_name)
 753                value: (_) @value
 754            )
 755        )
 756    ))
 757  )
 758)
 759(#eq? @languages "languages")
 760"#;
 761
 762fn replace_setting_in_languages(
 763    contents: &str,
 764    mat: &QueryMatch,
 765    query: &Query,
 766) -> Option<(Range<usize>, String)> {
 767    let setting_capture_ix = query.capture_index_for_name("setting_name")?;
 768    let setting_name_range = mat
 769        .nodes_for_capture_index(setting_capture_ix)
 770        .next()?
 771        .byte_range();
 772    let setting_name = contents.get(setting_name_range.clone())?;
 773    let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
 774
 775    Some((setting_name_range, new_setting_name.to_string()))
 776}
 777
 778static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
 779    LazyLock::new(|| {
 780        HashMap::from_iter([
 781            ("show_inline_completions", "show_edit_predictions"),
 782            (
 783                "inline_completions_disabled_in",
 784                "edit_predictions_disabled_in",
 785            ),
 786        ])
 787    });
 788
 789#[cfg(test)]
 790mod tests {
 791    use super::*;
 792
 793    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 794        let migrated = migrate_keymap(&input).unwrap();
 795        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 796    }
 797
 798    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 799        let migrated = migrate_settings(&input).unwrap();
 800        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 801    }
 802
 803    #[test]
 804    fn test_replace_array_with_single_string() {
 805        assert_migrate_keymap(
 806            r#"
 807            [
 808                {
 809                    "bindings": {
 810                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 811                    }
 812                }
 813            ]
 814            "#,
 815            Some(
 816                r#"
 817            [
 818                {
 819                    "bindings": {
 820                        "cmd-1": "workspace::ActivatePaneUp"
 821                    }
 822                }
 823            ]
 824            "#,
 825            ),
 826        )
 827    }
 828
 829    #[test]
 830    fn test_replace_action_argument_object_with_single_value() {
 831        assert_migrate_keymap(
 832            r#"
 833            [
 834                {
 835                    "bindings": {
 836                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 837                    }
 838                }
 839            ]
 840            "#,
 841            Some(
 842                r#"
 843            [
 844                {
 845                    "bindings": {
 846                        "cmd-1": ["editor::FoldAtLevel", 1]
 847                    }
 848                }
 849            ]
 850            "#,
 851            ),
 852        )
 853    }
 854
 855    #[test]
 856    fn test_replace_action_argument_object_with_single_value_2() {
 857        assert_migrate_keymap(
 858            r#"
 859            [
 860                {
 861                    "bindings": {
 862                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 863                    }
 864                }
 865            ]
 866            "#,
 867            Some(
 868                r#"
 869            [
 870                {
 871                    "bindings": {
 872                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 873                    }
 874                }
 875            ]
 876            "#,
 877            ),
 878        )
 879    }
 880
 881    #[test]
 882    fn test_rename_string_action() {
 883        assert_migrate_keymap(
 884            r#"
 885                [
 886                    {
 887                        "bindings": {
 888                            "cmd-1": "inline_completion::ToggleMenu"
 889                        }
 890                    }
 891                ]
 892            "#,
 893            Some(
 894                r#"
 895                [
 896                    {
 897                        "bindings": {
 898                            "cmd-1": "edit_prediction::ToggleMenu"
 899                        }
 900                    }
 901                ]
 902            "#,
 903            ),
 904        )
 905    }
 906
 907    #[test]
 908    fn test_rename_context_key() {
 909        assert_migrate_keymap(
 910            r#"
 911                [
 912                    {
 913                        "context": "Editor && inline_completion && !showing_completions"
 914                    }
 915                ]
 916            "#,
 917            Some(
 918                r#"
 919                [
 920                    {
 921                        "context": "Editor && edit_prediction && !showing_completions"
 922                    }
 923                ]
 924            "#,
 925            ),
 926        )
 927    }
 928
 929    #[test]
 930    fn test_string_of_array_replace() {
 931        assert_migrate_keymap(
 932            r#"
 933                [
 934                    {
 935                        "bindings": {
 936                            "ctrl-p": ["editor::GoToPrevHunk", { "center_cursor": true }],
 937                            "ctrl-q": ["editor::GoToPrevHunk"],
 938                            "ctrl-q": "editor::GoToPrevHunk", // should remain same
 939                        }
 940                    }
 941                ]
 942            "#,
 943            Some(
 944                r#"
 945                [
 946                    {
 947                        "bindings": {
 948                            "ctrl-p": ["editor::GoToPreviousHunk", { "center_cursor": true }],
 949                            "ctrl-q": ["editor::GoToPreviousHunk"],
 950                            "ctrl-q": "editor::GoToPrevHunk", // should remain same
 951                        }
 952                    }
 953                ]
 954            "#,
 955            ),
 956        )
 957    }
 958
 959    #[test]
 960    fn test_action_argument_snake_case() {
 961        // First performs transformations, then replacements
 962        assert_migrate_keymap(
 963            r#"
 964            [
 965                {
 966                    "bindings": {
 967                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
 968                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
 969                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
 970                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 971                    }
 972                }
 973            ]
 974            "#,
 975            Some(
 976                r#"
 977            [
 978                {
 979                    "bindings": {
 980                        "cmd-1": ["vim::PushObject", { "around": false }],
 981                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
 982                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
 983                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 984                    }
 985                }
 986            ]
 987            "#,
 988            ),
 989        )
 990    }
 991
 992    #[test]
 993    fn test_replace_setting_name() {
 994        assert_migrate_settings(
 995            r#"
 996                {
 997                    "show_inline_completions_in_menu": true,
 998                    "show_inline_completions": true,
 999                    "inline_completions_disabled_in": ["string"],
1000                    "inline_completions": { "some" : "value" }
1001                }
1002            "#,
1003            Some(
1004                r#"
1005                {
1006                    "show_edit_predictions_in_menu": true,
1007                    "show_edit_predictions": true,
1008                    "edit_predictions_disabled_in": ["string"],
1009                    "edit_predictions": { "some" : "value" }
1010                }
1011            "#,
1012            ),
1013        )
1014    }
1015
1016    #[test]
1017    fn test_nested_string_replace_for_settings() {
1018        assert_migrate_settings(
1019            r#"
1020                {
1021                    "features": {
1022                        "inline_completion_provider": "zed"
1023                    },
1024                }
1025            "#,
1026            Some(
1027                r#"
1028                {
1029                    "features": {
1030                        "edit_prediction_provider": "zed"
1031                    },
1032                }
1033            "#,
1034            ),
1035        )
1036    }
1037
1038    #[test]
1039    fn test_replace_settings_in_languages() {
1040        assert_migrate_settings(
1041            r#"
1042                {
1043                    "languages": {
1044                        "Astro": {
1045                            "show_inline_completions": true
1046                        }
1047                    }
1048                }
1049            "#,
1050            Some(
1051                r#"
1052                {
1053                    "languages": {
1054                        "Astro": {
1055                            "show_edit_predictions": true
1056                        }
1057                    }
1058                }
1059            "#,
1060            ),
1061        )
1062    }
1063}