diff --git a/src/web/keybindings/parsing.ts b/src/web/keybindings/parsing.ts index 09cd633..ae0ff82 100644 --- a/src/web/keybindings/parsing.ts +++ b/src/web/keybindings/parsing.ts @@ -327,6 +327,7 @@ const modeSpec = z.object({ .enum(['Line', 'Block', 'Underline', 'LineThin', 'BlockOutline', 'UnderlineThin']) .default('Line'), onType: doArgs.optional(), + fallbackBindings: z.string().optional().default(''), lineNumbers: z.enum(['relative', 'on', 'off', 'interval']).optional(), }); export type ModeSpec = z.output; @@ -385,6 +386,7 @@ export const bindingSpec = z default: false, recordEdits: false, highlight: 'Highlight', + fallbackBindings: '', }); } return xs; diff --git a/src/web/keybindings/processing.ts b/src/web/keybindings/processing.ts index f38c71a..23da3e1 100644 --- a/src/web/keybindings/processing.ts +++ b/src/web/keybindings/processing.ts @@ -48,7 +48,7 @@ export function processBindings(spec: FullBindingSpec): [Bindings, string[]] { const docs = resolveDocItems(indexedItems, spec.doc || [], problems); let items = indexedItems.map((item, i) => requireTransientSequence(item, i, problems)); items = expandPrefixes(items); - items = expandModes(items, spec.mode, problems); + items = expandModes(items, spec.mode, problems, spec.mode); items = expandDocsToDuplicates(items); const r = expandKeySequencesAndResolveDuplicates(items, problems); items = r[0]; @@ -467,27 +467,49 @@ function expandPrefixes(items: BindingItem[]) { }); } -function expandModes(items: BindingItem[], validModes: ModeSpec[], problems: string[]) { +function expandModes( + items: BindingItem[], + validModes: ModeSpec[], + problems: string[], + modes: ModeSpec[] +) { const defaultMode = validModes.filter(x => x.default)[0]; // validation should guarantee a single match + const fallbacks: Record = {}; + for (const mode of modes) { + if (mode.fallbackBindings) { + fallbacks[mode.fallbackBindings] = mode.name; + } + } return flatMap(items, (item: BindingItem): BindingItem[] => { - let modes = item.mode || [defaultMode.name]; - if (modes.length > 0 && modes[0].startsWith('!')) { - if (modes.some(x => !x.startsWith('!'))) { + let itemModes = item.mode || [defaultMode.name]; + if (itemModes.length > 0 && itemModes[0].startsWith('!')) { + if (itemModes.some(x => !x.startsWith('!'))) { problems.push( - `Either all or none of the modes for binding ${item.key} ` + + `Either all or none of the itemModes for binding ${item.key} ` + "must be prefixed with '!'" ); - modes = modes.filter(x => x.startsWith('!')); + itemModes = itemModes.filter(x => x.startsWith('!')); } - const exclude = modes.map(m => m.slice(1)); - modes = validModes + const exclude = itemModes.map(m => m.slice(1)); + itemModes = validModes .map(x => x.name) .filter(mode => !exclude.some(x => x === mode)); } - if (modes.length === 0) { + + // add modes that are implicitly present due to fallbacks + const implicitModes: string[] = []; + for (const mode of itemModes) { + const implicitMode = fallbacks[mode]; + if (implicitMode) { + implicitModes.push(implicitMode); + } + } + itemModes = itemModes.concat(implicitModes); + + if (itemModes.length === 0) { return [item]; } else { - return modes.map(m => ({...item, mode: [m]})); + return itemModes.map(m => ({...item, mode: [m]})); } }); } diff --git a/test/specs/config.ux.mts b/test/specs/config.ux.mts index afd62b2..74d5d65 100644 --- a/test/specs/config.ux.mts +++ b/test/specs/config.ux.mts @@ -59,6 +59,26 @@ describe('Configuration', () => { name = "insert" key = "ctrl+i" command = "master-key.enterInsert" + mode = "normal" + + [[bind]] + name = "normal-left mode" + key = "ctrl+u" + command = "master-key.setMode" + args.value = "normal-left" + + [[mode]] + name = "normal-left" + fallbackBindings = "normal" + + [[bind]] + path = "motion" + key = "ctrl+h" + mode = "normal-left" + name = "left" + when = "editorTextFocus" + command = "cursorMove" + args.to = "left" `); folder = fs.mkdtempSync(path.join(os.tmpdir(), 'master-key-test-')); @@ -153,6 +173,16 @@ describe('Configuration', () => { expect(statusBarClasses).not.toMatch(/warning-kind/); }); + it('Can add fallback bindings', async () => { + await editor.moveCursor(1, 1); + await enterModalKeys('escape'); + editor = await setupEditor('A simple test'); + await enterModalKeys(['ctrl', 'u']); + await waitForMode('normal-left'); + await movesCursorInEditor(() => enterModalKeys(['ctrl', 'l']), [0, 1], editor); + await movesCursorInEditor(() => enterModalKeys(['ctrl', 'h']), [0, -1], editor); + }); + it('Can be loaded from a directory', async () => { if (!editor) { editor = await setupEditor('A simple test');