migrator.rs

  1use anyhow::{Context, Result};
  2use collections::HashMap;
  3use convert_case::{Case, Casing};
  4use std::{cmp::Reverse, ops::Range, sync::LazyLock};
  5use streaming_iterator::StreamingIterator;
  6use tree_sitter::{Query, QueryMatch};
  7
  8fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
  9    let mut parser = tree_sitter::Parser::new();
 10    parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
 11    let syntax_tree = parser
 12        .parse(&text, None)
 13        .context("failed to parse settings")?;
 14
 15    let mut cursor = tree_sitter::QueryCursor::new();
 16    let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
 17
 18    let mut edits = vec![];
 19    while let Some(mat) = matches.next() {
 20        if let Some((_, callback)) = patterns.get(mat.pattern_index) {
 21            edits.extend(callback(&text, &mat, query));
 22        }
 23    }
 24
 25    edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
 26    edits.dedup_by(|(range_b, _), (range_a, _)| {
 27        range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
 28    });
 29
 30    if edits.is_empty() {
 31        Ok(None)
 32    } else {
 33        let mut new_text = text.to_string();
 34        for (range, replacement) in edits.iter().rev() {
 35            new_text.replace_range(range.clone(), replacement);
 36        }
 37        if new_text == text {
 38            log::error!(
 39                "Edits computed for configuration migration do not cause a change: {:?}",
 40                edits
 41            );
 42            Ok(None)
 43        } else {
 44            Ok(Some(new_text))
 45        }
 46    }
 47}
 48
 49pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
 50    let transformed_text = migrate(
 51        text,
 52        KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS,
 53        &KEYMAP_MIGRATION_TRANSFORMATION_QUERY,
 54    )?;
 55    let replacement_text = migrate(
 56        &transformed_text.as_ref().unwrap_or(&text.to_string()),
 57        KEYMAP_MIGRATION_REPLACEMENT_PATTERNS,
 58        &KEYMAP_MIGRATION_REPLACEMENT_QUERY,
 59    )?;
 60    Ok(replacement_text.or(transformed_text))
 61}
 62
 63pub fn migrate_settings(text: &str) -> Result<Option<String>> {
 64    migrate(
 65        &text,
 66        SETTINGS_MIGRATION_PATTERNS,
 67        &SETTINGS_MIGRATION_QUERY,
 68    )
 69}
 70
 71pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
 72    migrate(
 73        &text,
 74        &[(
 75            SETTINGS_REPLACE_NESTED_KEY,
 76            replace_edit_prediction_provider_setting,
 77        )],
 78        &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
 79    )
 80}
 81
 82type MigrationPatterns = &'static [(
 83    &'static str,
 84    fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
 85)];
 86
 87const KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS: MigrationPatterns = &[
 88    (ACTION_ARRAY_PATTERN, replace_array_with_single_string),
 89    (
 90        ACTION_ARGUMENT_OBJECT_PATTERN,
 91        replace_action_argument_object_with_single_value,
 92    ),
 93    (ACTION_STRING_PATTERN, rename_string_action),
 94    (CONTEXT_PREDICATE_PATTERN, rename_context_key),
 95];
 96
 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            "editor::GoToPrevDiagnostic",
