Support inline snippets; add Talon snippet api (#1329)

- Fixes #1324
- Fixes #1325 

Note that I created a Talon-side insertion api that supports the full
set of options, including substitutions, targets, and scopes, but
decided to leave it out for now because I'm not sure exactly how it
should look and we don't need it for the mathfly support we're planning
to use it for. In particular, we should probably figure out #803 before
we implement the more complex api because snippets really want a
destination, not a target. I did use the complex Talon-side api to
record some tests of the extension api, though

Here is the complex Talon snippet insertion api in case useful at some
point

<details><summary>Complex talon api</summary>

```diff
From e39e03e3a06a6db4f1edb245a5225037d0ea08d3 Mon Sep 17 00:00:00 2001
From: Pokey Rule <755842+pokey@users.noreply.github.com>
Date: Fri, 24 Mar 2023 14:22:40 +0000
Subject: [PATCH] Complex insert snippet Talon api

---
 cursorless-talon/src/snippets.py | 35 ++++++++++++++++++++++++--------
 1 file changed, 27 insertions(+), 8 deletions(-)

diff --git a/cursorless-talon/src/snippets.py b/cursorless-talon/src/snippets.py
index a9f100f58..409512011 100644
--- a/cursorless-talon/src/snippets.py
+++ b/cursorless-talon/src/snippets.py
@@ -91,15 +91,34 @@ class Actions:
             },
         )
 
-    def cursorless_insert_snippet(body: str):
+    def cursorless_insert_snippet(
+        body: str,
+        target: Optional[dict] = None,
+        scope: Optional[str] = None,
+        snippet_variable: Optional[str] = None,
+        text: Optional[str] = None,
+    ):
         """Inserts a custom snippet"""
-        actions.user.cursorless_implicit_target_command(
-            "insertSnippet",
-            {
-                "type": "custom",
-                "body": body,
-            },
-        )
+        snippet_arg: dict[str, Any] = {
+            "type": "custom",
+            "body": body,
+        }
+        if scope:
+            snippet_arg["scopeType"] = {"type": scope}
+        if snippet_variable:
+            snippet_arg["substitutions"] = {snippet_variable: text}
+
+        if target:
+            actions.user.cursorless_single_target_command_with_arg_list(
+                "insertSnippet",
+                target,
+                [snippet_arg],
+            )
+        else:
+            actions.user.cursorless_implicit_target_command(
+                "insertSnippet",
+                snippet_arg,
+            )
 
     def cursorless_wrap_with_snippet_by_name(
         name: str, variable_name: str, target: dict
-- 
2.39.2

```

</details>

## Checklist

- [x] I have added
[tests](https://www.cursorless.org/docs/contributing/test-case-recorder/)
- [x] 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
This commit is contained in:
Pokey Rule 2023-03-27 02:44:19 -07:00 committed by GitHub
parent cff8b4d59c
commit 94bab18f73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1133 additions and 105 deletions

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Literal
from typing import Literal, Union
from talon import Module, actions
@ -9,7 +9,7 @@ from ..paired_delimiter import paired_delimiters_map
@dataclass
class Wrapper:
type: Literal["pairedDelimiter", "snippet"]
extra_args: list[str]
extra_args: list[Union[str, dict]]
mod = Module()
@ -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

View File

@ -12,7 +12,7 @@ from .primitive_target import IMPLICIT_TARGET
mod = Module()
CURSORLESS_COMMAND_ID = "cursorless.command"
CURSORLESS_COMMAND_VERSION = 4
CURSORLESS_COMMAND_VERSION = 5
last_phrase = None

View File

@ -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)

View File

@ -1,3 +1,5 @@
from typing import Any, Optional
from talon import Module, actions, app
from .csv_overrides import init_csv_and_watch_changes
@ -27,13 +29,13 @@ mod.list("cursorless_phrase_terminator", "Contains term used to terminate a phra
@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.
@ -65,13 +67,76 @@ insertion_snippets_single_phrase = {
@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],
)

View File

@ -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

View File

@ -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.

View File

@ -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";

View File

@ -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

View File

@ -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;

View File

@ -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[];
}

View File

