Forge
csharp8f5a77c3
1using System.Diagnostics.CodeAnalysis;
2
3namespace CascadeIDE.Services.ChordNotation;
4
5/// <summary>
6/// Синтаксический слой «как в UI / hotkeys»: <c>Ctrl+K</c>, <c>Ctrl + Shift + P</c>, <c>⌘K</c>, последовательности через пробел.
7/// Разбор не смешивает платформенные строки с грамматикой Vim — результат тот же <see cref="NormalizedKeySequence"/>, что и после <see cref="ChordSemanticNormalizer.FromVimSteps"/>.
8/// </summary>
9public static class KeyGestureChordSyntax
10{
11 private const char CommandKeySymbol = '\u2318'; // ⌘
12
13 /// <summary>Пробельно-разделённые токены: каждый либо аккорд с «+», либо префикс ⌘, либо plain.</summary>
14 public static bool TryParseToNormalized(string? input, out NormalizedKeySequence? sequence, out string error)
15 {
16 sequence = null;
17 error = "";
18
19 if (string.IsNullOrEmpty(input))
20 {
21 sequence = NormalizedKeySequence.Empty;
22 return true;
23 }
24
25 var trimmed = CollapseSpacesAroundPlus(input.Trim());
26 var tokens = trimmed.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
27 var steps = new List<NormalizedSequenceStep>(tokens.Length);
28
29 foreach (var raw in tokens)
30 {
31 if (!TryParseOneToken(raw, out var step, out var err))
32 {
33 error = err;
34 return false;
35 }
36
37 steps.Add(step);
38 }
39
40 sequence = new NormalizedKeySequence(steps);
41 return true;
42 }
43
44 /// <summary>Схлопывает пробелы вокруг <c>+</c>, чтобы <c>Ctrl + Shift + P</c> оставался одним токеном при split по пробелам.</summary>
45 private static string CollapseSpacesAroundPlus(string input)
46 {
47 if (!input.Contains('+', StringComparison.Ordinal))
48 return input;
49 var parts = input.Split('+');
50 for (var i = 0; i < parts.Length; i++)
51 parts[i] = parts[i].Trim();
52 return string.Join("+", parts);
53 }
54
55 private static bool TryParseOneToken(string token, [NotNullWhen(true)] out NormalizedSequenceStep? step, out string error)
56 {
57 error = "";
58 step = null;
59 token = token.Trim();
60 if (token.Length == 0)
61 {
62 error = "Пустой токен.";
63 return false;
64 }
65
66 // ⌘K без плюса
67 if (token.Length >= 2 && token[0] == CommandKeySymbol)
68 {
69 var rest = token[1..];
70 step = new NormalizedChordStep(ChordModifierKeys.Meta, ChordSemanticNormalizer.NormalizeKeySymbol(rest));
71 return true;
72 }
73
74 if (!token.Contains('+', StringComparison.Ordinal))
75 {
76 step = new NormalizedPlainKeyStep(ChordSemanticNormalizer.NormalizeKeySymbol(token));
77 return true;
78 }
79
80 var parts = token.Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
81 if (parts.Length < 2)
82 {
83 error = $"Ожидалось хотя бы Mod+Key, получено: «{token}».";
84 return false;
85 }
86
87 ChordModifierKeys mods = 0;
88 for (var i = 0; i < parts.Length - 1; i++)
89 {
90 var mk = MapModifierWord(parts[i]);
91 if (mk == null)
92 {
93 error = $"Неизвестный модификатор: «{parts[i]}».";
94 return false;
95 }
96
97 mods |= mk.Value;
98 }
99
100 var key = ChordSemanticNormalizer.NormalizeKeySymbol(parts[^1]);
101 step = new NormalizedChordStep(mods, key);
102 return true;
103 }
104
105 private static ChordModifierKeys? MapModifierWord(string word)
106 {
107 if (word.Length == 1 && word[0] == CommandKeySymbol)
108 return ChordModifierKeys.Meta;
109
110 var w = word.Trim();
111 if (w.Length == 0)
112 return null;
113
114 return w.ToUpperInvariant() switch
115 {
116 "CTRL" or "CONTROL" => ChordModifierKeys.Control,
117 "ALT" or "OPTION" => ChordModifierKeys.Alt,
118 "SHIFT" => ChordModifierKeys.Shift,
119 "META" or "CMD" or "COMMAND" or "WIN" or "SUPER" => ChordModifierKeys.Meta,
120 _ => null
121 };
122 }
123}
124
View only · write via MCP/CIDE