1use collections::HashMap;
2use std::{ops::Range, sync::LazyLock};
3use tree_sitter::{Query, QueryMatch};
4
5use crate::MigrationPatterns;
6use crate::patterns::{
7 KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, KEYMAP_ACTION_ARRAY_PATTERN,
8 KEYMAP_ACTION_STRING_PATTERN, KEYMAP_CONTEXT_PATTERN,
9};
10
11pub const KEYMAP_PATTERNS: MigrationPatterns = &[
12 (
13 KEYMAP_ACTION_ARRAY_PATTERN,
14 replace_array_with_single_string,
15 ),
16 (
17 KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN,
18 replace_action_argument_object_with_single_value,
19 ),
20 (KEYMAP_ACTION_STRING_PATTERN, replace_string_action),
21 (KEYMAP_CONTEXT_PATTERN, rename_context_key),
22];
23
24fn replace_array_with_single_string(
25 contents: &str,
26 mat: &QueryMatch,
27 query: &Query,
28) -> Option<(Range<usize>, String)> {
29 let array_ix = query.capture_index_for_name("array")?;
30 let action_name_ix = query.capture_index_for_name("action_name")?;
31 let argument_ix = query.capture_index_for_name("argument")?;
32
33 let action_name = contents.get(
34 mat.nodes_for_capture_index(action_name_ix)
35 .next()?
36 .byte_range(),
37 )?;
38 let argument = contents.get(
39 mat.nodes_for_capture_index(argument_ix)
40 .next()?
41 .byte_range(),
42 )?;
43
44 let replacement = TRANSFORM_ARRAY.get(&(action_name, argument))?;
45 let replacement_as_string = format!("\"{replacement}\"");
46 let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
47
48 Some((range_to_replace, replacement_as_string))
49}
50
51static TRANSFORM_ARRAY: LazyLock<HashMap<(&str, &str), &str>> = LazyLock::new(|| {
52 HashMap::from_iter([
53 // activate
54 (
55 ("workspace::ActivatePaneInDirection", "Up"),
56 "workspace::ActivatePaneUp",
57 ),
58 (
59 ("workspace::ActivatePaneInDirection", "Down"),
60 "workspace::ActivatePaneDown",
61 ),
62 (
63 ("workspace::ActivatePaneInDirection", "Left"),
64 "workspace::ActivatePaneLeft",
65 ),
66 (
67 ("workspace::ActivatePaneInDirection", "Right"),
68 "workspace::ActivatePaneRight",
69 ),
70 // swap
71 (
72 ("workspace::SwapPaneInDirection", "Up"),
73 "workspace::SwapPaneUp",
74 ),
75 (
76 ("workspace::SwapPaneInDirection", "Down"),
77 "workspace::SwapPaneDown",
78 ),
79 (
80 ("workspace::SwapPaneInDirection", "Left"),
81 "workspace::SwapPaneLeft",
82 ),
83 (
84 ("workspace::SwapPaneInDirection", "Right"),
85 "workspace::SwapPaneRight",
86 ),
87 // menu
88 (
89 ("app_menu::NavigateApplicationMenuInDirection", "Left"),
90 "app_menu::ActivateMenuLeft",
91 ),
92 (
93 ("app_menu::NavigateApplicationMenuInDirection", "Right"),
94 "app_menu::ActivateMenuRight",
95 ),
96 // vim push
97 (("vim::PushOperator", "Change"), "vim::PushChange"),
98 (("vim::PushOperator", "Delete"), "vim::PushDelete"),
99 (("vim::PushOperator", "Yank"), "vim::PushYank"),
100 (("vim::PushOperator", "Replace"), "vim::PushReplace"),
101 (
102 ("vim::PushOperator", "DeleteSurrounds"),
103 "vim::PushDeleteSurrounds",
104 ),
105 (("vim::PushOperator", "Mark"), "vim::PushMark"),
106 (("vim::PushOperator", "Indent"), "vim::PushIndent"),
107 (("vim::PushOperator", "Outdent"), "vim::PushOutdent"),
108 (("vim::PushOperator", "AutoIndent"), "vim::PushAutoIndent"),
109 (("vim::PushOperator", "Rewrap"), "vim::PushRewrap"),
110 (
111 ("vim::PushOperator", "ShellCommand"),
112 "vim::PushShellCommand",
113 ),
114 (("vim::PushOperator", "Lowercase"), "vim::PushLowercase"),
115 (("vim::PushOperator", "Uppercase"), "vim::PushUppercase"),
116 (
117 ("vim::PushOperator", "OppositeCase"),
118 "vim::PushOppositeCase",
119 ),
120 (("vim::PushOperator", "Register"), "vim::PushRegister"),
121 (
122 ("vim::PushOperator", "RecordRegister"),
123 "vim::PushRecordRegister",
124 ),
125 (
126 ("vim::PushOperator", "ReplayRegister"),
127 "vim::PushReplayRegister",
128 ),
129 (
130 ("vim::PushOperator", "ReplaceWithRegister"),
131 "vim::PushReplaceWithRegister",
132 ),
133 (
134 ("vim::PushOperator", "ToggleComments"),
135 "vim::PushToggleComments",
136 ),
137 // vim switch
138 (("vim::SwitchMode", "Normal"), "vim::SwitchToNormalMode"),
139 (("vim::SwitchMode", "Insert"), "vim::SwitchToInsertMode"),
140 (("vim::SwitchMode", "Replace"), "vim::SwitchToReplaceMode"),
141 (("vim::SwitchMode", "Visual"), "vim::SwitchToVisualMode"),
142 (
143 ("vim::SwitchMode", "VisualLine"),
144 "vim::SwitchToVisualLineMode",
145 ),
146 (
147 ("vim::SwitchMode", "VisualBlock"),
148 "vim::SwitchToVisualBlockMode",
149 ),
150 (
151 ("vim::SwitchMode", "HelixNormal"),
152 "vim::SwitchToHelixNormalMode",
153 ),
154 // vim resize
155 (("vim::ResizePane", "Widen"), "vim::ResizePaneRight"),
156 (("vim::ResizePane", "Narrow"), "vim::ResizePaneLeft"),
157 (("vim::ResizePane", "Shorten"), "vim::ResizePaneDown"),
158 (("vim::ResizePane", "Lengthen"), "vim::ResizePaneUp"),
159 // fold at level
160 (("editor::FoldAtLevel", "1"), "editor::FoldAtLevel1"),
161 (("editor::FoldAtLevel", "2"), "editor::FoldAtLevel2"),
162 (("editor::FoldAtLevel", "3"), "editor::FoldAtLevel3"),
163 (("editor::FoldAtLevel", "4"), "editor::FoldAtLevel4"),
164 (("editor::FoldAtLevel", "5"), "editor::FoldAtLevel5"),
165 (("editor::FoldAtLevel", "6"), "editor::FoldAtLevel6"),
166 (("editor::FoldAtLevel", "7"), "editor::FoldAtLevel7"),
167 (("editor::FoldAtLevel", "8"), "editor::FoldAtLevel8"),
168 (("editor::FoldAtLevel", "9"), "editor::FoldAtLevel9"),
169 ])
170});
171
172/// [ "editor::FoldAtLevel", { "level": 1 } ] -> [ "editor::FoldAtLevel", 1 ]
173fn replace_action_argument_object_with_single_value(
174 contents: &str,
175 mat: &QueryMatch,
176 query: &Query,
177) -> Option<(Range<usize>, String)> {
178 let array_ix = query.capture_index_for_name("array")?;
179 let action_name_ix = query.capture_index_for_name("action_name")?;
180 let argument_key_ix = query.capture_index_for_name("argument_key")?;
181 let argument_value_ix = query.capture_index_for_name("argument_value")?;
182
183 let action_name = contents.get(
184 mat.nodes_for_capture_index(action_name_ix)
185 .next()?
186 .byte_range(),
187 )?;
188 let argument_key = contents.get(
189 mat.nodes_for_capture_index(argument_key_ix)
190 .next()?
191 .byte_range(),
192 )?;
193 let argument_value = contents.get(
194 mat.nodes_for_capture_index(argument_value_ix)
195 .next()?
196 .byte_range(),
197 )?;
198
199 let new_action_name = UNWRAP_OBJECTS.get(&action_name)?.get(&argument_key)?;
200
201 let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
202 let replacement = format!("[\"{}\", {}]", new_action_name, argument_value);
203 Some((range_to_replace, replacement))
204}
205
206/// "ctrl-k ctrl-1": [ "editor::PushOperator", { "Object": {} } ] -> [ "editor::vim::PushObject", {} ]
207static UNWRAP_OBJECTS: LazyLock<HashMap<&str, HashMap<&str, &str>>> = LazyLock::new(|| {
208 HashMap::from_iter([
209 (
210 "editor::FoldAtLevel",
211 HashMap::from_iter([("level", "editor::FoldAtLevel")]),
212 ),
213 (
214 "vim::PushOperator",
215 HashMap::from_iter([
216 ("Object", "vim::PushObject"),
217 ("FindForward", "vim::PushFindForward"),
218 ("FindBackward", "vim::PushFindBackward"),
219 ("Sneak", "vim::PushSneak"),
220 ("SneakBackward", "vim::PushSneakBackward"),
221 ("AddSurrounds", "vim::PushAddSurrounds"),
222 ("ChangeSurrounds", "vim::PushChangeSurrounds"),
223 ("Jump", "vim::PushJump"),
224 ("Digraph", "vim::PushDigraph"),
225 ("Literal", "vim::PushLiteral"),
226 ]),
227 ),
228 ])
229});
230
231fn replace_string_action(
232 contents: &str,
233 mat: &QueryMatch,
234 query: &Query,
235) -> Option<(Range<usize>, String)> {
236 let action_name_ix = query.capture_index_for_name("action_name")?;
237 let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?;
238 let action_name_range = action_name_node.byte_range();
239 let action_name = contents.get(action_name_range.clone())?;
240
241 if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
242 return Some((action_name_range, new_action_name.to_string()));
243 }
244
245 None
246}
247
248/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
249static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
250 HashMap::from_iter([
251 (
252 "inline_completion::ToggleMenu",
253 "edit_prediction::ToggleMenu",
254 ),
255 ("editor::NextInlineCompletion", "editor::NextEditPrediction"),
256 (
257 "editor::PreviousInlineCompletion",
258 "editor::PreviousEditPrediction",
259 ),
260 (
261 "editor::AcceptPartialInlineCompletion",
262 "editor::AcceptPartialEditPrediction",
263 ),
264 ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"),
265 (
266 "editor::AcceptInlineCompletion",
267 "editor::AcceptEditPrediction",
268 ),
269 (
270 "editor::ToggleInlineCompletions",
271 "editor::ToggleEditPrediction",
272 ),
273 ])
274});
275
276fn rename_context_key(
277 contents: &str,
278 mat: &QueryMatch,
279 query: &Query,
280) -> Option<(Range<usize>, String)> {
281 let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
282 let context_predicate_range = mat
283 .nodes_for_capture_index(context_predicate_ix)
284 .next()?
285 .byte_range();
286 let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
287 let mut new_predicate = old_predicate.to_string();
288 for (old_key, new_key) in CONTEXT_REPLACE.iter() {
289 new_predicate = new_predicate.replace(old_key, new_key);
290 }
291 if new_predicate != old_predicate {
292 Some((context_predicate_range, new_predicate))
293 } else {
294 None
295 }
296}
297
298/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
299pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
300 HashMap::from_iter([
301 ("inline_completion", "edit_prediction"),
302 (
303 "inline_completion_requires_modifier",
304 "edit_prediction_requires_modifier",
305 ),
306 ])
307});