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 isDiffToggleShortcut,
18 isRichTextBoldShortcut,
19 modelPickerJumpCommandForIndex,
20 modelPickerJumpIndexFromCommand,
21 isOpenFavoriteEditorShortcut,
22 isTerminalClearShortcut,
23 isTerminalCloseShortcut,
24 isTerminalNewShortcut,
25 isTerminalSplitShortcut,
26 isTerminalSplitVerticalShortcut,
27 isTerminalToggleShortcut,
28 resolveChatShortcutCommand,
29 resolveShortcutCommand,
30 shouldShowThreadJumpHintsForModifiers,
31 shortcutLabelForCommand,
32 terminalDeleteShortcutData,
33 terminalNavigationShortcutData,
34 threadJumpCommandForIndex,
35 threadJumpIndexFromCommand,
36 threadTraversalDirectionFromCommand,
37 type ShortcutEventLike,
38} from "./keybindings";
39
40function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike {
41 return {
42 key: "j",
43 metaKey: false,
44 ctrlKey: false,
45 shiftKey: false,
46 altKey: false,
47 ...overrides,
48 };
49}
50
51function modShortcut(
52 key: string,
53 overrides: Partial<Omit<KeybindingShortcut, "key">> = {},
54): KeybindingShortcut {
55 return {
56 key,
57 metaKey: false,
58 ctrlKey: false,
59 shiftKey: false,
60 altKey: false,
61 modKey: true,
62 ...overrides,
63 };
64}
65
66function whenIdentifier(name: string): KeybindingWhenNode {
67 return { type: "identifier", name };
68}
69
70function whenNot(node: KeybindingWhenNode): KeybindingWhenNode {
71 return { type: "not", node };
72}
73
74function whenAnd(left: KeybindingWhenNode, right: KeybindingWhenNode): KeybindingWhenNode {
75 return { type: "and", left, right };
76}
77
78interface TestBinding {
79 shortcut: KeybindingShortcut;
80 command: KeybindingCommand;
81 whenAst?: KeybindingWhenNode;
82}
83
84function compile(bindings: TestBinding[]): ResolvedKeybindingsConfig {
85 return bindings.map((binding) => ({
86 command: binding.command,
87 shortcut: binding.shortcut,
88 ...(binding.whenAst ? { whenAst: binding.whenAst } : {}),
89 }));
90}
91
92const DEFAULT_BINDINGS = compile([
93 { shortcut: modShortcut("b"), command: "sidebar.toggle" },
94 { shortcut: modShortcut("j"), command: "terminal.toggle" },
95 { shortcut: modShortcut("b", { altKey: true }), command: "rightPanel.toggle" },
96 {
97 shortcut: modShortcut("d"),
98 command: "terminal.split",
99 whenAst: whenIdentifier("terminalFocus"),
100 },
101 {
102 shortcut: modShortcut("d", { shiftKey: true }),
103 command: "terminal.splitVertical",
104 whenAst: whenIdentifier("terminalFocus"),
105 },
106 {
107 shortcut: modShortcut("n"),
108 command: "terminal.new",
109 whenAst: whenIdentifier("terminalFocus"),
110 },
111 {
112 shortcut: modShortcut("w"),
113 command: "terminal.close",
114 whenAst: whenIdentifier("terminalFocus"),
115 },
116 {
117 shortcut: modShortcut("w"),
118 command: "rightPanel.close",
119 whenAst: whenNot(whenIdentifier("terminalFocus")),
120 },
121 {
122 shortcut: modShortcut("d"),
123 command: "diff.toggle",
124 whenAst: whenNot(whenIdentifier("terminalFocus")),
125 },
126 {
127 shortcut: modShortcut("k"),
128 command: "commandPalette.toggle",
129 whenAst: whenNot(whenIdentifier("terminalFocus")),
130 },
131 {
132 shortcut: modShortcut("p"),
133 command: "filePicker.toggle",
134 whenAst: whenNot(whenIdentifier("terminalFocus")),
135 },
136 {
137 shortcut: modShortcut("f", { shiftKey: true }),
138 command: "projectSearch.toggle",
139 whenAst: whenNot(whenIdentifier("terminalFocus")),
140 },
141 {
142 shortcut: modShortcut("t", { altKey: true, shiftKey: true }),
143 command: "themeEditor.toggle",
144 },
145 {
146 shortcut: modShortcut("f"),
147 command: "chat.find",
148 whenAst: whenAnd(
149 whenNot(whenIdentifier("terminalFocus")),
150 whenNot(whenIdentifier("previewFocus")),
151 ),
152 },
153 {
154 shortcut: modShortcut("m", { shiftKey: true }),
155 command: "modelPicker.toggle",
156 whenAst: whenNot(whenIdentifier("terminalFocus")),
157 },
158 { shortcut: modShortcut("o", { shiftKey: true }), command: "chat.new" },
159 { shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newLocal" },
160 { shortcut: modShortcut("o"), command: "editor.openFavorite" },
161 { shortcut: modShortcut("[", { shiftKey: true }), command: "thread.previous" },
162 { shortcut: modShortcut("]", { shiftKey: true }), command: "thread.next" },
163 {
164 shortcut: modShortcut("c", { shiftKey: true }),
165 command: "thread.copyReference",
166 whenAst: whenNot(whenIdentifier("terminalFocus")),
167 },
168 {
169 shortcut: modShortcut("s", { shiftKey: true }),
170 command: "thread.settle",
171 whenAst: whenNot(whenIdentifier("terminalFocus")),
172 },
173 { shortcut: modShortcut("1"), command: "thread.jump.1" },
174 { shortcut: modShortcut("2"), command: "thread.jump.2" },
175 { shortcut: modShortcut("3"), command: "thread.jump.3" },
176 {
177 shortcut: modShortcut("1"),
178 command: "modelPicker.jump.1",
179 whenAst: whenIdentifier("modelPickerOpen"),
180 },
181 {
182 shortcut: modShortcut("2"),
183 command: "modelPicker.jump.2",
184 whenAst: whenIdentifier("modelPickerOpen"),
185 },
186 {
187 shortcut: modShortcut("3"),
188 command: "modelPicker.jump.3",
189 whenAst: whenIdentifier("modelPickerOpen"),
190 },
191]);
192
193describe("effectiveShortcutsForCommand", () => {
194 it("passes only effective preview shortcuts to the desktop bridge", () => {
195 const reopen = modShortcut("t", { shiftKey: true });
196 const second = modShortcut("y", { shiftKey: true });
197 const keybindings = compile([
198 { shortcut: reopen, command: "view.reopenClosed" },
199 {
200 shortcut: second,
201 command: "view.reopenClosed",
202 whenAst: whenIdentifier("previewFocus"),
203 },
204 {
205 shortcut: reopen,
206 command: "preview.toggle",
207 whenAst: whenIdentifier("previewFocus"),
208 },
209 ]);
210 assert.deepEqual(
211 effectiveShortcutsForCommand(keybindings, "view.reopenClosed", {
212 platform: "MacIntel",
213 context: { previewFocus: true, previewOpen: true },
214 }),
215 [second],
216 );
217 assert.deepEqual(
218 effectiveShortcutsForCommand(keybindings, "view.reopenClosed", {
219 platform: "MacIntel",
220 context: { previewFocus: false },
221 }),
222 [reopen],
223 );
224 });
225});
226
227describe("isTerminalToggleShortcut", () => {
228 it("matches Cmd+J on macOS", () => {
229 assert.isTrue(
230 isTerminalToggleShortcut(event({ metaKey: true }), DEFAULT_BINDINGS, {
231 platform: "MacIntel",
232 }),
233 );
234 });
235
236 it("matches Ctrl+J on non-macOS", () => {
237 assert.isTrue(
238 isTerminalToggleShortcut(event({ ctrlKey: true }), DEFAULT_BINDINGS, { platform: "Win32" }),
239 );
240 });
241
242 it("matches Ctrl+J on non-macOS while terminalFocus is true", () => {
243 assert.isTrue(
244 isTerminalToggleShortcut(event({ ctrlKey: true }), DEFAULT_BINDINGS, {
245 platform: "Win32",
246 context: { terminalFocus: true },
247 }),
248 );
249 });
250});
251
252describe("settle thread shortcut", () => {
253 it("resolves outside the terminal", () => {
254 assert.equal(
255 resolveShortcutCommand(event({ key: "s", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
256 platform: "MacIntel",
257 context: { terminalFocus: false },
258 }),
259 "thread.settle",
260 );
261 });
262
263 it("does not intercept the terminal", () => {
264 assert.isNull(
265 resolveShortcutCommand(event({ key: "s", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
266 platform: "Win32",
267 context: { terminalFocus: true },
268 }),
269 );
270 });
271});
272
273describe("thread undo shortcut", () => {
274 it("resolves mod+z with nothing editable focused", () => {
275 assert.equal(
276 resolveShortcutCommand(event({ key: "z", metaKey: true }), DEFAULT_RESOLVED_KEYBINDINGS, {
277 platform: "MacIntel",
278 context: { terminalFocus: false, editableFocus: false },
279 }),
280 "thread.undo",
281 );
282 });
283
284 it("leaves native undo alone inside text fields and terminals", () => {
285 assert.isNull(
286 resolveShortcutCommand(event({ key: "z", ctrlKey: true }), DEFAULT_RESOLVED_KEYBINDINGS, {
287 platform: "Win32",
288 context: { editableFocus: true },
289 }),
290 );
291 assert.isNull(
292 resolveShortcutCommand(event({ key: "z", ctrlKey: true }), DEFAULT_RESOLVED_KEYBINDINGS, {
293 platform: "Win32",
294 context: { terminalFocus: true },
295 }),
296 );
297 });
298});
299
300describe("copy thread reference shortcut", () => {
301 it("resolves Cmd+Shift+C on macOS and Ctrl+Shift+C elsewhere", () => {
302 assert.equal(
303 resolveShortcutCommand(event({ key: "c", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
304 platform: "MacIntel",
305 }),
306 "thread.copyReference",
307 );
308 assert.equal(
309 resolveShortcutCommand(event({ key: "c", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
310 platform: "Linux",
311 }),
312 "thread.copyReference",
313 );
314 });
315
316 it("leaves terminal copy untouched", () => {
317 assert.isNull(
318 resolveShortcutCommand(event({ key: "c", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
319 platform: "Linux",
320 context: { terminalFocus: true },
321 }),
322 );
323 });
324});
325
326describe("split/new/close terminal shortcuts", () => {
327 it("requires terminalFocus for default split/new/close bindings", () => {
328 assert.isFalse(
329 isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, {
330 platform: "MacIntel",
331 context: { terminalFocus: false },
332 }),
333 );
334 assert.isFalse(
335 isTerminalSplitVerticalShortcut(
336 event({ key: "d", metaKey: true, shiftKey: true }),
337 DEFAULT_BINDINGS,
338 {
339 platform: "MacIntel",
340 context: { terminalFocus: false },
341 },
342 ),
343 );
344 assert.isFalse(
345 isTerminalNewShortcut(event({ key: "n", ctrlKey: true }), DEFAULT_BINDINGS, {
346 platform: "Linux",
347 context: { terminalFocus: false },
348 }),
349 );
350 assert.isFalse(
351 isTerminalCloseShortcut(event({ key: "w", ctrlKey: true }), DEFAULT_BINDINGS, {
352 platform: "Linux",
353 context: { terminalFocus: false },
354 }),
355 );
356 });
357
358 it("matches split/new when terminalFocus is true", () => {
359 assert.isTrue(
360 isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, {
361 platform: "MacIntel",
362 context: { terminalFocus: true },
363 }),
364 );
365 assert.isTrue(
366 isTerminalSplitVerticalShortcut(
367 event({ key: "d", metaKey: true, shiftKey: true }),
368 DEFAULT_BINDINGS,
369 {
370 platform: "MacIntel",
371 context: { terminalFocus: true },
372 },
373 ),
374 );
375 assert.isTrue(
376 isTerminalNewShortcut(event({ key: "n", ctrlKey: true }), DEFAULT_BINDINGS, {
377 platform: "Linux",
378 context: { terminalFocus: true },
379 }),
380 );
381 assert.isTrue(
382 isTerminalCloseShortcut(event({ key: "w", ctrlKey: true }), DEFAULT_BINDINGS, {
383 platform: "Linux",
384 context: { terminalFocus: true },
385 }),
386 );
387 });
388
389 it("supports when expressions", () => {
390 const keybindings = compile([
391 {
392 shortcut: modShortcut("\\"),
393 command: "terminal.split",
394 whenAst: whenAnd(whenIdentifier("terminalOpen"), whenNot(whenIdentifier("terminalFocus"))),
395 },
396 {
397 shortcut: modShortcut("n", { shiftKey: true }),
398 command: "terminal.new",
399 whenAst: whenAnd(whenIdentifier("terminalOpen"), whenNot(whenIdentifier("terminalFocus"))),
400 },
401 { shortcut: modShortcut("j"), command: "terminal.toggle" },
402 ]);
403 assert.isTrue(
404 isTerminalSplitShortcut(event({ key: "\\", ctrlKey: true }), keybindings, {
405 platform: "Win32",
406 context: { terminalOpen: true, terminalFocus: false },
407 }),
408 );
409 assert.isFalse(
410 isTerminalSplitShortcut(event({ key: "\\", ctrlKey: true }), keybindings, {
411 platform: "Win32",
412 context: { terminalOpen: false, terminalFocus: false },
413 }),
414 );
415 assert.isTrue(
416 isTerminalNewShortcut(event({ key: "n", ctrlKey: true, shiftKey: true }), keybindings, {
417 platform: "Win32",
418 context: { terminalOpen: true, terminalFocus: false },
419 }),
420 );
421 });
422
423 it("supports when boolean literals", () => {
424 const keybindings = compile([
425 { shortcut: modShortcut("n"), command: "terminal.new", whenAst: whenIdentifier("true") },
426 { shortcut: modShortcut("m"), command: "terminal.new", whenAst: whenIdentifier("false") },
427 ]);
428
429 assert.isTrue(
430 isTerminalNewShortcut(event({ key: "n", ctrlKey: true }), keybindings, {
431 platform: "Linux",
432 }),
433 );
434 assert.isFalse(
435 isTerminalNewShortcut(event({ key: "m", ctrlKey: true }), keybindings, {
436 platform: "Linux",
437 }),
438 );
439 });
440});
441
442describe("shortcutLabelForCommand", () => {
443 it("returns the effective binding label", () => {
444 const bindings = compile([
445 {
446 shortcut: modShortcut("\\"),
447 command: "terminal.split",
448 whenAst: whenIdentifier("terminalFocus"),
449 },
450 {
451 shortcut: modShortcut("\\", { shiftKey: true }),
452 command: "terminal.split",
453 whenAst: whenNot(whenIdentifier("terminalFocus")),
454 },
455 ]);
456 assert.strictEqual(
457 shortcutLabelForCommand(bindings, "terminal.split", {
458 platform: "Linux",
459 context: { terminalFocus: false },
460 }),
461 "Ctrl+Shift+\\",
462 );
463 });
464
465 it("returns effective labels for non-terminal commands", () => {
466 assert.strictEqual(
467 shortcutLabelForCommand(DEFAULT_BINDINGS, "sidebar.toggle", "MacIntel"),
468 "⌘B",
469 );
470 assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "chat.new", "MacIntel"), "⇧⌘O");
471 assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "diff.toggle", "Linux"), "Ctrl+D");
472 assert.strictEqual(
473 shortcutLabelForCommand(DEFAULT_BINDINGS, "rightPanel.toggle", "MacIntel"),
474 "⌥⌘B",
475 );
476 assert.strictEqual(
477 shortcutLabelForCommand(DEFAULT_BINDINGS, "commandPalette.toggle", "MacIntel"),
478 "⌘K",
479 );
480 assert.strictEqual(
481 shortcutLabelForCommand(DEFAULT_BINDINGS, "filePicker.toggle", "MacIntel"),
482 "⌘P",
483 );
484 assert.strictEqual(
485 shortcutLabelForCommand(DEFAULT_BINDINGS, "projectSearch.toggle", "MacIntel"),
486 "⇧⌘F",
487 );
488 assert.strictEqual(
489 shortcutLabelForCommand(DEFAULT_BINDINGS, "modelPicker.toggle", "Linux"),
490 "Ctrl+Shift+M",
491 );
492 assert.strictEqual(
493 shortcutLabelForCommand(DEFAULT_BINDINGS, "editor.openFavorite", "Linux"),
494 "Ctrl+O",
495 );
496 assert.strictEqual(
497 shortcutLabelForCommand(DEFAULT_BINDINGS, "thread.jump.3", "MacIntel"),
498 "⌘3",
499 );
500 assert.strictEqual(
501 shortcutLabelForCommand(DEFAULT_BINDINGS, "thread.previous", "Linux"),
502 "Ctrl+Shift+[",
503 );
504 assert.strictEqual(
505 shortcutLabelForCommand(DEFAULT_BINDINGS, "modelPicker.jump.3", {
506 platform: "MacIntel",
507 context: { modelPickerOpen: true },
508 }),
509 "⌘3",
510 );
511 });
512
513 it("returns null for commands shadowed by a later conflicting shortcut", () => {
514 const bindings = compile([
515 { shortcut: modShortcut("1", { shiftKey: true }), command: "thread.jump.1" },
516 { shortcut: modShortcut("1", { shiftKey: true }), command: "thread.jump.7" },
517 ]);
518
519 assert.isNull(shortcutLabelForCommand(bindings, "thread.jump.1", "MacIntel"));
520 assert.strictEqual(shortcutLabelForCommand(bindings, "thread.jump.7", "MacIntel"), "⇧⌘1");
521 });
522
523 it("respects when-context while resolving labels", () => {
524 const bindings = compile([
525 { shortcut: modShortcut("d"), command: "diff.toggle" },
526 {
527 shortcut: modShortcut("d"),
528 command: "terminal.split",
529 whenAst: whenIdentifier("terminalFocus"),
530 },
531 ]);
532
533 assert.strictEqual(
534 shortcutLabelForCommand(bindings, "diff.toggle", {
535 platform: "Linux",
536 context: { terminalFocus: false },
537 }),
538 "Ctrl+D",
539 );
540 assert.isNull(
541 shortcutLabelForCommand(bindings, "diff.toggle", {
542 platform: "Linux",
543 context: { terminalFocus: true },
544 }),
545 );
546 assert.strictEqual(
547 shortcutLabelForCommand(bindings, "terminal.split", {
548 platform: "Linux",
549 context: { terminalFocus: true },
550 }),
551 "Ctrl+D",
552 );
553 });
554});
555
556describe("thread navigation helpers", () => {
557 it("maps jump commands to visible thread indices", () => {
558 assert.strictEqual(threadJumpCommandForIndex(0), "thread.jump.1");
559 assert.strictEqual(threadJumpCommandForIndex(2), "thread.jump.3");
560 assert.isNull(threadJumpCommandForIndex(9));
561 assert.strictEqual(threadJumpIndexFromCommand("thread.jump.1"), 0);
562 assert.strictEqual(threadJumpIndexFromCommand("thread.jump.3"), 2);
563 assert.isNull(threadJumpIndexFromCommand("thread.next"));
564 });
565
566 it("maps traversal commands to directions", () => {
567 assert.strictEqual(threadTraversalDirectionFromCommand("thread.previous"), "previous");
568 assert.strictEqual(threadTraversalDirectionFromCommand("thread.next"), "next");
569 assert.isNull(threadTraversalDirectionFromCommand("thread.jump.1"));
570 assert.isNull(threadTraversalDirectionFromCommand(null));
571 });
572
573 it("shows jump hints only when configured modifiers match", () => {
574 assert.isTrue(
575 shouldShowThreadJumpHintsForModifiers(event({ metaKey: true }), DEFAULT_BINDINGS, {
576 platform: "MacIntel",
577 }),
578 );
579 assert.isFalse(
580 shouldShowThreadJumpHintsForModifiers(
581 event({ metaKey: true, shiftKey: true }),
582 DEFAULT_BINDINGS,
583 {
584 platform: "MacIntel",
585 },
586 ),
587 );
588 assert.isTrue(
589 shouldShowThreadJumpHintsForModifiers(event({ ctrlKey: true }), DEFAULT_BINDINGS, {
590 platform: "Linux",
591 }),
592 );
593 });
594
595 it("never shows jump hints while the terminal is focused, even with an unrestricted binding", () => {
596 assert.isFalse(
597 shouldShowThreadJumpHintsForModifiers(event({ metaKey: true }), DEFAULT_BINDINGS, {
598 platform: "MacIntel",
599 context: { terminalFocus: true },
600 }),
601 );
602 assert.isTrue(
603 shouldShowThreadJumpHintsForModifiers(event({ metaKey: true }), DEFAULT_BINDINGS, {
604 platform: "MacIntel",
605 context: { terminalFocus: false },
606 }),
607 );
608 });
609
610 it("keeps default thread jumps off the web so the browser can switch tabs", () => {
611 const input = event({ key: "1", metaKey: true });
612 assert.isNull(
613 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
614 platform: "MacIntel",
615 context: { isDesktop: false },
616 }),
617 );
618 assert.strictEqual(
619 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
620 platform: "MacIntel",
621 context: { isDesktop: true },
622 }),
623 "thread.jump.1",
624 );
625 assert.isFalse(
626 shouldShowThreadJumpHintsForModifiers(
627 event({ metaKey: true }),
628 DEFAULT_RESOLVED_KEYBINDINGS,
629 {
630 platform: "MacIntel",
631 context: { isDesktop: false },
632 },
633 ),
634 );
635 assert.isTrue(
636 shouldShowThreadJumpHintsForModifiers(
637 event({ metaKey: true }),
638 DEFAULT_RESOLVED_KEYBINDINGS,
639 {
640 platform: "MacIntel",
641 context: { isDesktop: true },
642 },
643 ),
644 );
645 });
646});
647
648describe("model picker navigation helpers", () => {
649 it("maps jump commands to visible model indices", () => {
650 assert.strictEqual(modelPickerJumpCommandForIndex(0), "modelPicker.jump.1");
651 assert.strictEqual(modelPickerJumpCommandForIndex(2), "modelPicker.jump.3");
652 assert.isNull(modelPickerJumpCommandForIndex(9));
653 assert.strictEqual(modelPickerJumpIndexFromCommand("modelPicker.jump.1"), 0);
654 assert.strictEqual(modelPickerJumpIndexFromCommand("modelPicker.jump.3"), 2);
655 assert.isNull(modelPickerJumpIndexFromCommand("thread.jump.1"));
656 });
657
658 it("keeps default model jumps off the web even while the picker is open", () => {
659 const input = event({ key: "3", metaKey: true });
660 assert.isNull(
661 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
662 platform: "MacIntel",
663 context: { isDesktop: false, modelPickerOpen: true },
664 }),
665 );
666 assert.strictEqual(
667 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
668 platform: "MacIntel",
669 context: { isDesktop: true, modelPickerOpen: true },
670 }),
671 "modelPicker.jump.3",
672 );
673 assert.strictEqual(
674 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
675 platform: "MacIntel",
676 context: { isDesktop: true, modelPickerOpen: false },
677 }),
678 "thread.jump.3",
679 );
680 });
681});
682
683describe("chat/editor shortcuts", () => {
684 it("matches chat.new shortcut", () => {
685 assert.strictEqual(
686 resolveShortcutCommand(event({ key: "o", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
687 platform: "MacIntel",
688 }),
689 "chat.new",
690 );
691 assert.strictEqual(
692 resolveShortcutCommand(event({ key: "o", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
693 platform: "Linux",
694 }),
695 "chat.new",
696 );
697 });
698
699 it("matches chat.newLocal shortcut", () => {
700 assert.strictEqual(
701 resolveShortcutCommand(event({ key: "n", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
702 platform: "MacIntel",
703 }),
704 "chat.newLocal",
705 );
706 assert.strictEqual(
707 resolveShortcutCommand(event({ key: "n", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
708 platform: "Linux",
709 }),
710 "chat.newLocal",
711 );
712 });
713
714 it("matches editor.openFavorite shortcut", () => {
715 assert.isTrue(
716 isOpenFavoriteEditorShortcut(event({ key: "o", metaKey: true }), DEFAULT_BINDINGS, {
717 platform: "MacIntel",
718 }),
719 );
720 assert.isTrue(
721 isOpenFavoriteEditorShortcut(event({ key: "o", ctrlKey: true }), DEFAULT_BINDINGS, {
722 platform: "Linux",
723 }),
724 );
725 assert.isFalse(
726 isOpenFavoriteEditorShortcut(
727 event({ key: "o", metaKey: true, repeat: true }),
728 DEFAULT_BINDINGS,
729 { platform: "MacIntel" },
730 ),
731 );
732 });
733
734 it("matches commandPalette.toggle shortcut outside terminal focus", () => {
735 assert.strictEqual(
736 resolveShortcutCommand(event({ key: "k", metaKey: true }), DEFAULT_BINDINGS, {
737 platform: "MacIntel",
738 context: { terminalFocus: false },
739 }),
740 "commandPalette.toggle",
741 );
742 assert.notStrictEqual(
743 resolveShortcutCommand(event({ key: "k", metaKey: true }), DEFAULT_BINDINGS, {
744 platform: "MacIntel",
745 context: { terminalFocus: true },
746 }),
747 "commandPalette.toggle",
748 );
749 });
750
751 it("toggles the thread panel by default without taking terminal keystrokes", () => {
752 for (const platform of ["MacIntel", "Win32", "Linux"]) {
753 const input = event({
754 key: "b",
755 metaKey: platform === "MacIntel",
756 ctrlKey: platform !== "MacIntel",
757 shiftKey: true,
758 });
759 assert.strictEqual(
760 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
761 platform,
762 context: { terminalFocus: false, editableFocus: true },
763 }),
764 "threadPanel.toggle",
765 );
766 assert.isNull(
767 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
768 platform,
769 context: { terminalFocus: true },
770 }),
771 );
772 }
773 });
774
775 it("preserves a user-configured thread panel shortcut instead of its default", () => {
776 const bindings = mergeWithDefaultKeybindings(
777 compile([
778 {
779 shortcut: modShortcut("b", { altKey: true, shiftKey: true }),
780 command: "threadPanel.toggle",
781 },
782 ]),
783 );
784
785 assert.strictEqual(
786 resolveShortcutCommand(
787 event({ key: "b", metaKey: true, altKey: true, shiftKey: true }),
788 bindings,
789 { platform: "MacIntel" },
790 ),
791 "threadPanel.toggle",
792 );
793 assert.isNull(
794 resolveShortcutCommand(event({ key: "b", metaKey: true, shiftKey: true }), bindings, {
795 platform: "MacIntel",
796 }),
797 );
798 });
799
800 it("matches themeEditor.toggle on macOS and Windows", () => {
801 assert.strictEqual(
802 resolveShortcutCommand(
803 event({ key: "t", metaKey: true, altKey: true, shiftKey: true }),
804 DEFAULT_BINDINGS,
805 { platform: "MacIntel" },
806 ),
807 "themeEditor.toggle",
808 );
809 assert.strictEqual(
810 resolveShortcutCommand(
811 event({ key: "t", ctrlKey: true, altKey: true, shiftKey: true }),
812 DEFAULT_BINDINGS,
813 { platform: "Win32" },
814 ),
815 "themeEditor.toggle",
816 );
817 });
818
819 it("matches filePicker.toggle shortcut outside terminal focus", () => {
820 assert.strictEqual(
821 resolveShortcutCommand(event({ key: "p", metaKey: true }), DEFAULT_BINDINGS, {
822 platform: "MacIntel",
823 context: { terminalFocus: false },
824 }),
825 "filePicker.toggle",
826 );
827 assert.notStrictEqual(
828 resolveShortcutCommand(event({ key: "p", metaKey: true }), DEFAULT_BINDINGS, {
829 platform: "MacIntel",
830 context: { terminalFocus: true },
831 }),
832 "filePicker.toggle",
833 );
834 });
835
836 it("matches projectSearch.toggle shortcut outside terminal focus", () => {
837 assert.strictEqual(
838 resolveShortcutCommand(event({ key: "f", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
839 platform: "MacIntel",
840 context: { terminalFocus: false },
841 }),
842 "projectSearch.toggle",
843 );
844 assert.notStrictEqual(
845 resolveShortcutCommand(event({ key: "f", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
846 platform: "MacIntel",
847 context: { terminalFocus: true },
848 }),
849 "projectSearch.toggle",
850 );
851 });
852
853 it("matches diff.toggle shortcut outside terminal focus", () => {
854 assert.isTrue(
855 isDiffToggleShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, {
856 platform: "MacIntel",
857 context: { terminalFocus: false },
858 }),
859 );
860 assert.isFalse(
861 isDiffToggleShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, {
862 platform: "MacIntel",
863 context: { terminalFocus: true },
864 }),
865 );
866 });
867});
868
869describe("cross-command precedence", () => {
870 it("uses when + order so a later focused rule overrides a global rule", () => {
871 const keybindings = compile([
872 { shortcut: modShortcut("n"), command: "chat.new" },
873 {
874 shortcut: modShortcut("n"),
875 command: "terminal.new",
876 whenAst: whenIdentifier("terminalFocus"),
877 },
878 ]);
879
880 assert.isTrue(
881 isTerminalNewShortcut(event({ key: "n", metaKey: true }), keybindings, {
882 platform: "MacIntel",
883 context: { terminalFocus: true },
884 }),
885 );
886 assert.strictEqual(
887 resolveShortcutCommand(event({ key: "n", metaKey: true }), keybindings, {
888 platform: "MacIntel",
889 context: { terminalFocus: true },
890 }),
891 "terminal.new",
892 );
893 assert.isFalse(
894 isTerminalNewShortcut(event({ key: "n", metaKey: true }), keybindings, {
895 platform: "MacIntel",
896 context: { terminalFocus: false },
897 }),
898 );
899 assert.strictEqual(
900 resolveShortcutCommand(event({ key: "n", metaKey: true }), keybindings, {
901 platform: "MacIntel",
902 context: { terminalFocus: false },
903 }),
904 "chat.new",
905 );
906 });
907
908 it("still lets a later global rule win when both rules match", () => {
909 const keybindings = compile([
910 {
911 shortcut: modShortcut("n"),
912 command: "terminal.new",
913 whenAst: whenIdentifier("terminalFocus"),
914 },
915 { shortcut: modShortcut("n"), command: "chat.new" },
916 ]);
917
918 assert.isFalse(
919 isTerminalNewShortcut(event({ key: "n", ctrlKey: true }), keybindings, {
920 platform: "Linux",
921 context: { terminalFocus: true },
922 }),
923 );
924 assert.strictEqual(
925 resolveShortcutCommand(event({ key: "n", ctrlKey: true }), keybindings, {
926 platform: "Linux",
927 context: { terminalFocus: true },
928 }),
929 "chat.new",
930 );
931 });
932});
933
934describe("resolveChatShortcutCommand", () => {
935 it("runs the active environment's script shortcut instead of the primary environment's", () => {
936 const primary = compile([{ shortcut: modShortcut("r"), command: "script.setup.run" }]);
937 const active = compile([{ shortcut: modShortcut("t"), command: "script.setup.run" }]);
938
939 assert.strictEqual(
940 resolveChatShortcutCommand(event({ key: "t", ctrlKey: true }), primary, active, {
941 platform: "Linux",
942 }),
943 "script.setup.run",
944 );
945 assert.isNull(
946 resolveChatShortcutCommand(event({ key: "r", ctrlKey: true }), primary, active, {
947 platform: "Linux",
948 }),
949 );
950 });
951
952 it("resolves shared chords to the active environment's script ID", () => {
953 const primary = compile([{ shortcut: modShortcut("r"), command: "script.setup.run" }]);
954 const active = compile([{ shortcut: modShortcut("r"), command: "script.deploy.run" }]);
955
956 assert.strictEqual(
957 resolveChatShortcutCommand(event({ key: "r", metaKey: true }), primary, active, {
958 platform: "MacIntel",
959 }),
960 "script.deploy.run",
961 );
962 });
963
964 it("does not run primary scripts before the active environment's bindings arrive", () => {
965 const primary = compile([{ shortcut: modShortcut("r"), command: "script.setup.run" }]);
966
967 assert.isNull(
968 resolveChatShortcutCommand(event({ key: "r", ctrlKey: true }), primary, [], {
969 platform: "Linux",
970 }),
971 );
972 });
973
974 it("keeps primary app commands ahead of conflicting active script shortcuts", () => {
975 const primary = compile([{ shortcut: modShortcut("k"), command: "commandPalette.toggle" }]);
976 const active = compile([{ shortcut: modShortcut("k"), command: "script.setup.run" }]);
977
978 assert.strictEqual(
979 resolveChatShortcutCommand(event({ key: "k", ctrlKey: true }), primary, active, {
980 platform: "Linux",
981 }),
982 "commandPalette.toggle",
983 );
984 });
985
986 it("keeps app shortcuts on the primary environment when the active environment remaps them", () => {
987 const primary = compile([{ shortcut: modShortcut("j"), command: "terminal.toggle" }]);
988 const active = compile([{ shortcut: modShortcut("t"), command: "terminal.toggle" }]);
989
990 assert.strictEqual(
991 resolveChatShortcutCommand(event({ key: "j", ctrlKey: true }), primary, active, {
992 platform: "Linux",
993 }),
994 "terminal.toggle",
995 );
996 assert.isNull(
997 resolveChatShortcutCommand(event({ key: "t", ctrlKey: true }), primary, active, {
998 platform: "Linux",
999 }),
1000 );
1001 });
1002
1003 it.each([false, true])(
1004 "preserves active binding precedence with terminalFocus=%s",
1005 (terminalFocus) => {
1006 const primary = compile([{ shortcut: modShortcut("r"), command: "script.setup.run" }]);
1007 const active = compile([
1008 { shortcut: modShortcut("r"), command: "script.deploy.run" },
1009 {
1010 shortcut: modShortcut("r"),
1011 command: "terminal.new",
1012 whenAst: whenIdentifier("terminalFocus"),
1013 },
1014 ]);
1015 const options = { platform: "Linux", context: { terminalFocus } };
1016 const shortcut = event({ key: "r", ctrlKey: true });
1017
1018 assert.strictEqual(
1019 resolveChatShortcutCommand(shortcut, primary, active, options),
1020 terminalFocus ? null : "script.deploy.run",
1021 );
1022 assert.strictEqual(
1023 resolveChatShortcutCommand(shortcut, active, active, options),
1024 resolveShortcutCommand(shortcut, active, options),
1025 );
1026 },
1027 );
1028});
1029
1030describe("resolveShortcutCommand", () => {
1031 it("resolves a custom stop-thread shortcut", () => {
1032 const keybindings = compile([{ shortcut: modShortcut("escape"), command: "thread.stop" }]);
1033
1034 assert.strictEqual(
1035 resolveShortcutCommand(event({ key: "Escape", metaKey: true }), keybindings, {
1036 platform: "MacIntel",
1037 }),
1038 "thread.stop",
1039 );
1040 });
1041
1042 it("honors preview conditions for a stop-thread shortcut", () => {
1043 const keybindings = compile([
1044 {
1045 shortcut: modShortcut("escape"),
1046 command: "thread.stop",
1047 whenAst: whenIdentifier("previewFocus"),
1048 },
1049 ]);
1050 const input = event({ key: "Escape", metaKey: true });
1051
1052 assert.isNull(
1053 resolveShortcutCommand(input, keybindings, {
1054 platform: "MacIntel",
1055 context: { previewFocus: false },
1056 }),
1057 );
1058 assert.strictEqual(
1059 resolveShortcutCommand(input, keybindings, {
1060 platform: "MacIntel",
1061 context: { previewFocus: true },
1062 }),
1063 "thread.stop",
1064 );
1065 });
1066
1067 it("returns dynamic script commands", () => {
1068 const keybindings = compile([{ shortcut: modShortcut("r"), command: "script.setup.run" }]);
1069
1070 assert.strictEqual(
1071 resolveShortcutCommand(event({ key: "r", ctrlKey: true }), keybindings, {
1072 platform: "Linux",
1073 }),
1074 "script.setup.run",
1075 );
1076 });
1077
1078 it("routes mod+w to the terminal while focused and to the right panel otherwise", () => {
1079 const closeEvent = event({ key: "w", metaKey: true });
1080 assert.strictEqual(
1081 resolveShortcutCommand(closeEvent, DEFAULT_BINDINGS, {
1082 platform: "MacIntel",
1083 context: { terminalFocus: true },
1084 }),
1085 "terminal.close",
1086 );
1087 assert.strictEqual(
1088 resolveShortcutCommand(closeEvent, DEFAULT_BINDINGS, {
1089 platform: "MacIntel",
1090 context: { terminalFocus: false },
1091 }),
1092 "rightPanel.close",
1093 );
1094 });
1095
1096 it("resolves a custom right panel maximize binding", () => {
1097 const keybindings = compile([
1098 {
1099 shortcut: modShortcut("m", { shiftKey: true }),
1100 command: "rightPanel.toggleMaximized",
1101 },
1102 ]);
1103
1104 assert.strictEqual(
1105 resolveShortcutCommand(event({ key: "m", metaKey: true, shiftKey: true }), keybindings, {
1106 platform: "MacIntel",
1107 }),
1108 "rightPanel.toggleMaximized",
1109 );
1110 });
1111
1112 it("navigates history with mod+[ and mod+] outside the terminal", () => {
1113 const back = event({ key: "[", code: "BracketLeft", metaKey: true });
1114 const forward = event({ key: "]", code: "BracketRight", ctrlKey: true });
1115 assert.strictEqual(
1116 resolveShortcutCommand(back, DEFAULT_RESOLVED_KEYBINDINGS, { platform: "MacIntel" }),
1117 "navigation.back",
1118 );
1119 assert.strictEqual(
1120 resolveShortcutCommand(forward, DEFAULT_RESOLVED_KEYBINDINGS, { platform: "Linux" }),
1121 "navigation.forward",
1122 );
1123 assert.isNull(
1124 resolveShortcutCommand(back, DEFAULT_RESOLVED_KEYBINDINGS, {
1125 platform: "MacIntel",
1126 context: { terminalFocus: true },
1127 }),
1128 );
1129 });
1130
1131 it("matches bracket shortcuts using the physical key code", () => {
1132 assert.strictEqual(
1133 resolveShortcutCommand(
1134 event({ key: "{", code: "BracketLeft", metaKey: true, shiftKey: true }),
1135 DEFAULT_BINDINGS,
1136 {
1137 platform: "MacIntel",
1138 },
1139 ),
1140 "thread.previous",
1141 );
1142 assert.strictEqual(
1143 resolveShortcutCommand(
1144 event({ key: "}", code: "BracketRight", ctrlKey: true, shiftKey: true }),
1145 DEFAULT_BINDINGS,
1146 {
1147 platform: "Linux",
1148 },
1149 ),
1150 "thread.next",
1151 );
1152 });
1153
1154 it("matches punctuation shortcuts by physical key across keyboard layouts", () => {
1155 const keybindings = compile([
1156 { shortcut: modShortcut("'", { shiftKey: true }), command: "diff.toggle" },
1157 ]);
1158
1159 assert.strictEqual(
1160 resolveShortcutCommand(
1161 event({ key: "@", code: "Quote", metaKey: true, shiftKey: true }),
1162 keybindings,
1163 { platform: "MacIntel" },
1164 ),
1165 "diff.toggle",
1166 );
1167 assert.isNull(
1168 resolveShortcutCommand(
1169 event({ key: '"', code: "Digit2", metaKey: true, shiftKey: true }),
1170 keybindings,
1171 { platform: "MacIntel" },
1172 ),
1173 );
1174 });
1175
1176 it("does not let a punctuation position shadow a Latin layout key", () => {
1177 const keybindings = compile([
1178 { shortcut: modShortcut("m"), command: "diff.toggle" },
1179 { shortcut: modShortcut(";"), command: "sidebar.toggle" },
1180 ]);
1181
1182 assert.strictEqual(
1183 resolveShortcutCommand(event({ key: "m", code: "Semicolon", metaKey: true }), keybindings, {
1184 platform: "MacIntel",
1185 }),
1186 "diff.toggle",
1187 );
1188 });
1189
1190 it("matches Option-modified letters using the physical key code on macOS", () => {
1191 assert.strictEqual(
1192 resolveShortcutCommand(
1193 event({ key: "∫", code: "KeyB", metaKey: true, altKey: true }),
1194 DEFAULT_BINDINGS,
1195 { platform: "MacIntel" },
1196 ),
1197 "rightPanel.toggle",
1198 );
1199 });
1200
1201 it("matches non-Latin layout letters using the physical key code", () => {
1202 const keybindings = compile([{ shortcut: modShortcut("d"), command: "diff.toggle" }]);
1203
1204 assert.strictEqual(
1205 resolveShortcutCommand(event({ key: "в", code: "KeyD", metaKey: true }), keybindings, {
1206 platform: "MacIntel",
1207 }),
1208 "diff.toggle",
1209 );
1210 });
1211
1212 it("ignores the physical key code when the layout types a different Latin letter", () => {
1213 const keybindings = compile([{ shortcut: modShortcut("d"), command: "diff.toggle" }]);
1214
1215 // On a remapped layout the physical D key types "a"; only the physical
1216 // key whose layout output is "d" may trigger the shortcut.
1217 assert.isNull(
1218 resolveShortcutCommand(event({ key: "a", code: "KeyD", metaKey: true }), keybindings, {
1219 platform: "MacIntel",
1220 }),
1221 );
1222 assert.strictEqual(
1223 resolveShortcutCommand(event({ key: "d", code: "KeyL", metaKey: true }), keybindings, {
1224 platform: "MacIntel",
1225 }),
1226 "diff.toggle",
1227 );
1228 });
1229});
1230
1231describe("formatShortcutLabel", () => {
1232 it("formats labels for macOS", () => {
1233 assert.strictEqual(
1234 formatShortcutLabel(modShortcut("d", { shiftKey: true }), "MacIntel"),
1235 "⇧⌘D",
1236 );
1237 });
1238
1239 it("formats labels for non-macOS", () => {
1240 assert.strictEqual(
1241 formatShortcutLabel(modShortcut("d", { shiftKey: true }), "Linux"),
1242 "Ctrl+Shift+D",
1243 );
1244 });
1245
1246 it("formats labels for plus key", () => {
1247 assert.strictEqual(formatShortcutLabel(modShortcut("+"), "MacIntel"), "⌘+");
1248 assert.strictEqual(formatShortcutLabel(modShortcut("+"), "Linux"), "Ctrl++");
1249 });
1250});
1251
1252describe("isTerminalClearShortcut", () => {
1253 it("matches Ctrl+L on all platforms", () => {
1254 assert.isTrue(isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "Linux"));
1255 assert.isTrue(isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "MacIntel"));
1256 });
1257
1258 it("matches Cmd+K on macOS", () => {
1259 assert.isTrue(isTerminalClearShortcut(event({ key: "k", metaKey: true }), "MacIntel"));
1260 });
1261
1262 it("ignores non-keydown events", () => {
1263 assert.isFalse(
1264 isTerminalClearShortcut(event({ type: "keyup", key: "l", ctrlKey: true }), "Linux"),
1265 );
1266 });
1267});
1268
1269describe("isRichTextBoldShortcut", () => {
1270 it("matches Mod+B without extra modifiers", () => {
1271 assert.isTrue(isRichTextBoldShortcut(event({ key: "b", metaKey: true })));
1272 assert.isTrue(isRichTextBoldShortcut(event({ key: "B", ctrlKey: true })));
1273 });
1274
1275 it("matches the B key on non-Latin layouts, like the sidebar toggle does", () => {
1276 const cyrillicB = event({ key: "и", code: "KeyB", ctrlKey: true });
1277 assert.isTrue(isRichTextBoldShortcut(cyrillicB));
1278 assert.strictEqual(
1279 resolveShortcutCommand(cyrillicB, DEFAULT_BINDINGS, { platform: "Win32" }),
1280 "sidebar.toggle",
1281 );
1282 });
1283
1284 it("follows the letter a Latin layout types, not the physical key", () => {
1285 assert.isFalse(isRichTextBoldShortcut(event({ key: "x", code: "KeyB", ctrlKey: true })));
1286 assert.isTrue(isRichTextBoldShortcut(event({ key: "b", code: "KeyN", ctrlKey: true })));
1287 });
1288
1289 it("ignores shifted, alted, bare, and non-keydown presses", () => {
1290 assert.isFalse(isRichTextBoldShortcut(event({ key: "b", metaKey: true, shiftKey: true })));
1291 assert.isFalse(isRichTextBoldShortcut(event({ key: "b", metaKey: true, altKey: true })));
1292 assert.isFalse(isRichTextBoldShortcut(event({ key: "b" })));
1293 assert.isFalse(isRichTextBoldShortcut(event({ key: "i", metaKey: true })));
1294 assert.isFalse(isRichTextBoldShortcut(event({ type: "keyup", key: "b", metaKey: true })));
1295 });
1296});
1297
1298describe("terminalDeleteShortcutData", () => {
1299 it("maps Cmd+Backspace on macOS to delete-to-line-start", () => {
1300 assert.strictEqual(
1301 terminalDeleteShortcutData(event({ key: "Backspace", metaKey: true }), "MacIntel"),
1302 "\u0015",
1303 );
1304 });
1305
1306 it("ignores non-macOS platforms and modified variants", () => {
1307 assert.isNull(terminalDeleteShortcutData(event({ key: "Backspace", metaKey: true }), "Linux"));
1308 assert.isNull(
1309 terminalDeleteShortcutData(
1310 event({ key: "Backspace", metaKey: true, altKey: true }),
1311 "MacIntel",
1312 ),
1313 );
1314 });
1315
1316 it("ignores non-keydown events", () => {
1317 assert.isNull(
1318 terminalDeleteShortcutData(
1319 event({ type: "keyup", key: "Backspace", metaKey: true }),
1320 "MacIntel",
1321 ),
1322 );
1323 });
1324});
1325
1326describe("terminalNavigationShortcutData", () => {
1327 it("maps Option+Arrow on macOS to word movement", () => {
1328 assert.strictEqual(
1329 terminalNavigationShortcutData(event({ key: "ArrowLeft", altKey: true }), "MacIntel"),
1330 "\u001bb",
1331 );
1332 assert.strictEqual(
1333 terminalNavigationShortcutData(event({ key: "ArrowRight", altKey: true }), "MacIntel"),
1334 "\u001bf",
1335 );
1336 });
1337
1338 it("maps Cmd+Arrow on macOS to line movement", () => {
1339 assert.strictEqual(
1340 terminalNavigationShortcutData(event({ key: "ArrowLeft", metaKey: true }), "MacIntel"),
1341 "\u0001",
1342 );
1343 assert.strictEqual(
1344 terminalNavigationShortcutData(event({ key: "ArrowRight", metaKey: true }), "MacIntel"),
1345 "\u0005",
1346 );
1347 });
1348
1349 it("maps Ctrl+Arrow on non-macOS to word movement", () => {
1350 assert.strictEqual(
1351 terminalNavigationShortcutData(event({ key: "ArrowLeft", ctrlKey: true }), "Win32"),
1352 "\u001bb",
1353 );
1354 assert.strictEqual(
1355 terminalNavigationShortcutData(event({ key: "ArrowRight", ctrlKey: true }), "Linux"),
1356 "\u001bf",
1357 );
1358 });
1359
1360 it("rejects unsupported combinations", () => {
1361 assert.isNull(
1362 terminalNavigationShortcutData(
1363 event({ key: "ArrowLeft", shiftKey: true, altKey: true }),
1364 "MacIntel",
1365 ),
1366 );
1367 assert.isNull(
1368 terminalNavigationShortcutData(event({ key: "ArrowLeft", metaKey: true }), "Linux"),
1369 );
1370 assert.isNull(terminalNavigationShortcutData(event({ key: "a", altKey: true }), "MacIntel"));
1371 });
1372
1373 it("ignores non-keydown events", () => {
1374 assert.isNull(
1375 terminalNavigationShortcutData(
1376 event({ type: "keyup", key: "ArrowLeft", altKey: true }),
1377 "MacIntel",
1378 ),
1379 );
1380 });
1381});
1382
1383describe("plus key parsing", () => {
1384 it("matches the plus key shortcut", () => {
1385 const plusBindings = compile([{ shortcut: modShortcut("+"), command: "terminal.toggle" }]);
1386 assert.isTrue(
1387 isTerminalToggleShortcut(event({ key: "+", metaKey: true }), plusBindings, {
1388 platform: "MacIntel",
1389 }),
1390 );
1391 assert.isTrue(
1392 isTerminalToggleShortcut(event({ key: "+", ctrlKey: true }), plusBindings, {
1393 platform: "Linux",
1394 }),
1395 );
1396 });
1397});
1398
1399describe("composer and pull request shortcuts", () => {
1400 it("fills missing number shortcuts without replacing the saved URL binding", () => {
1401 const olderServerBindings = DEFAULT_RESOLVED_KEYBINDINGS.filter(
1402 (binding) =>
1403 binding.command !== "pullRequest.copyNumber" && binding.command !== "thread.copyReference",
1404 );
1405 const bindings = mergeWithDefaultKeybindings([
1406 ...olderServerBindings,
1407 ...compileResolvedKeybindingsConfig([
1408 { key: "mod+shift+8", command: "thread.copyReference", when: "!terminalFocus" },
1409 ]),
1410 ]);
1411 for (const [key, command] of [
1412 ["k", "pullRequest.copyNumber"],
1413 ["8", "thread.copyReference"],
1414 ["c", null],
1415 ["y", null],
1416 ] as const) {
1417 assert.strictEqual(
1418 resolveShortcutCommand(event({ key, metaKey: true, shiftKey: true }), bindings, {
1419 platform: "MacIntel",
1420 }),
1421 command,
1422 );
1423 }
1424 });
1425
1426 it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen", "isWeb", "isDesktop"])(
1427 "honors custom PR shortcut conditions for %s",
1428 (condition) => {
1429 const bindings = compileResolvedKeybindingsConfig([
1430 { key: "mod+shift+k", command: "thread.copyReference", when: condition },
1431 { key: "mod+shift+k", command: "pullRequest.copyNumber", when: `!${condition}` },
1432 ]);
1433 const input = event({ key: "k", ctrlKey: true, shiftKey: true });
1434 for (const enabled of [false, true]) {
1435 assert.strictEqual(
1436 resolveShortcutCommand(input, bindings, {
1437 platform: "Linux",
1438 context: { [condition]: enabled },
1439 }),
1440 enabled ? "thread.copyReference" : "pullRequest.copyNumber",
1441 );
1442 }
1443 },
1444 );
1445
1446 const shortcuts = [
1447 ["h", "composer.host"],
1448 ["e", "composer.effort"],
1449 ["a", "composer.mode"],
1450 ["x", "composer.workspace"],
1451 ["g", "composer.branch"],
1452 ["l", "composer.previousWorktree"],
1453 ["c", "thread.copyReference"],
1454 ["k", "pullRequest.copyNumber"],
1455 ["Enter", "thread.steerQueuedMessage"],
1456 ] as const;
1457
1458 it.each(["MacIntel", "Win32", "Linux"])(
1459 "separates queued steering and background start on %s",
1460 (platform) => {
1461 const modifier = {
1462 metaKey: platform === "MacIntel",
1463 ctrlKey: platform !== "MacIntel",
1464 };
1465 const queuedKey = event({ key: "Enter", shiftKey: true, ...modifier });
1466 const backgroundKey = event({ key: "Enter", ...modifier });
1467 assert.strictEqual(
1468 resolveShortcutCommand(queuedKey, DEFAULT_RESOLVED_KEYBINDINGS, {
1469 platform,
1470 context: { terminalFocus: false, draftThreadRoute: false },
1471 }),
1472 "thread.steerQueuedMessage",
1473 );
1474 assert.strictEqual(
1475 resolveShortcutCommand(backgroundKey, DEFAULT_RESOLVED_KEYBINDINGS, {
1476 platform,
1477 context: { terminalFocus: false, draftThreadRoute: true, composerFocus: true },
1478 }),
1479 "composer.sendBackground",
1480 );
1481 },
1482 );
1483
1484 for (const platform of ["MacIntel", "Win32", "Linux"]) {
1485 it.each(shortcuts)(
1486 `resolves %s on ${platform} and leaves terminal input alone`,
1487 (key, command) => {
1488 const input = event({
1489 key,
1490 shiftKey: true,
1491 metaKey: platform === "MacIntel",
1492 ctrlKey: platform !== "MacIntel",
1493 });
1494 assert.strictEqual(
1495 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
1496 platform,
1497 context: { terminalFocus: false },
1498 }),
1499 command,
1500 );
1501 assert.isNull(
1502 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
1503 platform,
1504 context: { terminalFocus: true },
1505 }),
1506 );
1507 },
1508 );
1509 }
1510
1511 it.each(["MacIntel", "Win32", "Linux"])(
1512 "edits the last queued message with Alt+ArrowUp from the composer on %s",
1513 (platform) => {
1514 const input = event({ key: "ArrowUp", altKey: true });
1515 assert.strictEqual(
1516 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
1517 platform,
1518 context: { composerFocus: true },
1519 }),
1520 "thread.editQueuedMessage",
1521 );
1522 assert.isNull(
1523 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
1524 platform,
1525 context: { composerFocus: false },
1526 }),
1527 );
1528 },
1529 );
1530
1531 for (const platform of ["MacIntel", "Win32", "Linux"]) {
1532 it.each([
1533 ["s", "thread.settle"],
1534 ["p", "thread.pin"],
1535 ])(`preserves the existing %s shortcut on ${platform}`, (key, command) => {
1536 assert.strictEqual(
1537 resolveShortcutCommand(
1538 event({
1539 key,
1540 shiftKey: true,
1541 metaKey: platform === "MacIntel",
1542 ctrlKey: platform !== "MacIntel",
1543 }),
1544 DEFAULT_RESOLVED_KEYBINDINGS,
1545 { platform },
1546 ),
1547 command,
1548 );
1549 });
1550 }
1551
1552 const altEffortBindings = compileResolvedKeybindingsConfig([
1553 { key: "mod+alt+e", command: "composer.effort", when: "!terminalFocus" },
1554 ]);
1555
1556 it("leaves AltGr text entry alone with a custom Alt binding", () => {
1557 for (const platform of ["Win32", "Linux"]) {
1558 const input = event({
1559 key: "€",
1560 code: "KeyE",
1561 ctrlKey: true,
1562 altKey: true,
1563 getModifierState: (key) => key === "AltGraph",
1564 });
1565 assert.isNull(resolveShortcutCommand(input, altEffortBindings, { platform }));
1566 assert.strictEqual(
1567 resolveShortcutCommand({ ...input, getModifierState: () => false }, altEffortBindings, {
1568 platform,
1569 }),
1570 "composer.effort",
1571 );
1572 }
1573 });
1574
1575 it("keeps Firefox modifier reporting usable on Windows and macOS", () => {
1576 const getModifierState = (key: string) => key === "AltGraph";
1577 assert.strictEqual(
1578 resolveShortcutCommand(
1579 event({ key: "e", ctrlKey: true, altKey: true, getModifierState }),
1580 altEffortBindings,
1581 { platform: "Win32" },
1582 ),
1583 "composer.effort",
1584 );
1585 assert.strictEqual(
1586 resolveShortcutCommand(
1587 event({ key: "´", code: "KeyE", metaKey: true, altKey: true, getModifierState }),
1588 altEffortBindings,
1589 { platform: "MacIntel" },
1590 ),
1591 "composer.effort",
1592 );
1593 });
1594
1595 it.each(shortcuts)("uses a custom binding for %s", (_key, command) => {
1596 const bindings = compileResolvedKeybindingsConfig([
1597 { key: "mod+shift+y", command, when: "!terminalFocus" },
1598 ]);
1599 assert.strictEqual(
1600 resolveShortcutCommand(
1601 event({ key: "Y", code: "KeyY", ctrlKey: true, shiftKey: true }),
1602 bindings,
1603 { platform: "Linux" },
1604 ),
1605 command,
1606 );
1607 });
1608
1609 for (const platform of ["MacIntel", "Win32", "Linux"]) {
1610 it.each([
1611 ["ArrowUp", "modelPicker.previousProvider"],
1612 ["ArrowDown", "modelPicker.nextProvider"],
1613 ] as const)(`limits %s to the model picker on ${platform}`, (key, command) => {
1614 const input = event({
1615 key,
1616 shiftKey: true,
1617 metaKey: platform === "MacIntel",
1618 ctrlKey: platform !== "MacIntel",
1619 });
1620 assert.strictEqual(
1621 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
1622 platform,
1623 context: { modelPickerOpen: true },
1624 }),
1625 command,
1626 );
1627 assert.isNull(
1628 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
1629 platform,
1630 context: { modelPickerOpen: false },
1631 }),
1632 );
1633 });
1634 }
1635});
1636
1637describe("Usage shortcuts", () => {
1638 it("scopes letter shortcuts to Usage", () => {
1639 assert.strictEqual(
1640 resolveShortcutCommand(event({ key: "t" }), DEFAULT_RESOLVED_KEYBINDINGS, {
1641 platform: "Linux",
1642 context: { usagePageOpen: true },
1643 }),
1644 "usage.tokens",
1645 );
1646 assert.isNull(
1647 resolveShortcutCommand(event({ key: "t" }), DEFAULT_RESOLVED_KEYBINDINGS, {
1648 platform: "Linux",
1649 }),
1650 );
1651 });
1652
1653 it.each(["Linux", "MacIntel"])(
1654 "preserves desktop numbered thread shortcuts on Usage on %s",
1655 (platform) => {
1656 const shortcut = event({
1657 key: "2",
1658 ctrlKey: platform === "Linux",
1659 metaKey: platform === "MacIntel",
1660 });
1661 assert.strictEqual(
1662 resolveShortcutCommand(shortcut, DEFAULT_RESOLVED_KEYBINDINGS, {
1663 platform,
1664 context: { usagePageOpen: true, isDesktop: true },
1665 }),
1666 "thread.jump.2",
1667 );
1668 assert.isNotNull(
1669 shortcutLabelForCommand(DEFAULT_RESOLVED_KEYBINDINGS, "thread.jump.2", {
1670 platform,
1671 context: { usagePageOpen: true, isDesktop: true },
1672 }),
1673 );
1674 },
1675 );
1676
1677 it("matches shifted number keys for periods and only on Usage", () => {
1678 const shortcut = event({ key: "!", code: "Digit1", ctrlKey: true, shiftKey: true });
1679 assert.strictEqual(
1680 resolveShortcutCommand(shortcut, DEFAULT_RESOLVED_KEYBINDINGS, {
1681 platform: "Linux",
1682 context: { usagePageOpen: true },
1683 }),
1684 "usage.period.day",
1685 );
1686 assert.isNull(
1687 resolveShortcutCommand(shortcut, DEFAULT_RESOLVED_KEYBINDINGS, {
1688 platform: "Linux",
1689 }),
1690 );
1691 });
1692});