Skip to content

Commit

Permalink
Community wrapper snippets (#1998)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
3 people authored Apr 21, 2024
1 parent 6c4a18c commit 409b3de
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 22 deletions.
6 changes: 6 additions & 0 deletions changelog/2024-04-community-snippets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
tags: [enhancement]
pullRequest: 1998
---

- 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.
16 changes: 16 additions & 0 deletions cursorless-talon-dev/src/spoken_form_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

mockedGetValue = ""

community_snippets_tag_name = "user.cursorless_use_community_snippets"


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

def private_cursorless_use_community_snippets(enable: bool):
"""Enable/disable cursorless community snippets in test mode"""
if enable:
tags = set(ctx.tags)
tags.add(community_snippets_tag_name)
ctx.tags = list(tags)
else:
tags = set(ctx.tags)
tags.remove(community_snippets_tag_name)
ctx.tags = list(tags)
# Note: Test harness hangs if we don't print anything because it's
# waiting for stdout
print(f"Set community snippet enablement to {enable}")

def private_cursorless_spoken_form_test(
phrase: str, mockedGetValue_: Optional[str]
):
Expand Down
9 changes: 0 additions & 9 deletions cursorless-talon/src/cursorless.talon
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ tag: user.cursorless
<user.cursorless_wrapper_paired_delimiter> {user.cursorless_wrap_action} <user.cursorless_target>:
user.private_cursorless_wrap_with_paired_delimiter(cursorless_wrap_action, cursorless_target, cursorless_wrapper_paired_delimiter)

{user.cursorless_insert_snippet_action} <user.cursorless_insertion_snippet>:
user.private_cursorless_insert_snippet(cursorless_insertion_snippet)

{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} <user.text> [{user.cursorless_phrase_terminator}]:
user.private_cursorless_insert_snippet_with_phrase(cursorless_insertion_snippet_single_phrase, text)

{user.cursorless_wrapper_snippet} {user.cursorless_wrap_action} <user.cursorless_target>:
user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet)

{user.cursorless_show_scope_visualizer} <user.cursorless_scope_type> [{user.cursorless_visualization_type}]:
user.private_cursorless_show_scope_visualizer(cursorless_scope_type, cursorless_visualization_type or "content")
{user.cursorless_hide_scope_visualizer}:
Expand Down
14 changes: 14 additions & 0 deletions cursorless-talon/src/snippet_cursorless.talon
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mode: command
mode: user.cursorless_spoken_form_test
tag: user.cursorless
and not tag: user.cursorless_use_community_snippets
-

{user.cursorless_insert_snippet_action} <user.cursorless_insertion_snippet>:
user.private_cursorless_insert_snippet(cursorless_insertion_snippet)

{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} <user.text> [{user.cursorless_phrase_terminator}]:
user.private_cursorless_insert_snippet_with_phrase(cursorless_insertion_snippet_single_phrase, text)

{user.cursorless_wrapper_snippet} {user.cursorless_wrap_action} <user.cursorless_target>:
user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet)
36 changes: 36 additions & 0 deletions cursorless-talon/src/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ class InsertionSnippet:
destination: CursorlessDestination


@dataclass
class CommunityInsertionSnippet:
body: str
scopes: list[str] = None


@dataclass
class CommunityWrapperSnippet:
body: str
variable_name: str
scope: str = None


mod = Module()

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

mod.tag(
"cursorless_use_community_snippets",
"If active use community snippets instead of Cursorless snippets",
)

mod.list("cursorless_wrapper_snippet", desc="Cursorless wrapper snippet")
mod.list(
"cursorless_insertion_snippet_no_phrase",
Expand Down Expand Up @@ -181,3 +199,21 @@ def cursorless_wrap_with_snippet(
snippet_arg,
target,
)

def private_cursorless_insert_community_snippet(
name: str, destination: CursorlessDestination
):
"""Cursorless: Insert community snippet <name>"""
snippet: CommunityInsertionSnippet = actions.user.get_insertion_snippet(name)
actions.user.cursorless_insert_snippet(
snippet.body, destination, snippet.scopes
)

def private_cursorless_wrap_with_community_snippet(
name: str, target: CursorlessTarget
):
"""Cursorless: Wrap target with community snippet <name>"""
snippet: CommunityWrapperSnippet = actions.user.get_wrapper_snippet(name)
actions.user.cursorless_wrap_with_snippet(
snippet.body, target, snippet.variable_name, snippet.scope
)
13 changes: 13 additions & 0 deletions cursorless-talon/src/snippets_community.talon
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
mode: command
mode: user.cursorless_spoken_form_test
tag: user.cursorless
and tag: user.cursorless_use_community_snippets
-

# These snippets are defined in community

{user.cursorless_insert_snippet_action} {user.snippet} <user.cursorless_destination>:
user.private_cursorless_insert_community_snippet(snippet, cursorless_destination)

{user.snippet_wrapper} {user.cursorless_wrap_action} <user.cursorless_target>:
user.private_cursorless_wrap_with_community_snippet(snippet_wrapper, cursorless_target)
10 changes: 10 additions & 0 deletions docs/user/experimental/snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ Note that each snippet can use `insertionScopeTypes` to indicate that it will au
| `"snippet funk"` | Function; phrase becomes name | Function ||
| `"snippet link"` | Markdown link; phrase becomes link text | ||

## Using community snippets

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:

```talon
tag(): user.cursorless_use_community_snippets
```

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.

## Customizing spoken forms

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ActionDescriptor } from "@cursorless/common";
import { spokenFormTest } from "./spokenFormTest";

