Skip to content

Commit fb214a6

Browse files
authored
Support repeated keys (#13495)
* Support repeated keys A key binding may now be specified with an option, `allowRepeat`. This option is now applied to: - Component list: Up/down keys - All text editors: Arrow keys, backspace, delete Fixes #12549. * Prettier
1 parent c40bbe2 commit fb214a6

File tree

2 files changed

+43
-22
lines changed

2 files changed

+43
-22
lines changed

app/gui/src/project-view/bindings.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ export const documentationEditorFormatBindings = defineKeybinds('documentation-e
1212
})
1313

1414
export const textEditorsCommonBindings = defineKeybinds('text-editors-common-bindings', {
15-
'textEditor.moveLeft': ['ArrowLeft'],
16-
'textEditor.moveRight': ['ArrowRight'],
17-
'textEditor.deleteBack': ['Backspace'],
18-
'textEditor.deleteForward': ['Delete'],
15+
'textEditor.moveLeft': [{ key: 'ArrowLeft', allowRepeat: true }],
16+
'textEditor.moveRight': [{ key: 'ArrowRight', allowRepeat: true }],
17+
'textEditor.deleteBack': [{ key: 'Backspace', allowRepeat: true }],
18+
'textEditor.deleteForward': [{ key: 'Delete', allowRepeat: true }],
1919
'textEditor.cut': ['Mod+X'],
2020
'textEditor.copy': ['Mod+C'],
2121
'textEditor.paste': ['Mod+V'],
@@ -27,8 +27,8 @@ export const textEditorsMultilineBindings = defineKeybinds('text-editors-multili
2727
})
2828

2929
export const listBindings = defineKeybinds('list', {
30-
'list.moveUp': ['ArrowUp'],
31-
'list.moveDown': ['ArrowDown'],
30+
'list.moveUp': [{ key: 'ArrowUp', allowRepeat: true }],
31+
'list.moveDown': [{ key: 'ArrowDown', allowRepeat: true }],
3232
'list.accept': ['Enter'],
3333
})
3434

app/gui/src/project-view/util/shortcuts.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isMacLike } from '@/composables/events'
22
import { assert } from '@/util/assert'
3+
import * as objects from 'enso-common/src/utilities/data/object'
34

45
/** All possible modifier keys. */
56
export type ModifierKey = keyof typeof RAW_MODIFIER_FLAG
@@ -258,8 +259,11 @@ type AutocompleteKeybind<T extends string, Key extends string = never> =
258259
: [Key] extends [never] ? SuggestedKeybindSegment
259260
: Key
260261

