Skip to content

Commit 7885247

Browse files
committed
Generate snippets
subrepo: subdir: "cursorless-talon" merged: "59c9118b" upstream: origin: "file:///Users/pokey/src/cursorless-talon-development" branch: "generate-snippet" commit: "59c9118b" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo" commit: "2f68596" Old generate snippets Fixes
1 parent 2a68c81 commit 7885247

File tree

5 files changed

+244
-1
lines changed

5 files changed

+244
-1
lines changed

cursorless-talon/src/actions/actions.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from .actions_callback import callback_action_defaults, callback_action_map
55
from .actions_custom import custom_action_defaults
66
from .actions_makeshift import makeshift_action_defaults, makeshift_action_map
7-
from .actions_simple import positional_action_defaults, simple_action_defaults
7+
from .actions_simple import (
8+
no_wait_actions,
9+
positional_action_defaults,
10+
simple_action_defaults,
11+
)
812

913
mod = Module()
1014

@@ -48,6 +52,10 @@ def cursorless_command(action_id: str, target: dict):
4852
actions.sleep(f"{talon_options.post_command_sleep_ms}ms")
4953

5054
return return_value
55+
elif action_id in no_wait_actions:
56+
return actions.user.cursorless_single_target_command_no_wait(
57+
action_id, target
58+
)
5159
else:
5260
return actions.user.cursorless_single_target_command(action_id, target)
5361

cursorless-talon/src/actions/actions_simple.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"shuffle": "randomizeTargets",
3232
"reverse": "reverseTargets",
3333
"scout all": "findInWorkspace",
34+
"snip make": "generateSnippet",
3435
"sort": "sortTargets",
3536
"take": "setSelection",
3637
"unfold": "unfoldRegion",
@@ -42,6 +43,11 @@
4243
"paste": "pasteFromClipboard",
4344
}
4445

