mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
* fix(sidebar): run previous and next worktree shortcuts from a text field The worktree list swallowed every key while a text field was focused, so the bound shortcut did nothing in chat, markdown, and the code editor. * fix(sidebar): keep the default worktree chord with text fields Previous/next worktree now carries a text-field rule in its keybinding definition. In a text field the default Cmd/Ctrl+Shift+Up/Down keeps its select-to-start/end meaning, while a chord the user bound still switches worktrees from chat, markdown and the code editor. Option and AltGr chords that type a character stay with the field. * fix(sidebar): let user-bound Option arrow chords leave text fields On Mac only Option chords that type a character or dead key stay with a focused text field, so a bound Option+Down now switches worktrees from chat, markdown and the code editor.
160 lines
4.8 KiB
TypeScript
160 lines
4.8 KiB
TypeScript
import type {
|
|
KeybindingActionId,
|
|
KeybindingInput,
|
|
KeybindingOverrides,
|
|
KeybindingMatchOptions,
|
|
ParsedKeybinding
|
|
} from './types'
|
|
import { DEFINITIONS_BY_ID, DIGIT_INDEX_KEY_PATTERN, isDigitIndexActionId } from './definitions'
|
|
import { canonicalizeParsedKeybinding, parseKeybinding } from './parser'
|
|
import {
|
|
platformModifiers,
|
|
modifierStateMatches,
|
|
keyMatches,
|
|
digitKeyMatches,
|
|
resolveModifierToken
|
|
} from './matching-key'
|
|
import {
|
|
getDefaultBindings,
|
|
getEffectiveKeybindingsForAction,
|
|
keybindingIsActiveInContext
|
|
} from './effective'
|
|
|
|
export function keybindingMatchesInput(
|
|
binding: string,
|
|
input: KeybindingInput,
|
|
platform: NodeJS.Platform
|
|
): boolean {
|
|
const parsed = parseKeybinding(binding)
|
|
if (!parsed) {
|
|
return false
|
|
}
|
|
// A double-tap binding matches only a synthetic double-tap input (and vice-versa), resolved per platform.
|
|
if (parsed.doubleTapModifier) {
|
|
return (
|
|
input.doubleTapModifier !== undefined &&
|
|
resolveModifierToken(parsed.doubleTapModifier, platform) ===
|
|
resolveModifierToken(input.doubleTapModifier, platform)
|
|
)
|
|
}
|
|
if (input.doubleTapModifier !== undefined) {
|
|
return false
|
|
}
|
|
return (
|
|
modifierStateMatches(parsed, input, platform) && keyMatches(parsed.key, input, parsed, platform)
|
|
)
|
|
}
|
|
|
|
export function keybindingConflictIdentityForParsed(
|
|
parsed: ParsedKeybinding,
|
|
platform: NodeJS.Platform
|
|
): string {
|
|
if (parsed.doubleTapModifier) {
|
|
return `DoubleTap:${resolveModifierToken(parsed.doubleTapModifier, platform)}`
|
|
}
|
|
const modifiers = platformModifiers(parsed, platform)
|
|
return [
|
|
modifiers.meta ? 'Meta' : '',
|
|
modifiers.control ? 'Control' : '',
|
|
modifiers.alt ? 'Alt' : '',
|
|
modifiers.shift ? 'Shift' : '',
|
|
parsed.key
|
|
].join('+')
|
|
}
|
|
|
|
export function getKeybindingConflictIdentity(binding: string, platform: NodeJS.Platform): string {
|
|
const parsed = parseKeybinding(binding)
|
|
return parsed ? keybindingConflictIdentityForParsed(parsed, platform) : binding
|
|
}
|
|
|
|
export function keybindingConflictIdentities(
|
|
actionId: KeybindingActionId,
|
|
binding: string,
|
|
platform: NodeJS.Platform
|
|
): readonly string[] {
|
|
const exact = getKeybindingConflictIdentity(binding, platform)
|
|
if (!isDigitIndexActionId(actionId)) {
|
|
return [exact]
|
|
}
|
|
const parsed = parseKeybinding(binding)
|
|
if (!parsed || parsed.doubleTapModifier || !DIGIT_INDEX_KEY_PATTERN.test(parsed.key)) {
|
|
return [exact]
|
|
}
|
|
return Array.from({ length: 9 }, (_, index) =>
|
|
keybindingConflictIdentityForParsed({ ...parsed, key: String(index + 1) }, platform)
|
|
)
|
|
}
|
|
|
|
export function keybindingMatchesAction(
|
|
actionId: KeybindingActionId,
|
|
input: KeybindingInput,
|
|
platform: NodeJS.Platform,
|
|
overrides?: KeybindingOverrides,
|
|
options: KeybindingMatchOptions = {}
|
|
): boolean {
|
|
const definition = DEFINITIONS_BY_ID.get(actionId)
|
|
if (!definition) {
|
|
return false
|
|
}
|
|
if (!keybindingIsActiveInContext(definition, options)) {
|
|
return false
|
|
}
|
|
return getEffectiveKeybindingsForAction(actionId, platform, overrides).some((binding) =>
|
|
keybindingMatchesInput(binding, input, platform)
|
|
)
|
|
}
|
|
|
|
// Why: decided from the definition and the pressed chord, so re-saving a default never takes it from the field.
|
|
export function keybindingRunsInTextField(
|
|
actionId: KeybindingActionId,
|
|
input: KeybindingInput,
|
|
platform: NodeJS.Platform
|
|
): boolean {
|
|
const definition = DEFINITIONS_BY_ID.get(actionId)
|
|
return (
|
|
definition?.allowInTextField === 'non-default-chords' &&
|
|
!getDefaultBindings(definition, platform).some((binding) =>
|
|
keybindingMatchesInput(binding, input, platform)
|
|
)
|
|
)
|
|
}
|
|
|
|
export function digitFromInput(input: KeybindingInput, platform: NodeJS.Platform): string | null {
|
|
for (let value = 1; value <= 9; value++) {
|
|
const digit = String(value)
|
|
if (digitKeyMatches(input, digit, platform)) {
|
|
return digit
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Why: a digit-index row's representative chord fires for any 1-9 — reuse its modifiers with the pressed digit via the normal matcher.
|
|
export function matchKeybindingDigitIndex(
|
|
actionId: KeybindingActionId,
|
|
input: KeybindingInput,
|
|
platform: NodeJS.Platform,
|
|
overrides?: KeybindingOverrides,
|
|
options: KeybindingMatchOptions = {}
|
|
): number | null {
|
|
const definition = DEFINITIONS_BY_ID.get(actionId)
|
|
if (!definition || !keybindingIsActiveInContext(definition, options)) {
|
|
return null
|
|
}
|
|
const digit = digitFromInput(input, platform)
|
|
if (!digit) {
|
|
return null
|
|
}
|
|
for (const binding of getEffectiveKeybindingsForAction(actionId, platform, overrides)) {
|
|
const parsed = parseKeybinding(binding)
|
|
if (!parsed || parsed.doubleTapModifier || !DIGIT_INDEX_KEY_PATTERN.test(parsed.key)) {
|
|
continue
|
|
}
|
|
const candidate = canonicalizeParsedKeybinding({ ...parsed, key: digit })
|
|
if (keybindingMatchesInput(candidate, input, platform)) {
|
|
return Number(digit) - 1
|
|
}
|
|
}
|
|
return null
|
|
}
|