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 ])
486});
487
488const CONTEXT_PREDICATE_PATTERN: &str = r#"(document
489 (array
490 (object
491 (pair
492 key: (string (string_content) @name)
493 value: (string (string_content) @context_predicate)
494 )
495 )
496 )
497 (#eq? @name "context")
498)"#;
499
500fn rename_context_key(
501 contents: &str,
502 mat: &QueryMatch,
503 query: &Query,
504) -> Option<(Range<usize>, String)> {
505 let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
506 let context_predicate_range = mat
507 .nodes_for_capture_index(context_predicate_ix)
508 .next()?
509 .byte_range();
510 let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
511 let mut new_predicate = old_predicate.to_string();
512 for (old_key, new_key) in CONTEXT_REPLACE.iter() {
513 new_predicate = new_predicate.replace(old_key, new_key);
514 }
515 if new_predicate != old_predicate {
516 Some((context_predicate_range, new_predicate.to_string()))
517 } else {
518 None
519 }
520}
521
522/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
523pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
524 HashMap::from_iter([
525 ("inline_completion", "edit_prediction"),
526 (
527 "inline_completion_requires_modifier",
528 "edit_prediction_requires_modifier",
529 ),
530 ])
531});
532
533const ACTION_ARGUMENT_SNAKE_CASE_PATTERN: &str = r#"(document
534 (array
535 (object
536 (pair
537 key: (string (string_content) @name)
538 value: (
539 (object
540 (pair
541 key: (string)
542 value: ((array
543 . (string (string_content) @action_name)
544 . (object
545 (pair
546 key: (string (string_content) @argument_key)
547 value: (_) @argument_value))
548 . ) @array
549 ))
550 )
551 )
552 )
553 )
554 )
555 (#eq? @name "bindings")
556)"#;
557
558fn to_snake_case(text: &str) -> String {
559 text.to_case(Case::Snake)
560}
561
562fn action_argument_snake_case(
563 contents: &str,
564 mat: &QueryMatch,
565 query: &Query,
566) -> Option<(Range<usize>, String)> {
567 let array_ix = query.capture_index_for_name("array")?;
568 let action_name_ix = query.capture_index_for_name("action_name")?;
569 let argument_key_ix = query.capture_index_for_name("argument_key")?;
570 let argument_value_ix = query.capture_index_for_name("argument_value")?;
571 let action_name = contents.get(
572 mat.nodes_for_capture_index(action_name_ix)
573 .next()?
574 .byte_range(),
575 )?;
576
577 let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?;
578 let argument_key = contents.get(
579 mat.nodes_for_capture_index(argument_key_ix)
580 .next()?
581 .byte_range(),
582 )?;
583
584 if argument_key != *replacement_key {
585 return None;
586 }
587
588 let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?;
589 let argument_value = contents.get(argument_value_node.byte_range())?;
590
591 let new_key = to_snake_case(argument_key);
592 let new_value = if argument_value_node.kind() == "string" {
593 format!("\"{}\"", to_snake_case(argument_value.trim_matches('"')))
594 } else {
595 argument_value.to_string()
596 };
597
598 let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
599 let replacement = format!(
600 "[\"{}\", {{ \"{}\": {} }}]",
601 action_name, new_key, new_value
602 );
603
604 Some((range_to_replace, replacement))
605}
606
607pub static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock<HashMap<&str, &str>> =
608 LazyLock::new(|| {
609 HashMap::from_iter([
610 ("vim::NextWordStart", "ignorePunctuation"),
611 ("vim::NextWordEnd", "ignorePunctuation"),
612 ("vim::PreviousWordStart", "ignorePunctuation"),
613 ("vim::PreviousWordEnd", "ignorePunctuation"),
614 ("vim::MoveToNext", "partialWord"),
615 ("vim::MoveToPrev", "partialWord"),
616 ("vim::Down", "displayLines"),
617 ("vim::Up", "displayLines"),
618 ("vim::EndOfLine", "displayLines"),
619 ("vim::StartOfLine", "displayLines"),
620 ("vim::FirstNonWhitespace", "displayLines"),
621 ("pane::CloseActiveItem", "saveIntent"),
622 ("vim::Paste", "preserveClipboard"),
623 ("vim::Word", "ignorePunctuation"),
624 ("vim::Subword", "ignorePunctuation"),
625 ("vim::IndentObj", "includeBelow"),
626 ])
627 });
628
629const SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[
630 (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name),
631 (
632 SETTINGS_NESTED_KEY_VALUE_PATTERN,
633 replace_edit_prediction_provider_setting,
634 ),
635 (
636 SETTINGS_NESTED_KEY_VALUE_PATTERN,
637 replace_tab_close_button_setting_key,
638 ),
639 (
640 SETTINGS_NESTED_KEY_VALUE_PATTERN,
641 replace_tab_close_button_setting_value,
642 ),
643 (
644 SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
645 replace_setting_in_languages,
646 ),
647];
648
649static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
650 Query::new(
651 &tree_sitter_json::LANGUAGE.into(),
652 &SETTINGS_MIGRATION_PATTERNS
653 .iter()
654 .map(|pattern| pattern.0)
655 .collect::<String>(),
656 )
657 .unwrap()
658});
659
660static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
661 Query::new(
662 &tree_sitter_json::LANGUAGE.into(),
663 SETTINGS_NESTED_KEY_VALUE_PATTERN,
664 )
665 .unwrap()
666});
667
668const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
669 (object
670 (pair
671 key: (string (string_content) @name)
672 value: (_)
673 )
674 )
675)"#;
676
677fn replace_setting_name(
678 contents: &str,
679 mat: &QueryMatch,
680 query: &Query,
681) -> Option<(Range<usize>, String)> {
682 let setting_capture_ix = query.capture_index_for_name("name")?;
683 let setting_name_range = mat
684 .nodes_for_capture_index(setting_capture_ix)
685 .next()?
686 .byte_range();
687 let setting_name = contents.get(setting_name_range.clone())?;
688 let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
689 Some((setting_name_range, new_setting_name.to_string()))
690}
691
692pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
693 LazyLock::new(|| {
694 HashMap::from_iter([
695 (
696 "show_inline_completions_in_menu",
697 "show_edit_predictions_in_menu",
698 ),
699 ("show_inline_completions", "show_edit_predictions"),
700 (
701 "inline_completions_disabled_in",
702 "edit_predictions_disabled_in",
703 ),
704 ("inline_completions", "edit_predictions"),
705 ])
706 });
707
708const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#"
709(object
710 (pair
711 key: (string (string_content) @parent_key)
712 value: (object
713 (pair
714 key: (string (string_content) @setting_name)
715 value: (_) @setting_value
716 )
717 )
718 )
719)
720"#;
721
722fn replace_edit_prediction_provider_setting(
723 contents: &str,
724 mat: &QueryMatch,
725 query: &Query,
726) -> Option<(Range<usize>, String)> {
727 let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
728 let parent_object_range = mat
729 .nodes_for_capture_index(parent_object_capture_ix)
730 .next()?
731 .byte_range();
732 let parent_object_name = contents.get(parent_object_range.clone())?;
733
734 let setting_name_ix = query.capture_index_for_name("setting_name")?;
735 let setting_range = mat
736 .nodes_for_capture_index(setting_name_ix)
737 .next()?
738 .byte_range();
739 let setting_name = contents.get(setting_range.clone())?;
740
741 if parent_object_name == "features" && setting_name == "inline_completion_provider" {
742 return Some((setting_range, "edit_prediction_provider".into()));
743 }
744
745 None
746}
747
748fn replace_tab_close_button_setting_key(
749 contents: &str,
750 mat: &QueryMatch,
751 query: &Query,
752) -> Option<(Range<usize>, String)> {
753 let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
754 let parent_object_range = mat
755 .nodes_for_capture_index(parent_object_capture_ix)
756 .next()?
757 .byte_range();
758 let parent_object_name = contents.get(parent_object_range.clone())?;
759
760 let setting_name_ix = query.capture_index_for_name("setting_name")?;
761 let setting_range = mat
762 .nodes_for_capture_index(setting_name_ix)
763 .next()?
764 .byte_range();
765 let setting_name = contents.get(setting_range.clone())?;
766
767 if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
768 return Some((setting_range, "show_close_button".into()));
769 }
770
771 None
772}
773
774fn replace_tab_close_button_setting_value(
775 contents: &str,
776 mat: &QueryMatch,
777 query: &Query,
778) -> Option<(Range<usize>, String)> {
779 let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
780 let parent_object_range = mat
781 .nodes_for_capture_index(parent_object_capture_ix)
782 .next()?
783 .byte_range();
784 let parent_object_name = contents.get(parent_object_range.clone())?;
785
786 let setting_name_ix = query.capture_index_for_name("setting_name")?;
787 let setting_name_range = mat
788 .nodes_for_capture_index(setting_name_ix)
789 .next()?
790 .byte_range();
791 let setting_name = contents.get(setting_name_range.clone())?;
792
793 let setting_value_ix = query.capture_index_for_name("setting_value")?;
794 let setting_value_range = mat
795 .nodes_for_capture_index(setting_value_ix)
796 .next()?
797 .byte_range();
798 let setting_value = contents.get(setting_value_range.clone())?;
799
800 if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
801 match setting_value {
802 "true" => {
803 return Some((setting_value_range, "\"always\"".to_string()));
804 }
805 "false" => {
806 return Some((setting_value_range, "\"hover\"".to_string()));
807 }
808 _ => {}
809 }
810 }
811
812 None
813}
814
815const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
816(object
817 (pair
818 key: (string (string_content) @languages)
819 value: (object
820 (pair
821 key: (string)
822 value: (object
823 (pair
824 key: (string (string_content) @setting_name)
825 value: (_) @value
826 )
827 )
828 ))
829 )
830)
831(#eq? @languages "languages")
832"#;
833
834fn replace_setting_in_languages(
835 contents: &str,
836 mat: &QueryMatch,
837 query: &Query,
838) -> Option<(Range<usize>, String)> {
839 let setting_capture_ix = query.capture_index_for_name("setting_name")?;
840 let setting_name_range = mat
841 .nodes_for_capture_index(setting_capture_ix)
842 .next()?
843 .byte_range();
844 let setting_name = contents.get(setting_name_range.clone())?;
845 let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
846
847 Some((setting_name_range, new_setting_name.to_string()))
848}
849
850static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
851 LazyLock::new(|| {
852 HashMap::from_iter([
853 ("show_inline_completions", "show_edit_predictions"),
854 (
855 "inline_completions_disabled_in",
856 "edit_predictions_disabled_in",
857 ),
858 ])
859 });
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
866 let migrated = migrate_keymap(&input).unwrap();
867 pretty_assertions::assert_eq!(migrated.as_deref(), output);
868 }
869
870 fn assert_migrate_settings(input: &str, output: Option<&str>) {
871 let migrated = migrate_settings(&input).unwrap();
872 pretty_assertions::assert_eq!(migrated.as_deref(), output);
873 }
874
875 #[test]
876 fn test_replace_array_with_single_string() {
877 assert_migrate_keymap(
878 r#"
879 [
880 {
881 "bindings": {
882 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
883 }
884 }
885 ]
886 "#,
887 Some(
888 r#"
889 [
890 {
891 "bindings": {
892 "cmd-1": "workspace::ActivatePaneUp"
893 }
894 }
895 ]
896 "#,
897 ),
898 )
899 }
900
901 #[test]
902 fn test_replace_action_argument_object_with_single_value() {
903 assert_migrate_keymap(
904 r#"
905 [
906 {
907 "bindings": {
908 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
909 }
910 }
911 ]
912 "#,
913 Some(
914 r#"
915 [
916 {
917 "bindings": {
918 "cmd-1": ["editor::FoldAtLevel", 1]
919 }
920 }
921 ]
922 "#,
923 ),
924 )
925 }
926
927 #[test]
928 fn test_replace_action_argument_object_with_single_value_2() {
929 assert_migrate_keymap(
930 r#"
931 [
932 {
933 "bindings": {
934 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
935 }
936 }
937 ]
938 "#,
939 Some(
940 r#"
941 [
942 {
943 "bindings": {
944 "cmd-1": ["vim::PushObject", { "some" : "value" }]
945 }
946 }
947 ]
948 "#,
949 ),
950 )
951 }
952
953 #[test]
954 fn test_rename_string_action() {
955 assert_migrate_keymap(
956 r#"
957 [
958 {
959 "bindings": {
960 "cmd-1": "inline_completion::ToggleMenu"
961 }
962 }
963 ]
964 "#,
965 Some(
966 r#"
967 [
968 {
969 "bindings": {
970 "cmd-1": "edit_prediction::ToggleMenu"
971 }
972 }
973 ]
974 "#,
975 ),
976 )
977 }
978
979 #[test]
980 fn test_rename_context_key() {
981 assert_migrate_keymap(
982 r#"
983 [
984 {
985 "context": "Editor && inline_completion && !showing_completions"
986 }
987 ]
988 "#,
989 Some(
990 r#"
991 [
992 {
993 "context": "Editor && edit_prediction && !showing_completions"
994 }
995 ]
996 "#,
997 ),
998 )
999 }
1000
1001 #[test]
1002 fn test_string_of_array_replace() {
1003 assert_migrate_keymap(
1004 r#"
1005 [
1006 {
1007 "bindings": {
1008 "ctrl-p": ["editor::GoToPrevHunk", { "center_cursor": true }],
1009 "ctrl-q": ["editor::GoToPrevHunk"],
1010 "ctrl-q": "editor::GoToPrevHunk", // should remain same
1011 }
1012 }
1013 ]
1014 "#,
1015 Some(
1016 r#"
1017 [
1018 {
1019 "bindings": {
1020 "ctrl-p": ["editor::GoToPreviousHunk", { "center_cursor": true }],
1021 "ctrl-q": ["editor::GoToPreviousHunk"],
1022 "ctrl-q": "editor::GoToPrevHunk", // should remain same
1023 }
1024 }
1025 ]
1026 "#,
1027 ),
1028 )
1029 }
1030
1031 #[test]
1032 fn test_action_argument_snake_case() {
1033 // First performs transformations, then replacements
1034 assert_migrate_keymap(
1035 r#"
1036 [
1037 {
1038 "bindings": {
1039 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
1040 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
1041 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
1042 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
1043 }
1044 }
1045 ]
1046 "#,
1047 Some(
1048 r#"
1049 [
1050 {
1051 "bindings": {
1052 "cmd-1": ["vim::PushObject", { "around": false }],
1053 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
1054 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
1055 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
1056 }
1057 }
1058 ]
1059 "#,
1060 ),
1061 )
1062 }
1063
1064 #[test]
1065 fn test_replace_setting_name() {
1066 assert_migrate_settings(
1067 r#"
1068 {
1069 "show_inline_completions_in_menu": true,
1070 "show_inline_completions": true,
1071 "inline_completions_disabled_in": ["string"],
1072 "inline_completions": { "some" : "value" }
1073 }
1074 "#,
1075 Some(
1076 r#"
1077 {
1078 "show_edit_predictions_in_menu": true,
1079 "show_edit_predictions": true,
1080 "edit_predictions_disabled_in": ["string"],
1081 "edit_predictions": { "some" : "value" }
1082 }
1083 "#,
1084 ),
1085 )
1086 }
1087
1088 #[test]
1089 fn test_nested_string_replace_for_settings() {
1090 assert_migrate_settings(
1091 r#"
1092 {
1093 "features": {
1094 "inline_completion_provider": "zed"
1095 },
1096 }
1097 "#,
1098 Some(
1099 r#"
1100 {
1101 "features": {
1102 "edit_prediction_provider": "zed"
1103 },
1104 }
1105 "#,
1106 ),
1107 )
1108 }
1109
1110 #[test]
1111 fn test_replace_settings_in_languages() {
1112 assert_migrate_settings(
1113 r#"
1114 {
1115 "languages": {
1116 "Astro": {
1117 "show_inline_completions": true
1118 }
1119 }
1120 }
1121 "#,
1122 Some(
1123 r#"
1124 {
1125 "languages": {
1126 "Astro": {
1127 "show_edit_predictions": true
1128 }
1129 }
1130 }
1131 "#,
1132 ),
1133 )
1134 }
1135}