Skip to content

Commit 95ae15f

Browse files
authored
Support custom spoken forms for actions (#2334)
## 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) - [x] I have not broken the cheatsheet - [x] I have run Talon-side tests
1 parent bc50059 commit 95ae15f

File tree

10 files changed

+162
-106
lines changed

10 files changed

+162
-106
lines changed

cursorless-talon/src/spoken_forms.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from talon import app, fs
66

7+
from .actions.actions import ACTION_LIST_NAMES
78
from .csv_overrides import (
89
SPOKEN_FORM_HEADER,
910
ListToSpokenForms,
@@ -70,6 +71,12 @@ def ret(filename: str, *args: P.args, **kwargs: P.kwargs) -> R:
7071
"scope_type": "simpleScopeTypeType",
7172
"glyph_scope_type": "complexScopeTypeType",
7273
"custom_regex_scope_type": "customRegex",
74+
**{
75+
action_list_name: "action"
76+
for action_list_name in ACTION_LIST_NAMES
77+
if action_list_name != "custom_action"
78+
},
79+
"custom_action": "customAction",
7380
}
7481

7582

data/fixtures/recorded/actions/parseTreeFile.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ command:
1010
- type: containingScope
1111
scopeType: {type: document}
1212
usePrePhraseSnapshot: true
13+
spokenFormError: >-
14+
action with id private.showParseTree; this is a private spoken form currently
15+
only for internal experimentation
1316
initialState:
1417
documentContents: const value = 2;
1518
selections:

packages/cursorless-engine/src/customCommandGrammar/lexer.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
simpleScopeTypeTypes,
66
surroundingPairNames,
77
} from "@cursorless/common";
8-
import { actions } from "../generateSpokenForm/defaultSpokenForms/actions";
98
import { marks } from "../generateSpokenForm/defaultSpokenForms/marks";
109
import { defaultSpokenFormMap } from "../spokenForms/defaultSpokenFormMap";
1110
import { connectives } from "../generateSpokenForm/defaultSpokenForms/connectives";
@@ -21,8 +20,8 @@ const tokens: Record<string, Token> = {};
2120
// FIXME: Remove the duplication below?
2221