@ -0,0 +1,351 @@
interface CursorMark {
type: "cursor";
}
interface ThatMark {
type: "that";
}
interface SourceMark {
type: "source";
}
interface NothingMark {
type: "nothing";
}
interface DecoratedSymbolMark {
type: "decoratedSymbol";
symbolColor: string;
character: string;
}
type LineNumberType = "absolute" | "relative" | "modulo100";
interface LineNumberMark {
type: "lineNumber";
lineNumberType: LineNumberType;
lineNumber: number;
}
/**
* Constructs a range between {@link anchor} and {@link active}
*/
interface RangeMark {
type: "range";
anchor: Mark;
active: Mark;
excludeAnchor?: boolean;
excludeActive?: boolean;
}
type Mark =
| CursorMark
| ThatMark
| SourceMark
| DecoratedSymbolMark
| NothingMark
| LineNumberMark
| RangeMark;
type SimpleSurroundingPairName =
| "angleBrackets"
| "backtickQuotes"
| "curlyBrackets"
| "doubleQuotes"
| "escapedDoubleQuotes"
| "escapedParentheses"
| "escapedSquareBrackets"
| "escapedSingleQuotes"
| "parentheses"
| "singleQuotes"
| "squareBrackets";
type ComplexSurroundingPairName = "string" | "any" | "collectionBoundary";
type SurroundingPairName =
| SimpleSurroundingPairName
| ComplexSurroundingPairName;
type SimpleScopeTypeType =
| "argumentOrParameter"
| "anonymousFunction"
| "attribute"
| "branch"
| "class"
| "className"
| "collectionItem"
| "collectionKey"
| "comment"
| "functionCall"
| "functionCallee"
| "functionName"
| "ifStatement"
| "list"
| "map"
| "name"
| "namedFunction"
| "regularExpression"
| "statement"
| "string"
| "type"
| "value"
| "condition"
| "section"
| "sectionLevelOne"
| "sectionLevelTwo"
| "sectionLevelThree"
| "sectionLevelFour"
| "sectionLevelFive"
| "sectionLevelSix"
| "selector"
| "switchStatementSubject"
| "unit"
| "xmlBothTags"
| "xmlElement"
| "xmlEndTag"
| "xmlStartTag"
// Latex scope types
| "part"
| "chapter"
| "subSection"
| "subSubSection"
| "namedParagraph"
| "subParagraph"
| "environment"
// Text based scopes
| "token"
| "line"
| "notebookCell"
| "paragraph"
| "document"
| "character"
| "word"
| "identifier"
| "nonWhitespaceSequence"
| "boundedNonWhitespaceSequence"
| "url";
interface SimpleScopeType {
type: SimpleScopeTypeType;
}
interface CustomRegexScopeType {
type: "customRegex";
regex: string;
}
type SurroundingPairDirection = "left" | "right";
interface SurroundingPairScopeType {
type: "surroundingPair";
delimiter: SurroundingPairName;
forceDirection?: SurroundingPairDirection;
/**
* If `true`, then only accept pairs where the pair completely contains the
* selection, ie without the edges touching.
*/
requireStrongContainment?: boolean;
}
interface OneOfScopeType {
type: "oneOf";
scopeTypes: ScopeType[];
}
type ScopeType =
| SimpleScopeType
| SurroundingPairScopeType
| CustomRegexScopeType
| OneOfScopeType;
interface InteriorOnlyModifier {
type: "interiorOnly";
}
interface ExcludeInteriorModifier {
type: "excludeInterior";
}
interface ContainingScopeModifier {
type: "containingScope";
scopeType: ScopeType;
ancestorIndex?: number;
}
interface EveryScopeModifier {
type: "everyScope";
scopeType: ScopeType;
}
/**
* Refer to scopes by absolute index relative to iteration scope, eg "first
* funk" to refer to the first function in a class.
*/
interface OrdinalScopeModifier {
type: "ordinalScope";
scopeType: ScopeType;
/** The start of the range. Start from end of iteration scope if `start` is negative */
start: number;
/** The number of scopes to include. Will always be positive. If greater than 1, will include scopes after {@link start} */
length: number;
}
type Direction = "forward" | "backward";
/**
* Refer to scopes by offset relative to input target, eg "next
* funk" to refer to the first function after the function containing the target input.
*/
interface RelativeScopeModifier {
type: "relativeScope";
scopeType: ScopeType;
/** Indicates how many scopes away to start relative to the input target.
* Note that if {@link direction} is `"backward"`, then this scope will be the
* end of the output range. */
offset: number;
/** The number of scopes to include. Will always be positive. If greater
* than 1, will include scopes in the direction of {@link direction} */
length: number;
/** Indicates which direction both {@link offset} and {@link length} go
* relative to input target */
direction: Direction;
}
/**
* Converts its input to a raw selection with no type information so for
* example if it is the destination of a bring or move it should inherit the
* type information such as delimiters from its source.
*/
interface RawSelectionModifier {
type: "toRawSelection";
}
interface LeadingModifier {
type: "leading";
}
interface TrailingModifier {
type: "trailing";
}
interface KeepContentFilterModifier {
type: "keepContentFilter";
}
interface KeepEmptyFilterModifier {
type: "keepEmptyFilter";
}
interface InferPreviousMarkModifier {
type: "inferPreviousMark";
}
type TargetPosition = "before" | "after" | "start" | "end";
interface PositionModifier {
type: "position";
position: TargetPosition;
}
interface PartialPrimitiveTargetDescriptor {
type: "primitive";
mark?: Mark;
modifiers?: Modifier[];
}
interface HeadTailModifier {
type: "extendThroughStartOf" | "extendThroughEndOf";
modifiers?: Modifier[];
}
/**
* Runs {@link modifier} if the target has no explicit scope type, ie if
* {@link Target.hasExplicitScopeType} is `false`.
*/
interface ModifyIfUntypedModifier {
type: "modifyIfUntyped";
/**
* The modifier to apply if the target is untyped
*/
modifier: Modifier;
}
/**
* Tries each of the modifiers in {@link modifiers} in turn until one of them
* doesn't throw an error, returning the output from the first modifier not
* throwing an error.
*/
interface CascadingModifier {
type: "cascading";
/**
* The modifiers to try in turn
*/
modifiers: Modifier[];
}
/**
* First applies {@link anchor} to input, then independently applies
* {@link active}, and forms a range between the two resulting targets
*/
interface RangeModifier {
type: "range";
anchor: Modifier;
active: Modifier;
excludeAnchor?: boolean;
excludeActive?: boolean;
}
type Modifier =
| PositionModifier
| InteriorOnlyModifier
| ExcludeInteriorModifier
| ContainingScopeModifier
| EveryScopeModifier
| OrdinalScopeModifier
| RelativeScopeModifier
| HeadTailModifier
| LeadingModifier
| TrailingModifier
| RawSelectionModifier
| ModifyIfUntypedModifier
| CascadingModifier
| RangeModifier
| KeepContentFilterModifier
| KeepEmptyFilterModifier
| InferPreviousMarkModifier;
// continuous is one single continuous selection between the two targets
// vertical puts a selection on each line vertically between the two targets
type RangeType = "continuous" | "vertical";
interface PartialRangeTargetDescriptor {
type: "range";
anchor: PartialPrimitiveTargetDescriptor | ImplicitTargetDescriptor;
active: PartialPrimitiveTargetDescriptor;
excludeAnchor: boolean;
excludeActive: boolean;
rangeType?: RangeType;
}
interface PartialListTargetDescriptor {
type: "list";
elements: (PartialPrimitiveTargetDescriptor | PartialRangeTargetDescriptor)[];
}
interface ImplicitTargetDescriptor {
type: "implicit";
}
export type PartialTargetDescriptorV4 =
| PartialPrimitiveTargetDescriptor
| PartialRangeTargetDescriptor
| PartialListTargetDescriptor
| ImplicitTargetDescriptor;

