migrator.rs

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