Skip to content
Source
Sign inSign up

keybindings.test.ts

986 lines30.0 KB

1import { assert, describe, it } from "vite-plus/test";
2import {
3 compileResolvedKeybindingsConfig,
4 DEFAULT_RESOLVED_KEYBINDINGS,
5 mergeWithDefaultKeybindings,
6} from "@t3tools/shared/keybindings";
7​
8import {
9 type KeybindingCommand,
10 type KeybindingShortcut,
11 type KeybindingWhenNode,
12 type ResolvedKeybindingsConfig,
13} from "@t3tools/contracts";
14import {
15 effectiveShortcutsForCommand,
16 formatShortcutLabel,
17 isRichTextBoldShortcut,
18 isTerminalNewShortcut,
19 isTerminalSplitShortcut,
20 resolveChatShortcutCommand,
21 resolveShortcutCommand,
22 shouldShowThreadJumpHintsForModifiers,
23 shortcutLabelForCommand,
24 terminalNavigationShortcutData,
25 type ShortcutEventLike,
26} from "./keybindings";
27​
28function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike {
29 return {
30 key: "j",
31 metaKey: false,
32 ctrlKey: false,
33 shiftKey: false,
34 altKey: false,
35 ...overrides,
36 };
38​
39describe("terminal word navigation", () => {
40 it.each(["Win32", "Linux x86_64"])(
41 "leaves Ctrl+Arrow to the terminal encoder on %s",
42 (platform) => {
43 for (const key of ["ArrowLeft", "ArrowRight"]) {
44 assert.equal(terminalNavigationShortcutData(event({ key, ctrlKey: true }), platform), null);
45 }
46 },
47 );
48​
49 it.each(["Win32", "Linux x86_64", "MacIntel"])(
50 "preserves Alt+Arrow word navigation on %s",
51 (platform) => {
52 assert.equal(
53 terminalNavigationShortcutData(event({ key: "ArrowLeft", altKey: true }), platform),
54 "\u001bb",
55 );
56 assert.equal(
57 terminalNavigationShortcutData(event({ key: "ArrowRight", altKey: true }), platform),
58 "\u001bf",
59 );
60 },
61 );
62});
63​
64function modShortcut(
65 key: string,
66 overrides: Partial<Omit<KeybindingShortcut, "key">> = {},
67): KeybindingShortcut {
68 return {
69 key,
70 metaKey: false,
71 ctrlKey: false,
72 shiftKey: false,
73 altKey: false,
74 modKey: true,
75 ...overrides,
76 };
78​
79function whenIdentifier(name: string): KeybindingWhenNode {
80 return { type: "identifier", name };
82​
83function whenNot(node: KeybindingWhenNode): KeybindingWhenNode {
84 return { type: "not", node };
86​
87function whenAnd(left: KeybindingWhenNode, right: KeybindingWhenNode): KeybindingWhenNode {
88 return { type: "and", left, right };
90​
91interface TestBinding {
92 shortcut: KeybindingShortcut;
93 command: KeybindingCommand;
94 whenAst?: KeybindingWhenNode;
96​
97function compile(bindings: TestBinding[]): ResolvedKeybindingsConfig {
98 return bindings.map((binding) => ({
99 command: binding.command,
100 shortcut: binding.shortcut,
101 ...(binding.whenAst ? { whenAst: binding.whenAst } : {}),
102 }));
104​
105const DEFAULT_BINDINGS = compile([
106 { shortcut: modShortcut("b"), command: "sidebar.toggle" },
107 { shortcut: modShortcut("j"), command: "terminal.toggle" },
108 { shortcut: modShortcut("b", { altKey: true }), command: "rightPanel.toggle" },
109 {
110 shortcut: modShortcut("d"),
111 command: "terminal.split",
112 whenAst: whenIdentifier("terminalFocus"),
113 },
114 {
115 shortcut: modShortcut("d", { shiftKey: true }),
116 command: "terminal.splitVertical",
117 whenAst: whenIdentifier("terminalFocus"),
118 },
119 {
120 shortcut: modShortcut("n"),
121 command: "terminal.new",
122 whenAst: whenIdentifier("terminalFocus"),
123 },
124 {
125 shortcut: modShortcut("w"),
126 command: "terminal.close",
127 whenAst: whenIdentifier("terminalFocus"),
128 },
129 {
130 shortcut: modShortcut("w"),
131 command: "rightPanel.close",
132 whenAst: whenNot(whenIdentifier("terminalFocus")),
133 },
134 {
135 shortcut: modShortcut("d"),
136 command: "diff.toggle",
137 whenAst: whenNot(whenIdentifier("terminalFocus")),
138 },
139 {
140 shortcut: modShortcut("k"),
141 command: "commandPalette.toggle",
142 whenAst: whenNot(whenIdentifier("terminalFocus")),
143 },
144 {
145 shortcut: modShortcut("p"),
146 command: "filePicker.toggle",
147 whenAst: whenNot(whenIdentifier("terminalFocus")),
148 },
149 {
150 shortcut: modShortcut("f", { shiftKey: true }),
151 command: "projectSearch.toggle",
152 whenAst: whenNot(whenIdentifier("terminalFocus")),
153 },
154 {
155 shortcut: modShortcut("t", { altKey: true, shiftKey: true }),
156 command: "themeEditor.toggle",
157 },
158 {
159 shortcut: modShortcut("f"),
160 command: "chat.find",
161 whenAst: whenAnd(
162 whenNot(whenIdentifier("terminalFocus")),
163 whenNot(whenIdentifier("previewFocus")),
164 ),
165 },
166 {
167 shortcut: modShortcut("m", { shiftKey: true }),
168 command: "modelPicker.toggle",
169 whenAst: whenNot(whenIdentifier("terminalFocus")),
170 },
171 { shortcut: modShortcut("o", { shiftKey: true }), command: "chat.new" },
172 { shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newLocal" },
173 { shortcut: modShortcut("o"), command: "editor.openFavorite" },
174 { shortcut: modShortcut("[", { shiftKey: true }), command: "thread.previous" },
175 { shortcut: modShortcut("]", { shiftKey: true }), command: "thread.next" },
176 {
177 shortcut: modShortcut("c", { shiftKey: true }),
178 command: "thread.copyReference",
179 whenAst: whenNot(whenIdentifier("terminalFocus")),
180 },
181 {
182 shortcut: modShortcut("s", { shiftKey: true }),
183 command: "thread.settle",
184 whenAst: whenNot(whenIdentifier("terminalFocus")),
185 },
186 { shortcut: modShortcut("1"), command: "thread.jump.1" },
187 { shortcut: modShortcut("2"), command: "thread.jump.2" },
188 { shortcut: modShortcut("3"), command: "thread.jump.3" },
189 {
190 shortcut: modShortcut("1"),
191 command: "modelPicker.jump.1",
192 whenAst: whenIdentifier("modelPickerOpen"),
193 },
194 {
195 shortcut: modShortcut("2"),
196 command: "modelPicker.jump.2",
197 whenAst: whenIdentifier("modelPickerOpen"),
198 },
199 {
200 shortcut: modShortcut("3"),
201 command: "modelPicker.jump.3",
202 whenAst: whenIdentifier("modelPickerOpen"),
203 },
204]);
205​
206describe("effectiveShortcutsForCommand", () => {
207 it("passes only effective preview shortcuts to the desktop bridge", () => {
208 const reopen = modShortcut("t", { shiftKey: true });
209 const second = modShortcut("y", { shiftKey: true });
210 const keybindings = compile([
211 { shortcut: reopen, command: "view.reopenClosed" },
212 {
213 shortcut: second,
214 command: "view.reopenClosed",
215 whenAst: whenIdentifier("previewFocus"),
216 },
217 {
218 shortcut: reopen,
219 command: "preview.toggle",
220 whenAst: whenIdentifier("previewFocus"),
221 },
222 ]);
223 assert.deepEqual(
224 effectiveShortcutsForCommand(keybindings, "view.reopenClosed", {
225 platform: "MacIntel",
226 context: { previewFocus: true, previewOpen: true },
227 }),
228 [second],
229 );
230 assert.deepEqual(
231 effectiveShortcutsForCommand(keybindings, "view.reopenClosed", {
232 platform: "MacIntel",
233 context: { previewFocus: false },
234 }),
235 [reopen],
236 );
237 });
238});
239​
240describe("settle thread shortcut", () => {
241 it("does not intercept the terminal", () => {
242 assert.isNull(
243 resolveShortcutCommand(event({ key: "s", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
244 platform: "Win32",
245 context: { terminalFocus: true },
246 }),
247 );
248 });
249});
250​
251describe("thread undo shortcut", () => {
252 it("leaves native undo alone inside text fields and terminals", () => {
253 assert.isNull(
254 resolveShortcutCommand(event({ key: "z", ctrlKey: true }), DEFAULT_RESOLVED_KEYBINDINGS, {
255 platform: "Win32",
256 context: { editableFocus: true },
257 }),
258 );
259 assert.isNull(
260 resolveShortcutCommand(event({ key: "z", ctrlKey: true }), DEFAULT_RESOLVED_KEYBINDINGS, {
261 platform: "Win32",
262 context: { terminalFocus: true },
263 }),
264 );
265 });
266});
267​
268describe("split/new/close terminal shortcuts", () => {
269 it("supports when expressions", () => {
270 const keybindings = compile([
271 {
272 shortcut: modShortcut("\\"),
273 command: "terminal.split",
274 whenAst: whenAnd(whenIdentifier("terminalOpen"), whenNot(whenIdentifier("terminalFocus"))),
275 },
276 {
277 shortcut: modShortcut("n", { shiftKey: true }),
278 command: "terminal.new",
279 whenAst: whenAnd(whenIdentifier("terminalOpen"), whenNot(whenIdentifier("terminalFocus"))),
280 },
281 { shortcut: modShortcut("j"), command: "terminal.toggle" },
282 ]);
283 assert.isTrue(
284 isTerminalSplitShortcut(event({ key: "\\", ctrlKey: true }), keybindings, {
285 platform: "Win32",
286 context: { terminalOpen: true, terminalFocus: false },
287 }),
288 );
289 assert.isFalse(
290 isTerminalSplitShortcut(event({ key: "\\", ctrlKey: true }), keybindings, {
291 platform: "Win32",
292 context: { terminalOpen: false, terminalFocus: false },
293 }),
294 );
295 assert.isTrue(
296 isTerminalNewShortcut(event({ key: "n", ctrlKey: true, shiftKey: true }), keybindings, {
297 platform: "Win32",
298 context: { terminalOpen: true, terminalFocus: false },
299 }),
300 );
301 });
302});
303​
304describe("shortcutLabelForCommand", () => {
305 it("returns null for commands shadowed by a later conflicting shortcut", () => {
306 const bindings = compile([
307 { shortcut: modShortcut("1", { shiftKey: true }), command: "thread.jump.1" },
308 { shortcut: modShortcut("1", { shiftKey: true }), command: "thread.jump.7" },
309 ]);
310​
311 assert.isNull(shortcutLabelForCommand(bindings, "thread.jump.1", "MacIntel"));
312 assert.strictEqual(shortcutLabelForCommand(bindings, "thread.jump.7", "MacIntel"), "⇧⌘1");
313 });
314​
315 it("respects when-context while resolving labels", () => {
316 const bindings = compile([
317 { shortcut: modShortcut("d"), command: "diff.toggle" },
318 {
319 shortcut: modShortcut("d"),
320 command: "terminal.split",
321 whenAst: whenIdentifier("terminalFocus"),
322 },
323 ]);
324​
325 assert.strictEqual(
326 shortcutLabelForCommand(bindings, "diff.toggle", {
327 platform: "Linux",
328 context: { terminalFocus: false },
329 }),
330 "Ctrl+D",
331 );
332 assert.isNull(
333 shortcutLabelForCommand(bindings, "diff.toggle", {
334 platform: "Linux",
335 context: { terminalFocus: true },
336 }),
337 );
338 assert.strictEqual(
339 shortcutLabelForCommand(bindings, "terminal.split", {
340 platform: "Linux",
341 context: { terminalFocus: true },
342 }),
343 "Ctrl+D",
344 );
345 });
346});
347​
348describe("thread navigation helpers", () => {
349 it("never shows jump hints while the terminal is focused, even with an unrestricted binding", () => {
350 assert.isFalse(
351 shouldShowThreadJumpHintsForModifiers(event({ metaKey: true }), DEFAULT_BINDINGS, {
352 platform: "MacIntel",
353 context: { terminalFocus: true },
354 }),
355 );
356 assert.isTrue(
357 shouldShowThreadJumpHintsForModifiers(event({ metaKey: true }), DEFAULT_BINDINGS, {
358 platform: "MacIntel",
359 context: { terminalFocus: false },
360 }),
361 );
362 });
363​
364 it("keeps default thread jumps off the web so the browser can switch tabs", () => {
365 const input = event({ key: "1", metaKey: true });
366 assert.isNull(
367 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
368 platform: "MacIntel",
369 context: { isDesktop: false },
370 }),
371 );
372 assert.strictEqual(
373 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
374 platform: "MacIntel",
375 context: { isDesktop: true },
376 }),
377 "thread.jump.1",
378 );
379 assert.isFalse(
380 shouldShowThreadJumpHintsForModifiers(
381 event({ metaKey: true }),
382 DEFAULT_RESOLVED_KEYBINDINGS,
383 {
384 platform: "MacIntel",
385 context: { isDesktop: false },
386 },
387 ),
388 );
389 assert.isTrue(
390 shouldShowThreadJumpHintsForModifiers(
391 event({ metaKey: true }),
392 DEFAULT_RESOLVED_KEYBINDINGS,
393 {
394 platform: "MacIntel",
395 context: { isDesktop: true },
396 },
397 ),
398 );
399 });
400});
401​
402describe("model picker navigation helpers", () => {
403 it("keeps default model jumps off the web even while the picker is open", () => {
404 const input = event({ key: "3", metaKey: true });
405 assert.isNull(
406 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
407 platform: "MacIntel",
408 context: { isDesktop: false, modelPickerOpen: true },
409 }),
410 );
411 assert.strictEqual(
412 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
413 platform: "MacIntel",
414 context: { isDesktop: true, modelPickerOpen: true },
415 }),
416 "modelPicker.jump.3",
417 );
418 assert.strictEqual(
419 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
420 platform: "MacIntel",
421 context: { isDesktop: true, modelPickerOpen: false },
422 }),
423 "thread.jump.3",
424 );
425 });
426});
427​
428describe("chat/editor shortcuts", () => {
429 it("toggles the thread panel by default without taking terminal keystrokes", () => {
430 for (const platform of ["MacIntel", "Win32", "Linux"]) {
431 const input = event({
432 key: "b",
433 metaKey: platform === "MacIntel",
434 ctrlKey: platform !== "MacIntel",
435 shiftKey: true,
436 });
437 assert.strictEqual(
438 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
439 platform,
440 context: { terminalFocus: false, editableFocus: true },
441 }),
442 "threadPanel.toggle",
443 );
444 assert.isNull(
445 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
446 platform,
447 context: { terminalFocus: true },
448 }),
449 );
450 }
451 });
452​
453 it("preserves a user-configured thread panel shortcut instead of its default", () => {
454 const bindings = mergeWithDefaultKeybindings(
455 compile([
456 {
457 shortcut: modShortcut("b", { altKey: true, shiftKey: true }),
458 command: "threadPanel.toggle",
459 },
460 ]),
461 );
462​
463 assert.strictEqual(
464 resolveShortcutCommand(
465 event({ key: "b", metaKey: true, altKey: true, shiftKey: true }),
466 bindings,
467 { platform: "MacIntel" },
468 ),
469 "threadPanel.toggle",
470 );
471 assert.isNull(
472 resolveShortcutCommand(event({ key: "b", metaKey: true, shiftKey: true }), bindings, {
473 platform: "MacIntel",
474 }),
475 );
476 });
477});
478​
479describe("cross-command precedence", () => {
480 it("uses when + order so a later focused rule overrides a global rule", () => {
481 const keybindings = compile([
482 { shortcut: modShortcut("n"), command: "chat.new" },
483 {
484 shortcut: modShortcut("n"),
485 command: "terminal.new",
486 whenAst: whenIdentifier("terminalFocus"),
487 },
488 ]);
489​
490 assert.isTrue(
491 isTerminalNewShortcut(event({ key: "n", metaKey: true }), keybindings, {
492 platform: "MacIntel",
493 context: { terminalFocus: true },
494 }),
495 );
496 assert.strictEqual(
497 resolveShortcutCommand(event({ key: "n", metaKey: true }), keybindings, {
498 platform: "MacIntel",
499 context: { terminalFocus: true },
500 }),
501 "terminal.new",
502 );
503 assert.isFalse(
504 isTerminalNewShortcut(event({ key: "n", metaKey: true }), keybindings, {
505 platform: "MacIntel",
506 context: { terminalFocus: false },
507 }),
508 );
509 assert.strictEqual(
510 resolveShortcutCommand(event({ key: "n", metaKey: true }), keybindings, {
511 platform: "MacIntel",
512 context: { terminalFocus: false },
513 }),
514 "chat.new",
515 );
516 });
517​
518 it("still lets a later global rule win when both rules match", () => {
519 const keybindings = compile([
520 {
521 shortcut: modShortcut("n"),
522 command: "terminal.new",
523 whenAst: whenIdentifier("terminalFocus"),
524 },
525 { shortcut: modShortcut("n"), command: "chat.new" },
526 ]);
527​
528 assert.isFalse(
529 isTerminalNewShortcut(event({ key: "n", ctrlKey: true }), keybindings, {
530 platform: "Linux",
531 context: { terminalFocus: true },
532 }),
533 );
534 assert.strictEqual(
535 resolveShortcutCommand(event({ key: "n", ctrlKey: true }), keybindings, {
536 platform: "Linux",
537 context: { terminalFocus: true },
538 }),
539 "chat.new",
540 );
541 });
542});
543​
544describe("resolveChatShortcutCommand", () => {
545 it("runs the active environment's script shortcut instead of the primary environment's", () => {
546 const primary = compile([{ shortcut: modShortcut("r"), command: "script.setup.run" }]);
547 const active = compile([{ shortcut: modShortcut("t"), command: "script.setup.run" }]);
548​
549 assert.strictEqual(
550 resolveChatShortcutCommand(event({ key: "t", ctrlKey: true }), primary, active, {
551 platform: "Linux",
552 }),
553 "script.setup.run",
554 );
555 assert.isNull(
556 resolveChatShortcutCommand(event({ key: "r", ctrlKey: true }), primary, active, {
557 platform: "Linux",
558 }),
559 );
560 });
561​
562 it("does not run primary scripts before the active environment's bindings arrive", () => {
563 const primary = compile([{ shortcut: modShortcut("r"), command: "script.setup.run" }]);
564​
565 assert.isNull(
566 resolveChatShortcutCommand(event({ key: "r", ctrlKey: true }), primary, [], {
567 platform: "Linux",
568 }),
569 );
570 });
571​
572 it("keeps primary app commands ahead of conflicting active script shortcuts", () => {
573 const primary = compile([{ shortcut: modShortcut("k"), command: "commandPalette.toggle" }]);
574 const active = compile([{ shortcut: modShortcut("k"), command: "script.setup.run" }]);
575​
576 assert.strictEqual(
577 resolveChatShortcutCommand(event({ key: "k", ctrlKey: true }), primary, active, {
578 platform: "Linux",
579 }),
580 "commandPalette.toggle",
581 );
582 });
583​
584 it("keeps app shortcuts on the primary environment when the active environment remaps them", () => {
585 const primary = compile([{ shortcut: modShortcut("j"), command: "terminal.toggle" }]);
586 const active = compile([{ shortcut: modShortcut("t"), command: "terminal.toggle" }]);
587​
588 assert.strictEqual(
589 resolveChatShortcutCommand(event({ key: "j", ctrlKey: true }), primary, active, {
590 platform: "Linux",
591 }),
592 "terminal.toggle",
593 );
594 assert.isNull(
595 resolveChatShortcutCommand(event({ key: "t", ctrlKey: true }), primary, active, {
596 platform: "Linux",
597 }),
598 );
599 });
600​
601 it.each([false, true])(
602 "preserves active binding precedence with terminalFocus=%s",
603 (terminalFocus) => {
604 const primary = compile([{ shortcut: modShortcut("r"), command: "script.setup.run" }]);
605 const active = compile([
606 { shortcut: modShortcut("r"), command: "script.deploy.run" },
607 {
608 shortcut: modShortcut("r"),
609 command: "terminal.new",
610 whenAst: whenIdentifier("terminalFocus"),
611 },
612 ]);
613 const options = { platform: "Linux", context: { terminalFocus } };
614 const shortcut = event({ key: "r", ctrlKey: true });
615​
616 assert.strictEqual(
617 resolveChatShortcutCommand(shortcut, primary, active, options),
618 terminalFocus ? null : "script.deploy.run",
619 );
620 assert.strictEqual(
621 resolveChatShortcutCommand(shortcut, active, active, options),
622 resolveShortcutCommand(shortcut, active, options),
623 );
624 },
625 );
626});
627​
628describe("resolveShortcutCommand", () => {
629 it("routes mod+w to the terminal while focused and to the right panel otherwise", () => {
630 const closeEvent = event({ key: "w", metaKey: true });
631 assert.strictEqual(
632 resolveShortcutCommand(closeEvent, DEFAULT_BINDINGS, {
633 platform: "MacIntel",
634 context: { terminalFocus: true },
635 }),
636 "terminal.close",
637 );
638 assert.strictEqual(
639 resolveShortcutCommand(closeEvent, DEFAULT_BINDINGS, {
640 platform: "MacIntel",
641 context: { terminalFocus: false },
642 }),
643 "rightPanel.close",
644 );
645 });
646​
647 it("navigates history with mod+[ and mod+] outside the terminal", () => {
648 const back = event({ key: "[", code: "BracketLeft", metaKey: true });
649 const forward = event({ key: "]", code: "BracketRight", ctrlKey: true });
650 assert.strictEqual(
651 resolveShortcutCommand(back, DEFAULT_RESOLVED_KEYBINDINGS, { platform: "MacIntel" }),
652 "navigation.back",
653 );
654 assert.strictEqual(
655 resolveShortcutCommand(forward, DEFAULT_RESOLVED_KEYBINDINGS, { platform: "Linux" }),
656 "navigation.forward",
657 );
658 assert.isNull(
659 resolveShortcutCommand(back, DEFAULT_RESOLVED_KEYBINDINGS, {
660 platform: "MacIntel",
661 context: { terminalFocus: true },
662 }),
663 );
664 });
665​
666 it("matches punctuation shortcuts by physical key across keyboard layouts", () => {
667 const keybindings = compile([
668 { shortcut: modShortcut("'", { shiftKey: true }), command: "diff.toggle" },
669 ]);
670​
671 assert.strictEqual(
672 resolveShortcutCommand(
673 event({ key: "@", code: "Quote", metaKey: true, shiftKey: true }),
674 keybindings,
675 { platform: "MacIntel" },
676 ),
677 "diff.toggle",
678 );
679 assert.isNull(
680 resolveShortcutCommand(
681 event({ key: '"', code: "Digit2", metaKey: true, shiftKey: true }),
682 keybindings,
683 { platform: "MacIntel" },
684 ),
685 );
686 });
687​
688 it('matches Ctrl+layout "+" as itself unless Shift, Alt or the number row is involved', () => {
689 assert.isNull(
690 resolveShortcutCommand(
691 event({ key: "+", code: "BracketRight", ctrlKey: true }),
692 DEFAULT_RESOLVED_KEYBINDINGS,
693 { platform: "Linux", context: { terminalFocus: false } },
694 ),
695 );
696 assert.strictEqual(
697 resolveShortcutCommand(
698 event({ key: "+", code: "Minus", ctrlKey: true }),
699 DEFAULT_RESOLVED_KEYBINDINGS,
700 { platform: "Linux", context: { previewFocus: true } },
701 ),
702 "preview.zoomIn",
703 );
704 assert.strictEqual(
705 resolveShortcutCommand(
706 event({ key: "+", code: "Digit1", ctrlKey: true }),
707 DEFAULT_RESOLVED_KEYBINDINGS,
708 { platform: "Linux", context: { isDesktop: true } },
709 ),
710 "thread.jump.1",
711 );
712 assert.strictEqual(
713 resolveShortcutCommand(
714 event({ key: "+", code: "BracketRight", altKey: true }),
715 compile([
716 {
717 shortcut: modShortcut("]", { modKey: false, altKey: true }),
718 command: "terminal.toggle",
719 },
720 ]),
721 { platform: "Linux" },
722 ),
723 "terminal.toggle",
724 );
725 assert.strictEqual(
726 resolveShortcutCommand(
727 event({ key: "+", code: "Equal", ctrlKey: true, shiftKey: true }),
728 compile([{ shortcut: modShortcut("=", { shiftKey: true }), command: "terminal.toggle" }]),
729 { platform: "Linux" },
730 ),
731 "terminal.toggle",
732 );
733 });
734​
735 it("does not let a punctuation position shadow a Latin layout key", () => {
736 const keybindings = compile([
737 { shortcut: modShortcut("m"), command: "diff.toggle" },
738 { shortcut: modShortcut(";"), command: "sidebar.toggle" },
739 ]);
740​
741 assert.strictEqual(
742 resolveShortcutCommand(event({ key: "m", code: "Semicolon", metaKey: true }), keybindings, {
743 platform: "MacIntel",
744 }),
745 "diff.toggle",
746 );
747 });
748​
749 it("matches Option-modified letters using the physical key code on macOS", () => {
750 assert.strictEqual(
751 resolveShortcutCommand(
752 event({ key: "∫", code: "KeyB", metaKey: true, altKey: true }),
753 DEFAULT_BINDINGS,
754 { platform: "MacIntel" },
755 ),
756 "rightPanel.toggle",
757 );
758 });
759​
760 it("matches non-Latin layout letters using the physical key code", () => {
761 const keybindings = compile([{ shortcut: modShortcut("d"), command: "diff.toggle" }]);
762​
763 assert.strictEqual(
764 resolveShortcutCommand(event({ key: "в", code: "KeyD", metaKey: true }), keybindings, {
765 platform: "MacIntel",
766 }),
767 "diff.toggle",
768 );
769 });
770​
771 it("ignores the physical key code when the layout types a different Latin letter", () => {
772 const keybindings = compile([{ shortcut: modShortcut("d"), command: "diff.toggle" }]);
773​
774 assert.isNull(
775 resolveShortcutCommand(event({ key: "a", code: "KeyD", metaKey: true }), keybindings, {
776 platform: "MacIntel",
777 }),
778 );
779 assert.strictEqual(
780 resolveShortcutCommand(event({ key: "d", code: "KeyL", metaKey: true }), keybindings, {
781 platform: "MacIntel",
782 }),
783 "diff.toggle",
784 );
785 });
786});
787​
788describe("isRichTextBoldShortcut", () => {
789 const selected = { isCollapsed: false };
790​
791 it("matches the B key on non-Latin layouts, like the sidebar toggle does", () => {
792 const cyrillicB = event({ key: "и", code: "KeyB", ctrlKey: true });
793 assert.isTrue(isRichTextBoldShortcut(cyrillicB, selected));
794 assert.strictEqual(
795 resolveShortcutCommand(cyrillicB, DEFAULT_BINDINGS, { platform: "Win32" }),
796 "sidebar.toggle",
797 );
798 });
799​
800 it("follows the letter a Latin layout types, not the physical key", () => {
801 assert.isFalse(
802 isRichTextBoldShortcut(event({ key: "x", code: "KeyB", ctrlKey: true }), selected),
803 );
804 assert.isTrue(
805 isRichTextBoldShortcut(event({ key: "b", code: "KeyN", ctrlKey: true }), selected),
806 );
807 });
808​
809 it("leaves the chord to the sidebar toggle when no text is selected", () => {
810 const modB = event({ key: "b", metaKey: true });
811 assert.isFalse(isRichTextBoldShortcut(modB, { isCollapsed: true }));
812 assert.isFalse(isRichTextBoldShortcut(modB, null));
813 });
814});
815​
816describe("composer and pull request shortcuts", () => {
817 it("uses the dictation chord outside the terminal and preserves terminal split", () => {
818 const chord = event({ key: "d", ctrlKey: true, shiftKey: true });
819 assert.strictEqual(
820 resolveShortcutCommand(chord, DEFAULT_RESOLVED_KEYBINDINGS, {
821 platform: "Linux",
822 context: { terminalFocus: false },
823 }),
824 "composer.dictation",
825 );
826 assert.strictEqual(
827 resolveShortcutCommand(chord, DEFAULT_RESOLVED_KEYBINDINGS, {
828 platform: "Linux",
829 context: { terminalFocus: true },
830 }),
831 "terminal.splitVertical",
832 );
833 });
834 it("fills missing number shortcuts without replacing the saved URL binding", () => {
835 const olderServerBindings = DEFAULT_RESOLVED_KEYBINDINGS.filter(
836 (binding) =>
837 binding.command !== "pullRequest.copyNumber" && binding.command !== "thread.copyReference",
838 );
839 const bindings = mergeWithDefaultKeybindings([
840 ...olderServerBindings,
841 ...compileResolvedKeybindingsConfig([
842 { key: "mod+shift+8", command: "thread.copyReference", when: "!terminalFocus" },
843 ]),
844 ]);
845 for (const [key, command] of [
846 ["k", "pullRequest.copyNumber"],
847 ["8", "thread.copyReference"],
848 ["c", null],
849 ["y", null],
850 ] as const) {
851 assert.strictEqual(
852 resolveShortcutCommand(event({ key, metaKey: true, shiftKey: true }), bindings, {
853 platform: "MacIntel",
854 }),
855 command,
856 );
857 }
858 });
859​
860 it.each(["MacIntel", "Win32", "Linux"])(
861 "reserves the alternate send chord for the focused composer on %s",
862 (platform) => {
863 const modifier = {
864 metaKey: platform === "MacIntel",
865 ctrlKey: platform !== "MacIntel",
866 };
867 const shiftedEnter = event({ key: "Enter", shiftKey: true, ...modifier });
868 for (const composerRequiresModifier of [false, true]) {
869 const context = {
870 composerFocus: true,
871 turnRunning: true,
872 composerRequiresModifier,
873 };
874 assert.strictEqual(
875 resolveShortcutCommand(shiftedEnter, DEFAULT_RESOLVED_KEYBINDINGS, {
876 platform,
877 context,
878 }),
879 composerRequiresModifier ? "composer.sendAlternate" : "thread.steerQueuedMessage",
880 );
881 assert.strictEqual(
882 shortcutLabelForCommand(DEFAULT_RESOLVED_KEYBINDINGS, "composer.sendAlternate", {
883 platform,
884 context,
885 }),
886 formatShortcutLabel(
887 modShortcut("enter", { shiftKey: composerRequiresModifier }),
888 platform,
889 ),
890 );
891 assert.strictEqual(
892 resolveShortcutCommand(shiftedEnter, DEFAULT_RESOLVED_KEYBINDINGS, {
893 platform,
894 context: { ...context, composerFocus: false },
895 }),
896 "thread.steerQueuedMessage",
897 );
898 }
899 },
900 );
901​
902 const altEffortBindings = compileResolvedKeybindingsConfig([
903 { key: "mod+alt+e", command: "composer.effort", when: "!terminalFocus" },
904 ]);
905​
906 it("leaves AltGr text entry alone with a custom Alt binding", () => {
907 for (const platform of ["Win32", "Linux"]) {
908 const input = event({
909 key: "€",
910 code: "KeyE",
911 ctrlKey: true,
912 altKey: true,
913 getModifierState: (key) => key === "AltGraph",
914 });
915 assert.isNull(resolveShortcutCommand(input, altEffortBindings, { platform }));
916 assert.strictEqual(
917 resolveShortcutCommand({ ...input, getModifierState: () => false }, altEffortBindings, {
918 platform,
919 }),
920 "composer.effort",
921 );
922 }
923 });
924​
925 it("keeps Firefox modifier reporting usable on Windows and macOS", () => {
926 const getModifierState = (key: string) => key === "AltGraph";
927 assert.strictEqual(
928 resolveShortcutCommand(
929 event({ key: "e", ctrlKey: true, altKey: true, getModifierState }),
930 altEffortBindings,
931 { platform: "Win32" },
932 ),
933 "composer.effort",
934 );
935 assert.strictEqual(
936 resolveShortcutCommand(
937 event({ key: "´", code: "KeyE", metaKey: true, altKey: true, getModifierState }),
938 altEffortBindings,
939 { platform: "MacIntel" },
940 ),
941 "composer.effort",
942 );
943 });
944});
945​
946describe("Usage shortcuts", () => {
947 it.each(["Linux", "MacIntel"])(
948 "preserves desktop numbered thread shortcuts on Usage on %s",
949 (platform) => {
950 const shortcut = event({
951 key: "2",
952 ctrlKey: platform === "Linux",
953 metaKey: platform === "MacIntel",
954 });
955 assert.strictEqual(
956 resolveShortcutCommand(shortcut, DEFAULT_RESOLVED_KEYBINDINGS, {
957 platform,
958 context: { usagePageOpen: true, isDesktop: true },
959 }),
960 "thread.jump.2",
961 );
962 assert.isNotNull(
963 shortcutLabelForCommand(DEFAULT_RESOLVED_KEYBINDINGS, "thread.jump.2", {
964 platform,
965 context: { usagePageOpen: true, isDesktop: true },
966 }),
967 );
968 },
969 );
970​
971 it("matches shifted number keys for periods and only on Usage", () => {
972 const shortcut = event({ key: "!", code: "Digit1", ctrlKey: true, shiftKey: true });
973 assert.strictEqual(
974 resolveShortcutCommand(shortcut, DEFAULT_RESOLVED_KEYBINDINGS, {
975 platform: "Linux",
976 context: { usagePageOpen: true },
977 }),
978 "usage.period.day",
979 );
980 assert.isNull(
981 resolveShortcutCommand(shortcut, DEFAULT_RESOLVED_KEYBINDINGS, {
982 platform: "Linux",
983 }),
984 );
985 });
986});