From 3754e88df6c0b725e28a02bd316b285bc178d028 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 21 Jan 2025 23:49:43 +0100 Subject: [PATCH 01/12] Added the ability to generate community snippets --- cursorless-talon/src/actions/actions.py | 3 +- .../src/actions/generate_snippet.py | 71 +++++ .../actions/snippets/snipMakeFunk2.yml | 55 ++++ .../actions/snippets/snipMakeState2.yml | 46 +++ .../actions/snippets/testSnippetMakeLine2.yml | 41 +++ .../src/types/command/ActionDescriptor.ts | 1 + packages/cursorless-engine/package.json | 1 + .../GenerateSnippet/GenerateSnippet.ts | 254 +---------------- .../GenerateSnippetCommunity.ts | 243 ++++++++++++++++ .../GenerateSnippet/GenerateSnippetLegacy.ts | 263 ++++++++++++++++++ .../src/actions/actions.types.ts | 6 +- .../cursorless-engine/src/core/Snippets.ts | 11 +- .../core/commandRunner/CommandRunnerImpl.ts | 1 + .../disabledComponents/DisabledSnippets.ts | 7 +- .../cursorless-vscode/src/VscodeSnippets.ts | 30 +- pnpm-lock.yaml | 8 + 16 files changed, 778 insertions(+), 263 deletions(-) create mode 100644 cursorless-talon/src/actions/generate_snippet.py create mode 100644 data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml create mode 100644 data/fixtures/recorded/actions/snippets/snipMakeState2.yml create mode 100644 data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml create mode 100644 packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts create mode 100644 packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetLegacy.ts diff --git a/cursorless-talon/src/actions/actions.py b/cursorless-talon/src/actions/actions.py index c9e75cf750..01281df341 100644 --- a/cursorless-talon/src/actions/actions.py +++ b/cursorless-talon/src/actions/actions.py @@ -55,7 +55,6 @@ # Don't wait for these actions to finish, usually because they hang on some kind of user interaction no_wait_actions = [ - "generateSnippet", "rename", ] @@ -99,6 +98,8 @@ def cursorless_command(action_name: str, target: CursorlessExplicitTarget): # p ) elif action_name == "callAsFunction": actions.user.private_cursorless_call(target) + elif action_name == "generateSnippet": + actions.user.private_cursorless_generate_snippet_action(target) elif action_name in no_wait_actions: action = {"name": action_name, "target": target} actions.user.private_cursorless_command_no_wait(action) diff --git a/cursorless-talon/src/actions/generate_snippet.py b/cursorless-talon/src/actions/generate_snippet.py new file mode 100644 index 0000000000..da52863e49 --- /dev/null +++ b/cursorless-talon/src/actions/generate_snippet.py @@ -0,0 +1,71 @@ +import glob +from pathlib import Path + +from talon import Context, Module, actions, settings + +from ..targets.target_types import CursorlessExplicitTarget + +mod = Module() + +ctx = Context() +ctx.matches = r""" +tag: user.cursorless_use_community_snippets +""" + + +@mod.action_class +class Actions: + def private_cursorless_generate_snippet_action(target: CursorlessExplicitTarget): # pyright: ignore [reportGeneralTypeIssues] + """Generate a snippet from the given target""" + actions.user.private_cursorless_command_no_wait( + { + "name": "generateSnippet", + "target": target, + } + ) + + +@ctx.action_class("user") +class UserActions: + def private_cursorless_generate_snippet_action(target: CursorlessExplicitTarget): # pyright: ignore [reportGeneralTypeIssues] + actions.user.private_cursorless_command_no_wait( + { + "name": "generateSnippet", + "target": target, + "dirPath": get_dir_path(), + } + ) + + +def get_dir_path() -> str: + settings_dir = get_setting_dir() + if settings_dir is not None: + return settings_dir + return get_community_snippets_dir() + + +def get_setting_dir() -> str | None: + try: + setting_dir = settings.get("user.snippets_dir") + if not setting_dir: + return None + + dir = Path(str(setting_dir)) + + if not dir.is_absolute(): + user_dir = Path(actions.path.talon_user()) + dir = user_dir / dir + + return str(dir.resolve()) + except Exception: + return None + + +def get_community_snippets_dir() -> str: + files = glob.iglob( + f"{actions.path.talon_user()}/**/snippets/snippets/*.snippet", + recursive=True, + ) + for file in files: + return str(Path(file).parent) + raise ValueError("Could not find community snippets directory") diff --git a/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml b/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml new file mode 100644 index 0000000000..556ca8ab5e --- /dev/null +++ b/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml @@ -0,0 +1,55 @@ +languageId: typescript +command: + version: 7 + spokenForm: snippet make funk + action: + name: generateSnippet + dirPath: "" + snippetName: snippetTest1 + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: namedFunction} + usePrePhraseSnapshot: true +spokenFormError: generateSnippet.snippetName +initialState: + documentContents: |2- + function helloWorld() { + const whatever = "hello"; + + if (whatever == null) { + console.log("hello") + } + } + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 23} + - anchor: {line: 3, character: 8} + active: {line: 5, character: 9} + marks: {} +finalState: + documentContents: | + name: snippetTest1 + language: typescript + phrase: + + $1.wrapperPhrase: + $0.wrapperPhrase: + - + function $1() { + const whatever = "hello"; + + $0 + } + --- + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 4} + end: {line: 6, character: 5} + isReversed: false + hasExplicitRange: true \ No newline at end of file diff --git a/data/fixtures/recorded/actions/snippets/snipMakeState2.yml b/data/fixtures/recorded/actions/snippets/snipMakeState2.yml new file mode 100644 index 0000000000..7f814e0155 --- /dev/null +++ b/data/fixtures/recorded/actions/snippets/snipMakeState2.yml @@ -0,0 +1,46 @@ +languageId: typescript +command: + version: 7 + spokenForm: snippet make state + action: + name: generateSnippet + dirPath: "" + snippetName: snippetTest1 + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + usePrePhraseSnapshot: true +spokenFormError: generateSnippet.snippetName +initialState: + documentContents: |- + if () { + console.log("hello") + } + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: | + name: snippetTest1 + language: typescript + phrase: + + $0.wrapperPhrase: + - + if ($0) { + console.log("hello") + } + --- + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 0} + end: {line: 2, character: 1} + isReversed: false + hasExplicitRange: true \ No newline at end of file diff --git a/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml b/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml new file mode 100644 index 0000000000..5bb8947f47 --- /dev/null +++ b/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml @@ -0,0 +1,41 @@ +languageId: plaintext +command: + version: 7 + spokenForm: test snippet make line + action: + name: generateSnippet + dirPath: "" + snippetName: testSnippet + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true +spokenFormError: generateSnippet.snippetName +initialState: + documentContents: \textbf{$foo} + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 12} + marks: {} +finalState: + documentContents: | + name: testSnippet + language: plaintext + phrase: + + $0.wrapperPhrase: + - + \textbf{\$$0} + --- + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 0} + end: {line: 0, character: 13} + isReversed: false + hasExplicitRange: true \ No newline at end of file diff --git a/packages/common/src/types/command/ActionDescriptor.ts b/packages/common/src/types/command/ActionDescriptor.ts index c064f28339..c1cde81507 100644 --- a/packages/common/src/types/command/ActionDescriptor.ts +++ b/packages/common/src/types/command/ActionDescriptor.ts @@ -135,6 +135,7 @@ export interface PasteActionDescriptor { export interface GenerateSnippetActionDescriptor { name: "generateSnippet"; + dirPath?: string; snippetName?: string; target: PartialTargetDescriptor; } diff --git a/packages/cursorless-engine/package.json b/packages/cursorless-engine/package.json index aff61cb103..fc6cf065a8 100644 --- a/packages/cursorless-engine/package.json +++ b/packages/cursorless-engine/package.json @@ -30,6 +30,7 @@ "lodash-es": "^4.17.21", "moo": "0.5.2", "nearley": "2.20.1", + "talon-snippets": "1.1.0", "uuid": "^10.0.0", "zod": "3.23.8" }, diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts index 791c368730..430f9eeca2 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts @@ -1,49 +1,9 @@ -import { FlashStyle, matchAll, Range } from "@cursorless/common"; import type { Snippets } from "../../core/Snippets"; -import { ide } from "../../singletons/ide.singleton"; import type { Target } from "../../typings/target.types"; -import { ensureSingleTarget, flashTargets } from "../../util/targetUtils"; import type { ActionReturnValue } from "../actions.types"; -import { constructSnippetBody } from "./constructSnippetBody"; -import { editText } from "./editText"; -import type { Offsets } from "./Offsets"; -import Substituter from "./Substituter"; +import GenerateSnippetCommunity from "./GenerateSnippetCommunity"; +import GenerateSnippetLegacy from "./GenerateSnippetLegacy"; -/** - * 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 { constructor(private snippets: Snippets) { this.run = this.run.bind(this); @@ -51,213 +11,15 @@ export default class GenerateSnippet { async run( targets: Target[], + dirPath?: string, snippetName?: string, ): Promise { - 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 - void flashTargets(ide(), targets, FlashStyle.referenced); - - if (snippetName == null) { - snippetName = await ide().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, - ), - ); - - const originalText = editor.document.getText(target.contentRange); - - /** - * The text of the snippet, with placeholders inserted for variables and - * special characters `$`, `\`, and `}` escaped twice to make it through - * both meta snippet and user snippet. - */ - const snippetBodyText = editText(originalText, [ - ...matchAll(originalText, /\$|\\/g, (match) => ({ - offsets: { - start: match.index!, - end: match.index! + match[0].length, - }, - text: match[0] === "\\" ? `\\${match[0]}` : `\\\\${match[0]}`, - })), - ...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 substituter 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), - ); - - const editableEditor = ide().getEditableTextEditor(editor); - - if (ide().runMode === "test") { - // If we're testing, we just overwrite the current document - await editableEditor.setSelections([ - editor.document.range.toSelection(false), - ]); - } else { - // Otherwise, we create and open a new document for the snippet in the - // user snippets dir - await this.snippets.openNewSnippetFile(snippetName); + if (dirPath == null) { + const action = new GenerateSnippetLegacy(this.snippets); + return action.run(targets, snippetName); } - // Insert the meta-snippet - await editableEditor.insertSnippet(snippetText); - - return { - thatSelections: targets.map(({ editor, contentSelection }) => ({ - editor, - selection: contentSelection, - })), - }; + const action = new GenerateSnippetCommunity(this.snippets); + return action.run(targets, dirPath, snippetName); } } - -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; -} diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts new file mode 100644 index 0000000000..905eea0903 --- /dev/null +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts @@ -0,0 +1,243 @@ +import { + FlashStyle, + matchAll, + Range, + type EditableTextEditor, + type Selection, + type TextEditor, +} from "@cursorless/common"; +import { + parseSnippetFile, + serializeSnippetFile, + type SnippetDocument, + type SnippetVariable, +} from "talon-snippets"; +import type { Snippets } from "../../core/Snippets"; +import { ide } from "../../singletons/ide.singleton"; +import type { Target } from "../../typings/target.types"; +import { ensureSingleTarget, flashTargets } from "../../util/targetUtils"; +import type { ActionReturnValue } from "../actions.types"; +import { constructSnippetBody } from "./constructSnippetBody"; +import { editText } from "./editText"; +import type { Offsets } from "./Offsets"; +import { getHeaderSnippet } from "talon-snippets"; + +/** + * 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 GenerateSnippetCommunity { + constructor(private snippets: Snippets) { + this.run = this.run.bind(this); + } + + async run( + targets: Target[], + dirPath: string, + snippetName?: string, + ): Promise { + 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 + void flashTargets(ide(), targets, FlashStyle.referenced); + + if (snippetName == null) { + snippetName = await ide().showInputBox({ + prompt: "Name of snippet", + placeHolder: "helloWorld", + }); + + // User cancelled; do nothing + if (!snippetName) { + return {}; + } + } + + const baseOffset = editor.document.offsetAt(target.contentRange.start); + + /** + * The variables that will appear in the user snippet. + */ + const selections = getsSnippetSelections(editor, target.contentRange); + const variables = selections.map( + (selection, index): Variable => ({ + offsets: { + start: editor.document.offsetAt(selection.start) - baseOffset, + end: editor.document.offsetAt(selection.end) - baseOffset, + }, + name: index === selections.length - 1 ? "0" : `${index + 1}`, + }), + ); + + /** + * 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, + ), + ); + + const originalText = editor.document.getText(target.contentRange); + + const snippetBodyText = editText(originalText, [ + ...matchAll(originalText, /\$|\\/g, (match) => ({ + offsets: { + start: match.index!, + end: match.index! + match[0].length, + }, + text: `\\${match[0]}`, + })), + ...variables.map(({ offsets, name }) => ({ + offsets, + text: `$${name}`, + })), + ]); + + const snippetLines = constructSnippetBody(snippetBodyText, linePrefix); + + let editableEditor: EditableTextEditor; + let snippetDocuments: SnippetDocument[]; + + if (ide().runMode === "test") { + // If we're testing, we just overwrite the current document + editableEditor = ide().getEditableTextEditor(editor); + snippetDocuments = []; + } else { + // Otherwise, we create and open a new document for the snippet + editableEditor = ide().getEditableTextEditor( + await this.snippets.openNewSnippetFile(snippetName, dirPath), + ); + snippetDocuments = parseSnippetFile(editableEditor.document.getText()); + } + + await editableEditor.setSelections([ + editableEditor.document.range.toSelection(false), + ]); + + const headerSnippet = getHeaderSnippet(snippetDocuments); + + /** The next placeholder index to use for the meta snippet */ + let currentPlaceholderIndex = 1; + + const phrases = + headerSnippet?.phrases != null + ? undefined + : [`${PLACEHOLDER}${currentPlaceholderIndex++}`]; + + const createVariable = (variable: Variable): SnippetVariable => { + const hasPhrase = headerSnippet?.variables?.some( + (v) => v.name === variable.name && v.wrapperPhrases != null, + ); + return { + name: variable.name, + wrapperPhrases: hasPhrase + ? undefined + : [`${PLACEHOLDER}${currentPlaceholderIndex++}`], + }; + }; + + const snippet: SnippetDocument = { + name: headerSnippet?.name === snippetName ? undefined : snippetName, + phrases, + languages: getSnippetLanguages(editor, headerSnippet), + body: snippetLines, + variables: variables.map(createVariable), + }; + + snippetDocuments.push(snippet); + + /** + * 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 metaSnippetText = serializeSnippetFile(snippetDocuments) + // Escape dollar signs in the snippet text so that they don't get used as + // placeholders in the meta snippet + .replace(/\$/g, "\\$") + // Replace constant with dollar sign for meta snippet placeholders + .replaceAll(PLACEHOLDER, "$"); + + // Insert the meta-snippet + await editableEditor.insertSnippet(metaSnippetText); + + return { + thatSelections: targets.map(({ editor, contentSelection }) => ({ + editor, + selection: contentSelection, + })), + }; + } +} + +function getSnippetLanguages( + editor: TextEditor, + header: SnippetDocument | undefined, +): string[] | undefined { + if (header?.languages?.includes(editor.document.languageId)) { + return undefined; + } + return [editor.document.languageId]; +} + +function getsSnippetSelections(editor: TextEditor, range: Range): Selection[] { + const selections = editor.selections.filter((selection) => + range.contains(selection), + ); + selections.sort((a, b) => a.start.compareTo(b.start)); + return selections; +} + +const PLACEHOLDER = "PLACEHOLDER_VFA77zcbLD6wXNmfMAay"; + +interface Variable { + /** + * The start an end offsets of the variable relative to the text of the + * snippet that contains it + */ + offsets: Offsets; + + /** + * The name for the variable + */ + name: string; +} diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetLegacy.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetLegacy.ts new file mode 100644 index 0000000000..76410fbba2 --- /dev/null +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetLegacy.ts @@ -0,0 +1,263 @@ +import { FlashStyle, matchAll, Range } from "@cursorless/common"; +import type { Snippets } from "../../core/Snippets"; +import { ide } from "../../singletons/ide.singleton"; +import type { Target } from "../../typings/target.types"; +import { ensureSingleTarget, flashTargets } from "../../util/targetUtils"; +import type { ActionReturnValue } from "../actions.types"; +import { constructSnippetBody } from "./constructSnippetBody"; +import { editText } from "./editText"; +import type { Offsets } from "./Offsets"; +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 GenerateSnippetLegacy { + constructor(private snippets: Snippets) { + this.run = this.run.bind(this); + } + + async run( + targets: Target[], + snippetName?: string, + ): Promise { + 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 + void flashTargets(ide(), targets, FlashStyle.referenced); + + if (snippetName == null) { + snippetName = await ide().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, + ), + ); + + const originalText = editor.document.getText(target.contentRange); + + /** + * The text of the snippet, with placeholders inserted for variables and + * special characters `$`, `\`, and `}` escaped twice to make it through + * both meta snippet and user snippet. + */ + const snippetBodyText = editText(originalText, [ + ...matchAll(originalText, /\$|\\/g, (match) => ({ + offsets: { + start: match.index!, + end: match.index! + match[0].length, + }, + text: match[0] === "\\" ? `\\${match[0]}` : `\\\\${match[0]}`, + })), + ...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 substituter 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), + ); + + const editableEditor = ide().getEditableTextEditor(editor); + + if (ide().runMode === "test") { + // If we're testing, we just overwrite the current document + await editableEditor.setSelections([ + editor.document.range.toSelection(false), + ]); + } else { + // Otherwise, we create and open a new document for the snippet in the + // user snippets dir + await this.snippets.openNewSnippetFile(snippetName); + } + + // Insert the meta-snippet + await editableEditor.insertSnippet(snippetText); + + return { + thatSelections: 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; +} diff --git a/packages/cursorless-engine/src/actions/actions.types.ts b/packages/cursorless-engine/src/actions/actions.types.ts index 8ded7fe1e3..976b03efdc 100644 --- a/packages/cursorless-engine/src/actions/actions.types.ts +++ b/packages/cursorless-engine/src/actions/actions.types.ts @@ -124,7 +124,11 @@ export interface ActionRecord extends Record { }; generateSnippet: { - run(targets: Target[], snippetName?: string): Promise; + run( + targets: Target[], + dirPath?: string, + snippetName?: string, + ): Promise; }; insertSnippet: { diff --git a/packages/cursorless-engine/src/core/Snippets.ts b/packages/cursorless-engine/src/core/Snippets.ts index 5b041dc93d..01da0ddf2f 100644 --- a/packages/cursorless-engine/src/core/Snippets.ts +++ b/packages/cursorless-engine/src/core/Snippets.ts @@ -1,4 +1,4 @@ -import type { Snippet, SnippetMap } from "@cursorless/common"; +import type { Snippet, SnippetMap, TextEditor } from "@cursorless/common"; /** * Handles all cursorless snippets, including core, third-party and @@ -29,8 +29,13 @@ export interface Snippets { getSnippetStrict(snippetName: string): Snippet; /** - * Opens a new snippet file in the users snippet directory. + * Opens a new snippet file * @param snippetName The name of the snippet + * @param dirPath The path to the directory where the snippet should be created + * @returns The text editor of the newly created snippet file */ - openNewSnippetFile(snippetName: string): Promise; + openNewSnippetFile( + snippetName: string, + dirPath?: string, + ): Promise; } diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index 77bfe82bc3..ff9f235a85 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -169,6 +169,7 @@ export class CommandRunnerImpl implements CommandRunner { case "generateSnippet": return this.actions.generateSnippet.run( this.getTargets(actionDescriptor.target), + actionDescriptor.dirPath, actionDescriptor.snippetName, ); diff --git a/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts b/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts index f9377c7251..705fe52074 100644 --- a/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts +++ b/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts @@ -1,4 +1,4 @@ -import type { SnippetMap, Snippet } from "@cursorless/common"; +import type { Snippet, SnippetMap, TextEditor } from "@cursorless/common"; import type { Snippets } from "../core/Snippets"; export class DisabledSnippets implements Snippets { @@ -17,7 +17,10 @@ export class DisabledSnippets implements Snippets { throw new Error("Snippets are not implemented."); } - openNewSnippetFile(_snippetName: string): Promise { + openNewSnippetFile( + _snippetName: string, + _dirPath?: string, + ): Promise { throw new Error("Snippets are not implemented."); } } diff --git a/packages/cursorless-vscode/src/VscodeSnippets.ts b/packages/cursorless-vscode/src/VscodeSnippets.ts index 37bbae3ad9..a4a2afd9c2 100644 --- a/packages/cursorless-vscode/src/VscodeSnippets.ts +++ b/packages/cursorless-vscode/src/VscodeSnippets.ts @@ -1,4 +1,4 @@ -import type { Snippet, SnippetMap } from "@cursorless/common"; +import type { Snippet, SnippetMap, TextEditor } from "@cursorless/common"; import { mergeStrict, showError, type IDE } from "@cursorless/common"; import { mergeSnippets, type Snippets } from "@cursorless/cursorless-engine"; import { walkFiles } from "@cursorless/node-common"; @@ -235,18 +235,28 @@ export class VscodeSnippets implements Snippets { return snippet; } - async openNewSnippetFile(snippetName: string) { - const userSnippetsDir = this.ide.configuration.getOwnConfiguration( - "experimental.snippetsDir", - ); + async openNewSnippetFile( + snippetName: string, + dirPath?: string, + ): Promise { + const path = (() => { + if (dirPath != null) { + return join(dirPath, `${snippetName}.snippet`); + } - if (!userSnippetsDir) { - throw new Error("User snippets dir not configured."); - } + const userSnippetsDir = this.ide.configuration.getOwnConfiguration( + "experimental.snippetsDir", + ); + + if (!userSnippetsDir) { + throw new Error("User snippets dir not configured."); + } + + return join(userSnippetsDir, `${snippetName}.cursorless-snippets`); + })(); - const path = join(userSnippetsDir, `${snippetName}.cursorless-snippets`); await touch(path); - await this.ide.openTextDocument(path); + return this.ide.openTextDocument(path); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b76027bcff..f879ab8f41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,9 @@ importers: nearley: specifier: 2.20.1 version: 2.20.1(patch_hash=mg2fc7wgvzub3myuz6m74hllma) + talon-snippets: + specifier: 1.1.0 + version: 1.1.0 uuid: specifier: ^10.0.0 version: 10.0.0 @@ -9242,6 +9245,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + talon-snippets@1.1.0: + resolution: {integrity: sha512-NOkb/8KOlezJXP2TVzYF4AJBdrew1c1636EqEUxEyese8Qpb1yQyRkZtY16YzAoTpwcEg4KYxX6vl8SaRlHyDA==} + tapable@1.1.3: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} engines: {node: '>=6'} @@ -20843,6 +20849,8 @@ snapshots: transitivePeerDependencies: - ts-node + talon-snippets@1.1.0: {} + tapable@1.1.3: {} tapable@2.2.1: {} From 325d9a60679243dab82cc0d61d179262b220aba7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:53:53 +0000 Subject: [PATCH 02/12] [pre-commit.ci lite] apply automatic fixes --- data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml | 2 +- data/fixtures/recorded/actions/snippets/snipMakeState2.yml | 2 +- .../fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml b/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml index 556ca8ab5e..976a86254a 100644 --- a/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml +++ b/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml @@ -52,4 +52,4 @@ finalState: start: {line: 0, character: 4} end: {line: 6, character: 5} isReversed: false - hasExplicitRange: true \ No newline at end of file + hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/snipMakeState2.yml b/data/fixtures/recorded/actions/snippets/snipMakeState2.yml index 7f814e0155..cddf338679 100644 --- a/data/fixtures/recorded/actions/snippets/snipMakeState2.yml +++ b/data/fixtures/recorded/actions/snippets/snipMakeState2.yml @@ -43,4 +43,4 @@ finalState: start: {line: 0, character: 0} end: {line: 2, character: 1} isReversed: false - hasExplicitRange: true \ No newline at end of file + hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml b/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml index 5bb8947f47..95a0ccef1a 100644 --- a/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml +++ b/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml @@ -38,4 +38,4 @@ finalState: start: {line: 0, character: 0} end: {line: 0, character: 13} isReversed: false - hasExplicitRange: true \ No newline at end of file + hasExplicitRange: true From 6d485772f53485f2269f7b2396797fbd8e9ca18a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 22 Jan 2025 00:02:57 +0100 Subject: [PATCH 03/12] Update comment --- .../GenerateSnippetCommunity.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts index 905eea0903..4bc66f9aeb 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts @@ -41,21 +41,15 @@ import { getHeaderSnippet } from "talon-snippets"; * 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. Replace cursor selections in text with snippet variables * 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 + * 5. Construct a javascript object that will be serialized 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. + * 6. Serialize the javascript object + * 7. Escape dollar signs and replace placeholder text with snippet placeholders. + * This modified json output is the meta snippet. + * 8. Open a new document in the 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 GenerateSnippetCommunity { constructor(private snippets: Snippets) { From 23629526c3c0877b67c40ea19a10ea95c97d35fe Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 22 Jan 2025 00:19:25 +0100 Subject: [PATCH 04/12] neovim skip generate snippet test --- packages/cursorless-neovim-e2e/src/shouldRunTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cursorless-neovim-e2e/src/shouldRunTest.ts b/packages/cursorless-neovim-e2e/src/shouldRunTest.ts index 5f24849b29..b2d4262772 100644 --- a/packages/cursorless-neovim-e2e/src/shouldRunTest.ts +++ b/packages/cursorless-neovim-e2e/src/shouldRunTest.ts @@ -50,6 +50,8 @@ function isFailingFixture(name: string, fixture: TestCaseFixtureLegacy) { return true; case "wrapWithSnippet": return true; + case "generateSnippet": + return true; // "recorded/actions/insertEmptyLines/floatThis*" -> wrong fixture.finalState.selections and fixture.thatMark.contentRange case "breakLine": return true; From d92e383394e2c2a11c47ce9aefe66b1a6341bb96 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 22 Jan 2025 13:24:59 +0100 Subject: [PATCH 05/12] Ordered imports --- .../src/actions/GenerateSnippet/GenerateSnippetCommunity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts index 4bc66f9aeb..e9f84c1422 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts @@ -7,6 +7,7 @@ import { type TextEditor, } from "@cursorless/common"; import { + getHeaderSnippet, parseSnippetFile, serializeSnippetFile, type SnippetDocument, @@ -20,7 +21,6 @@ import type { ActionReturnValue } from "../actions.types"; import { constructSnippetBody } from "./constructSnippetBody"; import { editText } from "./editText"; import type { Offsets } from "./Offsets"; -import { getHeaderSnippet } from "talon-snippets"; /** * This action can be used to automatically create a snippet from a target. Any From 7646f6e103b03d4f436102698ce9003dd16c6b1f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 23 Jan 2025 18:14:56 +0100 Subject: [PATCH 06/12] Use path return --- cursorless-talon/src/actions/generate_snippet.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cursorless-talon/src/actions/generate_snippet.py b/cursorless-talon/src/actions/generate_snippet.py index da52863e49..d32a49b2b8 100644 --- a/cursorless-talon/src/actions/generate_snippet.py +++ b/cursorless-talon/src/actions/generate_snippet.py @@ -32,19 +32,19 @@ def private_cursorless_generate_snippet_action(target: CursorlessExplicitTarget) { "name": "generateSnippet", "target": target, - "dirPath": get_dir_path(), + "dirPath": str(get_dir_path()), } ) -def get_dir_path() -> str: +def get_dir_path() -> Path: settings_dir = get_setting_dir() if settings_dir is not None: return settings_dir return get_community_snippets_dir() -def get_setting_dir() -> str | None: +def get_setting_dir() -> Path | None: try: setting_dir = settings.get("user.snippets_dir") if not setting_dir: @@ -56,16 +56,16 @@ def get_setting_dir() -> str | None: user_dir = Path(actions.path.talon_user()) dir = user_dir / dir - return str(dir.resolve()) + return dir.resolve() except Exception: return None -def get_community_snippets_dir() -> str: +def get_community_snippets_dir() -> Path: files = glob.iglob( f"{actions.path.talon_user()}/**/snippets/snippets/*.snippet", recursive=True, ) for file in files: - return str(Path(file).parent) + return Path(file).parent raise ValueError("Could not find community snippets directory") From b585a44850768f032b16a19e1ee784cdbc98f037 Mon Sep 17 00:00:00 2001 From: Phil Cohen Date: Thu, 23 Jan 2025 09:53:24 -0800 Subject: [PATCH 07/12] Update packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts --- .../src/actions/GenerateSnippet/GenerateSnippetCommunity.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts index e9f84c1422..af264d7fc8 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts @@ -221,6 +221,7 @@ function getsSnippetSelections(editor: TextEditor, range: Range): Selection[] { return selections; } +// Used to temporarily escape the $1, $2 snippet holes (the "meta snippet" holes that become live snippets when the user edits) so we can use traditional backslash escaping for the holes in the underlying snippet itself (the "user snippet" holes that will be saved as part of their new template). const PLACEHOLDER = "PLACEHOLDER_VFA77zcbLD6wXNmfMAay"; interface Variable { From 2ac4d44259b59588cc9c13962c291551ae4ac4a2 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 23 Jan 2025 18:54:18 +0100 Subject: [PATCH 08/12] Reflow comment --- .../src/actions/GenerateSnippet/GenerateSnippetCommunity.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts index af264d7fc8..0d55effc3e 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts @@ -221,7 +221,10 @@ function getsSnippetSelections(editor: TextEditor, range: Range): Selection[] { return selections; } -// Used to temporarily escape the $1, $2 snippet holes (the "meta snippet" holes that become live snippets when the user edits) so we can use traditional backslash escaping for the holes in the underlying snippet itself (the "user snippet" holes that will be saved as part of their new template). +// Used to temporarily escape the $1, $2 snippet holes (the "meta snippet" holes +// that become live snippets when the user edits) so we can use traditional +// backslash escaping for the holes in the underlying snippet itself (the "user +// snippet" holes that will be saved as part of their new template). const PLACEHOLDER = "PLACEHOLDER_VFA77zcbLD6wXNmfMAay"; interface Variable { From d51cea34b1190239d0c1c868dcb3be66de0e8ebc Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 23 Jan 2025 19:18:27 +0100 Subject: [PATCH 09/12] Update name to directory --- cursorless-talon/src/actions/generate_snippet.py | 2 +- data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml | 2 +- data/fixtures/recorded/actions/snippets/snipMakeState2.yml | 2 +- .../recorded/actions/snippets/testSnippetMakeLine2.yml | 2 +- packages/common/src/types/command/ActionDescriptor.ts | 2 +- .../src/actions/GenerateSnippet/GenerateSnippetCommunity.ts | 4 ++-- .../src/core/commandRunner/CommandRunnerImpl.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cursorless-talon/src/actions/generate_snippet.py b/cursorless-talon/src/actions/generate_snippet.py index d32a49b2b8..1b513b8280 100644 --- a/cursorless-talon/src/actions/generate_snippet.py +++ b/cursorless-talon/src/actions/generate_snippet.py @@ -32,7 +32,7 @@ def private_cursorless_generate_snippet_action(target: CursorlessExplicitTarget) { "name": "generateSnippet", "target": target, - "dirPath": str(get_dir_path()), + "directory": str(get_dir_path()), } ) diff --git a/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml b/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml index 976a86254a..5706bff670 100644 --- a/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml +++ b/data/fixtures/recorded/actions/snippets/snipMakeFunk2.yml @@ -4,7 +4,7 @@ command: spokenForm: snippet make funk action: name: generateSnippet - dirPath: "" + directory: "" snippetName: snippetTest1 target: type: primitive diff --git a/data/fixtures/recorded/actions/snippets/snipMakeState2.yml b/data/fixtures/recorded/actions/snippets/snipMakeState2.yml index cddf338679..b75d95a968 100644 --- a/data/fixtures/recorded/actions/snippets/snipMakeState2.yml +++ b/data/fixtures/recorded/actions/snippets/snipMakeState2.yml @@ -4,7 +4,7 @@ command: spokenForm: snippet make state action: name: generateSnippet - dirPath: "" + directory: "" snippetName: snippetTest1 target: type: primitive diff --git a/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml b/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml index 95a0ccef1a..28bc982668 100644 --- a/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml +++ b/data/fixtures/recorded/actions/snippets/testSnippetMakeLine2.yml @@ -4,7 +4,7 @@ command: spokenForm: test snippet make line action: name: generateSnippet - dirPath: "" + directory: "" snippetName: testSnippet target: type: primitive diff --git a/packages/common/src/types/command/ActionDescriptor.ts b/packages/common/src/types/command/ActionDescriptor.ts index c1cde81507..8b067b37f2 100644 --- a/packages/common/src/types/command/ActionDescriptor.ts +++ b/packages/common/src/types/command/ActionDescriptor.ts @@ -135,7 +135,7 @@ export interface PasteActionDescriptor { export interface GenerateSnippetActionDescriptor { name: "generateSnippet"; - dirPath?: string; + directory?: string; snippetName?: string; target: PartialTargetDescriptor; } diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts index 0d55effc3e..8f77d8a4d5 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts @@ -58,7 +58,7 @@ export default class GenerateSnippetCommunity { async run( targets: Target[], - dirPath: string, + directory: string, snippetName?: string, ): Promise { const target = ensureSingleTarget(targets); @@ -138,7 +138,7 @@ export default class GenerateSnippetCommunity { } else { // Otherwise, we create and open a new document for the snippet editableEditor = ide().getEditableTextEditor( - await this.snippets.openNewSnippetFile(snippetName, dirPath), + await this.snippets.openNewSnippetFile(snippetName, directory), ); snippetDocuments = parseSnippetFile(editableEditor.document.getText()); } diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index ff9f235a85..ede4ea49c4 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -169,7 +169,7 @@ export class CommandRunnerImpl implements CommandRunner { case "generateSnippet": return this.actions.generateSnippet.run( this.getTargets(actionDescriptor.target), - actionDescriptor.dirPath, + actionDescriptor.directory, actionDescriptor.snippetName, ); From 216309d4307577498b93f611ddd7993b0300889a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 23 Jan 2025 19:19:51 +0100 Subject: [PATCH 10/12] we name --- cursorless-talon/src/actions/generate_snippet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cursorless-talon/src/actions/generate_snippet.py b/cursorless-talon/src/actions/generate_snippet.py index 1b513b8280..ef5452d2ea 100644 --- a/cursorless-talon/src/actions/generate_snippet.py +++ b/cursorless-talon/src/actions/generate_snippet.py @@ -32,12 +32,12 @@ def private_cursorless_generate_snippet_action(target: CursorlessExplicitTarget) { "name": "generateSnippet", "target": target, - "directory": str(get_dir_path()), + "directory": str(get_directory_path()), } ) -def get_dir_path() -> Path: +def get_directory_path() -> Path: settings_dir = get_setting_dir() if settings_dir is not None: return settings_dir From 67f61335d0acfc48644a781355f7ebfd7bdb05fc Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 23 Jan 2025 19:21:04 +0100 Subject: [PATCH 11/12] Up dit --- .../src/actions/GenerateSnippet/GenerateSnippet.ts | 6 +++--- packages/cursorless-engine/src/actions/actions.types.ts | 2 +- packages/cursorless-engine/src/core/Snippets.ts | 4 ++-- packages/cursorless-vscode/src/VscodeSnippets.ts | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts index 430f9eeca2..b7e0aacde2 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts @@ -11,15 +11,15 @@ export default class GenerateSnippet { async run( targets: Target[], - dirPath?: string, + directory?: string, snippetName?: string, ): Promise { - if (dirPath == null) { + if (directory == null) { const action = new GenerateSnippetLegacy(this.snippets); return action.run(targets, snippetName); } const action = new GenerateSnippetCommunity(this.snippets); - return action.run(targets, dirPath, snippetName); + return action.run(targets, directory, snippetName); } } diff --git a/packages/cursorless-engine/src/actions/actions.types.ts b/packages/cursorless-engine/src/actions/actions.types.ts index 976b03efdc..a0eeef4810 100644 --- a/packages/cursorless-engine/src/actions/actions.types.ts +++ b/packages/cursorless-engine/src/actions/actions.types.ts @@ -126,7 +126,7 @@ export interface ActionRecord extends Record { generateSnippet: { run( targets: Target[], - dirPath?: string, + directory?: string, snippetName?: string, ): Promise; }; diff --git a/packages/cursorless-engine/src/core/Snippets.ts b/packages/cursorless-engine/src/core/Snippets.ts index 01da0ddf2f..190cb1e9ab 100644 --- a/packages/cursorless-engine/src/core/Snippets.ts +++ b/packages/cursorless-engine/src/core/Snippets.ts @@ -31,11 +31,11 @@ export interface Snippets { /** * Opens a new snippet file * @param snippetName The name of the snippet - * @param dirPath The path to the directory where the snippet should be created + * @param directory The path to the directory where the snippet should be created * @returns The text editor of the newly created snippet file */ openNewSnippetFile( snippetName: string, - dirPath?: string, + directory?: string, ): Promise; } diff --git a/packages/cursorless-vscode/src/VscodeSnippets.ts b/packages/cursorless-vscode/src/VscodeSnippets.ts index a4a2afd9c2..0a24f446ad 100644 --- a/packages/cursorless-vscode/src/VscodeSnippets.ts +++ b/packages/cursorless-vscode/src/VscodeSnippets.ts @@ -237,11 +237,11 @@ export class VscodeSnippets implements Snippets { async openNewSnippetFile( snippetName: string, - dirPath?: string, + directory?: string, ): Promise { const path = (() => { - if (dirPath != null) { - return join(dirPath, `${snippetName}.snippet`); + if (directory != null) { + return join(directory, `${snippetName}.snippet`); } const userSnippetsDir = this.ide.configuration.getOwnConfiguration( From c980df5a4aa2866639ee8bdeb88bbafa3a0fcae2 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 23 Jan 2025 19:22:11 +0100 Subject: [PATCH 12/12] fix --- .../src/disabledComponents/DisabledSnippets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts b/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts index 705fe52074..af33393be1 100644 --- a/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts +++ b/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts @@ -19,7 +19,7 @@ export class DisabledSnippets implements Snippets { openNewSnippetFile( _snippetName: string, - _dirPath?: string, + _directory?: string, ): Promise { throw new Error("Snippets are not implemented."); }