Skip to content

Commit

Permalink
Generate snippets
Browse files Browse the repository at this point in the history
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
  • Loading branch information
pokey committed Jun 25, 2022
1 parent dbf460b commit b2d40e7
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 0 deletions.
4 changes: 4 additions & 0 deletions cursorless-talon/src/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def cursorless_command(action_id: str, target: dict):
actions.sleep(f"{talon_options.post_command_sleep_ms}ms")

return return_value
elif action_id in no_wait_actions:
return actions.user.cursorless_single_target_command_no_wait(
action_id, target
)
else:
return actions.user.cursorless_single_target_command(action_id, target)

Expand Down
6 changes: 6 additions & 0 deletions cursorless-talon/src/actions/actions_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"shuffle": "randomizeTargets",
"reverse": "reverseTargets",
"scout all": "findInWorkspace",
"snip make": "generateSnippet",
"sort": "sortTargets",
"take": "setSelection",
"unfold": "unfoldRegion",
Expand All @@ -42,6 +43,11 @@
"paste": "pasteFromClipboard",
}

# Don't wait for these actions to finish, usually because they hang on some kind of user interaction
no_wait_actions = [
"generateSnippet",
]

mod = Module()
mod.list(
"cursorless_simple_action",
Expand Down
2 changes: 2 additions & 0 deletions src/actions/Actions.ts
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import ToggleBreakpoint from "./ToggleBreakpoint";
import Wrap from "./Wrap";
import WrapWithSnippet from "./WrapWithSnippet";
import InsertSnippet from "./InsertSnippet";
import GenerateSnippet from "./GenerateSnippet";

class Actions implements ActionRecord {
constructor(private graph: Graph) {}
Expand All @@ -57,6 +58,7 @@ class Actions implements ActionRecord {
foldRegion = new Fold(this.graph);
followLink = new FollowLink(this.graph);
getText = new GetText(this.graph);
generateSnippet = new GenerateSnippet(this.graph);
highlight = new Highlight(this.graph);
indentLine = new IndentLines(this.graph);
insertCopyAfter = new InsertCopyAfter(this.graph);
Expand Down
226 changes: 226 additions & 0 deletions src/actions/GenerateSnippet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { range, repeat, zip } from "lodash";
import { ensureSingleTarget } from "../util/targetUtils";

import { open } from "fs/promises";
import { join } from "path";
import { commands, window, workspace } from "vscode";
import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections";
import { Target } from "../typings/target.types";
import { Graph } from "../typings/Types";
import { performDocumentEdits } from "../util/performDocumentEdits";
import { Action, ActionReturnValue } from "./actions.types";

export default class GenerateSnippet implements Action {
constructor(private graph: Graph) {
this.run = this.run.bind(this);
}

async run(
[targets]: [Target[]],
snippetName?: string
): Promise<ActionReturnValue> {
const target = ensureSingleTarget(targets);
const editor = target.editor;

// NB: We don't await the pending edit decoration so that if they
// immediately start saying the name of the snippet, we're more likely to
// win the race and have the input box ready for them
this.graph.editStyles.displayPendingEditDecorations(
targets,
this.graph.editStyles.referenced
);

if (snippetName == null) {
snippetName = await window.showInputBox({
prompt: "Name of snippet",
placeHolder: "helloWorld",
});
}

if (snippetName == null) {
return {};
}

let placeholderIndex = 1;

const originalSelections = editor.selections.filter(
(selection) =>
!selection.isEmpty && target.contentRange.contains(selection)
);
const originalSelectionTexts = originalSelections.map((selection) =>
editor.document.getText(selection)
);

const variables = range(originalSelections.length).map((index) => ({
value: `variable${index + 1}`,
index: placeholderIndex++,
}));

const substituter = new Substituter();

const [placeholderRanges, [targetSelection]] =
await performEditsAndUpdateSelections(
this.graph.rangeUpdater,
editor,
originalSelections.map((selection, index) => ({
editor,
range: selection,
text: substituter.addSubstitution(
`\\$\${${variables[index].index}:${variables[index].value}}`
),
})),
[originalSelections, [target.contentSelection]]
);

const snippetLines: string[] = [];
let currentTabCount = 0;
let currentIndentationString: string | null = null;

const { start, end } = targetSelection;
const startLine = start.line;
const endLine = end.line;
range(startLine, endLine + 1).forEach((lineNumber) => {
const line = editor.document.lineAt(lineNumber);
const { text, firstNonWhitespaceCharacterIndex } = line;
const newIndentationString = text.substring(
0,
firstNonWhitespaceCharacterIndex
);

if (currentIndentationString != null) {
if (newIndentationString.length > currentIndentationString.length) {
currentTabCount++;
} else if (
newIndentationString.length < currentIndentationString.length
) {
currentTabCount--;
}
}

currentIndentationString = newIndentationString;

const lineContentStart = Math.max(
firstNonWhitespaceCharacterIndex,
lineNumber === startLine ? start.character : 0
);
const lineContentEnd = Math.min(
text.length,
lineNumber === endLine ? end.character : Infinity
);
const snippetIndentationString = repeat("\t", currentTabCount);
const lineContent = text.substring(lineContentStart, lineContentEnd);
snippetLines.push(snippetIndentationString + lineContent);
});

await performDocumentEdits(
this.graph.rangeUpdater,
editor,
zip(placeholderRanges, originalSelectionTexts).map(([range, text]) => ({
editor,
range: range!,
text: text!,
}))
);

const snippet = {
[snippetName]: {
definitions: [
{
scope: {
langIds: [editor.document.languageId],
},
body: snippetLines,
},
],
description: `$${placeholderIndex++}`,
variables:
originalSelections.length === 0
? undefined
: Object.fromEntries(
range(originalSelections.length).map((index) => [
`$${variables[index].index}`,
substituter.addSubstitution(`{$${placeholderIndex++}}`, true),
])
),
},
};
const snippetText = substituter.makeSubstitutions(
JSON.stringify(snippet, null, 2)
);
console.debug(snippetText);

const userSnippetsDir = workspace
.getConfiguration("cursorless.experimental")
.get<string>("snippetsDir");

if (!userSnippetsDir) {
throw new Error("User snippets dir not configured.");
}

const path = join(userSnippetsDir, `${snippetName}.cursorless-snippets`);
await touch(path);
const snippetDoc = await workspace.openTextDocument(path);
await window.showTextDocument(snippetDoc);

commands.executeCommand("editor.action.insertSnippet", {
snippet: snippetText,
});

return {
thatMark: targets.map(({ editor, contentSelection }) => ({
editor,
selection: contentSelection,
})),
};
}
}

interface Substitution {
randomId: string;
to: string;
isQuoted: boolean;
}

class Substituter {
private substitutions: Substitution[] = [];

addSubstitution(to: string, isQuoted: boolean = false) {
const randomId = makeid(10);

this.substitutions.push({
to,
randomId,
isQuoted,
});

return randomId;
}

makeSubstitutions(text: string) {
this.substitutions.forEach(({ to, randomId, isQuoted }) => {
const from = isQuoted ? `"${randomId}"` : randomId;
// NB: We use split / join instead of replace because the latter doesn't
// handle dollar signs well
text = text.split(from).join(to);
});

return text;
}
}

// From https://stackoverflow.com/a/1349426/2605678
function makeid(length: number) {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

async function touch(path: string) {
const file = await open(path, "w");
await file.close();
}
1 change: 1 addition & 0 deletions src/actions/actions.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type ActionType =
| "findInWorkspace"
| "foldRegion"
| "followLink"
| "generateSnippet"
| "getText"
| "highlight"
| "indentLine"
Expand Down

0 comments on commit b2d40e7

Please sign in to comment.