261-
type AutocompleteKeybinds<T extends string[]> = {
262-
[K in keyof T]: AutocompleteKeybind<T[K]>
262+
type AutocompleteKeybinds<T extends KeybindDefinition[]> = {
263+
[K in keyof T]: T[K] extends FullKeybindDefinition ?
264+
FullKeybindDefinition<AutocompleteKeybind<T[K]['key']>>
265+
: T[K] extends string ? AutocompleteKeybind<T[K]>
266+
: never
263267
}
264268

265269
/** Some keys have not human-friendly name, these are overwritten here for {@link BindingInfo}. */
@@ -275,7 +279,7 @@ const HUMAN_READABLE_KEYS: Partial<Record<Key, string>> = {
275279

276280
// `never extends T ? Result : InferenceSource` is a trick to unify `T` with the actual type of the
277281
// argument.
278-
type Keybinds<T extends Record<K, string[]>, K extends keyof T = keyof T> =
282+
type Keybinds<T extends Record<K, KeybindDefinition[]>, K extends keyof T = keyof T> =
279283
never extends T ?
280284
{
281285
[K in keyof T]: AutocompleteKeybinds<T[K]>
@@ -293,6 +297,14 @@ const definedNamespaces = new Set<string>()
293297

294298
export const DefaultHandler = Symbol('default handler')
295299

300+
interface KeybindOptions {
301+
allowRepeat?: boolean
302+
}
303+
export interface FullKeybindDefinition<T = string> extends KeybindOptions {
304+
key: T
305+
}
306+
export type KeybindDefinition = string | FullKeybindDefinition
307+
296308
/**
297309
* Define key bindings for given namespace.
298310
*
@@ -359,7 +371,7 @@ export const DefaultHandler = Symbol('default handler')
359371
* ```
360372
*/
361373
export function defineKeybinds<
362-
T extends Record<BindingName, [] | string[]>,
374+
T extends Record<BindingName, [] | KeybindDefinition[]>,
363375
BindingName extends keyof T = keyof T,
364376
>(namespace: string, bindings: Keybinds<T>) {
365377
if (definedNamespaces.has(namespace)) {
@@ -370,12 +382,21 @@ export function defineKeybinds<
370382
const keyboardShortcuts: Partial<Record<Key_, Record<ModifierFlags, Set<BindingName>>>> = {}
371383
const mouseShortcuts: Record<PointerButtonFlags, Record<ModifierFlags, Set<BindingName>>> = []
372384

385+
function fullKeybind(keybind: KeybindDefinition): FullKeybindDefinition {
386+
return typeof keybind === 'string' ? { key: keybind } : keybind
387+
}
388+
373389
const bindingsInfo = {} as Record<BindingName, BindingInfo>
374-
for (const [name_, keybindStrings] of Object.entries(bindings)) {
390+
const bindingsOptions = {} as Record<BindingName, KeybindOptions>
391+
for (const [name_, keybindValues] of Object.entries(bindings)) {
375392
const name = name_ as BindingName
376-
for (const keybindString of keybindStrings as string[]) {
377-
const { bind: keybind, info } = parseKeybindString(keybindString)
378-
if (bindingsInfo[name] == null) bindingsInfo[name] = info
393+
for (const keybindValue of keybindValues as KeybindDefinition[]) {
394+
const keybindDef = fullKeybind(keybindValue)
395+
const { bind: keybind, info } = parseKeybindString(keybindDef.key)
396+
if (bindingsInfo[name] == null) {
397+
bindingsInfo[name] = info
398+
bindingsOptions[name] = keybindDef
399+
}
379400
switch (keybind.type) {
380401
case 'keybind': {
381402
const shortcutsByKey = (keyboardShortcuts[keybind.key] ??= [])
@@ -414,27 +435,27 @@ export function defineKeybinds<
414435
handlers: Partial<
415436
Record<BindingName | typeof DefaultHandler, (event: Event_) => boolean | void>
416437
>,
417-
): (event: Event_, stopAndPrevent?: boolean) => boolean {
418-
return (event, stopAndPrevent = true) => {
419-
// Do not handle repeated keyboard events (held down key).
420-
if (event instanceof KeyboardEvent && event.repeat) return false
421-
438+
): (event: Event_) => boolean {
439+
return (event) => {
422440
const eventModifierFlags = modifierFlagsForEvent(event)
423441
const keybinds =
424442
event instanceof KeyboardEvent ?
425443
keyboardShortcuts[eventKey(event)]?.[eventModifierFlags]
426444
: mouseShortcuts[buttonFlagsForEvent(event)]?.[eventModifierFlags]
427445

446+
const isRepeat = event instanceof KeyboardEvent && event.repeat
428447
let handled = false
429448
if (keybinds != null) {
430-
for (const bindingName in handlers) {
449+
for (const bindingName of objects.unsafeKeys(handlers)) {
450+
if (bindingName === DefaultHandler) continue
451+
if (isRepeat && !bindingsOptions[bindingName].allowRepeat) continue
431452
if (keybinds.has(bindingName as BindingName)) {
432453
const handle = handlers[bindingName as BindingName]
433454
handled = handle && handle(event) !== false
434455
if (DEBUG_LOG)
435456
console.log(
436457
`Event ${event.type} (${event instanceof KeyboardEvent ? event.key : buttonFlagsForEvent(event)})`,
437-
`${handled ? 'handled' : 'processed'} by ${namespace}.${bindingName}`,
458+
`${handled ? 'handled' : 'processed'} by ${namespace}.${String(bindingName)}`,
438459
)
439460
if (handled) break
440461
}
@@ -443,7 +464,7 @@ export function defineKeybinds<
443464
if (!handled && handlers[DefaultHandler] != null) {
444465
handled = handlers[DefaultHandler](event) !== false
445466
}
446-
if (handled && stopAndPrevent) {
467+
if (handled) {
447468
event.stopImmediatePropagation()
448469
// We don't prevent default on PointerEvents, because it may prevent emitting
449470
// mousedown/mouseup events, on which external libraries may rely (like AGGrid for hiding

0 commit comments

Comments
 (0)