Skip to content

Commit 409b3de

Browse files
AndreasArvidssonpre-commit-ci-lite[bot]pokey
authored
Community wrapper snippets (#1998)
This is the Cursorless side of the community wrapper snippets talonhub/community#1315 ## Checklist - [-] 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 --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com>
1 parent 6c4a18c commit 409b3de

File tree

10 files changed

+175
-22
lines changed

10 files changed

+175
-22
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
tags: [enhancement]
3+
pullRequest: 1998
4+
---
5+
6+
- Add support for using community snippets for wrapping / cursorless insertion instead of snippets defined in Cursorless. See [Using community snippets](../docs/user/experimental/snippets.md#using-community-snippets) for more information.

cursorless-talon-dev/src/spoken_form_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
mockedGetValue = ""
3535

36+
community_snippets_tag_name = "user.cursorless_use_community_snippets"
37+
3638

3739
@ctx.action_class("user")
3840
class UserActions:
@@ -82,6 +84,20 @@ def private_cursorless_spoken_form_test_mode(enable: bool):
8284
"Cursorless spoken form tests are done. Talon microphone is re-enabled."
8385
)
8486

87+
def private_cursorless_use_community_snippets(enable: bool):
88+
"""Enable/disable cursorless community snippets in test mode"""
89+
if enable:
90+
tags = set(ctx.tags)
91+
tags.add(community_snippets_tag_name)
92+
ctx.tags = list(tags)
93+
else:
94+
tags = set(ctx.tags)
95+
tags.remove(community_snippets_tag_name)
96+
ctx.tags = list(tags)
97+
# Note: Test harness hangs if we don't print anything because it's
98+
# waiting for stdout
99+
print(f"Set community snippet enablement to {enable}")
100+
85101
def private_cursorless_spoken_form_test(
86102
phrase: str, mockedGetValue_: Optional[str]
87103
):

cursorless-talon/src/cursorless.talon

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,6 @@ tag: user.cursorless
2424
<user.cursorless_wrapper_paired_delimiter> {user.cursorless_wrap_action} <user.cursorless_target>:
2525
user.private_cursorless_wrap_with_paired_delimiter(cursorless_wrap_action, cursorless_target, cursorless_wrapper_paired_delimiter)
2626

27-
{user.cursorless_insert_snippet_action} <user.cursorless_insertion_snippet>:
28-
user.private_cursorless_insert_snippet(cursorless_insertion_snippet)
29-
30-
{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} <user.text> [{user.cursorless_phrase_terminator}]:
31-
user.private_cursorless_insert_snippet_with_phrase(cursorless_insertion_snippet_single_phrase, text)
32-
33-
{user.cursorless_wrapper_snippet} {user.cursorless_wrap_action} <user.cursorless_target>:
34-
user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet)
35-
3627
{user.cursorless_show_scope_visualizer} <user.cursorless_scope_type> [{user.cursorless_visualization_type}]:
3728
user.private_cursorless_show_scope_visualizer(cursorless_scope_type, cursorless_visualization_type or "content")
3829
{user.cursorless_hide_scope_visualizer}:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
mode: command
2+
mode: user.cursorless_spoken_form_test
3+
tag: user.cursorless
4+
and not tag: user.cursorless_use_community_snippets
5+
-
6+
7+
{user.cursorless_insert_snippet_action} <user.cursorless_insertion_snippet>:
8+
user.private_cursorless_insert_snippet(cursorless_insertion_snippet)
9+
10+
{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} <user.text> [{user.cursorless_phrase_terminator}]:
11+
user.private_cursorless_insert_snippet_with_phrase(cursorless_insertion_snippet_single_phrase, text)
12+
13+
{user.cursorless_wrapper_snippet} {user.cursorless_wrap_action} <user.cursorless_target>:
14+
user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet)

cursorless-talon/src/snippets.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ class InsertionSnippet:
1616
destination: CursorlessDestination
1717

1818

19+
@dataclass
20+
class CommunityInsertionSnippet:
21+
body: str
22+
scopes: list[str] = None
23+
24+
25+
@dataclass
26+
class CommunityWrapperSnippet:
27+
body: str
28+
variable_name: str
29+
scope: str = None
30+
31+
1932
mod = Module()
2033

2134
mod.list("cursorless_insert_snippet_action", desc="Cursorless insert snippet action")
@@ -27,6 +40,11 @@ class InsertionSnippet:
2740
desc="tag for enabling experimental snippet support",
2841
)
2942

