Skip to content

Commit

Permalink
Insert snippets squashed
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed Nov 11, 2021
1 parent a6bdb3a commit 9749800
Show file tree
Hide file tree
Showing 26 changed files with 765 additions and 155 deletions.
49 changes: 49 additions & 0 deletions cursorless-snippets/functionDeclaration.cursorless-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"functionDeclaration": {
"definitions": [
{
"scope": {
"langIds": [
"typescript",
"typescriptreact",
"javascript",
"javascriptreact"
]
},
"body": [
"function $name($parameterList) {",
"\t$body",
"}"
],
"variables": {
"name": {
"formatter": "camelCase"
}
}
},
{
"scope": {
"langIds": [
"python"
]
},
"body": [
"def $name($parameterList):",
"\t$body"
],
"variables": {
"name": {
"formatter": "snakeCase"
}
}
}
],
"description": "Function declaration",
"variables": {
"body": {
"wrapperScopeType": "statement"
}
},
"insertionScopeType": "statement"
}
}
5 changes: 3 additions & 2 deletions cursorless-snippets/ifElseStatement.cursorless-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"alternative": {
"wrapperScopeType": "statement"
}
}
},
"insertionScopeType": "statement"
}
}
}
5 changes: 3 additions & 2 deletions cursorless-snippets/ifStatement.cursorless-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"consequence": {
"wrapperScopeType": "statement"
}
}
},
"insertionScopeType": "statement"
}
}
}
5 changes: 3 additions & 2 deletions cursorless-snippets/tryCatchStatement.cursorless-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"exceptBody": {
"wrapperScopeType": "statement"
}
}
},
"insertionScopeType": "statement"
}
}
}
116 changes: 71 additions & 45 deletions schemas/cursorless-snippets.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,22 @@
"type": "array",
"descriptions": "List of possible definitions for this snippet",
"items": {
"type": "object",
"properties": {
"scope": {
"type": "object",
"description": "Scopes where this snippet is active",
"properties": {
"langIds": {
"type": "array",
"items": {
"type": "string"
}
},
"scopeType": {
"$ref": "#/$defs/scopeType",
"description": "Cursorless scopes in which this snippet is active. Allows, for example, to have different snippets to define a function if you're in a class or at global scope."
}
}
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Inline snippet text using VSCode snippet syntax; entries joined by newline. Named variables of the form $foo can be used as wrappers"
}
},
"required": [
"body"
]
"$ref": "#/$defs/snippetDefinition"
}
},
"variables": {
"type": "object",
"description": "For each named variable in the snippet, provides extra information about the variable.",
"additionalProperties": {
"type": "object",
"properties": {
"wrapperScopeType": {
"$ref": "#/$defs/scopeType",
"description": "Default to this scope type when wrapping a target without scope type specified"
},
"description": {
"type": "string",
"description": "Description of the snippet variable"
}
}
}
"$ref": "#/$defs/variables"
},
"description": {
"type": "string",
"description": "Description of the snippet"
},
"insertionScopeType": {
"$ref": "#/$defs/scopeType",
"description": "Default to this scope type when inserting this snippet before/after a target without scope type specified"
}
}
},
"additionalProperties": false
},
"$defs": {
"scopeType": {
Expand Down Expand Up @@ -92,6 +55,69 @@
"xmlEndTag",
"xmlStartTag"
]
},
"variables": {
"type": "object",
"description": "For each named variable in the snippet, provides extra information about the variable.",
"additionalProperties": {
"type": "object",
"properties": {
"wrapperScopeType": {
"$ref": "#/$defs/scopeType",
"description": "Default to this scope type when wrapping a target without scope type specified"
},
"description": {
"type": "string",
"description": "Description of the snippet variable"
},
"formatter": {
"type": "string",
"enum": [
"camelCase",
"pascalCase",
"snakeCase"
],
"description": "Format text inserted into this variable using the given formatter"
}
},
"additionalProperties": false
}
},
"snippetDefinition": {
"type": "object",
"properties": {
"scope": {
"type": "object",
"description": "Scopes where this snippet is active",
"properties": {
"langIds": {
"type": "array",
"items": {
"type": "string"
}
},
"scopeType": {
"$ref": "#/$defs/scopeType",
"description": "Cursorless scopes in which this snippet is active. Allows, for example, to have different snippets to define a function if you're in a class or at global scope."
}
},
"additionalProperties": false
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Inline snippet text using VSCode snippet syntax; entries joined by newline. Named variables of the form $foo can be used as wrappers"
},
"variables": {
"$ref": "#/$defs/variables"
}
},
"additionalProperties": false,
"required": [
"body"
]
}
}
}
159 changes: 159 additions & 0 deletions src/actions/InsertSnippet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { commands, DecorationRangeBehavior } from "vscode";
import {
Action,
ActionPreferences,
ActionReturnValue,
Graph,
TypedSelection,
} from "../typings/Types";
import displayPendingEditDecorations from "../util/editDisplayUtils";
import { ensureSingleEditor } from "../util/targetUtils";
import { SnippetParser } from "../vendor/snippet/snippetParser";
import {
findMatchingSnippetDefinition,
transformSnippetVariables,
} from "../util/snippet";
import textFormatters from "../core/textFormatters";
import { SnippetDefinition, Snippet } from "../typings/snippet";
import {
callFunctionAndUpdateSelectionInfos,
callFunctionAndUpdateSelections,
getSelectionInfo,
} from "../core/updateSelections/updateSelections";

