fix(ui): handle IME prefixes and select shortcut conflicts
This commit is contained in:
@@ -135,6 +135,7 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
open={openPicker === 'project'}
|
open={openPicker === 'project'}
|
||||||
onOpenChange={(open) => setOpenPicker(open ? 'project' : null)}
|
onOpenChange={(open) => setOpenPicker(open ? 'project' : null)}
|
||||||
onValueChange={handleProjectChange}
|
onValueChange={handleProjectChange}
|
||||||
|
disableGlobalShortcuts
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
ref={projectTriggerRef}
|
ref={projectTriggerRef}
|
||||||
@@ -160,6 +161,7 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
open={openPicker === 'worktree'}
|
open={openPicker === 'worktree'}
|
||||||
onOpenChange={(open) => setOpenPicker(open ? 'worktree' : null)}
|
onOpenChange={(open) => setOpenPicker(open ? 'worktree' : null)}
|
||||||
onValueChange={handleDirectoryChange}
|
onValueChange={handleDirectoryChange}
|
||||||
|
disableGlobalShortcuts
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
ref={worktreeTriggerRef}
|
ref={worktreeTriggerRef}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from 'bun:test';
|
import { expect, test } from 'bun:test';
|
||||||
import { getDropdownMenuNavigationKey } from './dropdown-menu-keyboard';
|
import { getDropdownNavigationKey } from './dropdown-navigation';
|
||||||
|
|
||||||
function keyEvent(key: string, modifiers: Partial<Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>> = {}) {
|
function keyEvent(key: string, modifiers: Partial<Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>> = {}) {
|
||||||
return {
|
return {
|
||||||
@@ -13,11 +13,11 @@ function keyEvent(key: string, modifiers: Partial<Pick<KeyboardEvent, 'ctrlKey'
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('maps only exact Ctrl+N and Ctrl+P to menu navigation keys', () => {
|
test('maps only exact Ctrl+N and Ctrl+P to menu navigation keys', () => {
|
||||||
expect(getDropdownMenuNavigationKey(keyEvent('n', { ctrlKey: true }))).toBe('ArrowDown');
|
expect(getDropdownNavigationKey(keyEvent('n', { ctrlKey: true }))).toBe('ArrowDown');
|
||||||
expect(getDropdownMenuNavigationKey(keyEvent('p', { ctrlKey: true }))).toBe('ArrowUp');
|
expect(getDropdownNavigationKey(keyEvent('p', { ctrlKey: true }))).toBe('ArrowUp');
|
||||||
expect(getDropdownMenuNavigationKey(keyEvent('N', { ctrlKey: true }))).toBe('ArrowDown');
|
expect(getDropdownNavigationKey(keyEvent('N', { ctrlKey: true }))).toBe('ArrowDown');
|
||||||
expect(getDropdownMenuNavigationKey(keyEvent('n'))).toBe(null);
|
expect(getDropdownNavigationKey(keyEvent('n'))).toBe(null);
|
||||||
expect(getDropdownMenuNavigationKey(keyEvent('n', { ctrlKey: true, shiftKey: true }))).toBe(null);
|
expect(getDropdownNavigationKey(keyEvent('n', { ctrlKey: true, shiftKey: true }))).toBe(null);
|
||||||
expect(getDropdownMenuNavigationKey(keyEvent('p', { ctrlKey: true, altKey: true }))).toBe(null);
|
expect(getDropdownNavigationKey(keyEvent('p', { ctrlKey: true, altKey: true }))).toBe(null);
|
||||||
expect(getDropdownMenuNavigationKey(keyEvent('p', { ctrlKey: true, metaKey: true }))).toBe(null);
|
expect(getDropdownNavigationKey(keyEvent('p', { ctrlKey: true, metaKey: true }))).toBe(null);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { Menu as BaseMenu } from "@base-ui/react/menu"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import { shortcutRegistry } from "@/lib/shortcuts";
|
import { shortcutRegistry } from "@/lib/shortcuts";
|
||||||
import { getDropdownMenuNavigationKey } from "./dropdown-menu-keyboard";
|
import { isIMECompositionEvent } from "@/lib/ime";
|
||||||
|
import { getDropdownNavigationKey } from "./dropdown-navigation";
|
||||||
import { dropdownMenuItemClass, dropdownMenuPopupClass, dropdownMenuSeparatorClass, dropdownMenuSubTriggerClass } from "./dropdown-menu.styles";
|
import { dropdownMenuItemClass, dropdownMenuPopupClass, dropdownMenuSeparatorClass, dropdownMenuSubTriggerClass } from "./dropdown-menu.styles";
|
||||||
|
|
||||||
type AsChildProps = { asChild?: boolean };
|
type AsChildProps = { asChild?: boolean };
|
||||||
@@ -141,8 +142,8 @@ function DropdownMenuContent({
|
|||||||
|
|
||||||
const handleKeyDown: NonNullable<React.ComponentProps<typeof BaseMenu.Popup>['onKeyDown']> = (event) => {
|
const handleKeyDown: NonNullable<React.ComponentProps<typeof BaseMenu.Popup>['onKeyDown']> = (event) => {
|
||||||
onKeyDown?.(event);
|
onKeyDown?.(event);
|
||||||
if (event.defaultPrevented || event.isPropagationStopped() || event.nativeEvent.isComposing) return;
|
if (event.defaultPrevented || event.isPropagationStopped() || isIMECompositionEvent(event)) return;
|
||||||
const navigationKey = getDropdownMenuNavigationKey(event);
|
const navigationKey = getDropdownNavigationKey(event);
|
||||||
if (!navigationKey) return;
|
if (!navigationKey) return;
|
||||||
|
|
||||||
event.currentTarget.dispatchEvent(new KeyboardEvent('keydown', {
|
event.currentTarget.dispatchEvent(new KeyboardEvent('keydown', {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
export function getDropdownMenuNavigationKey(event: Pick<KeyboardEvent, 'key' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>): 'ArrowDown' | 'ArrowUp' | null {
|
export function getDropdownNavigationKey(event: Pick<KeyboardEvent, 'key' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>): 'ArrowDown' | 'ArrowUp' | null {
|
||||||
if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return null;
|
if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return null;
|
||||||
if (event.key.toLowerCase() === 'n') return 'ArrowDown';
|
if (event.key.toLowerCase() === 'n') return 'ArrowDown';
|
||||||
if (event.key.toLowerCase() === 'p') return 'ArrowUp';
|
if (event.key.toLowerCase() === 'p') return 'ArrowUp';
|
||||||
@@ -8,6 +8,9 @@ import { cn } from "@/lib/utils"
|
|||||||
import { dropdownTriggerVariants } from "@/components/ui/dropdown-trigger"
|
import { dropdownTriggerVariants } from "@/components/ui/dropdown-trigger"
|
||||||
import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay";
|
import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay";
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
|
import { shortcutRegistry } from "@/lib/shortcuts";
|
||||||
|
import { isIMECompositionEvent } from "@/lib/ime";
|
||||||
|
import { getDropdownNavigationKey } from "./dropdown-navigation";
|
||||||
|
|
||||||
type AsChildProps = { asChild?: boolean };
|
type AsChildProps = { asChild?: boolean };
|
||||||
type AsChildRenderProps = {
|
type AsChildRenderProps = {
|
||||||
@@ -36,14 +39,21 @@ type SelectRootProps<Value extends string = string> = Omit<
|
|||||||
value?: Value;
|
value?: Value;
|
||||||
defaultValue?: Value;
|
defaultValue?: Value;
|
||||||
onValueChange?: (value: Value, eventDetails: SelectRootChangeEventDetails) => void;
|
onValueChange?: (value: Value, eventDetails: SelectRootChangeEventDetails) => void;
|
||||||
|
disableGlobalShortcuts?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Select<Value extends string = string>({
|
function Select<Value extends string = string>({
|
||||||
onValueChange,
|
onValueChange,
|
||||||
modal = false,
|
modal = false,
|
||||||
|
disableGlobalShortcuts = false,
|
||||||
|
open,
|
||||||
|
defaultOpen,
|
||||||
|
onOpenChange,
|
||||||
...props
|
...props
|
||||||
}: SelectRootProps<Value>) {
|
}: SelectRootProps<Value>) {
|
||||||
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
|
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
|
||||||
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false);
|
||||||
|
const isOpen = open ?? uncontrolledOpen;
|
||||||
const portalContextValue = React.useMemo<SelectPortalContextValue>(() => ({
|
const portalContextValue = React.useMemo<SelectPortalContextValue>(() => ({
|
||||||
portalContainer,
|
portalContainer,
|
||||||
setPortalContainer,
|
setPortalContainer,
|
||||||
@@ -58,9 +68,26 @@ function Select<Value extends string = string>({
|
|||||||
[onValueChange]
|
[onValueChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (!disableGlobalShortcuts || !isOpen) return;
|
||||||
|
return shortcutRegistry.suspend();
|
||||||
|
}, [disableGlobalShortcuts, isOpen]);
|
||||||
|
|
||||||
|
const handleOpenChange: NonNullable<React.ComponentProps<typeof BaseSelect.Root>['onOpenChange']> = (nextOpen, eventDetails) => {
|
||||||
|
if (open === undefined) setUncontrolledOpen(nextOpen);
|
||||||
|
onOpenChange?.(nextOpen, eventDetails);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectPortalContext.Provider value={portalContextValue}>
|
<SelectPortalContext.Provider value={portalContextValue}>
|
||||||
<BaseSelect.Root {...props} modal={modal} onValueChange={handleValueChange} />
|
<BaseSelect.Root
|
||||||
|
{...props}
|
||||||
|
modal={modal}
|
||||||
|
open={open}
|
||||||
|
defaultOpen={defaultOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
onValueChange={handleValueChange}
|
||||||
|
/>
|
||||||
</SelectPortalContext.Provider>
|
</SelectPortalContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,12 +201,28 @@ function SelectContent({
|
|||||||
sideOffset,
|
sideOffset,
|
||||||
side,
|
side,
|
||||||
align,
|
align,
|
||||||
|
onKeyDown,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof BaseSelect.Popup> & SelectContentExtra) {
|
}: React.ComponentProps<typeof BaseSelect.Popup> & SelectContentExtra) {
|
||||||
const portalContext = React.useContext(SelectPortalContext);
|
const portalContext = React.useContext(SelectPortalContext);
|
||||||
const alignItemWithTrigger = position === "item-aligned";
|
const alignItemWithTrigger = position === "item-aligned";
|
||||||
const portalContainer = portalContext?.portalContainer ?? null;
|
const portalContainer = portalContext?.portalContainer ?? null;
|
||||||
|
|
||||||
|
const handleKeyDown: NonNullable<React.ComponentProps<typeof BaseSelect.Popup>['onKeyDown']> = (event) => {
|
||||||
|
onKeyDown?.(event);
|
||||||
|
if (event.defaultPrevented || event.isPropagationStopped() || isIMECompositionEvent(event)) return;
|
||||||
|
const navigationKey = getDropdownNavigationKey(event);
|
||||||
|
if (!navigationKey) return;
|
||||||
|
|
||||||
|
event.currentTarget.dispatchEvent(new KeyboardEvent('keydown', {
|
||||||
|
key: navigationKey,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}));
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseSelect.Portal container={portalToBody ? undefined : portalContainer || undefined}>
|
<BaseSelect.Portal container={portalToBody ? undefined : portalContainer || undefined}>
|
||||||
<BaseSelect.Positioner
|
<BaseSelect.Positioner
|
||||||
@@ -203,6 +246,7 @@ function SelectContent({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<ScrollableOverlay
|
<ScrollableOverlay
|
||||||
outerClassName={cn(
|
outerClassName={cn(
|
||||||
@@ -248,7 +292,7 @@ function SelectItem({
|
|||||||
<BaseSelect.Item
|
<BaseSelect.Item
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[selected]:bg-interactive-selection data-[selected]:text-interactive-selection-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ The settings recorder also stops at two chords. It keeps the recording local unt
|
|||||||
|
|
||||||
# Dispatching
|
# Dispatching
|
||||||
|
|
||||||
`ShortcutDispatcher` is DOM-independent. It invokes only currently registered handlers, resolves bindings when dispatching, and holds an active sequence prefix for 1500ms. The application keydown route clears that prefix on window blur and consumes Escape only when it cancels a prefix. A handler returns `false` to leave the completed binding unconsumed. When a sequence prefix is active, only its second key is dispatched during window capture so local input handlers cannot block it; unconsumed keys retain local input behavior, while consumed keys are prevented and stopped. Normal application shortcuts remain window-bubble listeners.
|
`ShortcutDispatcher` is DOM-independent. It invokes only currently registered handlers, resolves bindings when dispatching, and holds an active sequence prefix for 1500ms. The application keydown route clears that prefix on window blur and consumes Escape only when it cancels a prefix. A handler returns `false` to leave the completed binding unconsumed. When a sequence prefix is active, only its second key is dispatched during window capture so local input handlers cannot block it; an exact second key remains eligible during IME composition and is prevented when handled, while an IME mismatch clears the prefix and retains normal composition input. Normal application shortcuts remain window-bubble listeners.
|
||||||
|
|
||||||
`shortcutRegistry.suspend()` disables all application handlers and returns an idempotent cleanup. Suspensions nest; handlers resume only after the final cleanup. Starting or ending a suspension invalidates every pending dispatcher prefix, so stale second keys and Escape cannot consume it.
|
`shortcutRegistry.suspend()` disables all application handlers and returns an idempotent cleanup. Suspensions nest; handlers resume only after the final cleanup. Starting or ending a suspension invalidates every pending dispatcher prefix, so stale second keys and Escape cannot consume it.
|
||||||
|
|
||||||
Shared `DropdownMenu` can opt into this boundary with `disableGlobalShortcuts`; it suspends while open for both controlled and uncontrolled menus and resumes on close or unmount.
|
Shared `DropdownMenu` and `Select` can opt into this boundary with `disableGlobalShortcuts`; they suspend while open for both controlled and uncontrolled popups and resume on close or unmount.
|
||||||
|
|
||||||
Terminal capture, Escape abort priming, and the shifted reverse-agent chord are input-boundary exceptions. They preserve their target-specific semantics and invoke the registered application handler rather than duplicating command behavior.
|
Terminal capture, Escape abort priming, and the shifted reverse-agent chord are input-boundary exceptions. They preserve their target-specific semantics and invoke the registered application handler rather than duplicating command behavior.
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,34 @@ describe('ShortcutDispatcher', () => {
|
|||||||
expect(calls).toEqual(['sequence']);
|
expect(calls).toEqual(['sequence']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('consumes a matching captured prefix key during IME composition', () => {
|
||||||
|
for (const compositionState of [{ isComposing: true }, { keyCode: 229 }]) {
|
||||||
|
const registry = new ShortcutRegistry();
|
||||||
|
const calls: string[] = [];
|
||||||
|
registry.register('open_session_list', () => { calls.push('sequence'); });
|
||||||
|
const dispatcher = new ShortcutDispatcher({ registry, getBinding: () => 'mod+s l' });
|
||||||
|
const secondKey = key('l', compositionState);
|
||||||
|
|
||||||
|
expect(dispatcher.dispatch(key('s', { ctrlKey: true }))).toBe(true);
|
||||||
|
expect(dispatcher.dispatchActivePrefix(secondKey)).toBe(true);
|
||||||
|
expect(dispatcher.consumeCapturedPrefixEvent(secondKey)).toBe(true);
|
||||||
|
expect(calls).toEqual(['sequence']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears an active prefix but preserves an unmatched IME key', () => {
|
||||||
|
const registry = new ShortcutRegistry();
|
||||||
|
const calls: string[] = [];
|
||||||
|
registry.register('open_session_list', () => { calls.push('sequence'); });
|
||||||
|
const dispatcher = new ShortcutDispatcher({ registry, getBinding: () => 'mod+s l' });
|
||||||
|
const secondKey = key('x', { isComposing: true });
|
||||||
|
|
||||||
|
dispatcher.dispatch(key('s', { ctrlKey: true }));
|
||||||
|
expect(dispatcher.dispatchActivePrefix(secondKey)).toBe(false);
|
||||||
|
expect(dispatcher.hasActivePrefix()).toBe(false);
|
||||||
|
expect(calls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test('stops after the first handler that accepts a conflicting binding', () => {
|
test('stops after the first handler that accepts a conflicting binding', () => {
|
||||||
const registry = new ShortcutRegistry();
|
const registry = new ShortcutRegistry();
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from './bindings';
|
} from './bindings';
|
||||||
import { type ShortcutHandler, ShortcutRegistry } from './registry';
|
import { type ShortcutHandler, ShortcutRegistry } from './registry';
|
||||||
import type { ShortcutActionId } from './schema';
|
import type { ShortcutActionId } from './schema';
|
||||||
|
import { isIMECompositionEvent } from '../ime';
|
||||||
|
|
||||||
const SEQUENCE_TIMEOUT_MS = 1500;
|
const SEQUENCE_TIMEOUT_MS = 1500;
|
||||||
const MODIFIER_KEYS = new Set(['alt', 'control', 'meta', 'shift']);
|
const MODIFIER_KEYS = new Set(['alt', 'control', 'meta', 'shift']);
|
||||||
@@ -38,7 +39,7 @@ export class ShortcutDispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(event: KeyboardEvent): boolean {
|
dispatch(event: KeyboardEvent): boolean {
|
||||||
if (event.repeat || event.isComposing || MODIFIER_KEYS.has(event.key.toLowerCase())) {
|
if (event.repeat || isIMECompositionEvent(event) || MODIFIER_KEYS.has(event.key.toLowerCase())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (event.key === 'Escape' && this.hasActivePrefix()) {
|
if (event.key === 'Escape' && this.hasActivePrefix()) {
|
||||||
@@ -48,11 +49,7 @@ export class ShortcutDispatcher {
|
|||||||
|
|
||||||
const matches = this.getMatches();
|
const matches = this.getMatches();
|
||||||
if (this.prefix) {
|
if (this.prefix) {
|
||||||
const pending = matches.filter((match) => (
|
const pending = this.getPrefixMatches(matches, event);
|
||||||
match.chords.length === 2
|
|
||||||
&& match.chords[0] === this.prefix
|
|
||||||
&& eventMatchesShortcut(event, match.chords[1])
|
|
||||||
));
|
|
||||||
if (pending.length > 0) {
|
if (pending.length > 0) {
|
||||||
this.clear();
|
this.clear();
|
||||||
return this.invoke(pending, event);
|
return this.invoke(pending, event);
|
||||||
@@ -109,6 +106,14 @@ export class ShortcutDispatcher {
|
|||||||
|
|
||||||
dispatchActivePrefix(event: KeyboardEvent): boolean {
|
dispatchActivePrefix(event: KeyboardEvent): boolean {
|
||||||
this.capturedPrefixEvents.add(event);
|
this.capturedPrefixEvents.add(event);
|
||||||
|
if (isIMECompositionEvent(event)) {
|
||||||
|
if (event.repeat || MODIFIER_KEYS.has(event.key.toLowerCase()) || !this.hasActivePrefix()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const pending = this.getPrefixMatches(this.getMatches(), event);
|
||||||
|
this.clear();
|
||||||
|
return pending.length > 0 ? this.invoke(pending, event) : false;
|
||||||
|
}
|
||||||
return this.dispatch(event);
|
return this.dispatch(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +132,14 @@ export class ShortcutDispatcher {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPrefixMatches(matches: BindingMatch[], event: KeyboardEvent): BindingMatch[] {
|
||||||
|
return matches.filter((match) => (
|
||||||
|
match.chords.length === 2
|
||||||
|
&& match.chords[0] === this.prefix
|
||||||
|
&& eventMatchesShortcut(event, match.chords[1])
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
private getMatches(): BindingMatch[] {
|
private getMatches(): BindingMatch[] {
|
||||||
const matches: BindingMatch[] = [];
|
const matches: BindingMatch[] = [];
|
||||||
for (const actionId of this.options.registry.actionIds()) {
|
for (const actionId of this.options.registry.actionIds()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user