Skip to content

Commit

Permalink
Implement fallbackBindings for mode specification (#40)
Browse files Browse the repository at this point in the history
This implements a `fallbackBinding` field. It can be set to the name of
an existing mode. It causes any unspecified keys for the given mode
fallback to the bindings provided in the fallback. A limitation of the
current implementation is that to work properly, the bindings for this
mode must be defined before the fallback bindings.
  • Loading branch information
haberdashPI authored Oct 10, 2024
1 parent 33d7315 commit 34b34cd
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/web/keybindings/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof modeSpec>;
Expand Down Expand Up @@ -385,6 +386,7 @@ export const bindingSpec = z
default: false,
recordEdits: false,
highlight: 'Highlight',
fallbackBindings: '',
});
}
return xs;
Expand Down
44 changes: 33 additions & 11 deletions src/web/keybindings/processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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<string, string> = {};
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]}));
}
});
}
Expand Down
30 changes: 30 additions & 0 deletions test/specs/config.ux.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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-'));
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit 34b34cd

Please sign in to comment.