429            "editor::GoToPreviousDiagnostic",
430        ),
431        ("editor::ContextMenuPrev", "editor::ContextMenuPrevious"),
432        ("search::SelectPrevMatch", "search::SelectPreviousMatch"),
433        ("file_finder::SelectPrev", "file_finder::SelectPrevious"),
434        ("menu::SelectPrev", "menu::SelectPrevious"),
435        ("editor::TabPrev", "editor::Backtab"),
436        ("pane::ActivatePrevItem", "pane::ActivatePreviousItem"),
437        ("vim::MoveToPrev", "vim::MoveToPrevious"),
438        ("vim::MoveToPrevMatch", "vim::MoveToPreviousMatch"),
439        ("editor::GoToPrevHunk", "editor::GoToPreviousHunk"),
440    ])
441});
442
443const CONTEXT_PREDICATE_PATTERN: &str = r#"(document
444    (array
445        (object
446            (pair
447                key: (string (string_content) @name)
448                value: (string (string_content) @context_predicate)
449            )
450        )
451    )
452    (#eq? @name "context")
453)"#;
454
455fn rename_context_key(
456    contents: &str,
457    mat: &QueryMatch,
458    query: &Query,
459) -> Option<(Range<usize>, String)> {
460    let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
461    let context_predicate_range = mat
462        .nodes_for_capture_index(context_predicate_ix)
463        .next()?
464        .byte_range();
465    let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
466    let mut new_predicate = old_predicate.to_string();
467    for (old_key, new_key) in CONTEXT_REPLACE.iter() {
468        new_predicate = new_predicate.replace(old_key, new_key);
469    }
470    if new_predicate != old_predicate {
471        Some((context_predicate_range, new_predicate.to_string()))
472    } else {
473        None
474    }
475}
476
477/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
478pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
479    HashMap::from_iter([
480        ("inline_completion", "edit_prediction"),
481        (
482            "inline_completion_requires_modifier",
483            "edit_prediction_requires_modifier",
484        ),
485    ])
486});
487
488const ACTION_ARGUMENT_SNAKE_CASE_PATTERN: &str = r#"(document
489    (array
490        (object
491            (pair
492                key: (string (string_content) @name)
493                value: (
494                    (object
495                        (pair
496                            key: (string)
497                            value: ((array
498                                . (string (string_content) @action_name)
499                                . (object
500                                    (pair
501                                    key: (string (string_content) @argument_key)
502                                    value: (_)  @argument_value))
503                                . ) @array
504                            ))
505                        )
506                    )
507                )
508            )
509        )
510    (#eq? @name "bindings")
511)"#;
512
513fn to_snake_case(text: &str) -> String {
514    text.to_case(Case::Snake)
515}
516
517fn action_argument_snake_case(
518    contents: &str,
519    mat: &QueryMatch,
520    query: &Query,
521) -> Option<(Range<usize>, String)> {
522    let array_ix = query.capture_index_for_name("array")?;
523    let action_name_ix = query.capture_index_for_name("action_name")?;
524    let argument_key_ix = query.capture_index_for_name("argument_key")?;
525    let argument_value_ix = query.capture_index_for_name("argument_value")?;
526    let action_name = contents.get(
527        mat.nodes_for_capture_index(action_name_ix)
528            .next()?
529            .byte_range(),
530    )?;
531
532    let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?;
533    let argument_key = contents.get(
534        mat.nodes_for_capture_index(argument_key_ix)
535            .next()?
536            .byte_range(),
537    )?;
538
539    if argument_key != *replacement_key {
540        return None;
541    }
542
543    let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?;
544    let argument_value = contents.get(argument_value_node.byte_range())?;
545
546    let new_key = to_snake_case(argument_key);
547    let new_value = if argument_value_node.kind() == "string" {
548        format!("\"{}\"", to_snake_case(argument_value.trim_matches('"')))
549    } else {
550        argument_value.to_string()
551    };
552
553    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
554    let replacement = format!(
555        "[\"{}\", {{ \"{}\": {} }}]",
556        action_name, new_key, new_value
557    );
558
559    Some((range_to_replace, replacement))
560}
561
562pub static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock<HashMap<&str, &str>> =
563    LazyLock::new(|| {
564        HashMap::from_iter([
565            ("vim::NextWordStart", "ignorePunctuation"),
566            ("vim::NextWordEnd", "ignorePunctuation"),
567            ("vim::PreviousWordStart", "ignorePunctuation"),
568            ("vim::PreviousWordEnd", "ignorePunctuation"),
569            ("vim::MoveToNext", "partialWord"),
570            ("vim::MoveToPrev", "partialWord"),
571            ("vim::Down", "displayLines"),
572            ("vim::Up", "displayLines"),
573            ("vim::EndOfLine", "displayLines"),
574            ("vim::StartOfLine", "displayLines"),
575            ("vim::FirstNonWhitespace", "displayLines"),
576            ("pane::CloseActiveItem", "saveIntent"),
577            ("vim::Paste", "preserveClipboard"),
578            ("vim::Word", "ignorePunctuation"),
579            ("vim::Subword", "ignorePunctuation"),
580            ("vim::IndentObj", "includeBelow"),
581        ])
582    });
583
584const SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[
585    (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name),
586    (
587        SETTINGS_REPLACE_NESTED_KEY,
588        replace_edit_prediction_provider_setting,
589    ),
590    (
591        SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
592        replace_setting_in_languages,
593    ),
594];
595
596static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
597    Query::new(
598        &tree_sitter_json::LANGUAGE.into(),
599        &SETTINGS_MIGRATION_PATTERNS
600            .iter()
601            .map(|pattern| pattern.0)
602            .collect::<String>(),
603    )
604    .unwrap()
605});
606
607static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
608    Query::new(
609        &tree_sitter_json::LANGUAGE.into(),
610        SETTINGS_REPLACE_NESTED_KEY,
611    )
612    .unwrap()
613});
614
615const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
616    (object
617        (pair
618            key: (string (string_content) @name)
619            value: (_)
620        )
621    )
622)"#;
623
624fn replace_setting_name(
625    contents: &str,
626    mat: &QueryMatch,
627    query: &Query,
628) -> Option<(Range<usize>, String)> {
629    let setting_capture_ix = query.capture_index_for_name("name")?;
630    let setting_name_range = mat
631        .nodes_for_capture_index(setting_capture_ix)
632        .next()?
633        .byte_range();
634    let setting_name = contents.get(setting_name_range.clone())?;
635    let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
636    Some((setting_name_range, new_setting_name.to_string()))
637}
638
639pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
640    LazyLock::new(|| {
641        HashMap::from_iter([
642            (
643                "show_inline_completions_in_menu",
644                "show_edit_predictions_in_menu",
645            ),
646            ("show_inline_completions", "show_edit_predictions"),
647            (
648                "inline_completions_disabled_in",
649                "edit_predictions_disabled_in",
650            ),
651            ("inline_completions", "edit_predictions"),
652        ])
653    });
654
655const SETTINGS_REPLACE_NESTED_KEY: &str = r#"
656(object
657  (pair
658    key: (string (string_content) @parent_key)
659    value: (object
660        (pair
661            key: (string (string_content) @setting_name)
662            value: (_) @value
663        )
664    )
665  )
666)
667"#;
668
669fn replace_edit_prediction_provider_setting(
670    contents: &str,
671    mat: &QueryMatch,
672    query: &Query,
673) -> Option<(Range<usize>, String)> {
674    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
675    let parent_object_range = mat
676        .nodes_for_capture_index(parent_object_capture_ix)
677        .next()?
678        .byte_range();
679    let parent_object_name = contents.get(parent_object_range.clone())?;
680
681    let setting_name_ix = query.capture_index_for_name("setting_name")?;
682    let setting_range = mat
683        .nodes_for_capture_index(setting_name_ix)
684        .next()?
685        .byte_range();
686    let setting_name = contents.get(setting_range.clone())?;
687
688    if parent_object_name == "features" && setting_name == "inline_completion_provider" {
689        return Some((setting_range, "edit_prediction_provider".into()));
690    }
691
692    None
693}
694
695const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
696(object
697  (pair
698    key: (string (string_content) @languages)
699    value: (object
700    (pair
701        key: (string)
702        value: (object
703            (pair
704                key: (string (string_content) @setting_name)
705                value: (_) @value
706            )
707        )
708    ))
709  )
710)
711(#eq? @languages "languages")
712"#;
713
714fn replace_setting_in_languages(
715    contents: &str,
716    mat: &QueryMatch,
717    query: &Query,
718) -> Option<(Range<usize>, String)> {
719    let setting_capture_ix = query.capture_index_for_name("setting_name")?;
720    let setting_name_range = mat
721        .nodes_for_capture_index(setting_capture_ix)
722        .next()?
723        .byte_range();
724    let setting_name = contents.get(setting_name_range.clone())?;
725    let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
726
727    Some((setting_name_range, new_setting_name.to_string()))
728}
729
730static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
731    LazyLock::new(|| {
732        HashMap::from_iter([
733            ("show_inline_completions", "show_edit_predictions"),
734            (
735                "inline_completions_disabled_in",
736                "edit_predictions_disabled_in",
737            ),
738        ])
739    });
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
746        let migrated = migrate_keymap(&input).unwrap();
747        pretty_assertions::assert_eq!(migrated.as_deref(), output);
748    }
749
750    fn assert_migrate_settings(input: &str, output: Option<&str>) {
751        let migrated = migrate_settings(&input).unwrap();
752        pretty_assertions::assert_eq!(migrated.as_deref(), output);
753    }
754
755    #[test]
756    fn test_replace_array_with_single_string() {
757        assert_migrate_keymap(
758            r#"
759            [
760                {
761                    "bindings": {
762                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
763                    }
764                }
765            ]
766            "#,
767            Some(
768                r#"
769            [
770                {
771                    "bindings": {
772                        "cmd-1": "workspace::ActivatePaneUp"
773                    }
774                }
775            ]
776            "#,
777            ),
778        )
779    }
780
781    #[test]
782    fn test_replace_action_argument_object_with_single_value() {
783        assert_migrate_keymap(
784            r#"
785            [
786                {
787                    "bindings": {
788                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
789                    }
790                }
791            ]
792            "#,
793            Some(
794                r#"
795            [
796                {
797                    "bindings": {
798                        "cmd-1": ["editor::FoldAtLevel", 1]
799                    }
800                }
801            ]
802            "#,
803            ),
804        )
805    }
806
807    #[test]
808    fn test_replace_action_argument_object_with_single_value_2() {
809        assert_migrate_keymap(
810            r#"
811            [
812                {
813                    "bindings": {
814                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
815                    }
816                }
817            ]
818            "#,
819            Some(
820                r#"
821            [
822                {
823                    "bindings": {
824                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
825                    }
826                }
827            ]
828            "#,
829            ),
830        )
831    }
832
833    #[test]
834    fn test_rename_string_action() {
835        assert_migrate_keymap(
836            r#"
837                [
838                    {
839                        "bindings": {
840                            "cmd-1": "inline_completion::ToggleMenu"
841                        }
842                    }
843                ]
844            "#,
845            Some(
846                r#"
847                [
848                    {
849                        "bindings": {
850                            "cmd-1": "edit_prediction::ToggleMenu"
851                        }
852                    }
853                ]
854            "#,
855            ),
856        )
857    }
858
859    #[test]
860    fn test_rename_context_key() {
861        assert_migrate_keymap(
862            r#"
863                [
864                    {
865                        "context": "Editor && inline_completion && !showing_completions"
866                    }
867                ]
868            "#,
869            Some(
870                r#"
871                [
872                    {
873                        "context": "Editor && edit_prediction && !showing_completions"
874                    }
875                ]
876            "#,
877            ),
878        )
879    }
880
881    #[test]
882    fn test_action_argument_snake_case() {
883        // First performs transformations, then replacements
884        assert_migrate_keymap(
885            r#"
886            [
887                {
888                    "bindings": {
889                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
890                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
891                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
892                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
893                    }
894                }
895            ]
896            "#,
897            Some(
898                r#"
899            [
900                {
901                    "bindings": {
902                        "cmd-1": ["vim::PushObject", { "around": false }],
903                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
904                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
905                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
906                    }
907                }
908            ]
909            "#,
910            ),
911        )
912    }
913
914    #[test]
915    fn test_replace_setting_name() {
916        assert_migrate_settings(
917            r#"
918                {
919                    "show_inline_completions_in_menu": true,
920                    "show_inline_completions": true,
921                    "inline_completions_disabled_in": ["string"],
922                    "inline_completions": { "some" : "value" }
923                }
924            "#,
925            Some(
926                r#"
927                {
928                    "show_edit_predictions_in_menu": true,
929                    "show_edit_predictions": true,
930                    "edit_predictions_disabled_in": ["string"],
931                    "edit_predictions": { "some" : "value" }
932                }
933            "#,
934            ),
935        )
936    }
937
938    #[test]
939    fn test_nested_string_replace_for_settings() {
940        assert_migrate_settings(
941            r#"
942                {
943                    "features": {
944                        "inline_completion_provider": "zed"
945                    },
946                }
947            "#,
948            Some(
949                r#"
950                {
951                    "features": {
952                        "edit_prediction_provider": "zed"
953                    },
954                }
955            "#,
956            ),
957        )
958    }
959
960    #[test]
961    fn test_replace_settings_in_languages() {
962        assert_migrate_settings(
963            r#"
964                {
965                    "languages": {
966                        "Astro": {
967                            "show_inline_completions": true
968                        }
969                    }
970                }
971            "#,
972            Some(
973                r#"
974                {
975                    "languages": {
976                        "Astro": {
977                            "show_edit_predictions": true
978                        }
979                    }
980                }
981            "#,
982            ),
983        )
984    }
985}