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}