43+
mod.tag(
44+
"cursorless_use_community_snippets",
45+
"If active use community snippets instead of Cursorless snippets",
46+
)
47+
3048
mod.list("cursorless_wrapper_snippet", desc="Cursorless wrapper snippet")
3149
mod.list(
3250
"cursorless_insertion_snippet_no_phrase",
@@ -181,3 +199,21 @@ def cursorless_wrap_with_snippet(
181199
snippet_arg,
182200
target,
183201
)
202+
203+
def private_cursorless_insert_community_snippet(
204+
name: str, destination: CursorlessDestination
205+
):
206+
"""Cursorless: Insert community snippet <name>"""
207+
snippet: CommunityInsertionSnippet = actions.user.get_insertion_snippet(name)
208+
actions.user.cursorless_insert_snippet(
209+
snippet.body, destination, snippet.scopes
210+
)
211+
212+
def private_cursorless_wrap_with_community_snippet(
213+
name: str, target: CursorlessTarget
214+
):
215+
"""Cursorless: Wrap target with community snippet <name>"""
216+
snippet: CommunityWrapperSnippet = actions.user.get_wrapper_snippet(name)
217+
actions.user.cursorless_wrap_with_snippet(
218+
snippet.body, target, snippet.variable_name, snippet.scope
219+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
mode: command
2+
mode: user.cursorless_spoken_form_test
3+
tag: user.cursorless
4+
and tag: user.cursorless_use_community_snippets
5+
-
6+
7+
# These snippets are defined in community
8+
9+
{user.cursorless_insert_snippet_action} {user.snippet} <user.cursorless_destination>:
10+
user.private_cursorless_insert_community_snippet(snippet, cursorless_destination)
11+
12+
{user.snippet_wrapper} {user.cursorless_wrap_action} <user.cursorless_target>:
13+
user.private_cursorless_wrap_with_community_snippet(snippet_wrapper, cursorless_target)

docs/user/experimental/snippets.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ Note that each snippet can use `insertionScopeTypes` to indicate that it will au
8888
| `"snippet funk"` | Function; phrase becomes name | Function ||
8989
| `"snippet link"` | Markdown link; phrase becomes link text | ||
9090

91+
## Using community snippets
92+
93+
The community Talon files now support their own snippet format. If you'd like to use these snippets for wrapping / cursorless insertion instead of snippets defined in Cursorless, add following line to your `settings.talon` file:
94+
95+
```talon
96+
tag(): user.cursorless_use_community_snippets
97+
```
98+
99+
Note that this line will also disable any Cursorless snippets defined in your Cursorless customization CSVs. You will need to migrate your Cursorless snippets to the new community snippet format [described in community](https://github.com/talonhub/community/blob/main/core/snippets/README.md). If you'd be interested in a tool to help with this migration, please leave a comment on [cursorless-dev/cursorless#2149](https://github.com/cursorless-dev/cursorless/issues/2149), ideally with a link to your custom snippets for us to look at.
100+
91101
## Customizing spoken forms
92102

93103
As usual, the spoken forms for these snippets can be [customized by csv](../customization.md). The csvs are in the files in `cursorless-settings/experimental` with `snippet` in their name.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ActionDescriptor } from "@cursorless/common";
2+
import { spokenFormTest } from "./spokenFormTest";
3+
4+
const verticalRangeAction: ActionDescriptor = {
5+
name: "insertSnippet",
6+
destination: {
7+
type: "primitive",
8+
insertionMode: "after",
9+
target: {
10+
type: "primitive",
11+
mark: {
12+
character: "a",
13+
symbolColor: "default",
14+
type: "decoratedSymbol",
15+
},
16+
},
17+
},
18+
snippetDescription: {
19+
body: "```\n$0\n```",
20+
type: "custom",
21+
},
22+
};
23+
24+
/**
25+
* These are spoken forms that have more than one way to say them, so we have to
26+
* pick one in our spoken form generator, meaning we can't test the other in our
27+
* Talon tests by relying on our recorded test fixtures alone.
28+
*/
29+
export const communitySnippetsSpokenFormsFixture = [
30+
spokenFormTest("snippet code after air", verticalRangeAction, undefined, {
31+
useCommunitySnippets: true,
32+
}),
33+
];

packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,38 @@ export interface SpokenFormTest {
2323
* {@link spokenForm} is spoken.
2424
*/
2525
commands: CommandV6[];
26+
27+
/**
28+
* If `true`, use community snippets instead of Cursorless snippets
29+
*/
30+
useCommunitySnippets: boolean;
2631
}
2732

2833
export function spokenFormTest(
2934
spokenForm: string,
3035
action: ActionDescriptor,
3136
mockedGetValue?: unknown,
37+
{ useCommunitySnippets = false }: SpokenFormTestOpts = {},
3238
): SpokenFormTest {
3339
return {
3440
spokenForm,
3541
mockedGetValue: wrapMockedGetValue(mockedGetValue),
3642
commands: [command(spokenForm, action)],
43+
useCommunitySnippets,
3744
};
3845
}
3946

4047
export function multiActionSpokenFormTest(
4148
spokenForm: string,
4249
actions: ActionDescriptor[],
4350
mockedGetValue?: unknown,
51+
{ useCommunitySnippets = false }: SpokenFormTestOpts = {},
4452
): SpokenFormTest {
4553
return {
4654
spokenForm,
4755
mockedGetValue: wrapMockedGetValue(mockedGetValue),
4856
commands: actions.map((action) => command(spokenForm, action)),
57+
useCommunitySnippets,
4958
};
5059
}
5160

@@ -63,3 +72,7 @@ function command(spokenForm: string, action: ActionDescriptor): CommandV6 {
6372
action,
6473
};
6574
}
75+
76+
export interface SpokenFormTestOpts {
77+
useCommunitySnippets?: boolean;
78+
}

packages/cursorless-engine/src/test/spokenForms.talon.test.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { TalonRepl } from "../testUtil/TalonRepl";
1414
import { synonymousSpokenFormsFixture } from "./fixtures/synonymousSpokenForms.fixture";
1515
import { talonApiFixture } from "./fixtures/talonApi.fixture";
1616
import { multiActionFixture } from "./fixtures/multiAction.fixture";
17+
import { communitySnippetsSpokenFormsFixture } from "./fixtures/communitySnippets.fixture";
18+
import { SpokenFormTestOpts } from "./fixtures/spokenFormTest";
1719

1820
suite("Talon spoken forms", async function () {
1921
const repl = new TalonRepl();
@@ -42,8 +44,13 @@ suite("Talon spoken forms", async function () {
4244
...synonymousSpokenFormsFixture,
4345
...talonApiFixture,
4446
...multiActionFixture,
45-
].forEach(({ spokenForm, commands, mockedGetValue }) =>
46-
test(spokenForm, () => runTest(repl, spokenForm, commands, mockedGetValue)),
47+
...communitySnippetsSpokenFormsFixture,
48+
].forEach(({ spokenForm, commands, mockedGetValue, useCommunitySnippets }) =>
49+
test(spokenForm, () =>
50+
runTest(repl, spokenForm, commands, mockedGetValue, {
51+
useCommunitySnippets,
52+
}),
53+
),
4754
);
4855
});
4956

@@ -76,6 +83,7 @@ async function runTest(
7683
spokenForm: string,
7784
commandsLegacy: Command[],
7885
mockedGetValue?: unknown,
86+
{ useCommunitySnippets = false }: SpokenFormTestOpts = {},
7987
) {
8088
const commandsExpected = commandsLegacy.map((command) => ({
8189
...canonicalizeAndValidateCommand(command),
@@ -101,24 +109,37 @@ async function runTest(
101109
? "None"
102110
: JSON.stringify(JSON.stringify(mockedGetValue));
103111

104-
const result = await repl.action(
105-
`user.private_cursorless_spoken_form_test("${spokenForm}", ${mockedGetValueString})`,
106-
);
112+
if (useCommunitySnippets) {
113+
await repl.action(`user.private_cursorless_use_community_snippets(True)`);
114+
}
107115

108-
const commandsActual = (() => {
109-
try {
110-
return JSON.parse(result);
111-
} catch (e) {
112-
throw Error(result);
116+
try {
117+
const result = await repl.action(
118+
`user.private_cursorless_spoken_form_test("${spokenForm}", ${mockedGetValueString})`,
119+
);
120+
121+
const commandsActual = (() => {
122+
try {
123+
return JSON.parse(result);
124+
} catch (e) {
125+
throw Error(result);
126+
}
127+
})();
128+
129+
assert.deepStrictEqual(commandsActual, commandsExpected);
130+
} finally {
131+
if (useCommunitySnippets) {
132+
await repl.action(
133+
`user.private_cursorless_use_community_snippets(False)`,
134+
);
113135
}
114-
})();
115-
116-
assert.deepStrictEqual(commandsActual, commandsExpected);
136+
}
117137
}
118138

119139
async function setTestMode(repl: TalonRepl, enabled: boolean) {
120140
const arg = enabled ? "True" : "False";
121141
await repl.action(`user.private_cursorless_spoken_form_test_mode(${arg})`);
142+
await repl.action(`user.private_cursorless_use_community_snippets(False)`);
122143

123144
// If you have warnings in your talon user files, they will be printed to the
124145
// repl when you run the above action. We need to eat them so that they don't

0 commit comments

Comments
 (0)