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}