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
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 rename_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_range = mat
394 .nodes_for_capture_index(action_name_ix)
395 .next()?
396 .byte_range();
397 let action_name = contents.get(action_name_range.clone())?;
398 let new_action_name = STRING_REPLACE.get(&action_name)?;
399 Some((action_name_range, new_action_name.to_string()))
400}
401
402/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
403static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
404 HashMap::from_iter([
405 (
406 "inline_completion::ToggleMenu",
407 "edit_prediction::ToggleMenu",
408 ),
409 ("editor::NextInlineCompletion", "editor::NextEditPrediction"),
410 (
411 "editor::PreviousInlineCompletion",
412 "editor::PreviousEditPrediction",
413 ),
414 (
415 "editor::AcceptPartialInlineCompletion",
416 "editor::AcceptPartialEditPrediction",
417 ),
418 ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"),
419 (
420 "editor::AcceptInlineCompletion",
421 "editor::AcceptEditPrediction",
422 ),
423 (
424 "editor::ToggleInlineCompletions",
425 "editor::ToggleEditPrediction",
426 ),
427 ])
428});
429
430const CONTEXT_PREDICATE_PATTERN: &str = r#"
431(array
432 (object
433 (pair
434 key: (string (string_content) @name)
435 value: (string (string_content) @context_predicate)
436 )
437 )
438)
439(#eq? @name "context")
440"#;
441
442fn rename_context_key(
443 contents: &str,
444 mat: &QueryMatch,
445 query: &Query,
446) -> Option<(Range<usize>, String)> {
447 let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
448 let context_predicate_range = mat
449 .nodes_for_capture_index(context_predicate_ix)
450 .next()?
451 .byte_range();
452 let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
453 let mut new_predicate = old_predicate.to_string();
454 for (old_key, new_key) in CONTEXT_REPLACE.iter() {
455 new_predicate = new_predicate.replace(old_key, new_key);
456 }
457 if new_predicate != old_predicate {
458 Some((context_predicate_range, new_predicate.to_string()))
459 } else {
460 None
461 }
462}
463
464/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
465pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
466 HashMap::from_iter([
467 ("inline_completion", "edit_prediction"),
468 (
469 "inline_completion_requires_modifier",
470 "edit_prediction_requires_modifier",
471 ),
472 ])
473});
474
475const ACTION_ARGUMENT_SNAKE_CASE_PATTERN: &str = r#"(document
476 (array
477 (object
478 (pair
479 key: (string (string_content) @name)
480 value: (
481 (object
482 (pair
483 key: (string)
484 value: ((array
485 . (string (string_content) @action_name)
486 . (object
487 (pair
488 key: (string (string_content) @argument_key)
489 value: (_) @argument_value))
490 . ) @array
491 ))
492 )
493 )
494 )
495 )
496 )
497 (#eq? @name "bindings")
498)"#;
499
500fn to_snake_case(text: &str) -> String {
501 text.to_case(Case::Snake)
502}
503
504fn action_argument_snake_case(
505 contents: &str,
506 mat: &QueryMatch,
507 query: &Query,
508) -> Option<(Range<usize>, String)> {
509 let array_ix = query.capture_index_for_name("array")?;
510 let action_name_ix = query.capture_index_for_name("action_name")?;
511 let argument_key_ix = query.capture_index_for_name("argument_key")?;
512 let argument_value_ix = query.capture_index_for_name("argument_value")?;
513 let action_name = contents.get(
514 mat.nodes_for_capture_index(action_name_ix)
515 .next()?
516 .byte_range(),
517 )?;
518
519 let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?;
520 let argument_key = contents.get(
521 mat.nodes_for_capture_index(argument_key_ix)
522 .next()?
523 .byte_range(),
524 )?;
525
526 if argument_key != *replacement_key {
527 return None;
528 }
529
530 let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?;
531 let argument_value = contents.get(argument_value_node.byte_range())?;
532
533 let new_key = to_snake_case(argument_key);
534 let new_value = if argument_value_node.kind() == "string" {
535 format!("\"{}\"", to_snake_case(argument_value.trim_matches('"')))
536 } else {
537 argument_value.to_string()
538 };
539
540 let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
541 let replacement = format!(
542 "[\"{}\", {{ \"{}\": {} }}]",
543 action_name, new_key, new_value
544 );
545
546 Some((range_to_replace, replacement))
547}
548
549pub static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock<HashMap<&str, &str>> =
550 LazyLock::new(|| {
551 HashMap::from_iter([
552 ("vim::NextWordStart", "ignorePunctuation"),
553 ("vim::NextWordEnd", "ignorePunctuation"),
554 ("vim::PreviousWordStart", "ignorePunctuation"),
555 ("vim::PreviousWordEnd", "ignorePunctuation"),
556 ("vim::MoveToNext", "partialWord"),
557 ("vim::MoveToPrev", "partialWord"),
558 ("vim::Down", "displayLines"),
559 ("vim::Up", "displayLines"),
560 ("vim::EndOfLine", "displayLines"),
561 ("vim::StartOfLine", "displayLines"),
562 ("vim::FirstNonWhitespace", "displayLines"),
563 ("pane::CloseActiveItem", "saveIntent"),
564 ("vim::Paste", "preserveClipboard"),
565 ("vim::Word", "ignorePunctuation"),
566 ("vim::Subword", "ignorePunctuation"),
567 ("vim::IndentObj", "includeBelow"),
568 ])
569 });
570
571const SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[
572 (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name),
573 (
574 SETTINGS_REPLACE_NESTED_KEY,
575 replace_edit_prediction_provider_setting,
576 ),
577 (
578 SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
579 replace_setting_in_languages,
580 ),
581];
582
583static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
584 Query::new(
585 &tree_sitter_json::LANGUAGE.into(),
586 &SETTINGS_MIGRATION_PATTERNS
587 .iter()
588 .map(|pattern| pattern.0)
589 .collect::<String>(),
590 )
591 .unwrap()
592});
593
594static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
595 Query::new(
596 &tree_sitter_json::LANGUAGE.into(),
597 SETTINGS_REPLACE_NESTED_KEY,
598 )
599 .unwrap()
600});
601
602const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document
603 (object
604 (pair
605 key: (string (string_content) @name)
606 value: (_)
607 )
608 )
609)"#;
610
611fn replace_setting_name(
612 contents: &str,
613 mat: &QueryMatch,
614 query: &Query,
615) -> Option<(Range<usize>, String)> {
616 let setting_capture_ix = query.capture_index_for_name("name")?;
617 let setting_name_range = mat
618 .nodes_for_capture_index(setting_capture_ix)
619 .next()?
620 .byte_range();
621 let setting_name = contents.get(setting_name_range.clone())?;
622 let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
623 Some((setting_name_range, new_setting_name.to_string()))
624}
625
626pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
627 LazyLock::new(|| {
628 HashMap::from_iter([
629 (
630 "show_inline_completions_in_menu",
631 "show_edit_predictions_in_menu",
632 ),
633 ("show_inline_completions", "show_edit_predictions"),
634 (
635 "inline_completions_disabled_in",
636 "edit_predictions_disabled_in",
637 ),
638 ("inline_completions", "edit_predictions"),
639 ])
640 });
641
642const SETTINGS_REPLACE_NESTED_KEY: &str = r#"
643(object
644 (pair
645 key: (string (string_content) @parent_key)
646 value: (object
647 (pair
648 key: (string (string_content) @setting_name)
649 value: (_) @value
650 )
651 )
652 )
653)
654"#;
655
656fn replace_edit_prediction_provider_setting(
657 contents: &str,
658 mat: &QueryMatch,
659 query: &Query,
660) -> Option<(Range<usize>, String)> {
661 let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
662 let parent_object_range = mat
663 .nodes_for_capture_index(parent_object_capture_ix)
664 .next()?
665 .byte_range();
666 let parent_object_name = contents.get(parent_object_range.clone())?;
667
668 let setting_name_ix = query.capture_index_for_name("setting_name")?;
669 let setting_range = mat
670 .nodes_for_capture_index(setting_name_ix)
671 .next()?
672 .byte_range();
673 let setting_name = contents.get(setting_range.clone())?;
674
675 if parent_object_name == "features" && setting_name == "inline_completion_provider" {
676 return Some((setting_range, "edit_prediction_provider".into()));
677 }
678
679 None
680}
681
682const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
683(object
684 (pair
685 key: (string (string_content) @languages)
686 value: (object
687 (pair
688 key: (string)
689 value: (object
690 (pair
691 key: (string (string_content) @setting_name)
692 value: (_) @value
693 )
694 )
695 ))
696 )
697)
698(#eq? @languages "languages")
699"#;
700
701fn replace_setting_in_languages(
702 contents: &str,
703 mat: &QueryMatch,
704 query: &Query,
705) -> Option<(Range<usize>, String)> {
706 let setting_capture_ix = query.capture_index_for_name("setting_name")?;
707 let setting_name_range = mat
708 .nodes_for_capture_index(setting_capture_ix)
709 .next()?
710 .byte_range();
711 let setting_name = contents.get(setting_name_range.clone())?;
712 let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
713
714 Some((setting_name_range, new_setting_name.to_string()))
715}
716
717static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
718 LazyLock::new(|| {
719 HashMap::from_iter([
720 ("show_inline_completions", "show_edit_predictions"),
721 (
722 "inline_completions_disabled_in",
723 "edit_predictions_disabled_in",
724 ),
725 ])
726 });
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
733 let migrated = migrate_keymap(&input).unwrap();
734 pretty_assertions::assert_eq!(migrated.as_deref(), output);
735 }
736
737 fn assert_migrate_settings(input: &str, output: Option<&str>) {
738 let migrated = migrate_settings(&input).unwrap();
739 pretty_assertions::assert_eq!(migrated.as_deref(), output);
740 }
741
742 #[test]
743 fn test_replace_array_with_single_string() {
744 assert_migrate_keymap(
745 r#"
746 [
747 {
748 "bindings": {
749 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
750 }
751 }
752 ]
753 "#,
754 Some(
755 r#"
756 [
757 {
758 "bindings": {
759 "cmd-1": "workspace::ActivatePaneUp"
760 }
761 }
762 ]
763 "#,
764 ),
765 )
766 }
767
768 #[test]
769 fn test_replace_action_argument_object_with_single_value() {
770 assert_migrate_keymap(
771 r#"
772 [
773 {
774 "bindings": {
775 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
776 }
777 }
778 ]
779 "#,
780 Some(
781 r#"
782 [
783 {
784 "bindings": {
785 "cmd-1": ["editor::FoldAtLevel", 1]
786 }
787 }
788 ]
789 "#,
790 ),
791 )
792 }
793
794 #[test]
795 fn test_replace_action_argument_object_with_single_value_2() {
796 assert_migrate_keymap(
797 r#"
798 [
799 {
800 "bindings": {
801 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
802 }
803 }
804 ]
805 "#,
806 Some(
807 r#"
808 [
809 {
810 "bindings": {
811 "cmd-1": ["vim::PushObject", { "some" : "value" }]
812 }
813 }
814 ]
815 "#,
816 ),
817 )
818 }
819
820 #[test]
821 fn test_rename_string_action() {
822 assert_migrate_keymap(
823 r#"
824 [
825 {
826 "bindings": {
827 "cmd-1": "inline_completion::ToggleMenu"
828 }
829 }
830 ]
831 "#,
832 Some(
833 r#"
834 [
835 {
836 "bindings": {
837 "cmd-1": "edit_prediction::ToggleMenu"
838 }
839 }
840 ]
841 "#,
842 ),
843 )
844 }
845
846 #[test]
847 fn test_rename_context_key() {
848 assert_migrate_keymap(
849 r#"
850 [
851 {
852 "context": "Editor && inline_completion && !showing_completions"
853 }
854 ]
855 "#,
856 Some(
857 r#"
858 [
859 {
860 "context": "Editor && edit_prediction && !showing_completions"
861 }
862 ]
863 "#,
864 ),
865 )
866 }
867
868 #[test]
869 fn test_action_argument_snake_case() {
870 // First performs transformations, then replacements
871 assert_migrate_keymap(
872 r#"
873 [
874 {
875 "bindings": {
876 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
877 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
878 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
879 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
880 }
881 }
882 ]
883 "#,
884 Some(
885 r#"
886 [
887 {
888 "bindings": {
889 "cmd-1": ["vim::PushObject", { "around": false }],
890 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
891 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
892 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
893 }
894 }
895 ]
896 "#,
897 ),
898 )
899 }
900
901 #[test]
902 fn test_replace_setting_name() {
903 assert_migrate_settings(
904 r#"
905 {
906 "show_inline_completions_in_menu": true,
907 "show_inline_completions": true,
908 "inline_completions_disabled_in": ["string"],
909 "inline_completions": { "some" : "value" }
910 }
911 "#,
912 Some(
913 r#"
914 {
915 "show_edit_predictions_in_menu": true,
916 "show_edit_predictions": true,
917 "edit_predictions_disabled_in": ["string"],
918 "edit_predictions": { "some" : "value" }
919 }
920 "#,
921 ),
922 )
923 }
924
925 #[test]
926 fn test_nested_string_replace_for_settings() {
927 assert_migrate_settings(
928 r#"
929 {
930 "features": {
931 "inline_completion_provider": "zed"
932 },
933 }
934 "#,
935 Some(
936 r#"
937 {
938 "features": {
939 "edit_prediction_provider": "zed"
940 },
941 }
942 "#,
943 ),
944 )
945 }
946
947 #[test]
948 fn test_replace_settings_in_languages() {
949 assert_migrate_settings(
950 r#"
951 {
952 "languages": {
953 "Astro": {
954 "show_inline_completions": true
955 }
956 }
957 }
958 "#,
959 Some(
960 r#"
961 {
962 "languages": {
963 "Astro": {
964 "show_edit_predictions": true
965 }
966 }
967 }
968 "#,
969 ),
970 )
971 }
972}