Skip to content

Support inline snippets; add Talon snippet api #1329

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 3 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 22 additions & 3 deletions cursorless-talon/src/actions/wrap.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Literal
from typing import Literal, Union

from talon import Module, actions

Expand All @@ -9,7 +9,7 @@
@dataclass
class Wrapper:
type: Literal["pairedDelimiter", "snippet"]
extra_args: list[str]
extra_args: list[Union[str, dict]]


mod = Module()
Expand All @@ -30,7 +30,26 @@ def cursorless_wrapper(m) -> Wrapper:
extra_args=[paired_delimiter_info.left, paired_delimiter_info.right],
)
except AttributeError:
return Wrapper(type="snippet", extra_args=[m.cursorless_wrapper_snippet])
snippet_name, variable_name = parse_snippet_location(
m.cursorless_wrapper_snippet
)
return Wrapper(
type="snippet",
extra_args=[
{
"type": "named",
"name": snippet_name,
"variableName": variable_name,
}
],
)


def parse_snippet_location(snippet_location: str) -> tuple[str, str]:
[snippet_name, variable_name] = snippet_location.split(".")
if snippet_name is None or variable_name is None:
raise Exception("Snippet location missing '.'")
return (snippet_name, variable_name)


# Maps from (action_type, wrapper_type) to action name
Expand Down
2 changes: 1 addition & 1 deletion cursorless-talon/src/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
mod = Module()

CURSORLESS_COMMAND_ID = "cursorless.command"
CURSORLESS_COMMAND_VERSION = 4
CURSORLESS_COMMAND_VERSION = 5
last_phrase = None


Expand Down
2 changes: 1 addition & 1 deletion cursorless-talon/src/cursorless_snippets.talon
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ tag: user.cursorless
user.cursorless_single_target_command(cursorless_insert_snippet_action, cursorless_positional_target, cursorless_insertion_snippet)

{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} <user.text> [{user.cursorless_phrase_terminator}]:
user.cursorless_insert_snippet_with_phrase(cursorless_insert_snippet_action, cursorless_insertion_snippet_single_phrase, text)
user.private_cursorless_insert_snippet_with_phrase(cursorless_insert_snippet_action, cursorless_insertion_snippet_single_phrase, text)
77 changes: 71 additions & 6 deletions cursorless-talon/src/snippets.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any, Optional

from talon import Module, actions, app

from .csv_overrides import init_csv_and_watch_changes
Expand Down Expand Up @@ -27,13 +29,13 @@
@mod.capture(
rule="{user.cursorless_insertion_snippet_no_phrase} | {user.cursorless_insertion_snippet_single_phrase}"
)
def cursorless_insertion_snippet(m) -> str:
def cursorless_insertion_snippet(m) -> dict:
try:
return m.cursorless_insertion_snippet_no_phrase
name = m.cursorless_insertion_snippet_no_phrase
except AttributeError:
pass
name = m.cursorless_insertion_snippet_single_phrase.split(".")[0]

return m.cursorless_insertion_snippet_single_phrase.split(".")[0]
return {"type": "named", "name": name}


# NOTE: Please do not change these dicts. Use the CSVs for customization.
Expand Down Expand Up @@ -65,13 +67,76 @@ def cursorless_insertion_snippet(m) -> str:

