Skip to content

Community wrapper snippets #1998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
217b584
Add the Cursorless side of community wrapper snippets
AndreasArvidsson Nov 4, 2023
0dc4382
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Nov 4, 2023
70cb29b
Added comment
AndreasArvidsson Nov 4, 2023
e016bf3
Merge branch 'community_wrapper_snippets' of github.com:cursorless-de…
AndreasArvidsson Nov 4, 2023
ebf98df
Update name
AndreasArvidsson Nov 4, 2023
d5a5561
Update action signature
AndreasArvidsson Nov 12, 2023
a8e6c73
Update
AndreasArvidsson Nov 14, 2023
d0e90c2
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Nov 14, 2023
0eecf7c
Update to new format
AndreasArvidsson Nov 16, 2023
33fd601
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Nov 16, 2023
61826a3
cleanup
AndreasArvidsson Nov 16, 2023
d1a1595
Merge branch 'community_wrapper_snippets' of github.com:cursorless-de…
AndreasArvidsson Nov 16, 2023
f1bd67a
Merge branch 'main' into community_wrapper_snippets
AndreasArvidsson Nov 16, 2023
8af103f
update
AndreasArvidsson Nov 16, 2023
12f30e7
fix
pokey Dec 18, 2023
b8fe0d9
Snake case variable name
AndreasArvidsson Dec 18, 2023
8cb5993
Merge branches 'community_wrapper_snippets' and 'community_wrapper_sn…
AndreasArvidsson Dec 18, 2023
ac0d196
Merge branch 'main' into community_wrapper_snippets
pokey Jan 26, 2024
b6ad426
Add tests
pokey Jan 30, 2024
c525d33
Merge branch 'main' into community_wrapper_snippets
pokey Apr 15, 2024
c507571
Improve docs
pokey Apr 16, 2024
6067772
Merge branch 'main' into community_wrapper_snippets
pokey Apr 16, 2024
77fe5f1
Merge branch 'main' into community_wrapper_snippets
pokey Apr 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -19,29 +19,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,
commands: [command(spokenForm, action)],
useCommunitySnippets,
};
}

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

Expand All @@ -53,3 +62,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
Loading