46+
# Don't wait for these actions to finish, usually because they hang on some kind of user interaction
47+
no_wait_actions = [
48+
"generateSnippet",
49+
]
50+
4551
mod = Module()
4652
mod.list(
4753
"cursorless_simple_action",

src/actions/Actions.ts

100644100755
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import ToggleBreakpoint from "./ToggleBreakpoint";
3939
import Wrap from "./Wrap";
4040
import WrapWithSnippet from "./WrapWithSnippet";
4141
import InsertSnippet from "./InsertSnippet";
42+
import GenerateSnippet from "./GenerateSnippet";
4243

4344
class Actions implements ActionRecord {
4445
constructor(private graph: Graph) {}
@@ -57,6 +58,7 @@ class Actions implements ActionRecord {
5758
foldRegion = new Fold(this.graph);
5859
followLink = new FollowLink(this.graph);
5960
getText = new GetText(this.graph);
61+
generateSnippet = new GenerateSnippet(this.graph);
6062
highlight = new Highlight(this.graph);
6163
indentLine = new IndentLines(this.graph);
6264
insertCopyAfter = new InsertCopyAfter(this.graph);

src/actions/GenerateSnippet.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { range, repeat, zip } from "lodash";
2+
import { ensureSingleTarget } from "../util/targetUtils";
3+
4+
import { open } from "fs/promises";
5+
import { join } from "path";
6+
import { commands, window, workspace } from "vscode";
7+
import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections";
8+
import { Target } from "../typings/target.types";
9+
import { Graph } from "../typings/Types";
10+
import { performDocumentEdits } from "../util/performDocumentEdits";
11+
import { Action, ActionReturnValue } from "./actions.types";
12+
13+
export default class GenerateSnippet implements Action {
14+
constructor(private graph: Graph) {
15+
this.run = this.run.bind(this);
16+
}
17+
18+
async run(
19+
[targets]: [Target[]],
20+
snippetName?: string
21+
): Promise<ActionReturnValue> {
22+
const target = ensureSingleTarget(targets);
23+
const editor = target.editor;
24+
25+
// NB: We don't await the pending edit decoration so that if they
26+
// immediately start saying the name of the snippet, we're more likely to
27+
// win the race and have the input box ready for them
28+
this.graph.editStyles.displayPendingEditDecorations(
29+
targets,
30+
this.graph.editStyles.referenced
31+
);
32+
33+
if (snippetName == null) {
34+
snippetName = await window.showInputBox({
35+
prompt: "Name of snippet",
36+
placeHolder: "helloWorld",
37+
});
38+
}
39+
40+
if (snippetName == null) {
41+
return {};
42+
}
43+
44+
let placeholderIndex = 1;
45+
46+
const originalSelections = editor.selections.filter(
47+
(selection) =>
48+
!selection.isEmpty && target.contentRange.contains(selection)
49+
);
50+
const originalSelectionTexts = originalSelections.map((selection) =>
51+
editor.document.getText(selection)
52+
);
53+
54+
const variables = range(originalSelections.length).map((index) => ({
55+
value: `variable${index + 1}`,
56+
index: placeholderIndex++,
57+
}));
58+
59+
const substituter = new Substituter();
60+
61+
const [placeholderRanges, [targetSelection]] =
62+
await performEditsAndUpdateSelections(
63+
this.graph.rangeUpdater,
64+
editor,
65+
originalSelections.map((selection, index) => ({
66+
editor,
67+
range: selection,
68+
text: substituter.addSubstitution(
69+
`\\$\${${variables[index].index}:${variables[index].value}}`
70+
),
71+
})),
72+
[originalSelections, [target.contentSelection]]
73+
);
74+
75+
const snippetLines: string[] = [];
76+
let currentTabCount = 0;
77+
let currentIndentationString: string | null = null;
78+
79+
const { start, end } = targetSelection;
80+
const startLine = start.line;
81+
const endLine = end.line;
82+
range(startLine, endLine + 1).forEach((lineNumber) => {
83+
const line = editor.document.lineAt(lineNumber);
84+
const { text, firstNonWhitespaceCharacterIndex } = line;
85+
const newIndentationString = text.substring(
86+
0,
87+
firstNonWhitespaceCharacterIndex
88+
);
89+
90+
if (currentIndentationString != null) {
91+
if (newIndentationString.length > currentIndentationString.length) {
92+
currentTabCount++;
93+
} else if (
94+
newIndentationString.length < currentIndentationString.length
95+
) {
96+
currentTabCount--;
97+
}
98+
}
99+
100+
currentIndentationString = newIndentationString;
101+
102+
const lineContentStart = Math.max(
103+
firstNonWhitespaceCharacterIndex,
104+
lineNumber === startLine ? start.character : 0
105+
);
106+
const lineContentEnd = Math.min(
107+
text.length,
108+
lineNumber === endLine ? end.character : Infinity
109+
);
110+
const snippetIndentationString = repeat("\t", currentTabCount);
111+
const lineContent = text.substring(lineContentStart, lineContentEnd);
112+
snippetLines.push(snippetIndentationString + lineContent);
113+
});
114+
115+
await performDocumentEdits(
116+
this.graph.rangeUpdater,
117+
editor,
118+
zip(placeholderRanges, originalSelectionTexts).map(([range, text]) => ({
119+
editor,
120+
range: range!,
121+
text: text!,
122+
}))
123+
);
124+
125+
const snippet = {
126+
[snippetName]: {
127+
definitions: [
128+
{
129+
scope: {
130+
langIds: [editor.document.languageId],
131+
},
132+
body: snippetLines,
133+
},
134+
],
135+
description: `$${placeholderIndex++}`,
136+
variables:
137+
originalSelections.length === 0
138+
? undefined
139+
: Object.fromEntries(
140+
range(originalSelections.length).map((index) => [
141+
`$${variables[index].index}`,
142+
substituter.addSubstitution(`{$${placeholderIndex++}}`, true),
143+
])
144+
),
145+
},
146+
};
147+
const snippetText = substituter.makeSubstitutions(
148+
JSON.stringify(snippet, null, 2)
149+
);
150+
console.debug(snippetText);
151+
152+
const userSnippetsDir = workspace
153+
.getConfiguration("cursorless.experimental")
154+
.get<string>("snippetsDir");
155+
156+
if (!userSnippetsDir) {
157+
throw new Error("User snippets dir not configured.");
158+
}
159+
160+
const path = join(userSnippetsDir, `${snippetName}.cursorless-snippets`);
161+
await touch(path);
162+
const snippetDoc = await workspace.openTextDocument(path);
163+
await window.showTextDocument(snippetDoc);
164+
165+
commands.executeCommand("editor.action.insertSnippet", {
166+
snippet: snippetText,
167+
});
168+
169+
return {
170+
thatMark: targets.map(({ editor, contentSelection }) => ({
171+
editor,
172+
selection: contentSelection,
173+
})),
174+
};
175+
}
176+
}
177+
178+
interface Substitution {
179+
randomId: string;
180+
to: string;
181+
isQuoted: boolean;
182+
}
183+
184+
class Substituter {
185+
private substitutions: Substitution[] = [];
186+
187+
addSubstitution(to: string, isQuoted: boolean = false) {
188+
const randomId = makeid(10);
189+
190+
this.substitutions.push({
191+
to,
192+
randomId,
193+
isQuoted,
194+
});
195+
196+
return randomId;
197+
}
198+
199+
makeSubstitutions(text: string) {
200+
this.substitutions.forEach(({ to, randomId, isQuoted }) => {
201+
const from = isQuoted ? `"${randomId}"` : randomId;
202+
// NB: We use split / join instead of replace because the latter doesn't
203+
// handle dollar signs well
204+
text = text.split(from).join(to);
205+
});
206+
207+
return text;
208+
}
209+
}
210+
211+
// From https://stackoverflow.com/a/1349426/2605678
212+
function makeid(length: number) {
213+
var result = "";
214+
var characters =
215+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
216+
var charactersLength = characters.length;
217+
for (var i = 0; i < length; i++) {
218+
result += characters.charAt(Math.floor(Math.random() * charactersLength));
219+
}
220+
return result;
221+
}
222+
223+
async function touch(path: string) {
224+
const file = await open(path, "w");
225+
await file.close();
226+
}

src/actions/actions.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type ActionType =
1616
| "findInWorkspace"
1717
| "foldRegion"
1818
| "followLink"
19+
| "generateSnippet"
1920
| "getText"
2021
| "highlight"
2122
| "indentLine"

0 commit comments

Comments
 (0)