2322
for (const simpleActionName of simpleActionNames) {
24-
const spokenForm = actions[simpleActionName];
25-
if (spokenForm != null) {
23+
const { spokenForms } = defaultSpokenFormMap.action[simpleActionName];
24+
for (const spokenForm of spokenForms) {
2625
tokens[spokenForm] = {
2726
type: "simpleActionName",
2827
value: simpleActionName,
@@ -36,8 +35,8 @@ const bringMoveActionNames: BringMoveActionDescriptor["name"][] = [
3635
];
3736

3837
for (const bringMoveActionName of bringMoveActionNames) {
39-
const spokenForm = actions[bringMoveActionName];
40-
if (spokenForm != null) {
38+
const { spokenForms } = defaultSpokenFormMap.action[bringMoveActionName];
39+
for (const spokenForm of spokenForms) {
4140
tokens[spokenForm] = {
4241
type: "bringMove",
4342
value: bringMoveActionName,
Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import assert from "node:assert";
22
import { CustomSpokenFormGeneratorImpl } from "./CustomSpokenFormGeneratorImpl";
3-
import { asyncSafety } from "@cursorless/common";
3+
import { LATEST_VERSION, asyncSafety } from "@cursorless/common";
44

55
suite("CustomSpokenFormGeneratorImpl", async function () {
66
test(
7-
"glyph",
7+
"basic",
88
asyncSafety(async () => {
99
const generator = new CustomSpokenFormGeneratorImpl({
1010
async getSpokenFormEntries() {
@@ -14,22 +14,47 @@ suite("CustomSpokenFormGeneratorImpl", async function () {
1414
id: "glyph",
1515
spokenForms: ["foo"],
1616
},
17+
{
18+
type: "action",
19+
id: "setSelection",
20+
spokenForms: ["bar"],
21+
},
1722
];
1823
},
1924
onDidChange: () => ({ dispose() {} }),
2025
});
2126

2227
await generator.customSpokenFormsInitialized;
2328

24-
const spokenForm = generator.scopeTypeToSpokenForm({
25-
type: "glyph",
26-
character: "a",
27-
});
28-
29-
assert.deepStrictEqual(spokenForm, {
30-
type: "success",
31-
spokenForms: ["foo air"],
32-
});
29+
assert.deepStrictEqual(
30+
generator.scopeTypeToSpokenForm({
31+
type: "glyph",
32+
character: "a",
33+
}),
34+
{
35+
type: "success",
36+
spokenForms: ["foo air"],
37+
},
38+
);
39+
assert.deepStrictEqual(
40+
generator.commandToSpokenForm({
41+
version: LATEST_VERSION,
42+
action: {
43+
name: "setSelection",
44+
target: {
45+
type: "primitive",
46+
mark: {
47+
type: "cursor",
48+
},
49+
},
50+
},
51+
usePrePhraseSnapshot: false,
52+
}),
53+
{
54+
type: "success",
55+
spokenForms: ["bar this"],
56+
},
57+
);
3358
}),
3459
);
3560
});

packages/cursorless-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ActionType,
23
CommandComplete,
34
Disposable,
45
Listener,
@@ -54,6 +55,10 @@ export class CustomSpokenFormGeneratorImpl
5455
return this.spokenFormGenerator.processScopeType(scopeType);
5556
}
5657

58+
actionIdToSpokenForm(actionId: ActionType) {
59+
return this.customSpokenForms.spokenFormMap.action[actionId];
60+
}
61+
5762
getCustomRegexScopeTypes() {
5863
return this.customSpokenForms.getCustomRegexScopeTypes();
5964
}

packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/actions.ts

Lines changed: 0 additions & 78 deletions
This file was deleted.

packages/cursorless-engine/src/generateSpokenForm/generateSpokenForm.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
camelCaseToAllDown,
99
} from "@cursorless/common";
1010
import { NoSpokenFormError } from "./NoSpokenFormError";
11-
import { actions } from "./defaultSpokenForms/actions";
1211
import { connectives } from "./defaultSpokenForms/connectives";
1312
import { surroundingPairDelimitersToSpokenForm } from "./defaultSpokenForms/modifiers";
1413
import {
@@ -111,25 +110,28 @@ export class SpokenFormGenerator {
111110
case "replaceWithTarget":
112111
case "moveToTarget":
113112
return [
114-
actions[action.name],
113+
this.spokenFormMap.action[action.name],
115114
this.handleTarget(action.source),
116115
this.handleDestination(action.destination),
117116
];
118117

119118
case "swapTargets":
120119
return [
121-
actions[action.name],
120+
this.spokenFormMap.action[action.name],
122121
this.handleTarget(action.target1),
123122
connectives.swapConnective,
124123
this.handleTarget(action.target2),
125124
];
126125

127126
case "callAsFunction":
128127
if (action.argument.type === "implicit") {
129-
return [actions[action.name], this.handleTarget(action.callee)];
128+
return [
129+
this.spokenFormMap.action[action.name],
130+
this.handleTarget(action.callee),
131+
];
130132
}
131133
return [
132-
actions[action.name],
134+
this.spokenFormMap.action[action.name],
133135
this.handleTarget(action.callee),
134136
"on",
135137
this.handleTarget(action.argument),
@@ -143,19 +145,19 @@ export class SpokenFormGenerator {
143145
action.left,
144146
action.right,
145147
),
146-
actions[action.name],
148+
this.spokenFormMap.action[action.name],
147149
this.handleTarget(action.target),
148150
];
149151

150152
case "pasteFromClipboard":
151153
return [
152-
actions[action.name],
154+
this.spokenFormMap.action[action.name],
153155
this.handleDestination(action.destination),
154156
];
155157

156158
case "insertSnippet":
157159
return [
158-
actions[action.name],
160+
this.spokenFormMap.action[action.name],
159161
insertionSnippetToSpokenForm(action.snippetDescription),
160162
this.handleDestination(action.destination),
161163
];
@@ -164,24 +166,33 @@ export class SpokenFormGenerator {
164166
if (action.snippetName != null) {
165167
throw new NoSpokenFormError(`${action.name}.snippetName`);
166168
}
167-
return [actions[action.name], this.handleTarget(action.target)];
169+
return [
170+
this.spokenFormMap.action[action.name],
171+
this.handleTarget(action.target),
172+
];
168173

169174
case "wrapWithSnippet":
170175
return [
171176
wrapperSnippetToSpokenForm(action.snippetDescription),
172-
actions[action.name],
177+
this.spokenFormMap.action[action.name],
173178
this.handleTarget(action.target),
174179
];
175180

176181
case "highlight": {
177182
if (action.highlightId != null) {
178183
throw new NoSpokenFormError(`${action.name}.highlightId`);
179184
}
180-
return [actions[action.name], this.handleTarget(action.target)];
185+
return [
186+
this.spokenFormMap.action[action.name],
187+
this.handleTarget(action.target),
188+
];
181189
}
182190

183191
default: {
184-
return [actions[action.name], this.handleTarget(action.target)];
192+
return [
193+
this.spokenFormMap.action[action.name],
194+
this.handleTarget(action.target),
195+
];
185196
}
186197
}
187198
}

packages/cursorless-engine/src/scopeProviders/TalonSpokenForms.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export const SUPPORTED_ENTRY_TYPES = [
2222
"complexScopeTypeType",
2323
"customRegex",
2424
"pairedDelimiter",
25+
"action",
26+
"customAction",
2527
] as const;
2628

2729
type SupportedEntryType = (typeof SUPPORTED_ENTRY_TYPES)[number];

packages/cursorless-engine/src/spokenForms/SpokenFormType.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ActionType,
23
ModifierType,
34
SimpleScopeTypeType,
45
SurroundingPairName,
@@ -33,6 +34,14 @@ export interface SpokenFormMapKeyTypes {
3334
*/
3435
modifierExtra: ModifierExtra;
3536
customRegex: string;
37+
38+
action: ActionType;
39+
40+
/**
41+
* These actions correspond to id's of app commands. Eg in VSCode, you can have
42+
* custom actions corresponding to id's of VSCode commands.
43+
*/
44+
customAction: string;
3645
}
3746

3847
/**

0 commit comments

Comments
 (0)