@mod.action_class
class Actions:
def cursorless_insert_snippet_with_phrase(
def private_cursorless_insert_snippet_with_phrase(
action: str, snippet_description: str, text: str
):
"""Perform cursorless wrap action"""
snippet_name, snippet_variable = snippet_description.split(".")
actions.user.cursorless_implicit_target_command(
action, snippet_name, {snippet_variable: text}
action,
{
"type": "named",
"name": snippet_name,
"substitutions": {snippet_variable: text},
},
)

def cursorless_insert_snippet_by_name(name: str):
"""Inserts a named snippet"""
actions.user.cursorless_implicit_target_command(
"insertSnippet",
{
"type": "named",
"name": name,
},
)

def cursorless_insert_snippet(body: str):
"""Inserts a custom snippet"""
actions.user.cursorless_implicit_target_command(
"insertSnippet",
{
"type": "custom",
"body": body,
},
)

def cursorless_wrap_with_snippet_by_name(
name: str, variable_name: str, target: dict
):
"""Wrap target with a named snippet"""
actions.user.cursorless_single_target_command_with_arg_list(
"wrapWithSnippet",
target,
[
{
"type": "named",
"name": name,
"variableName": variable_name,
}
],
)

def cursorless_wrap_with_snippet(
body: str,
target: dict,
variable_name: Optional[str] = None,
scope: Optional[str] = None,
):
"""Wrap target with a custom snippet"""
snippet_arg: dict[str, Any] = {
"type": "custom",
"body": body,
}
if scope is not None:
snippet_arg["scopeType"] = {"type": scope}
if variable_name is not None:
snippet_arg["variableName"] = variable_name

actions.user.cursorless_single_target_command_with_arg_list(
"wrapWithSnippet",
target,
[snippet_arg],
)


Expand Down
9 changes: 9 additions & 0 deletions docs/user/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ Cursorless exposes a couple talon actions and captures that you can use to defin
Performs a built-in IDE command on the given target
eg: `user.cursorless_ide_command("editor.action.addCommentLine", cursorless_target)`

#### Snippet actions

See [snippets](./experimental/snippets.md) for more information about Cursorless snippets.

- `user.cursorless_insert_snippet_by_name(name: str)`: Insert a snippet with the given name, eg `functionDeclaration`
- `user.cursorless_insert_snippet(body: str)`: Insert a snippet with the given body defined using our snippet body syntax (see the [snippet format docs](./experimental/snippet-format.md)). The body should be a single string, which could contain newline `\n` characters, rather than a list of strings as is expected in our snippet json representation.
- `user.cursorless_wrap_with_snippet_by_name(name: str, variable_name: str, target: dict)`: Wrap the given target with a snippet with the given name, eg `functionDeclaration`. Note that `variable_name` should be one of the variables defined in the named snippet. Eg, if the named snippet has a variable `$foo`, you can pass in `"foo"` for `variable_name`, and `target` will be inserted into the position of `$foo` in the given named snippet.
- `user.cursorless_wrap_with_snippet(body, target, variable_name, scope)`: Wrap the given target with a snippet with the given body defined using our snippet body syntax (see the [snippet format docs](./experimental/snippet-format.md)). The body should be a single string, which could contain newline `\n` characters, rather than a list of strings as is expected in our snippet json representation. Note that `variable_name` should be one of the variables defined in `body`. Eg, if `body` has a variable `$foo`, you can pass in `"foo"` for `variable_name`, and `target` will be inserted into the position of `$foo` in the given named snippet. The `scope` variable can be used to automatically expand the target to the given scope type, eg `"line"`.

### Example of combining capture and action

```talon
Expand Down
2 changes: 2 additions & 0 deletions docs/user/experimental/snippet-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
Cursorless has experimental support for snippets. Currently these snippets are just used for wrapping targets.

The best place to start is to look at the [core cursorless snippets](../../../cursorless-snippets). Additionally, there is autocomplete with documentation as you're writing a snippet.

Note that for `body`, we support [the full textmate syntax supported by VSCode](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax), but we prefer to use variable names (eg `$foo`) instead of placeholders (eg `$1`) so that it is easy to use snippets for wrapping.
3 changes: 2 additions & 1 deletion packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ export * from "./types/command/ActionCommand";
export * from "./types/command/legacy/CommandV0V1.types";
export * from "./types/command/legacy/CommandV2.types";
export * from "./types/command/legacy/CommandV3.types";
export * from "./types/command/legacy/CommandV4.types";
export * from "./types/command/legacy/targetDescriptorV2.types";
export * from "./types/command/CommandV4.types";
export * from "./types/command/CommandV5.types";
export * from "./types/command/legacy/PartialTargetDescriptorV3.types";
export * from "./types/CommandServerApi";
export * from "./util/itertools";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { PartialTargetDescriptor } from "./PartialTargetDescriptor.types";
import type { ActionCommand } from "./ActionCommand";

export interface CommandV4 {
export interface CommandV5 {
/**
* The version number of the command API
*/
version: 4;
version: 5;

/**
* The spoken form of the command if issued from a voice command system
Expand Down
13 changes: 10 additions & 3 deletions packages/common/src/types/command/command.types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import type { ActionCommand } from "./ActionCommand";
import type { CommandV5 } from "./CommandV5.types";
import type { CommandV0, CommandV1 } from "./legacy/CommandV0V1.types";
import type { CommandV2 } from "./legacy/CommandV2.types";
import type { CommandV3 } from "./legacy/CommandV3.types";
import type { CommandV4 } from "./CommandV4.types";
import type { CommandV4 } from "./legacy/CommandV4.types";

export type CommandComplete = Required<Omit<CommandLatest, "spokenForm">> &
Pick<CommandLatest, "spokenForm"> & { action: Required<ActionCommand> };

export const LATEST_VERSION = 4 as const;
export const LATEST_VERSION = 5 as const;

export type CommandLatest = Command & {
version: typeof LATEST_VERSION;
};

export type Command = CommandV0 | CommandV1 | CommandV2 | CommandV3 | CommandV4;
export type Command =
| CommandV0
| CommandV1
| CommandV2
| CommandV3
| CommandV4
| CommandV5;
95 changes: 95 additions & 0 deletions packages/common/src/types/command/legacy/CommandV4.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { PartialTargetDescriptorV4 } from "./PartialTargetDescriptorV4.types";

type ActionType =
| "callAsFunction"
| "clearAndSetSelection"
| "copyToClipboard"
| "cutToClipboard"
| "deselect"
| "editNew"
| "editNewLineAfter"
| "editNewLineBefore"
| "executeCommand"
| "extractVariable"
| "findInWorkspace"
| "foldRegion"
| "followLink"
| "generateSnippet"
| "getText"
| "highlight"
| "indentLine"
| "insertCopyAfter"
| "insertCopyBefore"
| "insertEmptyLineAfter"
| "insertEmptyLineBefore"
| "insertEmptyLinesAround"
| "insertSnippet"
| "moveToTarget"
| "outdentLine"
| "pasteFromClipboard"
| "randomizeTargets"
| "remove"
| "rename"
| "replace"
| "replaceWithTarget"
| "revealDefinition"
| "revealTypeDefinition"
| "reverseTargets"
| "rewrapWithPairedDelimiter"
| "scrollToBottom"
| "scrollToCenter"
| "scrollToTop"
| "setSelection"
| "setSelectionAfter"
| "setSelectionBefore"
| "showDebugHover"
| "showHover"
| "showQuickFix"
| "showReferences"
| "sortTargets"
| "swapTargets"
| "toggleLineBreakpoint"
| "toggleLineComment"
| "unfoldRegion"
| "wrapWithPairedDelimiter"
| "wrapWithSnippet";

export interface ActionCommandV4 {
/**
* The action to run
*/
name: ActionType;

/**
* A list of arguments expected by the given action.
*/
args?: unknown[];
}

export interface CommandV4 {
/**
* The version number of the command API
*/
version: 4;

/**
* The spoken form of the command if issued from a voice command system
*/
spokenForm?: string;

/**
* If the command is issued from a voice command system, this boolean indicates
* whether we should use the pre phrase snapshot. Only set this to true if the
* voice command system issues a pre phrase signal at the start of every
* phrase.
*/
usePrePhraseSnapshot: boolean;

action: ActionCommandV4;

/**
* A list of targets expected by the action. Inference will be run on the
* targets
*/
targets: PartialTargetDescriptorV4[];
}
Loading