migrator.rs

  1use collections::HashMap;
  2use convert_case::{Case, Casing};
  3use std::{cmp::Reverse, ops::Range, sync::LazyLock};
  4use tree_sitter::{Query, QueryMatch};
  5
  6fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Option<String> {
  7    let mut parser = tree_sitter::Parser::new();
  8    parser
  9        .set_language(&tree_sitter_json::LANGUAGE.into())
 10        .unwrap();
 11    let syntax_tree = parser.parse(&text, None).unwrap();
 12
 13    let mut cursor = tree_sitter::QueryCursor::new();
 14    let matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
 15
 16    let mut edits = vec![];
 17    for mat in matches {
 18        if let Some((_, callback)) = patterns.get(mat.pattern_index) {
 19            edits.extend(callback(&text, &mat, query));
 20        }
 21    }
 22
 23    edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
 24    edits.dedup_by(|(range_b, _), (range_a, _)| {
 25        range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
 26    });
 27
 28    if edits.is_empty() {
 29        None
 30    } else {
 31        let mut text = text.to_string();
 32        for (range, replacement) in edits.into_iter().rev() {
 33            text.replace_range(range, &replacement);
 34        }
 35        Some(text)
 36    }
 37}
 38
 39pub fn migrate_keymap(text: &str) -> Option<String> {
 40    let transformed_text = migrate(
 41        text,
 42        KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS,
 43        &KEYMAP_MIGRATION_TRANSFORMATION_QUERY,
 44    );
 45    let replacement_text = migrate(
 46        &transformed_text.as_ref().unwrap_or(&text.to_string()),
 47        KEYMAP_MIGRATION_REPLACEMENT_PATTERNS,
 48        &KEYMAP_MIGRATION_REPLACEMENT_QUERY,
 49    );
 50    replacement_text.or(transformed_text)
 51}
 52
 53pub fn migrate_settings(text: &str) -> Option<String> {
 54    migrate(
 55        &text,
 56        SETTINGS_MIGRATION_PATTERNS,
 57        &SETTINGS_MIGRATION_QUERY,
 58    )
 59}
 60
 61type MigrationPatterns = &'static [(
 62    &'static str,
 63    fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
 64)];
 65
 66static KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS: MigrationPatterns = &[
 67    (ACTION_ARRAY_PATTERN, replace_array_with_single_string),
 68    (
 69        ACTION_ARGUMENT_OBJECT_PATTERN,
 70        replace_action_argument_object_with_single_value,
 71    ),
 72    (ACTION_STRING_PATTERN, rename_string_action),
 73    (CONTEXT_PREDICATE_PATTERN, rename_context_key),
 74];
 75
 76static KEYMAP_MIGRATION_TRANSFORMATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 77    Query::new(
 78        &tree_sitter_json::LANGUAGE.into(),
 79        &KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS
 80            .iter()
 81            .map(|pattern| pattern.0)
 82            .collect::<String>(),
 83    )
 84    .unwrap()
 85});
 86
 87const ACTION_ARRAY_PATTERN: &str = r#"(document
 88    (array
 89   	    (object
 90            (pair
 91                key: (string (string_content) @name)
 92                value: (
 93                    (object
 94                        (pair
 95                            key: (string)
 96                            value: ((array
 97                                . (string (string_content) @action_name)
 98                                . (string (string_content) @argument)
 99                                .)) @array
100                        )
101                    )
102                )
103            )
104        )
105    )
106    (#eq? @name "bindings")
107)"#;
108
109fn replace_array_with_single_string(
110    contents: &str,
111    mat: &QueryMatch,
112    query: &Query,
113) -> Option<(Range<usize>, String)> {
114    let array_ix = query.capture_index_for_name("array").unwrap();
115    let action_name_ix = query.capture_index_for_name("action_name").unwrap();
116    let argument_ix = query.capture_index_for_name("argument").unwrap();
117
118    let action_name = contents.get(
119        mat.nodes_for_capture_index(action_name_ix)
120            .next()?
121            .byte_range(),
122    )?;
123    let argument = contents.get(
124        mat.nodes_for_capture_index(argument_ix)
125            .next()?
126            .byte_range(),
127    )?;
128
129    let replacement = TRANSFORM_ARRAY.get(&(action_name, argument))?;
130    let replacement_as_string = format!("\"{replacement}\"");
131    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
132
133    Some((range_to_replace, replacement_as_string))
134}
135
136#[rustfmt::skip]
137static TRANSFORM_ARRAY: LazyLock<HashMap<(&str, &str), &str>> = LazyLock::new(|| {
138    HashMap::from_iter([
139        // activate
140        (("workspace::ActivatePaneInDirection", "Up"), "workspace::ActivatePaneUp"),
141        (("workspace::ActivatePaneInDirection", "Down"), "workspace::ActivatePaneDown"),
142        (("workspace::ActivatePaneInDirection", "Left"), "workspace::ActivatePaneLeft"),
143        (("workspace::ActivatePaneInDirection", "Right"), "workspace::ActivatePaneRight"),
144        // swap
145        (("workspace::SwapPaneInDirection", "Up"), "workspace::SwapPaneUp"),
146        (("workspace::SwapPaneInDirection", "Down"), "workspace::SwapPaneDown"),
147        (("workspace::SwapPaneInDirection", "Left"), "workspace::SwapPaneLeft"),
148        (("workspace::SwapPaneInDirection", "Right"), "workspace::SwapPaneRight"),
149        // menu
150        (("app_menu::NavigateApplicationMenuInDirection", "Left"), "app_menu::ActivateMenuLeft"),
151        (("app_menu::NavigateApplicationMenuInDirection", "Right"), "app_menu::ActivateMenuRight"),
152        // vim push
153        (("vim::PushOperator", "Change"), "vim::PushChange"),
154        (("vim::PushOperator", "Delete"), "vim::PushDelete"),
155        (("vim::PushOperator", "Yank"), "vim::PushYank"),
156        (("vim::PushOperator", "Replace"), "vim::PushReplace"),
157        (("vim::PushOperator", "DeleteSurrounds"), "vim::PushDeleteSurrounds"),
158        (("vim::PushOperator", "Mark"), "vim::PushMark"),
159        (("vim::PushOperator", "Indent"), "vim::PushIndent"),
160        (("vim::PushOperator", "Outdent"), "vim::PushOutdent"),
161        (("vim::PushOperator", "AutoIndent"), "vim::PushAutoIndent"),
162        (("vim::PushOperator", "Rewrap"), "vim::PushRewrap"),
163        (("vim::PushOperator", "ShellCommand"), "vim::PushShellCommand"),
164        (("vim::PushOperator", "Lowercase"), "vim::PushLowercase"),
165        (("vim::PushOperator", "Uppercase"), "vim::PushUppercase"),
166        (("vim::PushOperator", "OppositeCase"), "vim::PushOppositeCase"),
167        (("vim::PushOperator", "Register"), "vim::PushRegister"),
168        (("vim::PushOperator", "RecordRegister"), "vim::PushRecordRegister"),
169        (("vim::PushOperator", "ReplayRegister"), "vim::PushReplayRegister"),
170        (("vim::PushOperator", "ReplaceWithRegister"), "vim::PushReplaceWithRegister"),
171        (("vim::PushOperator", "ToggleComments"), "vim::PushToggleComments"),
172        // vim switch
173        (("vim::SwitchMode", "Normal"), "vim::SwitchToNormalMode"),
174        (("vim::SwitchMode", "Insert"), "vim::SwitchToInsertMode"),
175        (("vim::SwitchMode", "Replace"), "vim::SwitchToReplaceMode"),
176        (("vim::SwitchMode", "Visual"), "vim::SwitchToVisualMode"),
177        (("vim::SwitchMode", "VisualLine"), "vim::SwitchToVisualLineMode"),
178        (("vim::SwitchMode", "VisualBlock"), "vim::SwitchToVisualBlockMode"),
179        (("vim::SwitchMode", "HelixNormal"), "vim::SwitchToHelixNormalMode"),
180        // vim resize
181        (("vim::ResizePane", "Widen"), "vim::ResizePaneRight"),
182        (("vim::ResizePane", "Narrow"), "vim::ResizePaneLeft"),
183        (("vim::ResizePane", "Shorten"), "vim::ResizePaneDown"),
184        (("vim::ResizePane", "Lengthen"), "vim::ResizePaneUp"),
185    ])
186});
187
188const ACTION_ARGUMENT_OBJECT_PATTERN: &str = r#"(document
189    (array
190        (object
191            (pair
192                key: (string (string_content) @name)
193                value: (
194                    (object
195                        (pair
196                            key: (string)
197                            value: ((array
198                                . (string (string_content) @action_name)
199                                . (object
200                                    (pair
201                                    key: (string (string_content) @action_key)
202                                    value: (_)  @argument))
203                                . ) @array
204                            ))
205                        )
206                    )
207                )
208            )
209        )
210        (#eq? @name "bindings")
211)"#;
212
213/// [ "editor::FoldAtLevel", { "level": 1 } ] -> [ "editor::FoldAtLevel", 1 ]
214fn replace_action_argument_object_with_single_value(
215    contents: &str,
216    mat: &QueryMatch,
217    query: &Query,
218) -> Option<(Range<usize>, String)> {
219    let array_ix = query.capture_index_for_name("array").unwrap();
220    let action_name_ix = query.capture_index_for_name("action_name").unwrap();
221    let action_key_ix = query.capture_index_for_name("action_key").unwrap();
222    let argument_ix = query.capture_index_for_name("argument").unwrap();
223
224    let action_name = contents.get(
225        mat.nodes_for_capture_index(action_name_ix)
226            .next()?
227            .byte_range(),
228    )?;
229    let action_key = contents.get(
230        mat.nodes_for_capture_index(action_key_ix)
231            .next()?
232            .byte_range(),
233    )?;
234    let argument = contents.get(
235        mat.nodes_for_capture_index(argument_ix)
236            .next()?
237            .byte_range(),
238    )?;
239
240    let new_action_name = UNWRAP_OBJECTS.get(&action_name)?.get(&action_key)?;
241
242    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
243    let replacement = format!("[\"{}\", {}]", new_action_name, argument);
244    Some((range_to_replace, replacement))
245}
246
247// "ctrl-k ctrl-1": [ "editor::PushOperator", { "Object": {} } ] -> [ "editor::vim::PushObject", {} ]
248static UNWRAP_OBJECTS: LazyLock<HashMap<&str, HashMap<&str, &str>>> = LazyLock::new(|| {
249    HashMap::from_iter([
250        (
251            "editor::FoldAtLevel",
252            HashMap::from_iter([("level", "editor::FoldAtLevel")]),
253        ),
254        (
255            "vim::PushOperator",
256            HashMap::from_iter([
257                ("Object", "vim::PushObject"),
258                ("FindForward", "vim::PushFindForward"),
259                ("FindBackward", "vim::PushFindBackward"),
260                ("Sneak", "vim::PushSneak"),
261                ("SneakBackward", "vim::PushSneakBackward"),
262                ("AddSurrounds", "vim::PushAddSurrounds"),
263                ("ChangeSurrounds", "vim::PushChangeSurrounds"),
264                ("Jump", "vim::PushJump"),
265                ("Digraph", "vim::PushDigraph"),
266                ("Literal", "vim::PushLiteral"),
267            ]),
268        ),
269    ])
270});
271
272static KEYMAP_MIGRATION_REPLACEMENT_PATTERNS: MigrationPatterns = &[(
273    ACTION_ARGUMENT_SNAKE_CASE_PATTERN,
274    action_argument_snake_case,
275)];
276
277static KEYMAP_MIGRATION_REPLACEMENT_QUERY: LazyLock<Query> = LazyLock::new(|| {
278    Query::new(
279        &tree_sitter_json::LANGUAGE.into(),
280        &KEYMAP_MIGRATION_REPLACEMENT_PATTERNS
281            .iter()
282            .map(|pattern| pattern.0)
283            .collect::<String>(),
284    )
285    .unwrap()
286});
287
288const ACTION_STRING_PATTERN: &str = r#"(document
289    (array
290        (object
291            (pair
292                key: (string (string_content) @name)
293                value: (
294                    (object
295                        (pair
296                            key: (string)
297                            value: (string (string_content) @action_name)
298                        )
299                    )
300                )
301            )
302        )
303    )
304    (#eq? @name "bindings")
305)"#;
306
307fn rename_string_action(
308    contents: &str,
309    mat: &QueryMatch,
310    query: &Query,
311) -> Option<(Range<usize>, String)> {
312    let action_name_ix = query.capture_index_for_name("action_name").unwrap();
313    let action_name_range = mat
314        .nodes_for_capture_index(action_name_ix)
315        .next()?
316        .byte_range();
317    let action_name = contents.get(action_name_range.clone())?;
318    let new_action_name = STRING_REPLACE.get(&action_name)?;
319    Some((action_name_range, new_action_name.to_string()))
320}
321
322// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
323#[rustfmt::skip]
324static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
325    HashMap::from_iter([
326        ("inline_completion::ToggleMenu", "edit_prediction::ToggleMenu"),
327        ("editor::NextInlineCompletion", "editor::NextEditPrediction"),
328        ("editor::PreviousInlineCompletion", "editor::PreviousEditPrediction"),
329        ("editor::AcceptPartialInlineCompletion", "editor::AcceptPartialEditPrediction"),
330        ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"),
331        ("editor::AcceptInlineCompletion", "editor::AcceptEditPrediction"),
332        ("editor::ToggleInlineCompletions", "editor::ToggleEditPrediction"),
333    ])
334});
335
336const CONTEXT_PREDICATE_PATTERN: &str = r#"
337(array
338    (object
339        (pair
340            key: (string (string_content) @name)
341            value: (string (string_content) @context_predicate)
342        )
343    )
344)
345(#eq? @name "context")
346"#;
347
348fn rename_context_key(
349    contents: &str,
350    mat: &QueryMatch,
351    query: &Query,
352) -> Option<(Range<usize>, String)> {
353    let context_predicate_ix = query.capture_index_for_name("context_predicate").unwrap();
354    let context_predicate_range = mat
355        .nodes_for_capture_index(context_predicate_ix)
356        .next()?
357        .byte_range();
358    let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
359    let mut new_predicate = old_predicate.to_string();
360    for (old_key, new_key) in CONTEXT_REPLACE.iter() {
361        new_predicate = new_predicate.replace(old_key, new_key);
362    }
363    if new_predicate != old_predicate {
364        Some((context_predicate_range, new_predicate.to_string()))
365    } else {
366        None
367    }
368}
369
370const ACTION_ARGUMENT_SNAKE_CASE_PATTERN: &str = r#"(document
371    (array
372        (object
373            (pair
374                key: (string (string_content) @name)
375                value: (
376                    (object
377                        (pair
378                            key: (string)
379                            value: ((array
380                                . (string (string_content) @action_name)
381                                . (object
382                                    (pair
383                                    key: (string (string_content) @argument_key)
384                                    value: (_)  @argument_value))
385                                . ) @array
386                            ))
387                        )
388                    )
389                )
390            )
391        )
392    (#eq? @name "bindings")
393)"#;
394
395fn is_snake_case(text: &str) -> bool {
396    text == text.to_case(Case::Snake)
397}
398
399fn to_snake_case(text: &str) -> String {
400    text.to_case(Case::Snake)
401}
402
403/// [ "editor::FoldAtLevel", { "SomeKey": "Value" } ] -> [ "editor::FoldAtLevel", { "some_key" : "value" } ]
404fn action_argument_snake_case(
405    contents: &str,
406    mat: &QueryMatch,
407    query: &Query,
408) -> Option<(Range<usize>, String)> {
409    let array_ix = query.capture_index_for_name("array").unwrap();
410    let action_name_ix = query.capture_index_for_name("action_name").unwrap();
411    let argument_key_ix = query.capture_index_for_name("argument_key").unwrap();
412    let argument_value_ix = query.capture_index_for_name("argument_value").unwrap();
413    let action_name = contents.get(
414        mat.nodes_for_capture_index(action_name_ix)
415            .next()?
416            .byte_range(),
417    )?;
418
419    let argument_key = contents.get(
420        mat.nodes_for_capture_index(argument_key_ix)
421            .next()?
422            .byte_range(),
423    )?;
424
425    let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?;
426    let argument_value = contents.get(argument_value_node.byte_range())?;
427
428    let mut needs_replacement = false;
429    let mut new_key = argument_key.to_string();
430    if !is_snake_case(argument_key) {
431        new_key = to_snake_case(argument_key);
432        needs_replacement = true;
433    }
434
435    let mut new_value = argument_value.to_string();
436    if argument_value_node.kind() == "string" {
437        let inner_value = argument_value.trim_matches('"');
438        if !is_snake_case(inner_value) {
439            new_value = format!("\"{}\"", to_snake_case(inner_value));
440            needs_replacement = true;
441        }
442    }
443
444    if !needs_replacement {
445        return None;
446    }
447
448    let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
449    let replacement = format!(
450        "[\"{}\", {{ \"{}\": {} }}]",
451        action_name, new_key, new_value
452    );
453
454    Some((range_to_replace, replacement))
455}
456
457// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
458pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
459    HashMap::from_iter([
460        ("inline_completion", "edit_prediction"),
461        (
462            "inline_completion_requires_modifier",
463            "edit_prediction_requires_modifier",
464        ),
465    ])
466});
467
468static SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[
469    (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name),
470    (SETTINGS_REPLACE_NESTED_KEY, replace_setting_nested_key),
471    (
472        SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
473        replace_setting_in_languages,
474    ),
475];
476
477static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
478    Query::new(
479        &tree_sitter_json::LANGUAGE.into(),
480        &SETTINGS_MIGRATION_PATTERNS
481            .iter()
482            .map(|pattern| pattern.0)
483            .collect::<String>(),
484    )
485    .unwrap()
486});
487
488static SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
489    (object
490        (pair
491            key: (string (string_content) @name)
492            value: (_)
493        )
494    )
495)"#;
496
497fn replace_setting_name(
498    contents: &str,
499    mat: &QueryMatch,
500    query: &Query,
501) -> Option<(Range<usize>, String)> {
502    let setting_capture_ix = query.capture_index_for_name("name").unwrap();
503    let setting_name_range = mat
504        .nodes_for_capture_index(setting_capture_ix)
505        .next()?
506        .byte_range();
507    let setting_name = contents.get(setting_name_range.clone())?;
508    let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
509    Some((setting_name_range, new_setting_name.to_string()))
510}
511
512#[rustfmt::skip]
513pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
514    HashMap::from_iter([
515        ("show_inline_completions_in_menu", "show_edit_predictions_in_menu"),
516        ("show_inline_completions", "show_edit_predictions"),
517        ("inline_completions_disabled_in", "edit_predictions_disabled_in"),
518        ("inline_completions", "edit_predictions")
519    ])
520});
521
522static SETTINGS_REPLACE_NESTED_KEY: &str = r#"
523(object
524  (pair
525    key: (string (string_content) @parent_key)
526    value: (object
527        (pair
528            key: (string (string_content) @setting_name)
529            value: (_) @value
530        )
531    )
532  )
533)
534"#;
535
536fn replace_setting_nested_key(
537    contents: &str,
538    mat: &QueryMatch,
539    query: &Query,
540) -> Option<(Range<usize>, String)> {
541    let parent_object_capture_ix = query.capture_index_for_name("parent_key").unwrap();
542    let parent_object_range = mat
543        .nodes_for_capture_index(parent_object_capture_ix)
544        .next()?
545        .byte_range();
546    let parent_object_name = contents.get(parent_object_range.clone())?;
547
548    let setting_name_ix = query.capture_index_for_name("setting_name").unwrap();
549    let setting_range = mat
550        .nodes_for_capture_index(setting_name_ix)
551        .next()?
552        .byte_range();
553    let setting_name = contents.get(setting_range.clone())?;
554
555    let new_setting_name = SETTINGS_NESTED_STRING_REPLACE
556        .get(&parent_object_name)?
557        .get(setting_name)?;
558
559    Some((setting_range, new_setting_name.to_string()))
560}
561
562// "features": {
563//   "inline_completion_provider": "copilot"
564// },
565pub static SETTINGS_NESTED_STRING_REPLACE: LazyLock<
566    HashMap<&'static str, HashMap<&'static str, &'static str>>,
567> = LazyLock::new(|| {
568    HashMap::from_iter([(
569        "features",
570        HashMap::from_iter([("inline_completion_provider", "edit_prediction_provider")]),
571    )])
572});
573
574static SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
575(object
576  (pair
577    key: (string (string_content) @languages)
578    value: (object
579    (pair
580        key: (string)
581        value: (object
582            (pair
583                key: (string (string_content) @setting_name)
584                value: (_) @value
585            )
586        )
587    ))
588  )
589)
590(#eq? @languages "languages")
591"#;
592
593fn replace_setting_in_languages(
594    contents: &str,
595    mat: &QueryMatch,
596    query: &Query,
597) -> Option<(Range<usize>, String)> {
598    let setting_capture_ix = query.capture_index_for_name("setting_name").unwrap();
599    let setting_name_range = mat
600        .nodes_for_capture_index(setting_capture_ix)
601        .next()?
602        .byte_range();
603    let setting_name = contents.get(setting_name_range.clone())?;
604    let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
605
606    Some((setting_name_range, new_setting_name.to_string()))
607}
608
609#[rustfmt::skip]
610static LANGUAGE_SETTINGS_REPLACE: LazyLock<
611    HashMap<&'static str, &'static str>,
612> = LazyLock::new(|| {
613    HashMap::from_iter([
614        ("show_inline_completions", "show_edit_predictions"),
615        ("inline_completions_disabled_in", "edit_predictions_disabled_in"),
616    ])
617});
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
624        let migrated = migrate_keymap(&input);
625        pretty_assertions::assert_eq!(migrated.as_deref(), output);
626    }
627
628    fn assert_migrate_settings(input: &str, output: Option<&str>) {
629        let migrated = migrate_settings(&input);
630        pretty_assertions::assert_eq!(migrated.as_deref(), output);
631    }
632
633    #[test]
634    fn test_replace_array_with_single_string() {
635        assert_migrate_keymap(
636            r#"
637            [
638                {
639                    "bindings": {
640                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
641                    }
642                }
643            ]
644            "#,
645            Some(
646                r#"
647            [
648                {
649                    "bindings": {
650                        "cmd-1": "workspace::ActivatePaneUp"
651                    }
652                }
653            ]
654            "#,
655            ),
656        )
657    }
658
659    #[test]
660    fn test_replace_action_argument_object_with_single_value() {
661        assert_migrate_keymap(
662            r#"
663            [
664                {
665                    "bindings": {
666                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
667                    }
668                }
669            ]
670            "#,
671            Some(
672                r#"
673            [
674                {
675                    "bindings": {
676                        "cmd-1": ["editor::FoldAtLevel", 1]
677                    }
678                }
679            ]
680            "#,
681            ),
682        )
683    }
684
685    #[test]
686    fn test_replace_action_argument_object_with_single_value_2() {
687        assert_migrate_keymap(
688            r#"
689            [
690                {
691                    "bindings": {
692                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
693                    }
694                }
695            ]
696            "#,
697            Some(
698                r#"
699            [
700                {
701                    "bindings": {
702                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
703                    }
704                }
705            ]
706            "#,
707            ),
708        )
709    }
710
711    #[test]
712    fn test_rename_string_action() {
713        assert_migrate_keymap(
714            r#"
715                [
716                    {
717                        "bindings": {
718                            "cmd-1": "inline_completion::ToggleMenu"
719                        }
720                    }
721                ]
722            "#,
723            Some(
724                r#"
725                [
726                    {
727                        "bindings": {
728                            "cmd-1": "edit_prediction::ToggleMenu"
729                        }
730                    }
731                ]
732            "#,
733            ),
734        )
735    }
736
737    #[test]
738    fn test_rename_context_key() {
739        assert_migrate_keymap(
740            r#"
741                [
742                    {
743                        "context": "Editor && inline_completion && !showing_completions"
744                    }
745                ]
746            "#,
747            Some(
748                r#"
749                [
750                    {
751                        "context": "Editor && edit_prediction && !showing_completions"
752                    }
753                ]
754            "#,
755            ),
756        )
757    }
758
759    #[test]
760    fn test_action_argument_snake_case() {
761        // First performs transformations, then replacements
762        assert_migrate_keymap(
763            r#"
764            [
765                {
766                    "bindings": {
767                        "cmd-1": ["vim::PushOperator", { "Object": { "SomeKey": "Value" } }],
768                        "cmd-2": ["vim::SomeOtherAction", { "OtherKey": "Value" }],
769                        "cmd-3": ["vim::SomeDifferentAction", { "OtherKey": true }],
770                        "cmd-4": ["vim::OneMore", { "OtherKey": 4 }]
771                    }
772                }
773            ]
774            "#,
775            Some(
776                r#"
777            [
778                {
779                    "bindings": {
780                        "cmd-1": ["vim::PushObject", { "some_key": "value" }],
781                        "cmd-2": ["vim::SomeOtherAction", { "other_key": "value" }],
782                        "cmd-3": ["vim::SomeDifferentAction", { "other_key": true }],
783                        "cmd-4": ["vim::OneMore", { "other_key": 4 }]
784                    }
785                }
786            ]
787            "#,
788            ),
789        )
790    }
791
792    #[test]
793    fn test_replace_setting_name() {
794        assert_migrate_settings(
795            r#"
796                {
797                    "show_inline_completions_in_menu": true,
798                    "show_inline_completions": true,
799                    "inline_completions_disabled_in": ["string"],
800                    "inline_completions": { "some" : "value" }
801                }
802            "#,
803            Some(
804                r#"
805                {
806                    "show_edit_predictions_in_menu": true,
807                    "show_edit_predictions": true,
808                    "edit_predictions_disabled_in": ["string"],
809                    "edit_predictions": { "some" : "value" }
810                }
811            "#,
812            ),
813        )
814    }
815
816    #[test]
817    fn test_nested_string_replace_for_settings() {
818        assert_migrate_settings(
819            r#"
820                {
821                    "features": {
822                        "inline_completion_provider": "zed"
823                    },
824                }
825            "#,
826            Some(
827                r#"
828                {
829                    "features": {
830                        "edit_prediction_provider": "zed"
831                    },
832                }
833            "#,
834            ),
835        )
836    }
837
838    #[test]
839    fn test_replace_settings_in_languages() {
840        assert_migrate_settings(
841            r#"
842                {
843                    "languages": {
844                        "Astro": {
845                            "show_inline_completions": true
846                        }
847                    }
848                }
849            "#,
850            Some(
851                r#"
852                {
853                    "languages": {
854                        "Astro": {
855                            "show_edit_predictions": true
856                        }
857                    }
858                }
859            "#,
860            ),
861        )
862    }
863}