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_REPLACE_NESTED_KEY,
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_REPLACE_NESTED_KEY,
636 replace_edit_prediction_provider_setting,
637 ),
638 (
639 SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
640 replace_setting_in_languages,
641 ),
642];
643
644static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
645 Query::new(
646 &tree_sitter_json::LANGUAGE.into(),
647 &SETTINGS_MIGRATION_PATTERNS
648 .iter()
649 .map(|pattern| pattern.0)
650 .collect::<String>(),
651 )
652 .unwrap()
653});
654
655static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
656 Query::new(
657 &tree_sitter_json::LANGUAGE.into(),
658 SETTINGS_REPLACE_NESTED_KEY,
659 )
660 .unwrap()
661});
662
663const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
664 (object
665 (pair
666 key: (string (string_content) @name)
667 value: (_)
668 )
669 )
670)"#;
671
672fn replace_setting_name(
673 contents: &str,
674 mat: &QueryMatch,
675 query: &Query,
676) -> Option<(Range<usize>, String)> {
677 let setting_capture_ix = query.capture_index_for_name("name")?;
678 let setting_name_range = mat
679 .nodes_for_capture_index(setting_capture_ix)
680 .next()?
681 .byte_range();
682 let setting_name = contents.get(setting_name_range.clone())?;
683 let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
684 Some((setting_name_range, new_setting_name.to_string()))
685}
686
687pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
688 LazyLock::new(|| {
689 HashMap::from_iter([
690 (
691 "show_inline_completions_in_menu",
692 "show_edit_predictions_in_menu",
693 ),
694 ("show_inline_completions", "show_edit_predictions"),
695 (
696 "inline_completions_disabled_in",
697 "edit_predictions_disabled_in",
698 ),
699 ("inline_completions", "edit_predictions"),
700 ])
701 });
702
703const SETTINGS_REPLACE_NESTED_KEY: &str = r#"
704(object
705 (pair
706 key: (string (string_content) @parent_key)
707 value: (object
708 (pair
709 key: (string (string_content) @setting_name)
710 value: (_) @value
711 )
712 )
713 )
714)
715"#;
716
717fn replace_edit_prediction_provider_setting(
718 contents: &str,
719 mat: &QueryMatch,
720 query: &Query,
721) -> Option<(Range<usize>, String)> {
722 let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
723 let parent_object_range = mat
724 .nodes_for_capture_index(parent_object_capture_ix)
725 .next()?
726 .byte_range();
727 let parent_object_name = contents.get(parent_object_range.clone())?;
728
729 let setting_name_ix = query.capture_index_for_name("setting_name")?;
730 let setting_range = mat
731 .nodes_for_capture_index(setting_name_ix)
732 .next()?
733 .byte_range();
734 let setting_name = contents.get(setting_range.clone())?;
735
736 if parent_object_name == "features" && setting_name == "inline_completion_provider" {
737 return Some((setting_range, "edit_prediction_provider".into()));
738 }
739
740 None
741}
742
743const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
744(object
745 (pair
746 key: (string (string_content) @languages)
747 value: (object
748 (pair
749 key: (string)
750 value: (object
751 (pair
752 key: (string (string_content) @setting_name)
753 value: (_) @value
754 )
755 )
756 ))
757 )
758)
759(#eq? @languages "languages")
760"#;
761
762fn replace_setting_in_languages(
763 contents: &str,
764 mat: &QueryMatch,
765 query: &Query,
766) -> Option<(Range<usize>, String)> {
767 let setting_capture_ix = query.capture_index_for_name("setting_name")?;
768 let setting_name_range = mat
769 .nodes_for_capture_index(setting_capture_ix)
770 .next()?
771 .byte_range();
772 let setting_name = contents.get(setting_name_range.clone())?;
773 let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
774
775 Some((setting_name_range, new_setting_name.to_string()))
776}
777
778static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
779 LazyLock::new(|| {
780 HashMap::from_iter([
781 ("show_inline_completions", "show_edit_predictions"),
782 (
783 "inline_completions_disabled_in",
784 "edit_predictions_disabled_in",
785 ),
786 ])
787 });
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792
793 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
794 let migrated = migrate_keymap(&input).unwrap();
795 pretty_assertions::assert_eq!(migrated.as_deref(), output);
796 }
797
798 fn assert_migrate_settings(input: &str, output: Option<&str>) {
799 let migrated = migrate_settings(&input).unwrap();
800 pretty_assertions::assert_eq!(migrated.as_deref(), output);
801 }
802
803 #[test]
804 fn test_replace_array_with_single_string() {
805 assert_migrate_keymap(
806 r#"
807 [
808 {
809 "bindings": {
810 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
811 }
812 }
813 ]
814 "#,
815 Some(
816 r#"
817 [
818 {
819 "bindings": {
820 "cmd-1": "workspace::ActivatePaneUp"
821 }
822 }
823 ]
824 "#,
825 ),
826 )
827 }
828
829 #[test]
830 fn test_replace_action_argument_object_with_single_value() {
831 assert_migrate_keymap(
832 r#"
833 [
834 {
835 "bindings": {
836 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
837 }
838 }
839 ]
840 "#,
841 Some(
842 r#"
843 [
844 {
845 "bindings": {
846 "cmd-1": ["editor::FoldAtLevel", 1]
847 }
848 }
849 ]
850 "#,
851 ),
852 )
853 }
854
855 #[test]
856 fn test_replace_action_argument_object_with_single_value_2() {
857 assert_migrate_keymap(
858 r#"
859 [
860 {
861 "bindings": {
862 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
863 }
864 }
865 ]
866 "#,
867 Some(
868 r#"
869 [
870 {
871 "bindings": {
872 "cmd-1": ["vim::PushObject", { "some" : "value" }]
873 }
874 }
875 ]
876 "#,
877 ),
878 )
879 }
880
881 #[test]
882 fn test_rename_string_action() {
883 assert_migrate_keymap(
884 r#"
885 [
886 {
887 "bindings": {
888 "cmd-1": "inline_completion::ToggleMenu"
889 }
890 }
891 ]
892 "#,
893 Some(
894 r#"
895 [
896 {
897 "bindings": {
898 "cmd-1": "edit_prediction::ToggleMenu"
899 }
900 }
901 ]
902 "#,
903 ),
904 )
905 }
906
907 #[test]
908 fn test_rename_context_key() {
909 assert_migrate_keymap(
910 r#"
911 [
912 {
913 "context": "Editor && inline_completion && !showing_completions"
914 }
915 ]
916 "#,
917 Some(
918 r#"
919 [
920 {
921 "context": "Editor && edit_prediction && !showing_completions"
922 }
923 ]
924 "#,
925 ),
926 )
927 }
928
929 #[test]
930 fn test_string_of_array_replace() {
931 assert_migrate_keymap(
932 r#"
933 [
934 {
935 "bindings": {
936 "ctrl-p": ["editor::GoToPrevHunk", { "center_cursor": true }],
937 "ctrl-q": ["editor::GoToPrevHunk"],
938 "ctrl-q": "editor::GoToPrevHunk", // should remain same
939 }
940 }
941 ]
942 "#,
943 Some(
944 r#"
945 [
946 {
947 "bindings": {
948 "ctrl-p": ["editor::GoToPreviousHunk", { "center_cursor": true }],
949 "ctrl-q": ["editor::GoToPreviousHunk"],
950 "ctrl-q": "editor::GoToPrevHunk", // should remain same
951 }
952 }
953 ]
954 "#,
955 ),
956 )
957 }
958
959 #[test]
960 fn test_action_argument_snake_case() {
961 // First performs transformations, then replacements
962 assert_migrate_keymap(
963 r#"
964 [
965 {
966 "bindings": {
967 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
968 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
969 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
970 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
971 }
972 }
973 ]
974 "#,
975 Some(
976 r#"
977 [
978 {
979 "bindings": {
980 "cmd-1": ["vim::PushObject", { "around": false }],
981 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
982 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
983 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
984 }
985 }
986 ]
987 "#,
988 ),
989 )
990 }
991
992 #[test]
993 fn test_replace_setting_name() {
994 assert_migrate_settings(
995 r#"
996 {
997 "show_inline_completions_in_menu": true,
998 "show_inline_completions": true,
999 "inline_completions_disabled_in": ["string"],
1000 "inline_completions": { "some" : "value" }
1001 }
1002 "#,
1003 Some(
1004 r#"
1005 {
1006 "show_edit_predictions_in_menu": true,
1007 "show_edit_predictions": true,
1008 "edit_predictions_disabled_in": ["string"],
1009 "edit_predictions": { "some" : "value" }
1010 }
1011 "#,
1012 ),
1013 )
1014 }
1015
1016 #[test]
1017 fn test_nested_string_replace_for_settings() {
1018 assert_migrate_settings(
1019 r#"
1020 {
1021 "features": {
1022 "inline_completion_provider": "zed"
1023 },
1024 }
1025 "#,
1026 Some(
1027 r#"
1028 {
1029 "features": {
1030 "edit_prediction_provider": "zed"
1031 },
1032 }
1033 "#,
1034 ),
1035 )
1036 }
1037
1038 #[test]
1039 fn test_replace_settings_in_languages() {
1040 assert_migrate_settings(
1041 r#"
1042 {
1043 "languages": {
1044 "Astro": {
1045 "show_inline_completions": true
1046 }
1047 }
1048 }
1049 "#,
1050 Some(
1051 r#"
1052 {
1053 "languages": {
1054 "Astro": {
1055 "show_edit_predictions": true
1056 }
1057 }
1058 }
1059 "#,
1060 ),
1061 )
1062 }
1063}