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}