const verticalRangeAction: ActionDescriptor = {
name: "insertSnippet",
destination: {
type: "primitive",
insertionMode: "after",
target: {
type: "primitive",
mark: {
character: "a",
symbolColor: "default",
type: "decoratedSymbol",
},
},
},
snippetDescription: {
body: "```\n$0\n```",
type: "custom",
},
};

/**
* These are spoken forms that have more than one way to say them, so we have to
* pick one in our spoken form generator, meaning we can't test the other in our
* Talon tests by relying on our recorded test fixtures alone.
*/
export const communitySnippetsSpokenFormsFixture = [
spokenFormTest("snippet code after air", verticalRangeAction, undefined, {
useCommunitySnippets: true,
}),
];
13 changes: 13 additions & 0 deletions packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,38 @@ export interface SpokenFormTest {
* {@link spokenForm} is spoken.
*/
commands: CommandV6[];

/**
* If `true`, use community snippets instead of Cursorless snippets
*/
useCommunitySnippets: boolean;
}

export function spokenFormTest(
spokenForm: string,
action: ActionDescriptor,
mockedGetValue?: unknown,
{ useCommunitySnippets = false }: SpokenFormTestOpts = {},
): SpokenFormTest {
return {
spokenForm,
mockedGetValue: wrapMockedGetValue(mockedGetValue),
commands: [command(spokenForm, action)],
useCommunitySnippets,
};
}

export function multiActionSpokenFormTest(
spokenForm: string,
actions: ActionDescriptor[],
mockedGetValue?: unknown,
{ useCommunitySnippets = false }: SpokenFormTestOpts = {},
): SpokenFormTest {
return {
spokenForm,
mockedGetValue: wrapMockedGetValue(mockedGetValue),
commands: actions.map((action) => command(spokenForm, action)),
useCommunitySnippets,
};
}

Expand All @@ -63,3 +72,7 @@ function command(spokenForm: string, action: ActionDescriptor): CommandV6 {
action,
};
}

export interface SpokenFormTestOpts {
useCommunitySnippets?: boolean;
}
47 changes: 34 additions & 13 deletions packages/cursorless-engine/src/test/spokenForms.talon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { TalonRepl } from "../testUtil/TalonRepl";
import { synonymousSpokenFormsFixture } from "./fixtures/synonymousSpokenForms.fixture";
import { talonApiFixture } from "./fixtures/talonApi.fixture";
import { multiActionFixture } from "./fixtures/multiAction.fixture";
import { communitySnippetsSpokenFormsFixture } from "./fixtures/communitySnippets.fixture";
import { SpokenFormTestOpts } from "./fixtures/spokenFormTest";

suite("Talon spoken forms", async function () {
const repl = new TalonRepl();
Expand Down Expand Up @@ -42,8 +44,13 @@ suite("Talon spoken forms", async function () {
...synonymousSpokenFormsFixture,
...talonApiFixture,
...multiActionFixture,
].forEach(({ spokenForm, commands, mockedGetValue }) =>
test(spokenForm, () => runTest(repl, spokenForm, commands, mockedGetValue)),
...communitySnippetsSpokenFormsFixture,
].forEach(({ spokenForm, commands, mockedGetValue, useCommunitySnippets }) =>
test(spokenForm, () =>
runTest(repl, spokenForm, commands, mockedGetValue, {
useCommunitySnippets,
}),
),
);
});

Expand Down Expand Up @@ -76,6 +83,7 @@ async function runTest(
spokenForm: string,
commandsLegacy: Command[],
mockedGetValue?: unknown,
{ useCommunitySnippets = false }: SpokenFormTestOpts = {},
) {
const commandsExpected = commandsLegacy.map((command) => ({
...canonicalizeAndValidateCommand(command),
Expand All @@ -101,24 +109,37 @@ async function runTest(
? "None"
: JSON.stringify(JSON.stringify(mockedGetValue));

const result = await repl.action(
`user.private_cursorless_spoken_form_test("${spokenForm}", ${mockedGetValueString})`,
);
if (useCommunitySnippets) {
await repl.action(`user.private_cursorless_use_community_snippets(True)`);
}

const commandsActual = (() => {
try {
return JSON.parse(result);
} catch (e) {
throw Error(result);
try {
const result = await repl.action(
`user.private_cursorless_spoken_form_test("${spokenForm}", ${mockedGetValueString})`,
);

const commandsActual = (() => {
try {
return JSON.parse(result);
} catch (e) {
throw Error(result);
}
})();

assert.deepStrictEqual(commandsActual, commandsExpected);
} finally {
if (useCommunitySnippets) {
await repl.action(
`user.private_cursorless_use_community_snippets(False)`,
);
}
})();

assert.deepStrictEqual(commandsActual, commandsExpected);
}
}

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

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

0 comments on commit 409b3de

Please sign in to comment.