View File

@ -1,5 +1,6 @@
import {
RangeExpansionBehavior,
ScopeType,
Snippet,
SnippetDefinition,
textFormatters,
@ -20,57 +21,113 @@ import { Target } from "../typings/target.types";
import { ensureSingleEditor } from "../util/targetUtils";
import { Action, ActionReturnValue } from "./actions.types";
interface NamedSnippetArg {
type: "named";
name: string;
substitutions?: Record<string, string>;
}
interface CustomSnippetArg {
type: "custom";
body: string;
scopeType?: ScopeType;
substitutions?: Record<string, string>;
}
type InsertSnippetArg = NamedSnippetArg | CustomSnippetArg;
export default class InsertSnippet implements Action {
private snippetParser = new SnippetParser();
getPrePositionStages(snippetName: string) {
const snippet = this.graph.snippets.getSnippetStrict(snippetName);
const defaultScopeTypes = snippet.insertionScopeTypes;
if (defaultScopeTypes == null) {
return [];
}
return [
new ModifyIfUntypedExplicitStage({
type: "cascading",
modifiers: defaultScopeTypes.map((scopeType) => ({
type: "containingScope",
scopeType: {
type: scopeType,
},
})),
}),
];
}
constructor(private graph: Graph) {
this.run = this.run.bind(this);
}
getPrePositionStages(snippetDescription: InsertSnippetArg) {
const defaultScopeTypes = this.getScopeTypes(snippetDescription);
return defaultScopeTypes.length === 0
? []
: [
new ModifyIfUntypedExplicitStage({
type: "cascading",
modifiers: defaultScopeTypes.map((scopeType) => ({
type: "containingScope",
scopeType,
})),
}),
];
}
private getScopeTypes(snippetDescription: InsertSnippetArg): ScopeType[] {
if (snippetDescription.type === "named") {
const { name } = snippetDescription;
const snippet = this.graph.snippets.getSnippetStrict(name);
const scopeTypeTypes = snippet.insertionScopeTypes;
return scopeTypeTypes == null
? []
: scopeTypeTypes.map((scopeTypeType) => ({
type: scopeTypeType,
}));
} else {
return snippetDescription.scopeType == null
? []
: [snippetDescription.scopeType];
}
}
private getSnippetInfo(
snippetDescription: InsertSnippetArg,
targets: Target[],
) {
if (snippetDescription.type === "named") {
const { name } = snippetDescription;
const snippet = this.graph.snippets.getSnippetStrict(name);
const definition = findMatchingSnippetDefinitionStrict(
targets,
snippet.definitions,
);
return {
body: definition.body.join("\n"),
formatSubstitutions(substitutions: Record<string, string> | undefined) {
return substitutions == null
? undefined
: formatSubstitutions(snippet, definition, substitutions);
},
};
} else {
return {
body: snippetDescription.body,
formatSubstitutions(substitutions: Record<string, string> | undefined) {
return substitutions;
},
};
}
}
async run(
[targets]: [Target[]],
snippetName: string,
substitutions: Record<string, string>,
snippetDescription: InsertSnippetArg,
): Promise<ActionReturnValue> {
const snippet = this.graph.snippets.getSnippetStrict(snippetName);
const editor = ide().getEditableTextEditor(ensureSingleEditor(targets));
const definition = findMatchingSnippetDefinitionStrict(
const { body, formatSubstitutions } = this.getSnippetInfo(
snippetDescription,
targets,
snippet.definitions,
);
const parsedSnippet = this.snippetParser.parse(definition.body.join("\n"));
const parsedSnippet = this.snippetParser.parse(body);
const formattedSubstitutions =
substitutions == null
? undefined
: formatSubstitutions(snippet, definition, substitutions);
transformSnippetVariables(parsedSnippet, null, formattedSubstitutions);
transformSnippetVariables(
parsedSnippet,
null,
formatSubstitutions(snippetDescription.substitutions),
);
const snippetString = parsedSnippet.toTextmateString();

View File

@ -1,5 +1,6 @@
import { FlashStyle } from "@cursorless/common";
import { FlashStyle, ScopeType } from "@cursorless/common";
import { callFunctionAndUpdateSelections } from "../core/updateSelections/updateSelections";
import { Graph } from "../index";
import { ModifyIfUntypedStage } from "../processTargets/modifiers/ConditionalModifierStages";
import { ide } from "../singletons/ide.singleton";
import {
@ -8,21 +9,31 @@ import {
} from "../snippets/snippet";
import { SnippetParser } from "../snippets/vendor/vscodeSnippet/snippetParser";
import { Target } from "../typings/target.types";
import { Graph } from "../typings/Graph";
import { ensureSingleEditor, flashTargets } from "../util/targetUtils";
import { Action, ActionReturnValue } from "./actions.types";
interface NamedSnippetArg {
type: "named";
name: string;
variableName: string;
}
interface CustomSnippetArg {
type: "custom";
body: string;
variableName?: string;
scopeType?: ScopeType;
}
type WrapWithSnippetArg = NamedSnippetArg | CustomSnippetArg;
export default class WrapWithSnippet implements Action {
private snippetParser = new SnippetParser();
getFinalStages(snippetLocation: string) {
const [snippetName, placeholderName] =
parseSnippetLocation(snippetLocation);
constructor(private graph: Graph) {
this.run = this.run.bind(this);
}
const snippet = this.graph.snippets.getSnippetStrict(snippetName);
const variables = snippet.variables ?? {};
const defaultScopeType = variables[placeholderName]?.wrapperScopeType;
getFinalStages(snippet: WrapWithSnippetArg) {
const defaultScopeType = this.getScopeType(snippet);
if (defaultScopeType == null) {
return [];
@ -33,37 +44,63 @@ export default class WrapWithSnippet implements Action {
type: "modifyIfUntyped",
modifier: {
type: "containingScope",
scopeType: {
type: defaultScopeType,
},
scopeType: defaultScopeType,
},
}),
];
}
constructor(private graph: Graph) {
this.run = this.run.bind(this);
private getScopeType(
snippetDescription: WrapWithSnippetArg,
): ScopeType | undefined {
if (snippetDescription.type === "named") {
const { name, variableName } = snippetDescription;
const snippet = this.graph.snippets.getSnippetStrict(name);
const variables = snippet.variables ?? {};
const scopeTypeType = variables[variableName]?.wrapperScopeType;
return scopeTypeType == null
? undefined
: {
type: scopeTypeType,
};
} else {
return snippetDescription.scopeType;
}
}
private getBody(
snippetDescription: WrapWithSnippetArg,
targets: Target[],
): string {
if (snippetDescription.type === "named") {
const { name } = snippetDescription;
const snippet = this.graph.snippets.getSnippetStrict(name);
const definition = findMatchingSnippetDefinitionStrict(
targets,
snippet.definitions,
);
return definition.body.join("\n");
} else {
return snippetDescription.body;
}
}
async run(
[targets]: [Target[]],
snippetLocation: string,
snippetDescription: WrapWithSnippetArg,
): Promise<ActionReturnValue> {
const [snippetName, placeholderName] =
parseSnippetLocation(snippetLocation);
const snippet = this.graph.snippets.getSnippetStrict(snippetName);
const editor = ide().getEditableTextEditor(ensureSingleEditor(targets));
const definition = findMatchingSnippetDefinitionStrict(
targets,
snippet.definitions,
);
const body = this.getBody(snippetDescription, targets);
const parsedSnippet = this.snippetParser.parse(definition.body.join("\n"));
const parsedSnippet = this.snippetParser.parse(body);
transformSnippetVariables(parsedSnippet, placeholderName);
transformSnippetVariables(parsedSnippet, snippetDescription.variableName);
const snippetString = parsedSnippet.toTextmateString();
@ -88,11 +125,3 @@ export default class WrapWithSnippet implements Action {
};
}
}
function parseSnippetLocation(snippetLocation: string): [string, string] {
const [snippetName, placeholderName] = snippetLocation.split(".");
if (snippetName == null || placeholderName == null) {
throw new Error("Snippet location missing '.'");
}
return [snippetName, placeholderName];
}

View File

@ -1,29 +1,26 @@
import {
ActionType,
Command,
CommandComplete,
CommandLatest,
EnforceUndefined,
LATEST_VERSION,
Modifier,
OutdatedExtensionError,
PartialTargetDescriptor,
showWarning,
SimpleScopeTypeType,
} from "@cursorless/common";
import { ide } from "../../singletons/ide.singleton";
import { Graph } from "../../typings/Graph";
import { getPartialPrimitiveTargets } from "../../util/getPrimitiveTargets";
import { ActionType } from "@cursorless/common";
import {
Command,
CommandComplete,
CommandLatest,
LATEST_VERSION,
} from "@cursorless/common";
import {
Modifier,
PartialTargetDescriptor,
SimpleScopeTypeType,
} from "@cursorless/common";
import canonicalizeActionName from "./canonicalizeActionName";
import canonicalizeTargets from "./canonicalizeTargets";
import { upgradeV0ToV1 } from "./upgradeV0ToV1";
import { upgradeV1ToV2 } from "./upgradeV1ToV2";
import { upgradeV2ToV3 } from "./upgradeV2ToV3";
import { upgradeV3ToV4 } from "./upgradeV3ToV4";
import { upgradeV4ToV5 } from "./upgradeV4ToV5/upgradeV4ToV5";
/**
* Given a command argument which comes from the client, normalize it so that it
@ -79,6 +76,9 @@ function upgradeCommand(command: Command): CommandLatest {
case 3:
command = upgradeV3ToV4(command);
break;
case 4:
command = upgradeV4ToV5(command);
break;
default:
throw new Error(
`Can't upgrade from unknown version ${command.version}`,

View File

@ -1,14 +1,12 @@
import { CommandV4 } from "@cursorless/common";
import { CommandV3 } from "@cursorless/common";
import {
PartialPrimitiveTargetDescriptorV3,
PartialTargetDescriptorV3,
} from "@cursorless/common";
import {
CommandV3,
CommandV4,
ImplicitTargetDescriptor,
PartialPrimitiveTargetDescriptor,
PartialPrimitiveTargetDescriptorV3,
PartialRangeTargetDescriptor,
PartialTargetDescriptor,
PartialTargetDescriptorV3,
} from "@cursorless/common";
export function upgradeV3ToV4(command: CommandV3): CommandV4 {

View File

@ -0,0 +1 @@
export * from "./upgradeV4ToV5";

View File

@ -0,0 +1,57 @@
import {
ActionCommand,
ActionCommandV4,
CommandV4,
CommandV5,
} from "@cursorless/common";
export function upgradeV4ToV5(command: CommandV4): CommandV5 {
return {
...command,
version: 5,
action: upgradeAction(command.action),
};
}
function upgradeAction(action: ActionCommandV4): ActionCommand {
switch (action.name) {
case "wrapWithSnippet": {
const [name, variableName] = parseSnippetLocation(
action.args![0] as string,
);
return {
name: "wrapWithSnippet",
args: [
{
type: "named",
name,
variableName,
},
],
};
}
case "insertSnippet": {
const [name, substitutions] = action.args!;
return {
name: "insertSnippet",
args: [
{
type: "named",
name,
substitutions,
},
],
};
}
default:
return action;
}
}
function parseSnippetLocation(snippetLocation: string): [string, string] {
const [snippetName, placeholderName] = snippetLocation.split(".");
if (snippetName == null || placeholderName == null) {
throw new Error("Snippet location missing '.'");
}
return [snippetName, placeholderName];
}

View File

@ -0,0 +1,29 @@
languageId: plaintext
command:
version: 5
spokenForm: custom insert
action:
name: insertSnippet
args:
- {type: custom, body: 'dummy snippet hole1: ($hole1), hole2: ($hole2)'}
targets:
- {type: implicit}
usePrePhraseSnapshot: true
initialState:
documentContents: ""
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: "dummy snippet hole1: (), hole2: ()"
selections:
- anchor: {line: 0, character: 22}
active: {line: 0, character: 22}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 0, character: 34}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,35 @@
languageId: plaintext
command:
version: 5
spokenForm: custom insert after whale
action:
name: insertSnippet
args:
- {type: custom, body: 'dummy snippet hole1: ($hole1), hole2: ($hole2)'}
targets:
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: w}
modifiers:
- {type: position, position: after}
usePrePhraseSnapshot: true
initialState:
documentContents: hello world
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks:
default.w:
start: {line: 0, character: 6}
end: {line: 0, character: 11}
finalState:
documentContents: "hello world dummy snippet hole1: (), hole2: ()"
selections:
- anchor: {line: 0, character: 34}
active: {line: 0, character: 34}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 12}
end: {line: 0, character: 46}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,39 @@
languageId: plaintext
command:
version: 5
spokenForm: custom insert after whale
action:
name: insertSnippet
args:
- type: custom
body: "dummy snippet hole1: ($hole1), hole2: ($hole2)"
scopeType: {type: line}
targets:
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: w}
modifiers:
- {type: position, position: after}
usePrePhraseSnapshot: true
initialState:
documentContents: hello world
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks:
default.w:
start: {line: 0, character: 6}
end: {line: 0, character: 11}
finalState:
documentContents: |-
hello world
dummy snippet hole1: (), hole2: ()
selections:
- anchor: {line: 1, character: 22}
active: {line: 1, character: 22}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 1, character: 0}
end: {line: 1, character: 34}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,31 @@
languageId: plaintext
command:
version: 5
spokenForm: custom insert hello world
action:
name: insertSnippet
args:
- type: custom
body: "dummy snippet hole1: ($hole1), hole2: ($hole2)"
substitutions: {hole2: hello world}
targets:
- {type: implicit}
usePrePhraseSnapshot: true
initialState:
documentContents: ""
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: "dummy snippet hole1: (), hole2: (hello world)"
selections:
- anchor: {line: 0, character: 22}
active: {line: 0, character: 22}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 0, character: 45}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,38 @@
languageId: plaintext
command:
version: 5
spokenForm: custom wrap harp
action:
name: wrapWithSnippet
args:
- type: custom
body: "dummy snippet hole1: ($hole1), hole2: ($hole2)"
scopeType: {type: line}
variableName: hole1
targets:
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: h}
usePrePhraseSnapshot: true
initialState:
documentContents: |
hello world
selections:
- anchor: {line: 1, character: 0}
active: {line: 1, character: 0}
marks:
default.h:
start: {line: 0, character: 0}
end: {line: 0, character: 5}
finalState:
documentContents: |
dummy snippet hole1: (hello world), hole2: ()
selections:
- anchor: {line: 0, character: 44}
active: {line: 0, character: 44}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 0, character: 45}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,32 @@
languageId: plaintext
command:
version: 5
spokenForm: custom wrap line
action:
name: wrapWithSnippet
args:
- {type: custom, body: 'dummy snippet hole1: ($hole1), hole2: ($hole2)', variableName: hole1}
targets:
- type: primitive
modifiers:
- type: containingScope
scopeType: {type: line}
usePrePhraseSnapshot: true
initialState:
documentContents: hello world
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: "dummy snippet hole1: (hello world), hole2: ()"
selections:
- anchor: {line: 0, character: 44}
active: {line: 0, character: 44}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 0, character: 45}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,32 @@
languageId: plaintext
command:
version: 5
spokenForm: custom wrap line
action:
name: wrapWithSnippet
args:
- {type: custom, body: 'dummy snippet hole1: ($TM_SELECTED_TEXT), hole2: ($hole2)'}
targets:
- type: primitive
modifiers:
- type: containingScope
scopeType: {type: line}
usePrePhraseSnapshot: true
initialState:
documentContents: hello world
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: "dummy snippet hole1: (hello world), hole2: ()"
selections:
- anchor: {line: 0, character: 44}
active: {line: 0, character: 44}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 0, character: 45}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,32 @@
languageId: typescript
command:
version: 5
spokenForm: snip funk
action:
name: insertSnippet
args:
- {type: named, name: functionDeclaration}
targets:
- {type: implicit}
usePrePhraseSnapshot: true
initialState:
documentContents: ""
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: |-
function () {
}
selections:
- anchor: {line: 0, character: 9}
active: {line: 0, character: 9}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 2, character: 1}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,34 @@
languageId: typescript
command:
version: 5
spokenForm: snip funk hello world
action:
name: insertSnippet
args:
- type: named
name: functionDeclaration
substitutions: {name: hello world}
targets:
- {type: implicit}
usePrePhraseSnapshot: true
initialState:
documentContents: ""
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: |-
function helloWorld() {
}
selections:
- anchor: {line: 0, character: 20}
active: {line: 0, character: 20}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 2, character: 1}
isReversed: false
hasExplicitRange: true

View File

@ -0,0 +1,35 @@
languageId: typescript
command:
version: 5
spokenForm: try wrap this
action:
name: wrapWithSnippet
args:
- {type: named, name: tryCatchStatement, variableName: body}
targets:
- type: primitive
mark: {type: cursor}
usePrePhraseSnapshot: true
initialState:
documentContents: const foo = "bar";
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: |-
try {
const foo = "bar";
} catch (err) {
}
selections:
- anchor: {line: 3, character: 4}
active: {line: 3, character: 4}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 4, character: 1}
isReversed: false
hasExplicitRange: true

View File

@ -1,6 +1,6 @@
languageId: plaintext
command:
version: 4
version: 5
spokenForm: take harp
action: {name: setSelection}
targets: