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