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