-
-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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 * Some cleanup * Some cleanup * Stop modifying document * Fixed tests * Update doc * More cleanup * docs * More cleanup * Docs * Cleanup * Remove executable bet on actions Co-authored-by: Andreas Arvidsson <andreas.arvidsson87@gmail.com>
- Loading branch information
1 parent
56cc6cb
commit 57ace86
Showing
12 changed files
with
609 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
import { ensureSingleTarget } from "../../util/targetUtils"; | ||
|
||
import { commands, Range, window } from "vscode"; | ||
import { Offsets } from "../../processTargets/modifiers/surroundingPair/types"; | ||
import isTesting from "../../testUtil/isTesting"; | ||
import { Target } from "../../typings/target.types"; | ||
import { Graph } from "../../typings/Types"; | ||
import { getDocumentRange } from "../../util/range"; | ||
import { selectionFromRange } from "../../util/selectionUtils"; | ||
import { Action, ActionReturnValue } from "../actions.types"; | ||
import { constructSnippetBody } from "./constructSnippetBody"; | ||
import { editText } from "./editText"; | ||
import { openNewSnippetFile } from "./openNewSnippetFile"; | ||
import Substituter from "./Substituter"; | ||
|
||
/** | ||
* This action can be used to automatically create a snippet from a target. Any | ||
* cursor selections inside the target will become placeholders in the final | ||
* snippet. This action creates a new file, and inserts a snippet that the user | ||
* can fill out to construct their desired snippet. | ||
* | ||
* Note that there are two snippets involved in this implementation: | ||
* | ||
* - The snippet that the user is trying to create. We refer to this snippet as | ||
* the user snippet. | ||
* - The snippet that we insert that the user can use to build their snippet. We | ||
* refer to this as the meta snippet. | ||
* | ||
* We proceed as follows: | ||
* | ||
* 1. Ask user for snippet name if not provided as arg | ||
* 2. Find all cursor selections inside target - these will become the user | ||
* snippet variables | ||
* 3. Extract text of target | ||
* 4. Replace cursor selections in text with random ids that won't be affected | ||
* by json serialization. After serialization we'll replace these id's by | ||
* snippet placeholders. | ||
* 4. Construct the user snippet body as a list of strings | ||
* 5. Construct a javascript object that will be json-ified to become the meta | ||
* snippet | ||
* 6. Serialize the javascript object to json | ||
* 7. Perform replacements on the random id's appearing in this json to get the | ||
* text we desire. This modified json output is the meta snippet. | ||
* 8. Open a new document in user custom snippets dir to hold the new snippet. | ||
* 9. Insert the meta snippet so that the user can construct their snippet. | ||
* | ||
* Note that we avoid using JS interpolation strings here because the syntax is | ||
* very similar to snippet placeholders, so we would end up with lots of | ||
* confusing escaping. | ||
*/ | ||
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 the user | ||
// immediately starts saying the name of the snippet (eg command chain | ||
// "snippet make funk camel my function"), 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", | ||
}); | ||
} | ||
|
||
// User cancelled; don't do anything | ||
if (snippetName == null) { | ||
return {}; | ||
} | ||
|
||
/** The next placeholder index to use for the meta snippet */ | ||
let currentPlaceholderIndex = 1; | ||
|
||
const baseOffset = editor.document.offsetAt(target.contentRange.start); | ||
|
||
/** | ||
* The variables that will appear in the user snippet. Note that | ||
* `placeholderIndex` here is the placeholder index in the meta snippet not | ||
* the user snippet. | ||
*/ | ||
const variables: Variable[] = editor.selections | ||
.filter((selection) => target.contentRange.contains(selection)) | ||
.map((selection, index) => ({ | ||
offsets: { | ||
start: editor.document.offsetAt(selection.start) - baseOffset, | ||
end: editor.document.offsetAt(selection.end) - baseOffset, | ||
}, | ||
defaultName: `variable${index + 1}`, | ||
placeholderIndex: currentPlaceholderIndex++, | ||
})); | ||
|
||
/** | ||
* Constructs random ids that can be put into the text that won't be | ||
* modified by json serialization. | ||
*/ | ||
const substituter = new Substituter(); | ||
|
||
/** | ||
* Text before the start of the snippet in the snippet start line. We need | ||
* to pass this to {@link constructSnippetBody} so that it knows the | ||
* baseline indentation of the snippet | ||
*/ | ||
const linePrefix = editor.document.getText( | ||
new Range( | ||
target.contentRange.start.with(undefined, 0), | ||
target.contentRange.start | ||
) | ||
); | ||
|
||
/** The text of the snippet, with placeholders inserted for variables */ | ||
const snippetBodyText = editText( | ||
editor.document.getText(target.contentRange), | ||
variables.map(({ offsets, defaultName, placeholderIndex }) => ({ | ||
offsets, | ||
// Note that the reason we use the substituter here is primarily so | ||
// that the `\` below doesn't get escaped upon conversion to json. | ||
text: substituter.addSubstitution( | ||
[ | ||
// This `\$` will end up being a `$` in the final document. It | ||
// indicates the start of a variable in the user snippet. We need | ||
// the `\` so that the meta-snippet doesn't see it as one of its | ||
// placeholders. | ||
"\\$", | ||
|
||
// The remaining text here is a placeholder in the meta-snippet | ||
// that the user can use to name their snippet variable that will | ||
// be in the user snippet. | ||
"${", | ||
placeholderIndex, | ||
":", | ||
defaultName, | ||
"}", | ||
].join("") | ||
), | ||
})) | ||
); | ||
|
||
const snippetLines = constructSnippetBody(snippetBodyText, linePrefix); | ||
|
||
/** | ||
* Constructs a key-value entry for use in the variable description section | ||
* of the user snippet definition. It contains tabstops for use in the | ||
* meta-snippet. | ||
* @param variable The variable | ||
* @returns A [key, value] pair for use in the meta-snippet | ||
*/ | ||
const constructVariableDescriptionEntry = ({ | ||
placeholderIndex, | ||
}: Variable): [string, string] => { | ||
// The key will have the same placeholder index as the other location | ||
// where this variable appears. | ||
const key = "$" + placeholderIndex; | ||
|
||
// The value will end up being an empty object with a tabstop in the | ||
// middle so that the user can add information about the variable, such | ||
// as wrapperScopeType. Ie the output will look like `{|}` (with the `|` | ||
// representing a tabstop in the meta-snippet) | ||
// | ||
// NB: We use the subsituter here, with `isQuoted=true` because in order | ||
// to make this work for the meta-snippet, we want to end up with | ||
// something like `{$3}`, which is not valid json. So we instead arrange | ||
// to end up with json like `"hgidfsivhs"`, and then replace the whole | ||
// string (including quotes) with `{$3}` after json-ification | ||
const value = substituter.addSubstitution( | ||
"{$" + currentPlaceholderIndex++ + "}", | ||
true | ||
); | ||
|
||
return [key, value]; | ||
}; | ||
|
||
/** An object that will be json-ified to become the meta-snippet */ | ||
const snippet = { | ||
[snippetName]: { | ||
definitions: [ | ||
{ | ||
scope: { | ||
langIds: [editor.document.languageId], | ||
}, | ||
body: snippetLines, | ||
}, | ||
], | ||
description: "$" + currentPlaceholderIndex++, | ||
variables: | ||
variables.length === 0 | ||
? undefined | ||
: Object.fromEntries( | ||
variables.map(constructVariableDescriptionEntry) | ||
), | ||
}, | ||
}; | ||
|
||
/** | ||
* This is the text of the meta-snippet in Textmate format that we will | ||
* insert into the new document where the user will fill out their snippet | ||
* definition | ||
*/ | ||
const snippetText = substituter.makeSubstitutions( | ||
JSON.stringify(snippet, null, 2) | ||
); | ||
|
||
if (isTesting()) { | ||
// If we're testing, we just overwrite the current document | ||
editor.selections = [ | ||
selectionFromRange(false, getDocumentRange(editor.document)), | ||
]; | ||
} else { | ||
// Otherwise, we create and open a new document for the snippet in the | ||
// user snippets dir | ||
await openNewSnippetFile(snippetName); | ||
} | ||
|
||
// Insert the meta-snippet | ||
await commands.executeCommand("editor.action.insertSnippet", { | ||
snippet: snippetText, | ||
}); | ||
|
||
return { | ||
thatMark: targets.map(({ editor, contentSelection }) => ({ | ||
editor, | ||
selection: contentSelection, | ||
})), | ||
}; | ||
} | ||
} | ||
|
||
interface Variable { | ||
/** | ||
* The start an end offsets of the variable relative to the text of the | ||
* snippet that contains it | ||
*/ | ||
offsets: Offsets; | ||
|
||
/** | ||
* The default name for the given variable that will appear as the placeholder | ||
* text in the meta snippet | ||
*/ | ||
defaultName: string; | ||
|
||
/** | ||
* The placeholder to use when filling out the name of this variable in the | ||
* meta snippet. | ||
*/ | ||
placeholderIndex: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
interface Substitution { | ||
randomId: string; | ||
to: string; | ||
isQuoted: boolean; | ||
} | ||
|
||
/** | ||
* This class constructs random strings that can be used as placeholders for the | ||
* strings you'd like to insert into a document. This functionality is useful if | ||
* the strings you'd like to insert might get modified by something like json | ||
* serialization. You proceed by calling {@link addSubstitution} for each string you'd | ||
* like to put into your document. This function returns a random id that you | ||
* can put into your text. When you are done, call {@link makeSubstitutions} | ||
* on the final text to replace the random id's with the original strings you | ||
* desired. | ||
*/ | ||
export default class Substituter { | ||
private substitutions: Substitution[] = []; | ||
|
||
/** | ||
* Get a random id that can be put into your text body that will then be | ||
* replaced by {@link to} when you call {@link makeSubstitutions}. | ||
* @param to The string that you'd like to end up in the final document after | ||
* replacements | ||
* @param isQuoted Use this variable to indicate that in the final text the | ||
* variable will end up quoted. This occurs if you use the replacement string | ||
* as a stand alone string in a json document and then you serialize it | ||
* @returns A unique random id that can be put into the document that will | ||
* then be substituted later | ||
*/ | ||
addSubstitution(to: string, isQuoted: boolean = false) { | ||
const randomId = makeid(10); | ||
|
||
this.substitutions.push({ | ||
to, | ||
randomId, | ||
isQuoted, | ||
}); | ||
|
||
return randomId; | ||
} | ||
|
||
/** | ||
* Performs substitutions on {@link text}, replacing the random ids generated | ||
* by {@link addSubstitution} with the values passed in for `to`. | ||
* @param text The text to perform substitutions on | ||
* @returns The text with variable substituted for the original values you | ||
* desired | ||
*/ | ||
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; | ||
} | ||
} | ||
|
||
/** | ||
* Constructs a random id of the given length. | ||
* | ||
* From https://stackoverflow.com/a/1349426/2605678 | ||
* | ||
* @param length Length of the string to generate | ||
* @returns A string of random digits of length {@param length} | ||
*/ | ||
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; | ||
} |
Oops, something went wrong.