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, replace_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 replace_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_node = mat.nodes_for_capture_index(action_name_ix).next()?;
 394    let action_name_range = action_name_node.byte_range();
 395    let action_name = contents.get(action_name_range.clone())?;
 396
 397    if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
 398        return Some((action_name_range, new_action_name.to_string()));
 399    }
 400
 401    if let Some((new_action_name, options)) = STRING_TO_ARRAY_REPLACE.get(action_name) {
 402        let full_string_range = action_name_node.parent()?.byte_range();
 403        let mut options_parts = Vec::new();
 404        for (key, value) in options.iter() {
 405            options_parts.push(format!("\"{}\": {}", key, value));
 406        }
 407        let options_str = options_parts.join(", ");
 408        let replacement = format!("[\"{}\", {{ {} }}]", new_action_name, options_str);
 409        return Some((full_string_range, replacement));
 410    }
 411
 412    None
 413}
 414
 415/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
 416static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
 417    HashMap::from_iter([
 418        (
 419            "inline_completion::ToggleMenu",
 420            "edit_prediction::ToggleMenu",
 421        ),
 422        ("editor::NextInlineCompletion", "editor::NextEditPrediction"),
 423        (
 424            "editor::PreviousInlineCompletion",
 425            "editor::PreviousEditPrediction",
 426        ),
 427        (
 428            "editor::AcceptPartialInlineCompletion",
 429            "editor::AcceptPartialEditPrediction",
 430        ),
 431        ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"),
 432        (
 433            "editor::AcceptInlineCompletion",
 434            "editor::AcceptEditPrediction",
 435        ),
 436        (
 437            "editor::ToggleInlineCompletions",
 438            "editor::ToggleEditPrediction",
 439        ),
 440        (
 441            "editor::GoToPrevDiagnostic",
 442            "editor::GoToPreviousDiagnostic",
 443        ),
 444        ("editor::ContextMenuPrev", "editor::ContextMenuPrevious"),
 445        ("search::SelectPrevMatch", "search::SelectPreviousMatch"),
 446        ("file_finder::SelectPrev", "file_finder::SelectPrevious"),
 447        ("menu::SelectPrev", "menu::SelectPrevious"),
 448        ("editor::TabPrev", "editor::Backtab"),
 449        ("pane::ActivatePrevItem", "pane::ActivatePreviousItem"),
 450        ("vim::MoveToPrev", "vim::MoveToPrevious"),
 451        ("vim::MoveToPrevMatch", "vim::MoveToPreviousMatch"),
 452    ])
 453});
 454
 455/// "editor::GoToPrevHunk" -> ["editor::GoToPreviousHunk", { "center_cursor": true }]
 456static STRING_TO_ARRAY_REPLACE: LazyLock<HashMap<&str, (&str, HashMap<&str, bool>)>> =
 457    LazyLock::new(|| {
 458        HashMap::from_iter([
 459            (
 460                "editor::GoToHunk",
 461                (
 462                    "editor::GoToHunk",
 463                    HashMap::from_iter([("center_cursor", true)]),
 464                ),
 465            ),
 466            (
 467                "editor::GoToPrevHunk",
 468                (
 469                    "editor::GoToPreviousHunk",
 470                    HashMap::from_iter([("center_cursor", true)]),
 471                ),
 472            ),
 473        ])
 474    });
 475
 476const CONTEXT_PREDICATE_PATTERN: &str = r#"(document
 477    (array
 478        (object
 479            (pair
 480                key: (string (string_content) @name)
 481                value: (string (string_content) @context_predicate)
 482            )
 483        )
 484    )
 485    (#eq? @name "context")
 486)"#;
 487
 488fn rename_context_key(
 489    contents: &str,
 490    mat: &QueryMatch,
 491    query: &Query,
 492) -> Option<(Range<usize>, String)> {
 493    let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
 494    let context_predicate_range = mat
 495        .nodes_for_capture_index(context_predicate_ix)
 496        .next()?
 497        .byte_range();
 498    let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
 499    let mut new_predicate = old_predicate.to_string();
 500    for (old_key, new_key) in CONTEXT_REPLACE.iter() {
 501        new_predicate = new_predicate.replace(old_key, new_key);
 502    }
 503    if new_predicate != old_predicate {
 504        Some((context_predicate_range, new_predicate.to_string()))
 505    } else {
 506        None
 507    }
 508}
 509
 510/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
 511pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
 512    HashMap::from_iter([
 513        ("inline_completion", "edit_prediction"),
 514        (
 515            "inline_completion_requires_modifier",
 516            "edit_prediction_requires_modifier",
 517        ),
 518    ])
 519});
 520
 521const ACTION_ARGUMENT_SNAKE_CASE_PATTERN: &str = r#"(document
 522    (array
 523        (object
 524            (pair
 525                key: (string (string_content) @name)
 526                value: (
 527                    (object
 528                        (pair
 529                            key: (string)
 530                            value: ((array
 531                                . (string (string_content) @action_name)
 532                                . (object
 533                                    (pair
 534                                    key: (string (string_content) @argument_key)
 535                                    value: (_)  @argument_value))
 536                                . ) @array
 537                            ))
 538                        )
 539                    )
 540                )
 541            )
 542        )
 543    (#eq? @name "bindings")
 544)"#;
 545
 546fn to_snake_case(text: &str) -> String {
 547    text.to_case(Case::Snake)
 548}
 549
 550fn action_argument_snake_case(
 551    contents: &str,
 552    mat: &QueryMatch,
 553    query: &Query,
 554) -> Option<(Range<usize>, String)> {
 555    let array_ix = query.capture_index_for_name("array")?;
 556    let action_name_ix = query.capture_index_for_name("action_name")?;
 557    let argument_key_ix = query.capture_index_for_name("argument_key")?;
 558    let argument_value_ix = query.capture_index_for_name("argument_value")?;
 559    let action_name = contents.get(
 560        mat.nodes_for_capture_index(action_name_ix)
 561            .next()?
 562            .byte_range(),
 563    )?;
 564
 565    let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?;
 566    let argument_key = contents.get(
 567        mat.nodes_for_capture_index(argument_key_ix)
 568            .next()?
 569            .byte_range(),
 570    )?;
 571
 572    if argument_key != *replacement_key {
 573        return None;
 574    }
 575
 576    let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?;
 577    let argument_value = contents.get(argument_value_node.byte_range())?;
 578
 579    let new_key = to_snake_case(argument_key);
 580    let new_value = if argument_value_node.kind() == "string" {
 581        format!("\"{}\"", to_snake_case(argument_value.trim_matches('"')))
 582    } else {
 583        argument_value.to_string()
 584    };
 585
 586    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
 587    let replacement = format!(
 588        "[\"{}\", {{ \"{}\": {} }}]",
 589        action_name, new_key, new_value
 590    );
 591
 592    Some((range_to_replace, replacement))
 593}
 594
 595pub static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock<HashMap<&str, &str>> =
 596    LazyLock::new(|| {
 597        HashMap::from_iter([
 598            ("vim::NextWordStart", "ignorePunctuation"),
 599            ("vim::NextWordEnd", "ignorePunctuation"),
 600            ("vim::PreviousWordStart", "ignorePunctuation"),
 601            ("vim::PreviousWordEnd", "ignorePunctuation"),
 602            ("vim::MoveToNext", "partialWord"),
 603            ("vim::MoveToPrev", "partialWord"),
 604            ("vim::Down", "displayLines"),
 605            ("vim::Up", "displayLines"),
 606            ("vim::EndOfLine", "displayLines"),
 607            ("vim::StartOfLine", "displayLines"),
 608            ("vim::FirstNonWhitespace", "displayLines"),
 609            ("pane::CloseActiveItem", "saveIntent"),
 610            ("vim::Paste", "preserveClipboard"),
 611            ("vim::Word", "ignorePunctuation"),
 612            ("vim::Subword", "ignorePunctuation"),
 613            ("vim::IndentObj", "includeBelow"),
 614        ])
 615    });
 616
 617const SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[
 618    (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name),
 619    (
 620        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 621        replace_edit_prediction_provider_setting,
 622    ),
 623    (
 624        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 625        replace_tab_close_button_setting_key,
 626    ),
 627    (
 628        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 629        replace_tab_close_button_setting_value,
 630    ),
 631    (
 632        SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
 633        replace_setting_in_languages,
 634    ),
 635];
 636
 637static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 638    Query::new(
 639        &tree_sitter_json::LANGUAGE.into(),
 640        &SETTINGS_MIGRATION_PATTERNS
 641            .iter()
 642            .map(|pattern| pattern.0)
 643            .collect::<String>(),
 644    )
 645    .unwrap()
 646});
 647
 648static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 649    Query::new(
 650        &tree_sitter_json::LANGUAGE.into(),
 651        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 652    )
 653    .unwrap()
 654});
 655
 656const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
 657    (object
 658        (pair
 659            key: (string (string_content) @name)
 660            value: (_)
 661        )
 662    )
 663)"#;
 664
 665fn replace_setting_name(
 666    contents: &str,
 667    mat: &QueryMatch,
 668    query: &Query,
 669) -> Option<(Range<usize>, String)> {
 670    let setting_capture_ix = query.capture_index_for_name("name")?;
 671    let setting_name_range = mat
 672        .nodes_for_capture_index(setting_capture_ix)
 673        .next()?
 674        .byte_range();
 675    let setting_name = contents.get(setting_name_range.clone())?;
 676    let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
 677    Some((setting_name_range, new_setting_name.to_string()))
 678}
 679
 680pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
 681    LazyLock::new(|| {
 682        HashMap::from_iter([
 683            (
 684                "show_inline_completions_in_menu",
 685                "show_edit_predictions_in_menu",
 686            ),
 687            ("show_inline_completions", "show_edit_predictions"),
 688            (
 689                "inline_completions_disabled_in",
 690                "edit_predictions_disabled_in",
 691            ),
 692            ("inline_completions", "edit_predictions"),
 693        ])
 694    });
 695
 696const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#"
 697(object
 698  (pair
 699    key: (string (string_content) @parent_key)
 700    value: (object
 701        (pair
 702            key: (string (string_content) @setting_name)
 703            value: (_) @setting_value
 704        )
 705    )
 706  )
 707)
 708"#;
 709
 710fn replace_edit_prediction_provider_setting(
 711    contents: &str,
 712    mat: &QueryMatch,
 713    query: &Query,
 714) -> Option<(Range<usize>, String)> {
 715    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 716    let parent_object_range = mat
 717        .nodes_for_capture_index(parent_object_capture_ix)
 718        .next()?
 719        .byte_range();
 720    let parent_object_name = contents.get(parent_object_range.clone())?;
 721
 722    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 723    let setting_range = mat
 724        .nodes_for_capture_index(setting_name_ix)
 725        .next()?
 726        .byte_range();
 727    let setting_name = contents.get(setting_range.clone())?;
 728
 729    if parent_object_name == "features" && setting_name == "inline_completion_provider" {
 730        return Some((setting_range, "edit_prediction_provider".into()));
 731    }
 732
 733    None
 734}
 735
 736fn replace_tab_close_button_setting_key(
 737    contents: &str,
 738    mat: &QueryMatch,
 739    query: &Query,
 740) -> Option<(Range<usize>, String)> {
 741    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 742    let parent_object_range = mat
 743        .nodes_for_capture_index(parent_object_capture_ix)
 744        .next()?
 745        .byte_range();
 746    let parent_object_name = contents.get(parent_object_range.clone())?;
 747
 748    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 749    let setting_range = mat
 750        .nodes_for_capture_index(setting_name_ix)
 751        .next()?
 752        .byte_range();
 753    let setting_name = contents.get(setting_range.clone())?;
 754
 755    if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
 756        return Some((setting_range, "show_close_button".into()));
 757    }
 758
 759    None
 760}
 761
 762fn replace_tab_close_button_setting_value(
 763    contents: &str,
 764    mat: &QueryMatch,
 765    query: &Query,
 766) -> Option<(Range<usize>, String)> {
 767    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
 768    let parent_object_range = mat
 769        .nodes_for_capture_index(parent_object_capture_ix)
 770        .next()?
 771        .byte_range();
 772    let parent_object_name = contents.get(parent_object_range.clone())?;
 773
 774    let setting_name_ix = query.capture_index_for_name("setting_name")?;
 775    let setting_name_range = mat
 776        .nodes_for_capture_index(setting_name_ix)
 777        .next()?
 778        .byte_range();
 779    let setting_name = contents.get(setting_name_range.clone())?;
 780
 781    let setting_value_ix = query.capture_index_for_name("setting_value")?;
 782    let setting_value_range = mat
 783        .nodes_for_capture_index(setting_value_ix)
 784        .next()?
 785        .byte_range();
 786    let setting_value = contents.get(setting_value_range.clone())?;
 787
 788    if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
 789        match setting_value {
 790            "true" => {
 791                return Some((setting_value_range, "\"always\"".to_string()));
 792            }
 793            "false" => {
 794                return Some((setting_value_range, "\"hover\"".to_string()));
 795            }
 796            _ => {}
 797        }
 798    }
 799
 800    None
 801}
 802
 803const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
 804(object
 805  (pair
 806    key: (string (string_content) @languages)
 807    value: (object
 808    (pair
 809        key: (string)
 810        value: (object
 811            (pair
 812                key: (string (string_content) @setting_name)
 813                value: (_) @value
 814            )
 815        )
 816    ))
 817  )
 818)
 819(#eq? @languages "languages")
 820"#;
 821
 822fn replace_setting_in_languages(
 823    contents: &str,
 824    mat: &QueryMatch,
 825    query: &Query,
 826) -> Option<(Range<usize>, String)> {
 827    let setting_capture_ix = query.capture_index_for_name("setting_name")?;
 828    let setting_name_range = mat
 829        .nodes_for_capture_index(setting_capture_ix)
 830        .next()?
 831        .byte_range();
 832    let setting_name = contents.get(setting_name_range.clone())?;
 833    let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
 834
 835    Some((setting_name_range, new_setting_name.to_string()))
 836}
 837
 838static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
 839    LazyLock::new(|| {
 840        HashMap::from_iter([
 841            ("show_inline_completions", "show_edit_predictions"),
 842            (
 843                "inline_completions_disabled_in",
 844                "edit_predictions_disabled_in",
 845            ),
 846        ])
 847    });
 848
 849#[cfg(test)]
 850mod tests {
 851    use super::*;
 852
 853    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 854        let migrated = migrate_keymap(&input).unwrap();
 855        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 856    }
 857
 858    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 859        let migrated = migrate_settings(&input).unwrap();
 860        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 861    }
 862
 863    #[test]
 864    fn test_replace_array_with_single_string() {
 865        assert_migrate_keymap(
 866            r#"
 867            [
 868                {
 869                    "bindings": {
 870                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 871                    }
 872                }
 873            ]
 874            "#,
 875            Some(
 876                r#"
 877            [
 878                {
 879                    "bindings": {
 880                        "cmd-1": "workspace::ActivatePaneUp"
 881                    }
 882                }
 883            ]
 884            "#,
 885            ),
 886        )
 887    }
 888
 889    #[test]
 890    fn test_replace_action_argument_object_with_single_value() {
 891        assert_migrate_keymap(
 892            r#"
 893            [
 894                {
 895                    "bindings": {
 896                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 897                    }
 898                }
 899            ]
 900            "#,
 901            Some(
 902                r#"
 903            [
 904                {
 905                    "bindings": {
 906                        "cmd-1": ["editor::FoldAtLevel", 1]
 907                    }
 908                }
 909            ]
 910            "#,
 911            ),
 912        )
 913    }
 914
 915    #[test]
 916    fn test_replace_action_argument_object_with_single_value_2() {
 917        assert_migrate_keymap(
 918            r#"
 919            [
 920                {
 921                    "bindings": {
 922                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 923                    }
 924                }
 925            ]
 926            "#,
 927            Some(
 928                r#"
 929            [
 930                {
 931                    "bindings": {
 932                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 933                    }
 934                }
 935            ]
 936            "#,
 937            ),
 938        )
 939    }
 940
 941    #[test]
 942    fn test_rename_string_action() {
 943        assert_migrate_keymap(
 944            r#"
 945                [
 946                    {
 947                        "bindings": {
 948                            "cmd-1": "inline_completion::ToggleMenu"
 949                        }
 950                    }
 951                ]
 952            "#,
 953            Some(
 954                r#"
 955                [
 956                    {
 957                        "bindings": {
 958                            "cmd-1": "edit_prediction::ToggleMenu"
 959                        }
 960                    }
 961                ]
 962            "#,
 963            ),
 964        )
 965    }
 966
 967    #[test]
 968    fn test_rename_context_key() {
 969        assert_migrate_keymap(
 970            r#"
 971                [
 972                    {
 973                        "context": "Editor && inline_completion && !showing_completions"
 974                    }
 975                ]
 976            "#,
 977            Some(
 978                r#"
 979                [
 980                    {
 981                        "context": "Editor && edit_prediction && !showing_completions"
 982                    }
 983                ]
 984            "#,
 985            ),
 986        )
 987    }
 988
 989    #[test]
 990    fn test_string_to_array_replace() {
 991        assert_migrate_keymap(
 992            r#"
 993                [
 994                    {
 995                        "bindings": {
 996                            "ctrl-q": "editor::GoToHunk",
 997                            "ctrl-w": "editor::GoToPrevHunk"
 998                        }
 999                    }
1000                ]
1001            "#,
1002            Some(
1003                r#"
1004                [
1005                    {
1006                        "bindings": {
1007                            "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }],
1008                            "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }]
1009                        }
1010                    }
1011                ]
1012            "#,
1013            ),
1014        )
1015    }
1016
1017    #[test]
1018    fn test_action_argument_snake_case() {
1019        // First performs transformations, then replacements
1020        assert_migrate_keymap(
1021            r#"
1022            [
1023                {
1024                    "bindings": {
1025                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
1026                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
1027                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
1028                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
1029                    }
1030                }
1031            ]
1032            "#,
1033            Some(
1034                r#"
1035            [
1036                {
1037                    "bindings": {
1038                        "cmd-1": ["vim::PushObject", { "around": false }],
1039                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
1040                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
1041                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
1042                    }
1043                }
1044            ]
1045            "#,
1046            ),
1047        )
1048    }
1049
1050    #[test]
1051    fn test_replace_setting_name() {
1052        assert_migrate_settings(
1053            r#"
1054                {
1055                    "show_inline_completions_in_menu": true,
1056                    "show_inline_completions": true,
1057                    "inline_completions_disabled_in": ["string"],
1058                    "inline_completions": { "some" : "value" }
1059                }
1060            "#,
1061            Some(
1062                r#"
1063                {
1064                    "show_edit_predictions_in_menu": true,
1065                    "show_edit_predictions": true,
1066                    "edit_predictions_disabled_in": ["string"],
1067                    "edit_predictions": { "some" : "value" }
1068                }
1069            "#,
1070            ),
1071        )
1072    }
1073
1074    #[test]
1075    fn test_nested_string_replace_for_settings() {
1076        assert_migrate_settings(
1077            r#"
1078                {
1079                    "features": {
1080                        "inline_completion_provider": "zed"
1081                    },
1082                }
1083            "#,
1084            Some(
1085                r#"
1086                {
1087                    "features": {
1088                        "edit_prediction_provider": "zed"
1089                    },
1090                }
1091            "#,
1092            ),
1093        )
1094    }
1095
1096    #[test]
1097    fn test_replace_settings_in_languages() {
1098        assert_migrate_settings(
1099            r#"
1100                {
1101                    "languages": {
1102                        "Astro": {
1103                            "show_inline_completions": true
1104                        }
1105                    }
1106                }
1107            "#,
1108            Some(
1109                r#"
1110                {
1111                    "languages": {
1112                        "Astro": {
1113                            "show_edit_predictions": true
1114                        }
1115                    }
1116                }
1117            "#,
1118            ),
1119        )
1120    }
1121}