export default class InsertSnippet implements Action {
private snippetParser = new SnippetParser();

getTargetPreferences(snippetName: string): ActionPreferences[] {
const snippet = this.graph.snippets.getSnippet(snippetName);

if (snippet == null) {
throw new Error(`Couldn't find snippet ${snippetName}`);
}

const defaultScopeType = snippet.insertionScopeType;

return [
{
insideOutsideType: "outside",
modifier:
defaultScopeType == null
? undefined
: {
type: "containingScope",
scopeType: defaultScopeType,
includeSiblings: false,
},
},
];
}

constructor(private graph: Graph) {
this.run = this.run.bind(this);
}

async run(
[targets]: [TypedSelection[]],
snippetName: string,
substitutions: Record<string, string>
): Promise<ActionReturnValue> {
const snippet = this.graph.snippets.getSnippet(snippetName)!;

const editor = ensureSingleEditor(targets);

// Find snippet definition matching context.
// NB: We only look at the first target to create our context. This means
// that if there are two snippets that match two different contexts, and
// the two targets match those two different contexts, we will just use the
// snippet that matches the first context for both targets
const definition = findMatchingSnippetDefinition(
targets[0],
snippet.definitions
);

if (definition == null) {
throw new Error("Couldn't find matching snippet definition");
}

const parsedSnippet = this.snippetParser.parse(definition.body.join("\n"));

const formattedSubstitutions =
substitutions == null
? undefined
: formatSubstitutions(snippet, definition, substitutions);

transformSnippetVariables(parsedSnippet, null, formattedSubstitutions);

const snippetString = parsedSnippet.toTextmateString();

await displayPendingEditDecorations(
targets,
this.graph.editStyles.pendingModification0
);

const targetSelections = targets.map(
(target) => target.selection.selection
);

// TODO: Fix "insert before" once we have the new update selections code
// TODO: Remove undo stop once we have the new update selections code
await this.graph.actions.setSelection.run([targets]);

// NB: We do this to auto insert the delimiter if necessary
await this.graph.actions.replace.run([targets], [""]);

const targetSelectionInfos = targetSelections.map((selection) =>
getSelectionInfo(
editor.document,
selection,
DecorationRangeBehavior.OpenOpen
)
);

// NB: We used the command "editor.action.insertSnippet" instead of calling editor.insertSnippet
// because the latter doesn't support special variables like CLIPBOARD
const [updatedTargetSelections] = await callFunctionAndUpdateSelectionInfos(
this.graph.rangeUpdater,
() =>
commands.executeCommand("editor.action.insertSnippet", {
snippet: snippetString,
}),
editor.document,
[targetSelectionInfos]
);

return {
thatMark: updatedTargetSelections.map((selection) => ({
editor,
selection,
})),
};
}
}
function formatSubstitutions(
snippet: Snippet,
definition: SnippetDefinition,
substitutions: Record<string, string>
) {
return Object.fromEntries(
Object.entries(substitutions).map(([variableName, value]) => {
const formatterName =
(definition.variables ?? {})[variableName]?.formatter ??
(snippet.variables ?? {})[variableName]?.formatter;

if (formatterName == null) {
return [variableName, value];
}

const formatter = textFormatters[formatterName];

if (formatter == null) {
throw new Error(
`Couldn't find formatter ${formatterName} for variable ${variableName}`
);
}

return [variableName, formatter(value.split(" "))];
})
);
}
2 changes: 1 addition & 1 deletion src/actions/Replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { performEditsAndUpdateSelections } from "../core/updateSelections/update

type RangeGenerator = { start: number };

export default class implements Action {
export default class Replace implements Action {
getTargetPreferences: () => ActionPreferences[] = () => [
{ insideOutsideType: null },
];
Expand Down

0 comments on commit 9749800

Please sign in to comment.