Skip to content

Commit db951fb

Browse files
committed
Insert snippets squashed
1 parent d131a50 commit db951fb

26 files changed

+765
-155
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"functionDeclaration": {
3+
"definitions": [
4+
{
5+
"scope": {
6+
"langIds": [
7+
"typescript",
8+
"typescriptreact",
9+
"javascript",
10+
"javascriptreact"
11+
]
12+
},
13+
"body": [
14+
"function $name($parameterList) {",
15+
"\t$body",
16+
"}"
17+
],
18+
"variables": {
19+
"name": {
20+
"formatter": "camelCase"
21+
}
22+
}
23+
},
24+
{
25+
"scope": {
26+
"langIds": [
27+
"python"
28+
]
29+
},
30+
"body": [
31+
"def $name($parameterList):",
32+
"\t$body"
33+
],
34+
"variables": {
35+
"name": {
36+
"formatter": "snakeCase"
37+
}
38+
}
39+
}
40+
],
41+
"description": "Function declaration",
42+
"variables": {
43+
"body": {
44+
"wrapperScopeType": "statement"
45+
}
46+
},
47+
"insertionScopeType": "statement"
48+
}
49+
}

cursorless-snippets/ifElseStatement.cursorless-snippets

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"alternative": {
4545
"wrapperScopeType": "statement"
4646
}
47-
}
47+
},
48+
"insertionScopeType": "statement"
4849
}
49-
}
50+
}

cursorless-snippets/ifStatement.cursorless-snippets

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"consequence": {
3838
"wrapperScopeType": "statement"
3939
}
40-
}
40+
},
41+
"insertionScopeType": "statement"
4142
}
42-
}
43+
}

cursorless-snippets/tryCatchStatement.cursorless-snippets

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"exceptBody": {
4545
"wrapperScopeType": "statement"
4646
}
47-
}
47+
},
48+
"insertionScopeType": "statement"
4849
}
49-
}
50+
}

schemas/cursorless-snippets.json

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,59 +9,22 @@
99
"type": "array",
1010
"descriptions": "List of possible definitions for this snippet",
1111
"items": {
12-
"type": "object",
13-
"properties": {
14-
"scope": {
15-
"type": "object",
16-
"description": "Scopes where this snippet is active",
17-
"properties": {
18-
"langIds": {
19-
"type": "array",
20-
"items": {
21-
"type": "string"
22-
}
23-
},
24-
"scopeType": {
25-
"$ref": "#/$defs/scopeType",
26-
"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."
27-
}
28-
}
29-
},
30-
"body": {
31-
"type": "array",
32-
"items": {
33-
"type": "string"
34-
},
35-
"description": "Inline snippet text using VSCode snippet syntax; entries joined by newline. Named variables of the form $foo can be used as wrappers"
36-
}
37-
},
38-
"required": [
39-
"body"
40-
]
12+
"$ref": "#/$defs/snippetDefinition"
4113
}
4214
},
4315
"variables": {
44-
"type": "object",
45-
"description": "For each named variable in the snippet, provides extra information about the variable.",
46-
"additionalProperties": {
47-
"type": "object",
48-
"properties": {
49-
"wrapperScopeType": {
50-
"$ref": "#/$defs/scopeType",
51-
"description": "Default to this scope type when wrapping a target without scope type specified"
52-
},
53-
"description": {
54-
"type": "string",
55-
"description": "Description of the snippet variable"
56-
}
57-
}
58-
}
16+
"$ref": "#/$defs/variables"
5917
},
6018
"description": {
6119
"type": "string",
6220
"description": "Description of the snippet"
21+
},
22+
"insertionScopeType": {
23+
"$ref": "#/$defs/scopeType",
24+
"description": "Default to this scope type when inserting this snippet before/after a target without scope type specified"
6325
}
64-
}
26+
},
27+
"additionalProperties": false
6528
},
6629
"$defs": {
6730
"scopeType": {
@@ -92,6 +55,69 @@
9255
"xmlEndTag",
9356
"xmlStartTag"
9457
]
58+
},
59+
"variables": {
60+
"type": "object",
61+
"description": "For each named variable in the snippet, provides extra information about the variable.",
62+
"additionalProperties": {
63+
"type": "object",
64+
"properties": {
65+
"wrapperScopeType": {
66+
"$ref": "#/$defs/scopeType",
67+
"description": "Default to this scope type when wrapping a target without scope type specified"
68+
},
69+
"description": {
70+
"type": "string",
71+
"description": "Description of the snippet variable"
72+
},
73+
"formatter": {
74+
"type": "string",
75+
"enum": [
76+
"camelCase",
77+
"pascalCase",
78+
"snakeCase"
79+
],
80+
"description": "Format text inserted into this variable using the given formatter"
81+
}
82+
},
83+
"additionalProperties": false
84+
}
85+
},
86+
"snippetDefinition": {
87+
"type": "object",
88+
"properties": {
89+
"scope": {
90+
"type": "object",
91+
"description": "Scopes where this snippet is active",
92+
"properties": {
93+
"langIds": {
94+
"type": "array",
95+
"items": {
96+
"type": "string"
97+
}
98+
},
99+
"scopeType": {
100+
"$ref": "#/$defs/scopeType",
101+
"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."
102+
}
103+
},
104+
"additionalProperties": false
105+
},
106+
"body": {
107+
"type": "array",
108+
"items": {
109+
"type": "string"
110+
},
111+
"description": "Inline snippet text using VSCode snippet syntax; entries joined by newline. Named variables of the form $foo can be used as wrappers"
112+
},
113+
"variables": {
114+
"$ref": "#/$defs/variables"
115+
}
116+
},
117+
"additionalProperties": false,
118+
"required": [
119+
"body"
120+
]
95121
}
96122
}
97123
}

