diff --git a/packages/cursorless-vscode-e2e/src/suite/keyboard/basic.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/keyboard/basic.vscode.test.ts index c216316019..734eb1218e 100644 --- a/packages/cursorless-vscode-e2e/src/suite/keyboard/basic.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/keyboard/basic.vscode.test.ts @@ -76,6 +76,20 @@ const testCases: TestCase[] = [ keySequence: ["da", "aw", "wp", "aw", "wp"], finalContent: "((a))\n", }, + { + name: "modifier range", + initialContent: "aaa bbb ccc ddd", + // clear bat past its next token + keySequence: ["db", "fk", "n", "st", "c"], + finalContent: "aaa ddd", + }, + { + name: "modifier list", + initialContent: "aaa bbb ccc ddd", + // clear bat and its second next token + keySequence: ["db", "fa", "2", "n", "st", "c"], + finalContent: "aaa ccc ", + }, ]; suite("Basic keyboard test", async function () { diff --git a/packages/cursorless-vscode/src/keyboard/KeyboardCommandHandler.ts b/packages/cursorless-vscode/src/keyboard/KeyboardCommandHandler.ts index 0d4114f9ef..0a851ae5b3 100644 --- a/packages/cursorless-vscode/src/keyboard/KeyboardCommandHandler.ts +++ b/packages/cursorless-vscode/src/keyboard/KeyboardCommandHandler.ts @@ -5,7 +5,9 @@ import { SimpleKeyboardActionDescriptor, SpecificKeyboardActionDescriptor, } from "./KeyboardActionType"; -import KeyboardCommandsTargeted from "./KeyboardCommandsTargeted"; +import KeyboardCommandsTargeted, { + TargetingMode, +} from "./KeyboardCommandsTargeted"; import { ModalVscodeCommandDescriptor } from "./TokenTypes"; import { surroundingPairsDelimiters } from "@cursorless/cursorless-engine"; import { isString } from "lodash"; @@ -89,8 +91,14 @@ export class KeyboardCommandHandler { ); } - modifyTarget({ modifier }: { modifier: Modifier }) { - this.targeted.targetModifier(modifier); + modifyTarget({ + modifier, + mode, + }: { + modifier: Modifier; + mode?: TargetingMode; + }) { + this.targeted.targetModifier(modifier, mode); } } @@ -99,7 +107,7 @@ interface DecoratedMarkArg { color?: HatColor; shape?: HatShape; }; - mode: "replace" | "extend" | "append"; + mode: TargetingMode; } interface WrapActionArg { diff --git a/packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts b/packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts index 48e72ef4ff..373b6638b0 100644 --- a/packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts +++ b/packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts @@ -14,7 +14,7 @@ import KeyboardCommandsModal from "./KeyboardCommandsModal"; import KeyboardHandler from "./KeyboardHandler"; import { SimpleKeyboardActionDescriptor } from "./KeyboardActionType"; -type TargetingMode = "replace" | "extend" | "append"; +export type TargetingMode = "replace" | "extend" | "append"; interface TargetDecoratedMarkArgument { color?: HatColor; @@ -84,53 +84,56 @@ export default class KeyboardCommandsTargeted { return; } - let target: PartialTargetDescriptor = { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: getStyleName(color, shape), - character, - }, - }; + return await setKeyboardTarget( + this.applyTargetingMode( + { + type: "primitive", + mark: { + type: "decoratedSymbol", + symbolColor: getStyleName(color, shape), + character, + }, + }, + mode, + ), + ); + }; + private applyTargetingMode( + target: PartialPrimitiveTargetDescriptor, + mode: TargetingMode, + ): PartialTargetDescriptor { switch (mode) { case "extend": - target = { + return { type: "range", - anchor: { - type: "primitive", - mark: { - type: "that", - }, - }, + anchor: getKeyboardTarget(), active: target, excludeActive: false, excludeAnchor: false, }; - break; case "append": - target = { + return { type: "list", - elements: [ - { - type: "primitive", - mark: { - type: "that", - }, - }, - target, - ], + elements: [getKeyboardTarget(), target], }; - break; case "replace": - break; + return target; } + } - return await executeCursorlessCommand({ - name: "private.setKeyboardTarget", - target, - }); - }; + /** + * Applies {@link modifier} to the current target + * @param param0 Describes the desired modifier + * @returns A promise that resolves to the result of the cursorless command + */ + targetModifier = async ( + modifier: Modifier, + mode: TargetingMode = "replace", + ) => + await setKeyboardTarget( + this.applyTargetingMode(getKeyboardTarget(modifier), mode), + ); /** * Expands the current target to the containing {@link scopeType} @@ -141,37 +144,9 @@ export default class KeyboardCommandsTargeted { scopeType, type = "containingScope", }: ModifyTargetContainingScopeArgument) => - await executeCursorlessCommand({ - name: "private.setKeyboardTarget", - target: { - type: "primitive", - modifiers: [ - { - type, - scopeType, - }, - ], - mark: { - type: "keyboard", - }, - }, - }); - - /** - * Applies {@link modifier} to the current target - * @param param0 Describes the desired modifier - * @returns A promise that resolves to the result of the cursorless command - */ - targetModifier = async (modifier: Modifier) => - await executeCursorlessCommand({ - name: "private.setKeyboardTarget", - target: { - type: "primitive", - modifiers: [modifier], - mark: { - type: "keyboard", - }, - }, + await this.targetModifier({ + type, + scopeType, }); /** @@ -249,12 +224,7 @@ export default class KeyboardCommandsTargeted { ) => ActionDescriptor, { exitCursorlessMode }: PerformActionOpts, ) => { - const action = constructActionPayload({ - type: "primitive", - mark: { - type: "keyboard", - }, - }); + const action = constructActionPayload(getKeyboardTarget()); const returnValue = await executeCursorlessCommand(action); if (exitCursorlessMode) { @@ -264,13 +234,10 @@ export default class KeyboardCommandsTargeted { } else { // If we're not exiting cursorless mode, preserve the keyboard mark // FIXME: Better to just not clobber the keyboard mark on each action? - await executeCursorlessCommand({ - name: "private.setKeyboardTarget", - target: { - type: "primitive", - mark: { - type: "that", - }, + await setKeyboardTarget({ + type: "primitive", + mark: { + type: "that", }, }); } @@ -295,16 +262,9 @@ export default class KeyboardCommandsTargeted { exitCursorlessMode, }: VscodeCommandOnTargetOptions = {}, ) => { - const target: PartialPrimitiveTargetDescriptor = { - type: "primitive", - mark: { - type: "keyboard", - }, - }; - const returnValue = await executeCursorlessCommand({ name: "executeCommand", - target, + target: getKeyboardTarget(), commandId, options: { restoreSelection: !keepChangedSelection, @@ -327,13 +287,10 @@ export default class KeyboardCommandsTargeted { * @returns A promise that resolves to the result of the cursorless command */ targetSelection = () => - executeCursorlessCommand({ - name: "private.setKeyboardTarget", - target: { - type: "primitive", - mark: { - type: "cursor", - }, + setKeyboardTarget({ + type: "primitive", + mark: { + type: "cursor", }, }); @@ -367,6 +324,25 @@ interface VscodeCommandOnTargetOptions { exitCursorlessMode?: boolean; } +function setKeyboardTarget(target: PartialTargetDescriptor) { + return executeCursorlessCommand({ + name: "private.setKeyboardTarget", + target, + }); +} + +function getKeyboardTarget( + ...modifiers: Modifier[] +): PartialPrimitiveTargetDescriptor { + return { + type: "primitive", + modifiers: modifiers.length > 0 ? modifiers : undefined, + mark: { + type: "keyboard", + }, + }; +} + function executeCursorlessCommand(action: ActionDescriptor) { return runCursorlessCommand({ action, diff --git a/packages/cursorless-vscode/src/keyboard/grammar/generated/grammar.ts b/packages/cursorless-vscode/src/keyboard/grammar/generated/grammar.ts index 7c23f4f672..611384fa8b 100644 --- a/packages/cursorless-vscode/src/keyboard/grammar/generated/grammar.ts +++ b/packages/cursorless-vscode/src/keyboard/grammar/generated/grammar.ts @@ -64,6 +64,8 @@ const grammar: Grammar = { command("targetDecoratedMark", { decoratedMark: $1, mode: "append" }) }, {"name": "main", "symbols": ["modifier"], "postprocess": command("modifyTarget", { modifier: $0 })}, + {"name": "main", "symbols": [(keyboardLexer.has("makeRange") ? {type: "makeRange"} : makeRange), "modifier"], "postprocess": command("modifyTarget", { modifier: $1, mode: "extend" })}, + {"name": "main", "symbols": [(keyboardLexer.has("makeList") ? {type: "makeList"} : makeList), "modifier"], "postprocess": command("modifyTarget", { modifier: $1, mode: "append" })}, {"name": "main", "symbols": [(keyboardLexer.has("simpleAction") ? {type: "simpleAction"} : simpleAction)], "postprocess": command("performSimpleActionOnTarget", ["actionDescriptor"])}, {"name": "main", "symbols": [(keyboardLexer.has("wrap") ? {type: "wrap"} : wrap), (keyboardLexer.has("pairedDelimiter") ? {type: "pairedDelimiter"} : pairedDelimiter)], "postprocess": command("performWrapActionOnTarget", ["actionDescriptor", "delimiter"]) diff --git a/packages/cursorless-vscode/src/keyboard/grammar/grammar.ne b/packages/cursorless-vscode/src/keyboard/grammar/grammar.ne index f5f0d3fba0..202823ba61 100644 --- a/packages/cursorless-vscode/src/keyboard/grammar/grammar.ne +++ b/packages/cursorless-vscode/src/keyboard/grammar/grammar.ne @@ -29,6 +29,8 @@ main -> %makeList decoratedMark {% # --------------------------- Modifier -------------------------- main -> modifier {% command("modifyTarget", { modifier: $0 }) %} +main -> %makeRange modifier {% command("modifyTarget", { modifier: $1, mode: "extend" }) %} +main -> %makeList modifier {% command("modifyTarget", { modifier: $1, mode: "append" }) %} # --------------------------- Actions --------------------------