Open History to see the last commit
1import { assert, describe, expect, it } from "vite-plus/test";
2import type { ResolvedKeybindingsConfig } from "@t3tools/contracts";
3import {
4 compileResolvedKeybindingsConfig,
5 parseKeybindingShortcut,
6} from "@t3tools/shared/keybindings";
7
8import { resolveShortcutCommand } from "../../keybindings";
9
10import {
11 buildKeybindingRows,
12 buildWhenVariableOptions,
13 keybindingConflictLabels,
14 keybindingFromKeyboardEvent,
15 parseWhenExpressionDraft,
16 shortcutToKeybindingInput,
17 unknownWhenVariables,
18 whenAstToExpression,
19} from "./KeybindingsSettings.logic";
20
21describe("KeybindingsSettings.logic", () => {
22 it("keeps sibling row identities when a saved binding moves to the end", () => {
23 const bindings = compileResolvedKeybindingsConfig([
24 { command: "chat.new", key: "mod+n" },
25 { command: "terminal.toggle", key: "mod+j" },
26 { command: "terminal.toggle", key: "mod+k", when: "terminalFocus" },
27 ]);
28 const before = buildKeybindingRows(bindings);
29 const after = buildKeybindingRows([
30 ...bindings.slice(1),
31 ...compileResolvedKeybindingsConfig([{ command: "chat.new", key: "mod+shift+n" }]),
32 ]);
33
34 expect(after.filter((row) => row.command === "terminal.toggle").map((row) => row.id)).toEqual(
35 before.filter((row) => row.command === "terminal.toggle").map((row) => row.id),
36 );
37 expect(after.find((row) => row.command === "chat.new")?.id).not.toBe(
38 before.find((row) => row.command === "chat.new")?.id,
39 );
40 });
41
42 it("shares a stable editor for resolved duplicates but separates commands, shortcuts, and conditions", () => {
43 const bindings = compileResolvedKeybindingsConfig([
44 { command: "terminal.toggle", key: "mod+esc", when: "terminalFocus" },
45 { command: "terminal.toggle", key: "mod+escape", when: "(terminalFocus)" },
46 { command: "terminal.toggle", key: "mod+esc", when: "!terminalFocus" },
47 { command: "terminal.toggle", key: "mod+j", when: "terminalFocus" },
48 { command: "chat.new", key: "mod+esc", when: "terminalFocus" },
49 ]);
50 const rows = buildKeybindingRows(bindings);
51 expect(rows.map(({ command, key, when }) => ({ command, key, when }))).toEqual([
52 { command: "chat.new", key: "mod+esc", when: "terminalFocus" },
53 { command: "terminal.toggle", key: "mod+esc", when: "terminalFocus" },
54 { command: "terminal.toggle", key: "mod+esc", when: "!terminalFocus" },
55 { command: "terminal.toggle", key: "mod+j", when: "terminalFocus" },
56 ]);
57 expect(new Set(rows.map((row) => row.id)).size).toBe(4);
58 expect(rows.find((row) => row.command === "chat.new")?.conflicts).toEqual(["Terminal: Toggle"]);
59 expect(buildKeybindingRows(bindings.slice(1)).map((row) => row.id)).toEqual(
60 rows.map((row) => row.id),
61 );
62 });
63
64 it("captures platform-specific mod shortcuts", () => {
65 expect(
66 keybindingFromKeyboardEvent(
67 { key: "K", code: "KeyK", metaKey: true, ctrlKey: false, altKey: false, shiftKey: true },
68 "MacIntel",
69 ),
70 ).toBe("mod+shift+k");
71 expect(
72 keybindingFromKeyboardEvent(
73 { key: "K", code: "KeyK", metaKey: false, ctrlKey: true, altKey: false, shiftKey: true },
74 "Win32",
75 ),
76 ).toBe("mod+shift+k");
77 });
78
79 it("waits for a key when only a modifier is pressed", () => {
80 expect(
81 keybindingFromKeyboardEvent(
82 {
83 key: "Meta",
84 code: "MetaLeft",
85 metaKey: true,
86 ctrlKey: false,
87 altKey: false,
88 shiftKey: false,
89 },
90 "MacIntel",
91 ),
92 ).toBeNull();
93 });
94
95 it.each([
96 ["@", "Digit2", "mod+shift+2"],
97 ['"', "Digit2", "mod+shift+2"],
98 ["@", "Quote", "mod+shift+'"],
99 ])("captures %s at %s by physical key", (key, code, expected) => {
100 expect(
101 keybindingFromKeyboardEvent(
102 {
103 key,
104 code,
105 metaKey: true,
106 ctrlKey: false,
107 altKey: false,
108 shiftKey: true,
109 },
110 "MacIntel",
111 ),
112 ).toBe(expected);
113 });
114
115 it("records layout punctuation while preserving physical Alt shortcuts; saved bindings still trigger", () => {
116 for (const { key, code, altKey = false, shiftKey, expected, defaultInput } of [
117 { key: "#", code: "Backslash", shiftKey: false, expected: "#", defaultInput: "\\" },
118 {
119 key: "'",
120 code: "Backslash",
121 shiftKey: true,
122 expected: "shift+'",
123 defaultInput: "shift+\\",
124 },
125 {
126 key: "¡",
127 code: "Digit1",
128 altKey: true,
129 shiftKey: false,
130 expected: "alt+1",
131 defaultInput: "alt+1",
132 },
133 ]) {
134 const pressed = {
135 key,
136 code,
137 metaKey: false,
138 ctrlKey: false,
139 altKey,
140 shiftKey,
141 };
142 expect(keybindingFromKeyboardEvent(pressed, "MacIntel")).toBe(defaultInput);
143 const input = keybindingFromKeyboardEvent(pressed, "MacIntel", { layoutCharacter: true });
144 expect(input).toBe(expected);
145 assert(input);
146 const shortcut = parseKeybindingShortcut(input);
147 assert(shortcut);
148 expect(
149 resolveShortcutCommand(pressed, [{ command: "chat.new", shortcut }], {
150 platform: "MacIntel",
151 }),
152 ).toBe("chat.new");
153 }
154 });
155
156 it("captures Latin layout keys instead of their punctuation position", () => {
157 expect(
158 keybindingFromKeyboardEvent(
159 {
160 key: "m",
161 code: "Semicolon",
162 metaKey: true,
163 ctrlKey: false,
164 altKey: false,
165 shiftKey: false,
166 },
167 "MacIntel",
168 ),
169 ).toBe("mod+m");
170 });
171
172 it("serializes shortcuts and when expressions for upserts", () => {
173 expect(
174 shortcutToKeybindingInput({
175 key: " ",
176 modKey: true,
177 metaKey: false,
178 ctrlKey: false,
179 altKey: true,
180 shiftKey: false,
181 }),
182 ).toBe("mod+alt+space");
183
184 expect(
185 whenAstToExpression({
186 type: "and",
187 left: { type: "identifier", name: "editorFocus" },
188 right: {
189 type: "not",
190 node: { type: "identifier", name: "terminalFocus" },
191 },
192 }),
193 ).toBe("editorFocus && !terminalFocus");
194
195 expect(parseWhenExpressionDraft("editorFocus && (!terminalFocus || modelPickerOpen)")).toEqual({
196 ok: true,
197 value: {
198 type: "and",
199 left: { type: "identifier", name: "editorFocus" },
200 right: {
201 type: "or",
202 left: {
203 type: "not",
204 node: { type: "identifier", name: "terminalFocus" },
205 },
206 right: { type: "identifier", name: "modelPickerOpen" },
207 },
208 },
209 });
210 const previewOpenDraft = parseWhenExpressionDraft("previewOpen && !previewFocus");
211 assert(previewOpenDraft.ok);
212 expect(unknownWhenVariables(previewOpenDraft.value)).toEqual([]);
213 expect(buildWhenVariableOptions()).toContain("previewOpen");
214 expect(parseWhenExpressionDraft("editorFocus &&")).toEqual({
215 ok: false,
216 message: "Use variables with !, &&, ||, and parentheses.",
217 });
218
219 expect(parseWhenExpressionDraft("!(terminalFocus || modelPickerOpen)")).toEqual({
220 ok: true,
221 value: {
222 type: "not",
223 node: {
224 type: "or",
225 left: { type: "identifier", name: "terminalFocus" },
226 right: { type: "identifier", name: "modelPickerOpen" },
227 },
228 },
229 });
230 });
231
232 it("reports conflicting shortcuts that share an active when context", () => {
233 const rows = buildKeybindingRows([
234 {
235 command: "chat.new",
236 shortcut: {
237 key: "n",
238 modKey: true,
239 metaKey: false,
240 ctrlKey: false,
241 altKey: false,
242 shiftKey: false,
243 },
244 whenAst: {
245 type: "not",
246 node: { type: "identifier", name: "terminalFocus" },
247 },
248 },
249 {
250 command: "chat.newLocal",
251 shortcut: {
252 key: "n",
253 modKey: true,
254 metaKey: false,
255 ctrlKey: false,
256 altKey: false,
257 shiftKey: false,
258 },
259 whenAst: {
260 type: "not",
261 node: { type: "identifier", name: "terminalFocus" },
262 },
263 },
264 ] satisfies ResolvedKeybindingsConfig);
265
266 expect(rows[0]?.conflicts).toEqual(["Chat: New Local"]);
267 expect(
268 keybindingConflictLabels(rows, {
269 rowId: rows[0]?.id ?? "",
270 key: "mod+n",
271 when: "",
272 }),
273 ).toEqual(["Chat: New Local"]);
274 });
275
276 it.each([
277 { platform: "Linux x86_64", ctrl: ["Chat: New"], meta: [] },
278 { platform: "Win32", ctrl: ["Chat: New"], meta: [] },
279 { platform: "MacIntel", ctrl: [], meta: ["Chat: New"] },
280 ])("resolves mod to the $platform modifier when reporting conflicts", (expected) => {
281 const rows = buildKeybindingRows(
282 compileResolvedKeybindingsConfig([
283 { command: "chat.new", key: "mod+k" },
284 { command: "chat.newLocal", key: "ctrl+k" },
285 { command: "thread.copyReference", key: "meta+k" },
286 ]),
287 expected.platform,
288 );
289 const conflictsFor = (key: string) => rows.find((row) => row.key === key)?.conflicts;
290
291 expect(conflictsFor("ctrl+k")).toEqual(expected.ctrl);
292 expect(conflictsFor("meta+k")).toEqual(expected.meta);
293 expect(
294 keybindingConflictLabels(rows, { rowId: "new", key: "ctrl+k", when: "" }, expected.platform),
295 ).toEqual(["Chat: New Local", ...expected.ctrl].toSorted());
296 });
297
298 it("keeps mod and ctrl shortcuts apart when their when contexts differ", () => {
299 const rows = buildKeybindingRows(
300 compileResolvedKeybindingsConfig([
301 { command: "chat.new", key: "mod+k", when: "!terminalFocus" },
302 { command: "chat.newLocal", key: "ctrl+k", when: "terminalFocus" },
303 ]),
304 "Linux x86_64",
305 );
306
307 expect(rows.map((row) => row.conflicts)).toEqual([[], []]);
308 });
309});