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