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        ("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_NESTED_KEY_VALUE_PATTERN,
 636        replace_edit_prediction_provider_setting,
 637    ),
 638    (
 639        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 640        replace_tab_close_button_setting_key,
 641    ),
 642    (
 643        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 644        replace_tab_close_button_setting_value,
 645    ),
 646    (
 647        SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
 648        replace_setting_in_languages,
 649    ),
 650];
 651
 652static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 653    Query::new(
 654        &tree_sitter_json::LANGUAGE.into(),
 655        &SETTINGS_MIGRATION_PATTERNS
 656            .iter()
 657            .map(|pattern| pattern.0)
 658            .collect::<String>(),
 659    )
 660    .unwrap()
 661});
 662
 663static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 664    Query::new(
 665        &tree_sitter_json::LANGUAGE.into(),
 666        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 667    )
 668    .unwrap()
 669});
 670
 671const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
 672    (object
 673        (pair
 674            key: (string (string_content) @name)
 675            value: (_)
 676        )
 677    )
 678)"#;
 679
 680fn replace_setting_name(
 681    contents: &str,
 682    mat: &QueryMatch,
 683    query: &Query,
 684) -> Option<(Range<usize>, String)> {
 685    let setting_capture_ix = query.capture_index_for_name("name")?;
 686    let setting_name_range = mat
 687        .nodes_for_capture_index(setting_capture_ix)
 688        .next()?
 689        .byte_range();
 690    let setting_name = contents.get(setting_name_range.clone())?;
 691    let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
 692    Some((setting_name_range, new_setting_name.to_string()))
 693}
 694
 695pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
 696    LazyLock::new(|| {
 697        HashMap::from_iter([
 698            (
 699                "show_inline_completions_in_menu",
 700                "show_edit_predictions_in_menu",
 701            ),
 702            ("show_inline_completions", "show_edit_predictions"),
 703            (
 704                "inline_completions_disabled_in",
 705                "edit_predictions_disabled_in",
 706            ),
 707            ("inline_completions", "edit_predictions"),
 708        ])
 709    });
 710
 711const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#"
 712(object
 713  (pair
 714    key: (string (string_content) @parent_key)
 715    value: (object
 716        (pair
 717            key: (string (string_content) @setting_name)
 718            value: (_) @setting_value
 719        )
 720    )
 721  )
 722)
 723"#;
 724
 725fn replace_edit_prediction_provider_setting(
 726    contents: &str,
 727    mat: &QueryMatch,
 728    query: &Query,
 729) -> Option<(Range<usize>, String)> {
 730    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 731    let parent_object_range = mat
 732        .nodes_for_capture_index(parent_object_capture_ix)
 733        .next()?
 734        .byte_range();
 735    let parent_object_name = contents.get(parent_object_range.clone())?;
 736
 737    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 738    let setting_range = mat
 739        .nodes_for_capture_index(setting_name_ix)
 740        .next()?
 741        .byte_range();
 742    let setting_name = contents.get(setting_range.clone())?;
 743
 744    if parent_object_name == "features" && setting_name == "inline_completion_provider" {
 745        return Some((setting_range, "edit_prediction_provider".into()));
 746    }
 747
 748    None
 749}
 750
 751fn replace_tab_close_button_setting_key(
 752    contents: &str,
 753    mat: &QueryMatch,
 754    query: &Query,
 755) -> Option<(Range<usize>, String)> {
 756    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 757    let parent_object_range = mat
 758        .nodes_for_capture_index(parent_object_capture_ix)
 759        .next()?
 760        .byte_range();
 761    let parent_object_name = contents.get(parent_object_range.clone())?;
 762
 763    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 764    let setting_range = mat
 765        .nodes_for_capture_index(setting_name_ix)
 766        .next()?
 767        .byte_range();
 768    let setting_name = contents.get(setting_range.clone())?;
 769
 770    if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
 771        return Some((setting_range, "show_close_button".into()));
 772    }
 773
 774    None
 775}
 776
 777fn replace_tab_close_button_setting_value(
 778    contents: &str,
 779    mat: &QueryMatch,
 780    query: &Query,
 781) -> Option<(Range<usize>, String)> {
 782    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 783    let parent_object_range = mat
 784        .nodes_for_capture_index(parent_object_capture_ix)
 785        .next()?
 786        .byte_range();
 787    let parent_object_name = contents.get(parent_object_range.clone())?;
 788
 789    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 790    let setting_name_range = mat
 791        .nodes_for_capture_index(setting_name_ix)
 792        .next()?
 793        .byte_range();
 794    let setting_name = contents.get(setting_name_range.clone())?;
 795
 796    let setting_value_ix = query.capture_index_for_name("setting_value")?;
 797    let setting_value_range = mat
 798        .nodes_for_capture_index(setting_value_ix)
 799        .next()?
 800        .byte_range();
 801    let setting_value = contents.get(setting_value_range.clone())?;
 802
 803    if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
 804        match setting_value {
 805            "true" => {
 806                return Some((setting_value_range, "\"always\"".to_string()));
 807            }
 808            "false" => {
 809                return Some((setting_value_range, "\"hover\"".to_string()));
 810            }
 811            _ => {}
 812        }
 813    }
 814
 815    None
 816}
 817
 818const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
 819(object
 820  (pair
 821    key: (string (string_content) @languages)
 822    value: (object
 823    (pair
 824        key: (string)
 825        value: (object
 826            (pair
 827                key: (string (string_content) @setting_name)
 828                value: (_) @value
 829            )
 830        )
 831    ))
 832  )
 833)
 834(#eq? @languages "languages")
 835"#;
 836
 837fn replace_setting_in_languages(
 838    contents: &str,
 839    mat: &QueryMatch,
 840    query: &Query,
 841) -> Option<(Range<usize>, String)> {
 842    let setting_capture_ix = query.capture_index_for_name("setting_name")?;
 843    let setting_name_range = mat
 844        .nodes_for_capture_index(setting_capture_ix)
 845        .next()?
 846        .byte_range();
 847    let setting_name = contents.get(setting_name_range.clone())?;
 848    let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
 849
 850    Some((setting_name_range, new_setting_name.to_string()))
 851}
 852
 853static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
 854    LazyLock::new(|| {
 855        HashMap::from_iter([
 856            ("show_inline_completions", "show_edit_predictions"),
 857            (
 858                "inline_completions_disabled_in",
 859                "edit_predictions_disabled_in",
 860            ),
 861        ])
 862    });
 863
 864#[cfg(test)]
 865mod tests {
 866    use super::*;
 867
 868    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 869        let migrated = migrate_keymap(&input).unwrap();
 870        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 871    }
 872
 873    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 874        let migrated = migrate_settings(&input).unwrap();
 875        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 876    }
 877
 878    #[test]
 879    fn test_replace_array_with_single_string() {
 880        assert_migrate_keymap(
 881            r#"
 882            [
 883                {
 884                    "bindings": {
 885                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 886                    }
 887                }
 888            ]
 889            "#,
 890            Some(
 891                r#"
 892            [
 893                {
 894                    "bindings": {
 895                        "cmd-1": "workspace::ActivatePaneUp"
 896                    }
 897                }
 898            ]
 899            "#,
 900            ),
 901        )
 902    }
 903
 904    #[test]
 905    fn test_replace_action_argument_object_with_single_value() {
 906        assert_migrate_keymap(
 907            r#"
 908            [
 909                {
 910                    "bindings": {
 911                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 912                    }
 913                }
 914            ]
 915            "#,
 916            Some(
 917                r#"
 918            [
 919                {
 920                    "bindings": {
 921                        "cmd-1": ["editor::FoldAtLevel", 1]
 922                    }
 923                }
 924            ]
 925            "#,
 926            ),
 927        )
 928    }
 929
 930    #[test]
 931    fn test_replace_action_argument_object_with_single_value_2() {
 932        assert_migrate_keymap(
 933            r#"
 934            [
 935                {
 936                    "bindings": {
 937                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 938                    }
 939                }
 940            ]
 941            "#,
 942            Some(
 943                r#"
 944            [
 945                {
 946                    "bindings": {
 947                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 948                    }
 949                }
 950            ]
 951            "#,
 952            ),
 953        )
 954    }
 955
 956    #[test]
 957    fn test_rename_string_action() {
 958        assert_migrate_keymap(
 959            r#"
 960                [
 961                    {
 962                        "bindings": {
 963                            "cmd-1": "inline_completion::ToggleMenu"
 964                        }
 965                    }
 966                ]
 967            "#,
 968            Some(
 969                r#"
 970                [
 971                    {
 972                        "bindings": {
 973                            "cmd-1": "edit_prediction::ToggleMenu"
 974                        }
 975                    }
 976                ]
 977            "#,
 978            ),
 979        )
 980    }
 981
 982    #[test]
 983    fn test_rename_context_key() {
 984        assert_migrate_keymap(
 985            r#"
 986                [
 987                    {
 988                        "context": "Editor && inline_completion && !showing_completions"
 989                    }
 990                ]
 991            "#,
 992            Some(
 993                r#"
 994                [
 995                    {
 996                        "context": "Editor && edit_prediction && !showing_completions"
 997                    }
 998                ]
 999            "#,
1000            ),
1001        )
1002    }
1003
1004    #[test]
1005    fn test_string_of_array_replace() {
1006        assert_migrate_keymap(
1007            r#"
1008                [
1009                    {
1010                        "bindings": {
1011                            "ctrl-p": ["editor::GoToPrevHunk", { "center_cursor": true }],
1012                            "ctrl-q": ["editor::GoToPrevHunk"],
1013                            "ctrl-q": "editor::GoToPrevHunk", // should remain same
1014                        }
1015                    }
1016                ]
1017            "#,
1018            Some(
1019                r#"
1020                [
1021                    {
1022                        "bindings": {
1023                            "ctrl-p": ["editor::GoToPreviousHunk", { "center_cursor": true }],
1024                            "ctrl-q": ["editor::GoToPreviousHunk"],
1025                            "ctrl-q": "editor::GoToPrevHunk", // should remain same
1026                        }
1027                    }
1028                ]
1029            "#,
1030            ),
1031        )
1032    }
1033
1034    #[test]
1035    fn test_action_argument_snake_case() {
1036        // First performs transformations, then replacements
1037        assert_migrate_keymap(
1038            r#"
1039            [
1040                {
1041                    "bindings": {
1042                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
1043                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
1044                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
1045                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
1046                    }
1047                }
1048            ]
1049            "#,
1050            Some(
1051                r#"
1052            [
1053                {
1054                    "bindings": {
1055                        "cmd-1": ["vim::PushObject", { "around": false }],
1056                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
1057                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
1058                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
1059                    }
1060                }
1061            ]
1062            "#,
1063            ),
1064        )
1065    }
1066
1067    #[test]
1068    fn test_replace_setting_name() {
1069        assert_migrate_settings(
1070            r#"
1071                {
1072                    "show_inline_completions_in_menu": true,
1073                    "show_inline_completions": true,
1074                    "inline_completions_disabled_in": ["string"],
1075                    "inline_completions": { "some" : "value" }
1076                }
1077            "#,
1078            Some(
1079                r#"
1080                {
1081                    "show_edit_predictions_in_menu": true,
1082                    "show_edit_predictions": true,
1083                    "edit_predictions_disabled_in": ["string"],
1084                    "edit_predictions": { "some" : "value" }
1085                }
1086            "#,
1087            ),
1088        )
1089    }
1090
1091    #[test]
1092    fn test_nested_string_replace_for_settings() {
1093        assert_migrate_settings(
1094            r#"
1095                {
1096                    "features": {
1097                        "inline_completion_provider": "zed"
1098                    },
1099                }
1100            "#,
1101            Some(
1102                r#"
1103                {
1104                    "features": {
1105                        "edit_prediction_provider": "zed"
1106                    },
1107                }
1108            "#,
1109            ),
1110        )
1111    }
1112
1113    #[test]
1114    fn test_replace_settings_in_languages() {
1115        assert_migrate_settings(
1116            r#"
1117                {
1118                    "languages": {
1119                        "Astro": {
1120                            "show_inline_completions": true
1121                        }
1122                    }
1123                }
1124            "#,
1125            Some(
1126                r#"
1127                {
1128                    "languages": {
1129                        "Astro": {
1130                            "show_edit_predictions": true
1131                        }
1132                    }
1133                }
1134            "#,
1135            ),
1136        )
1137    }
1138}