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