Skip to content

Commit f92c6f8

Browse files
authored
Add ability to repeat last cursorless command (#2419)
- Fixes #2411 ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [-] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [-] I have not broken the cheatsheet
1 parent 663c5d3 commit f92c6f8

File tree

7 files changed

+71
-36
lines changed

7 files changed

+71
-36
lines changed

packages/common/src/cursorlessCommandIds.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class VisibleCommand extends Command implements CommandDescription {
2424

2525
export const cursorlessCommandIds = [
2626
"cursorless.command",
27+
"cursorless.repeatPreviousCommand",
2728
"cursorless.internal.updateCheatsheetDefaults",
2829
"cursorless.private.logQuickActions",
2930
"cursorless.keyboard.escape",
@@ -90,6 +91,9 @@ export const cursorlessCommandDescriptions: Record<
9091
),
9192

9293
["cursorless.command"]: new HiddenCommand("The core cursorless command"),
94+
["cursorless.repeatPreviousCommand"]: new VisibleCommand(
95+
"Repeat the previous Cursorless command",
96+
),
9397
["cursorless.showQuickPick"]: new HiddenCommand(
9498
"Pop up a quick pick of all cursorless commands",
9599
),

packages/cursorless-engine/src/api/CursorlessEngineApi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export interface CommandApi {
4646
* the command args are of the correct shape.
4747
*/
4848
runCommandSafe(...args: unknown[]): Promise<CommandResponse | unknown>;
49+
50+
/**
51+
* Repeats the previous command.
52+
*/
53+
repeatPreviousCommand(): Promise<CommandResponse | unknown>;
4954
}
5055

5156
export interface CommandRunnerDecorator {

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -70,36 +70,40 @@ export async function createCursorlessEngine(
7070

7171
const commandRunnerDecorators: CommandRunnerDecorator[] = [];
7272

73+
let previousCommand: Command | undefined = undefined;
74+
75+
const runCommandClosure = (command: Command) => {
76+
previousCommand = command;
77+
return runCommand(
78+
treeSitter,
79+
commandServerApi,
80+
debug,
81+
hatTokenMap,
82+
snippets,
83+
storedTargets,
84+
languageDefinitions,
85+
rangeUpdater,
86+
commandRunnerDecorators,
87+
command,
88+
);
89+
};
90+
7391
return {
7492
commandApi: {
7593
runCommand(command: Command) {
76-
return runCommand(
77-
treeSitter,
78-
commandServerApi,
79-
debug,
80-
hatTokenMap,
81-
snippets,
82-
storedTargets,
83-
languageDefinitions,
84-
rangeUpdater,
85-
commandRunnerDecorators,
86-
command,
87-
);
94+
return runCommandClosure(command);
8895
},
8996

90-
async runCommandSafe(...args: unknown[]) {
91-
return runCommand(
92-
treeSitter,
93-
commandServerApi,
94-
debug,
95-
hatTokenMap,
96-
snippets,
97-
storedTargets,
98-
languageDefinitions,
99-
rangeUpdater,
100-
commandRunnerDecorators,
101-
ensureCommandShape(args),
102-
);
97+
runCommandSafe(...args: unknown[]) {
98+
return runCommandClosure(ensureCommandShape(args));
99+
},
100+
101+
repeatPreviousCommand() {
102+
if (previousCommand == null) {
103+
throw new Error("No previous command");
104+
}
105+
106+
return runCommandClosure(previousCommand);
103107
},
104108
},
105109
scopeProvider: createScopeProvider(

packages/cursorless-vscode-e2e/src/suite/keyboard/basic.vscode.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ const testCases: TestCase[] = [
9797
keySequence: ["db", "fa", "2", "n", "st", "c"],
9898
finalContent: "aaa ccc ",
9999
},
100+
{
101+
name: "repeat command",
102+
initialContent: "aaa bbb ccc ddd",
103+
// keyboard air
104+
// keyboard next token twice
105+
// clear keyboard
106+
keySequence: ["da", "nst", " ", "c"],
107+
finalContent: "aaa bbb ddd",
108+
},
100109
];
101110

102111
suite("Basic keyboard test", async function () {

packages/cursorless-vscode/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@
116116
"title": "Cursorless: The core cursorless command",
117117
"enablement": "false"
118118
},
119+
{
120+
"command": "cursorless.repeatPreviousCommand",
121+
"title": "Cursorless: Repeat the previous Cursorless command"
122+
},
119123
{
120124
"command": "cursorless.showQuickPick",
121125
"title": "Cursorless: Pop up a quick pick of all cursorless commands",

packages/cursorless-vscode/src/keyboard/keyboard-config.fixture.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@
8282
"executeAtTarget": true,
8383
"keepChangedSelection": true,
8484
"exitCursorlessMode": true
85-
}
85+
},
86+
" ": "cursorless.repeatPreviousCommand"
8687
},
8788
"cursorless.experimental.keyboard.modal.keybindings.pairedDelimiter": {
8889
"wl": "angleBrackets",

packages/cursorless-vscode/src/registerCommands.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,27 @@ export function registerCommands(
3131
keyboardCommands: KeyboardCommands,
3232
hats: VscodeHats,
3333
): void {
34+
const runCommandWrapper = async (run: () => Promise<unknown>) => {
35+
try {
36+
return await run();
37+
} catch (e) {
38+
if (!isTesting()) {
39+
const err = e as Error;
40+
console.error(err.stack);
41+
vscodeIde.handleCommandError(err);
42+
}
43+
throw e;
44+
}
45+
};
46+
3447
const commands: Record<CursorlessCommandId, (...args: any[]) => any> = {
3548
// The core Cursorless command
3649
[CURSORLESS_COMMAND_ID]: async (...args: unknown[]) => {
37-
try {
38-
return await commandApi.runCommandSafe(...args);
39-
} catch (e) {
40-
if (!isTesting()) {
41-
const err = e as Error;
42-
console.error(err.stack);
43-
vscodeIde.handleCommandError(err);
44-
}
45-
throw e;
46-
}
50+
return runCommandWrapper(() => commandApi.runCommandSafe(...args));
51+
},
52+
53+
["cursorless.repeatPreviousCommand"]: async () => {
54+
return runCommandWrapper(() => commandApi.repeatPreviousCommand());
4755
},
4856

4957
// Cheatsheet commands

0 commit comments

Comments
 (0)