src/actions/InsertSnippet.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { commands, DecorationRangeBehavior } from "vscode";
2+
import {
3+
Action,
4+
ActionPreferences,
5+
ActionReturnValue,
6+
Graph,
7+
TypedSelection,
8+
} from "../typings/Types";
9+
import displayPendingEditDecorations from "../util/editDisplayUtils";
10+
import { ensureSingleEditor } from "../util/targetUtils";
11+
import { SnippetParser } from "../vendor/snippet/snippetParser";
12+
import {
13+
findMatchingSnippetDefinition,
14+
transformSnippetVariables,
15+
} from "../util/snippet";
16+
import textFormatters from "../core/textFormatters";
17+
import { SnippetDefinition, Snippet } from "../typings/snippet";
18+
import {
19+
callFunctionAndUpdateSelectionInfos,
20+
callFunctionAndUpdateSelections,
21+
getSelectionInfo,
22+
} from "../core/updateSelections/updateSelections";
23+
24+
export default class InsertSnippet implements Action {
25+
private snippetParser = new SnippetParser();
26+
27+
getTargetPreferences(snippetName: string): ActionPreferences[] {
28+
const snippet = this.graph.snippets.getSnippet(snippetName);
29+
30+
if (snippet == null) {
31+
throw new Error(`Couldn't find snippet ${snippetName}`);
32+
}
33+
34+
const defaultScopeType = snippet.insertionScopeType;
35+
36+
return [
37+
{
38+
insideOutsideType: "outside",
39+
modifier:
40+
defaultScopeType == null
41+
? undefined
42+
: {
43+
type: "containingScope",
44+
scopeType: defaultScopeType,
45+
includeSiblings: false,
46+
},
47+
},
48+
];
49+
}
50+
51+
constructor(private graph: Graph) {
52+
this.run = this.run.bind(this);
53+
}
54+
55+
async run(
56+
[targets]: [TypedSelection[]],
57+
snippetName: string,
58+
substitutions: Record<string, string>
59+
): Promise<ActionReturnValue> {
60+
const snippet = this.graph.snippets.getSnippet(snippetName)!;
61+
62+
const editor = ensureSingleEditor(targets);
63+
64+
// Find snippet definition matching context.
65+
// NB: We only look at the first target to create our context. This means
66+
// that if there are two snippets that match two different contexts, and
67+
// the two targets match those two different contexts, we will just use the
68+
// snippet that matches the first context for both targets
69+
const definition = findMatchingSnippetDefinition(
70+
targets[0],
71+
snippet.definitions
72+
);
73+
74+
if (definition == null) {
75+
throw new Error("Couldn't find matching snippet definition");
76+
}
77+
78+
const parsedSnippet = this.snippetParser.parse(definition.body.join("\n"));
79+
80+
const formattedSubstitutions =
81+
substitutions == null
82+
? undefined
83+
: formatSubstitutions(snippet, definition, substitutions);
84+
85+
transformSnippetVariables(parsedSnippet, null, formattedSubstitutions);
86+
87+
const snippetString = parsedSnippet.toTextmateString();
88+
89+
await displayPendingEditDecorations(
90+
targets,
91+
this.graph.editStyles.pendingModification0
92+
);
93+
94+
const targetSelections = targets.map(
95+
(target) => target.selection.selection
96+
);
97+
98+
// TODO: Fix "insert before" once we have the new update selections code
99+
// TODO: Remove undo stop once we have the new update selections code
100+
await this.graph.actions.setSelection.run([targets]);
101+
102+
// NB: We do this to auto insert the delimiter if necessary
103+
await this.graph.actions.replace.run([targets], [""]);
104+
105+
const targetSelectionInfos = targetSelections.map((selection) =>
106+
getSelectionInfo(
107+
editor.document,
108+
selection,
109+
DecorationRangeBehavior.OpenOpen
110+
)
111+
);
112+
113+
// NB: We used the command "editor.action.insertSnippet" instead of calling editor.insertSnippet
114+
// because the latter doesn't support special variables like CLIPBOARD
115+
const [updatedTargetSelections] = await callFunctionAndUpdateSelectionInfos(
116+
this.graph.rangeUpdater,
117+
() =>
118+
commands.executeCommand("editor.action.insertSnippet", {
119+
snippet: snippetString,
120+
}),
121+
editor.document,
122+
[targetSelectionInfos]
123+
);
124+
125+
return {
126+
thatMark: updatedTargetSelections.map((selection) => ({
127+
editor,
128+
selection,
129+
})),
130+
};
131+
}
132+
}
133+
function formatSubstitutions(
134+
snippet: Snippet,
135+
definition: SnippetDefinition,
136+
substitutions: Record<string, string>
137+
) {
138+
return Object.fromEntries(
139+
Object.entries(substitutions).map(([variableName, value]) => {
140+
const formatterName =
141+
(definition.variables ?? {})[variableName]?.formatter ??
142+
(snippet.variables ?? {})[variableName]?.formatter;
143+
144+
if (formatterName == null) {
145+
return [variableName, value];
146+
}
147+
148+
const formatter = textFormatters[formatterName];
149+
150+
if (formatter == null) {
151+
throw new Error(
152+
`Couldn't find formatter ${formatterName} for variable ${variableName}`
153+
);
154+
}
155+
156+
return [variableName, formatter(value.split(" "))];
157+
})
158+
);
159+
}

src/actions/Replace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { performEditsAndUpdateSelections } from "../core/updateSelections/update
1313

1414
type RangeGenerator = { start: number };
1515

16-
export default class implements Action {
16+
export default class Replace implements Action {
1717
getTargetPreferences: () => ActionPreferences[] = () => [
1818
{ insideOutsideType: null },
1919
];

0 commit comments

Comments
 (0)