From a6e2e34fcd20ca850ec3a113b307f380a4ff1276 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 18:00:56 +0100 Subject: [PATCH 01/11] Started working on removing deprecated snippets --- .../src/snippets/snippets_deprecated.py | 54 --- cursorless-talon/src/spoken_forms.json | 6 - cursorless-talon/src/spoken_forms.py | 24 -- packages/common/src/cursorlessCommandIds.ts | 4 - packages/common/src/errors.ts | 7 + packages/common/src/index.ts | 1 - packages/common/src/types/snippet.types.ts | 89 ----- .../cursorless-engine/src/actions/Actions.ts | 2 - .../src/actions/InsertSnippet.ts | 22 +- .../src/actions/WrapWithSnippet.ts | 18 +- .../snippetsLegacy/InsertSnippetLegacy.ts | 193 ---------- .../snippetsLegacy/WrapWithSnippetLegacy.ts | 122 ------ .../legacySnippetsNotification.ts | 19 - .../src/actions/snippetsLegacy/snippet.ts | 212 ----------- .../actions/snippetsLegacy/textFormatters.ts | 35 -- .../cursorless-engine/src/core/Snippets.ts | 16 +- .../disabledComponents/DisabledSnippets.ts | 6 +- .../src/typings/target.types.ts | 4 - .../cursorless-neovim/src/registerCommands.ts | 1 - .../src/docs/user/customization.md | 4 - .../src/docs/user/experimental/snippets.md | 166 --------- .../cursorless-vscode/src/VscodeSnippets.ts | 248 +------------ packages/cursorless-vscode/src/extension.ts | 4 - .../cursorless-vscode/src/migrateSnippets.ts | 351 ------------------ .../src/migrateSnippets.vscode.test.ts | 238 ------------ .../cursorless-vscode/src/registerCommands.ts | 5 - .../compareSnippetDefinitions.ts | 95 ----- .../src/snippetsLegacy/mergeSnippets.ts | 81 ---- packages/neovim-common/src/getExtensionApi.ts | 9 - 29 files changed, 21 insertions(+), 2015 deletions(-) delete mode 100644 cursorless-talon/src/snippets/snippets_deprecated.py delete mode 100644 packages/common/src/types/snippet.types.ts delete mode 100644 packages/cursorless-engine/src/actions/snippetsLegacy/InsertSnippetLegacy.ts delete mode 100644 packages/cursorless-engine/src/actions/snippetsLegacy/WrapWithSnippetLegacy.ts delete mode 100644 packages/cursorless-engine/src/actions/snippetsLegacy/legacySnippetsNotification.ts delete mode 100644 packages/cursorless-engine/src/actions/snippetsLegacy/snippet.ts delete mode 100644 packages/cursorless-engine/src/actions/snippetsLegacy/textFormatters.ts delete mode 100644 packages/cursorless-org-docs/src/docs/user/experimental/snippets.md delete mode 100644 packages/cursorless-vscode/src/migrateSnippets.ts delete mode 100644 packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts delete mode 100644 packages/cursorless-vscode/src/snippetsLegacy/compareSnippetDefinitions.ts delete mode 100644 packages/cursorless-vscode/src/snippetsLegacy/mergeSnippets.ts diff --git a/cursorless-talon/src/snippets/snippets_deprecated.py b/cursorless-talon/src/snippets/snippets_deprecated.py deleted file mode 100644 index fcfa316860..0000000000 --- a/cursorless-talon/src/snippets/snippets_deprecated.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import Any - -from talon import Module, app, registry - -mod = Module() - -# DEPRECATED @ 2025-02-01 - -tags = [ - "cursorless_experimental_snippets", - "cursorless_use_community_snippets", -] - -lists = [ - "cursorless_insertion_snippet_no_phrase", - "cursorless_insertion_snippet_single_phrase", - "cursorless_wrapper_snippet", - "cursorless_phrase_terminator", -] - -for tag in tags: - mod.tag(tag, desc="DEPRECATED") - -for list in lists: - mod.list(list, desc="DEPRECATED") - - -@mod.action_class -class Actions: - def cursorless_insert_snippet_by_name(name: str): # pyright: ignore [reportGeneralTypeIssues] - """[DEPRECATED] Cursorless: Insert named snippet """ - raise NotImplementedError( - "Cursorless snippets are deprecated. Please use community snippets. Update to latest cursorless-talon and say 'cursorless migrate snippets'." - ) - - def cursorless_wrap_with_snippet_by_name( - name: str, # pyright: ignore [reportGeneralTypeIssues] - variable_name: str, - target: Any, - ): - """[DEPRECATED] Cursorless: Wrap target with a named snippet """ - raise NotImplementedError( - "Cursorless snippets are deprecated. Please use community snippets. Update to latest cursorless-talon and say 'cursorless migrate snippets'." - ) - - -def on_ready(): - for tag in tags: - name = f"user.{tag}" - if name in registry.tags: - print(f"WARNING tag: '{name}' is deprecated and should not be used anymore") - - -app.register("ready", on_ready) diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index dd332db2ab..b864dad1e8 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -235,12 +235,6 @@ "from": "experimental.setInstanceReference" } }, - "experimental/wrapper_snippets.csv": {}, - "experimental/insertion_snippets.csv": {}, - "experimental/insertion_snippets_single_phrase.csv": {}, - "experimental/miscellaneous.csv": { - "phrase_terminator": { "over": "phraseTerminator" } - }, "experimental/actions_custom.csv": {}, "experimental/regex_scope_types.csv": {}, "hat_styles.csv": { diff --git a/cursorless-talon/src/spoken_forms.py b/cursorless-talon/src/spoken_forms.py index a4495c34ec..96589bb6fc 100644 --- a/cursorless-talon/src/spoken_forms.py +++ b/cursorless-talon/src/spoken_forms.py @@ -165,30 +165,6 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): ], default_list_name="scope_type", ), - # DEPRECATED @ 2025-02-01 - handle_csv( - "experimental/wrapper_snippets.csv", - deprecated=True, - allow_unknown_values=True, - default_list_name="wrapper_snippet", - ), - handle_csv( - "experimental/insertion_snippets.csv", - deprecated=True, - allow_unknown_values=True, - default_list_name="insertion_snippet_no_phrase", - ), - handle_csv( - "experimental/insertion_snippets_single_phrase.csv", - deprecated=True, - allow_unknown_values=True, - default_list_name="insertion_snippet_single_phrase", - ), - handle_csv( - "experimental/miscellaneous.csv", - deprecated=True, - ), - # --- handle_csv( "experimental/actions_custom.csv", headers=[SPOKEN_FORM_HEADER, "VSCode command"], diff --git a/packages/common/src/cursorlessCommandIds.ts b/packages/common/src/cursorlessCommandIds.ts index 6a6dbc9a3e..3d5c5a99ee 100644 --- a/packages/common/src/cursorlessCommandIds.ts +++ b/packages/common/src/cursorlessCommandIds.ts @@ -38,7 +38,6 @@ export const cursorlessCommandIds = [ "cursorless.keyboard.targeted.targetHat", "cursorless.keyboard.targeted.targetScope", "cursorless.keyboard.targeted.targetSelection", - "cursorless.migrateSnippets", "cursorless.pauseRecording", "cursorless.recomputeDecorationStyles", "cursorless.recordTestCase", @@ -170,7 +169,4 @@ export const cursorlessCommandDescriptions: Record< ["cursorless.keyboard.redoTarget"]: new HiddenCommand( "Redo keyboard targeting changes", ), - ["cursorless.migrateSnippets"]: new HiddenCommand( - "Migrate snippets from the old Cursorless format to the new community format", - ), }; diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts index 90ee2b4c81..7941b6c92e 100644 --- a/packages/common/src/errors.ts +++ b/packages/common/src/errors.ts @@ -40,3 +40,10 @@ export class NoContainingScopeError extends Error { this.name = "NoContainingScopeError"; } } + +export class NamedSnippetsDeprecationError extends Error { + constructor() { + super("Named snippets are deprecated and not supported anymore"); + this.name = "NamedSnippetsDeprecationError"; + } +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 7744e99d3c..49e9084636 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -80,7 +80,6 @@ export * from "./types/RangeOffsets"; export * from "./types/RevealLineAt"; export * from "./types/ScopeProvider"; export * from "./types/Selection"; -export * from "./types/snippet.types"; export * from "./types/SpokenForm"; export * from "./types/SpokenFormType"; export * from "./types/StringRecord"; diff --git a/packages/common/src/types/snippet.types.ts b/packages/common/src/types/snippet.types.ts deleted file mode 100644 index a383920770..0000000000 --- a/packages/common/src/types/snippet.types.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { SimpleScopeTypeType } from "./command/PartialTargetDescriptor.types"; - -// FIXME: Legacy snippets - -export interface SnippetScope { - /** - * VSCode language ids where this snippet definition should be active - */ - langIds?: string[]; - - /** - * Cursorless scopes in which this snippet is active. Allows, for example, to - * have different snippets to define a function if you're in a class or at - * global scope. - */ - scopeTypes?: SimpleScopeTypeType[]; - - /** - * Exclude regions of {@link scopeTypes} that are descendants of these scope - * types. For example, if you have a snippet that should be active in a class - * but not in a function within the class, you can specify - * `scopeTypes: ["class"], excludeDescendantScopeTypes: ["namedFunction"]`. - */ - excludeDescendantScopeTypes?: SimpleScopeTypeType[]; -} - -export type SnippetBody = string[]; - -export interface SnippetDefinition { - /** - * Inline snippet text using VSCode snippet syntax; entries joined by newline. - * Named variables of the form `$foo` can be used as placeholders - */ - body: SnippetBody; - - /** - * Scopes where this snippet is active - */ - scope?: SnippetScope; - - /** - * Scope-specific overrides for the variables defined in the snippet - */ - variables?: Record; -} - -export interface SnippetVariable { - /** - * Default to this scope type when wrapping a target without scope type - * specified. - */ - wrapperScopeType?: SimpleScopeTypeType; - - /** - * Description of the snippet variable - */ - description?: string; - - /** - * Format text inserted into this variable using the given formatter - */ - formatter?: string; -} - -export interface Snippet { - /** - * List of possible definitions for this snippet - */ - definitions: SnippetDefinition[]; - - /** - * For each named variable in the snippet, provides extra information about the variable. - */ - variables?: Record; - - /** - * Description of the snippet - */ - description?: string; - - /** - * Try to expand target to this scope type when inserting this snippet - * before/after a target without scope type specified. If multiple scope types - * are specified try them each in order until one of them matches. - */ - insertionScopeTypes?: SimpleScopeTypeType[]; -} - -export type SnippetMap = Record; diff --git a/packages/cursorless-engine/src/actions/Actions.ts b/packages/cursorless-engine/src/actions/Actions.ts index ef82809510..346f9ac0a6 100644 --- a/packages/cursorless-engine/src/actions/Actions.ts +++ b/packages/cursorless-engine/src/actions/Actions.ts @@ -131,7 +131,6 @@ export class Actions implements ActionRecord { ); this.insertSnippet = new InsertSnippet( rangeUpdater, - snippets, this, modifierStageFactory, ); @@ -170,7 +169,6 @@ export class Actions implements ActionRecord { this.wrapWithPairedDelimiter = new Wrap(rangeUpdater); this.wrapWithSnippet = new WrapWithSnippet( rangeUpdater, - snippets, modifierStageFactory, ); diff --git a/packages/cursorless-engine/src/actions/InsertSnippet.ts b/packages/cursorless-engine/src/actions/InsertSnippet.ts index 391e0ce74e..f4756c0828 100644 --- a/packages/cursorless-engine/src/actions/InsertSnippet.ts +++ b/packages/cursorless-engine/src/actions/InsertSnippet.ts @@ -1,6 +1,8 @@ import type { InsertSnippetArg } from "@cursorless/common"; -import { RangeExpansionBehavior } from "@cursorless/common"; -import type { Snippets } from "../core/Snippets"; +import { + NamedSnippetsDeprecationError, + RangeExpansionBehavior, +} from "@cursorless/common"; import { getPreferredSnippet } from "../core/getPreferredSnippet"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; @@ -14,14 +16,12 @@ import type { Destination } from "../typings/target.types"; import { ensureSingleEditor } from "../util/targetUtils"; import type { Actions } from "./Actions"; import type { ActionReturnValue } from "./actions.types"; -import InsertSnippetLegacy from "./snippetsLegacy/InsertSnippetLegacy"; export default class InsertSnippet { private snippetParser = new SnippetParser(); constructor( private rangeUpdater: RangeUpdater, - private snippets: Snippets, private actions: Actions, private modifierStageFactory: ModifierStageFactory, ) { @@ -33,7 +33,7 @@ export default class InsertSnippet { snippetDescription: InsertSnippetArg, ): ModifierStage[] { if (snippetDescription.type === "named") { - return this.legacy().getFinalStages(snippetDescription); + throw new NamedSnippetsDeprecationError(); } const editor = ensureSingleEditor(destinations); @@ -60,7 +60,7 @@ export default class InsertSnippet { snippetDescription: InsertSnippetArg, ): Promise { if (snippetDescription.type === "named") { - return this.legacy().run(destinations, snippetDescription); + throw new NamedSnippetsDeprecationError(); } const editor = ide().getEditableTextEditor( @@ -103,14 +103,4 @@ export default class InsertSnippet { })), }; } - - // DEPRECATED @ 2025-02-01 - private legacy() { - return new InsertSnippetLegacy( - this.rangeUpdater, - this.snippets, - this.actions, - this.modifierStageFactory, - ); - } } diff --git a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts index 26178e3353..bf847d9e58 100644 --- a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts +++ b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts @@ -1,6 +1,5 @@ import type { WrapWithSnippetArg } from "@cursorless/common"; -import { FlashStyle } from "@cursorless/common"; -import type { Snippets } from "../core/Snippets"; +import { FlashStyle, NamedSnippetsDeprecationError } from "@cursorless/common"; import { getPreferredSnippet } from "../core/getPreferredSnippet"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; @@ -13,14 +12,12 @@ import { SnippetParser } from "../snippets/vendor/vscodeSnippet/snippetParser"; import type { Target } from "../typings/target.types"; import { ensureSingleEditor, flashTargets } from "../util/targetUtils"; import type { ActionReturnValue } from "./actions.types"; -import WrapWithSnippetLegacy from "./snippetsLegacy/WrapWithSnippetLegacy"; export default class WrapWithSnippet { private snippetParser = new SnippetParser(); constructor( private rangeUpdater: RangeUpdater, - private snippets: Snippets, private modifierStageFactory: ModifierStageFactory, ) { this.run = this.run.bind(this); @@ -31,7 +28,7 @@ export default class WrapWithSnippet { snippetDescription: WrapWithSnippetArg, ): ModifierStage[] { if (snippetDescription.type === "named") { - return this.legacy().getFinalStages(snippetDescription); + throw new NamedSnippetsDeprecationError(); } const editor = ensureSingleEditor(targets); @@ -60,7 +57,7 @@ export default class WrapWithSnippet { snippetDescription: WrapWithSnippetArg, ): Promise { if (snippetDescription.type === "named") { - return this.legacy().run(targets, snippetDescription); + throw new NamedSnippetsDeprecationError(); } const editor = ide().getEditableTextEditor(ensureSingleEditor(targets)); @@ -97,13 +94,4 @@ export default class WrapWithSnippet { })), }; } - - // DEPRECATED @ 2025-02-01 - private legacy() { - return new WrapWithSnippetLegacy( - this.rangeUpdater, - this.snippets, - this.modifierStageFactory, - ); - } } diff --git a/packages/cursorless-engine/src/actions/snippetsLegacy/InsertSnippetLegacy.ts b/packages/cursorless-engine/src/actions/snippetsLegacy/InsertSnippetLegacy.ts deleted file mode 100644 index 4accd9c0e0..0000000000 --- a/packages/cursorless-engine/src/actions/snippetsLegacy/InsertSnippetLegacy.ts +++ /dev/null @@ -1,193 +0,0 @@ -import type { - NamedInsertSnippetArg, - ScopeType, - Snippet, - SnippetDefinition, -} from "@cursorless/common"; -import { RangeExpansionBehavior } from "@cursorless/common"; -import type { Snippets } from "../../core/Snippets"; -import type { RangeUpdater } from "../../core/updateSelections/RangeUpdater"; -import { performEditsAndUpdateSelections } from "../../core/updateSelections/updateSelections"; -import type { ModifierStageFactory } from "../../processTargets/ModifierStageFactory"; -import { ModifyIfUntypedExplicitStage } from "../../processTargets/modifiers/ConditionalModifierStages"; -import { UntypedTarget } from "../../processTargets/targets"; -import { ide } from "../../singletons/ide.singleton"; -import { SnippetParser } from "../../snippets/vendor/vscodeSnippet/snippetParser"; -import type { Destination, Target } from "../../typings/target.types"; -import { ensureSingleEditor } from "../../util/targetUtils"; -import type { Actions } from "../Actions"; -import type { ActionReturnValue } from "../actions.types"; -import { - findMatchingSnippetDefinitionStrict, - transformSnippetVariables, -} from "../snippetsLegacy/snippet"; -import { showLegacySnippetsNotification } from "./legacySnippetsNotification"; -import { textFormatters, type TextFormatterName } from "./textFormatters"; - -export default class InsertSnippetLegacy { - private snippetParser = new SnippetParser(); - - constructor( - private rangeUpdater: RangeUpdater, - private snippets: Snippets, - private actions: Actions, - private modifierStageFactory: ModifierStageFactory, - ) { - this.run = this.run.bind(this); - } - - getFinalStages(snippetDescription: NamedInsertSnippetArg) { - const defaultScopeTypes = this.getScopeTypes(snippetDescription); - - return defaultScopeTypes.length === 0 - ? [] - : [ - new ModifyIfUntypedExplicitStage(this.modifierStageFactory, { - type: "fallback", - modifiers: defaultScopeTypes.map((scopeType) => ({ - type: "containingScope", - scopeType, - })), - }), - ]; - } - - private getScopeTypes( - snippetDescription: NamedInsertSnippetArg, - ): ScopeType[] { - const { name } = snippetDescription; - - const snippet = this.snippets.getSnippetStrict(name); - - const scopeTypeTypes = snippet.insertionScopeTypes; - return scopeTypeTypes == null - ? [] - : scopeTypeTypes.map((scopeTypeType) => ({ - type: scopeTypeType, - })); - } - - private getSnippetInfo( - snippetDescription: NamedInsertSnippetArg, - targets: Target[], - ) { - const { name } = snippetDescription; - - const snippet = this.snippets.getSnippetStrict(name); - - const definition = findMatchingSnippetDefinitionStrict( - this.modifierStageFactory, - targets, - snippet.definitions, - ); - - return { - body: definition.body.join("\n"), - - formatSubstitutions(substitutions: Record | undefined) { - return substitutions == null - ? undefined - : formatSubstitutions(snippet, definition, substitutions); - }, - }; - } - - async run( - destinations: Destination[], - snippetDescription: NamedInsertSnippetArg, - ): Promise { - showLegacySnippetsNotification(); - - const editor = ide().getEditableTextEditor( - ensureSingleEditor(destinations), - ); - - await this.actions.editNew.run(destinations); - - const { body, formatSubstitutions } = this.getSnippetInfo( - snippetDescription, - // Use new selection locations instead of original targets because - // that's where we'll be doing the snippet insertion - editor.selections.map( - (selection) => - new UntypedTarget({ - editor, - contentRange: selection, - isReversed: false, - hasExplicitRange: true, - }), - ), - ); - - const parsedSnippet = this.snippetParser.parse(body); - - transformSnippetVariables( - parsedSnippet, - null, - formatSubstitutions(snippetDescription.substitutions), - ); - - const snippetString = parsedSnippet.toTextmateString(); - - const { editorSelections: updatedThatSelections } = - await performEditsAndUpdateSelections({ - rangeUpdater: this.rangeUpdater, - editor, - callback: () => editor.insertSnippet(snippetString), - preserveCursorSelections: true, - selections: { - editorSelections: { - selections: editor.selections, - behavior: RangeExpansionBehavior.openOpen, - }, - }, - }); - - return { - thatSelections: updatedThatSelections.map((selection) => ({ - editor, - selection, - })), - }; - } -} - -/** - * Applies the appropriate formatters to the given variable substitution values - * in {@link substitutions} based on the formatter specified for the given - * variables as defined in {@link snippet} and {@link definition}. - * @param snippet The full snippet info - * @param definition The specific definition chosen for the given target context - * @param substitutions The original unformatted substitution strings - * @returns A new map of substitution strings with the values formatted - */ -function formatSubstitutions( - snippet: Snippet, - definition: SnippetDefinition, - substitutions: Record, -): Record { - return Object.fromEntries( - Object.entries(substitutions).map(([variableName, value]) => { - // We prefer the variable formatters from the contextually relevant - // snippet definition if they exist, otherwise we fall back to the - // global definitions for the given snippet. - const formatterName = - (definition.variables ?? {})[variableName]?.formatter ?? - (snippet.variables ?? {})[variableName]?.formatter; - - if (formatterName == null) { - return [variableName, value]; - } - - const formatter = textFormatters[formatterName as TextFormatterName]; - - if (formatter == null) { - throw new Error( - `Couldn't find formatter ${formatterName} for variable ${variableName}`, - ); - } - - return [variableName, formatter(value.split(" "))]; - }), - ); -} diff --git a/packages/cursorless-engine/src/actions/snippetsLegacy/WrapWithSnippetLegacy.ts b/packages/cursorless-engine/src/actions/snippetsLegacy/WrapWithSnippetLegacy.ts deleted file mode 100644 index 99f094c218..0000000000 --- a/packages/cursorless-engine/src/actions/snippetsLegacy/WrapWithSnippetLegacy.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { NamedWrapWithSnippetArg, ScopeType } from "@cursorless/common"; -import { FlashStyle } from "@cursorless/common"; -import type { Snippets } from "../../core/Snippets"; -import type { RangeUpdater } from "../../core/updateSelections/RangeUpdater"; -import { performEditsAndUpdateSelections } from "../../core/updateSelections/updateSelections"; -import type { ModifierStageFactory } from "../../processTargets/ModifierStageFactory"; -import { ModifyIfUntypedStage } from "../../processTargets/modifiers/ConditionalModifierStages"; -import { ide } from "../../singletons/ide.singleton"; -import { SnippetParser } from "../../snippets/vendor/vscodeSnippet/snippetParser"; -import type { Target } from "../../typings/target.types"; -import { ensureSingleEditor, flashTargets } from "../../util/targetUtils"; -import type { ActionReturnValue } from "../actions.types"; -import { - findMatchingSnippetDefinitionStrict, - transformSnippetVariables, -} from "../snippetsLegacy/snippet"; -import { showLegacySnippetsNotification } from "./legacySnippetsNotification"; - -export default class WrapWithSnippetLegacy { - private snippetParser = new SnippetParser(); - - constructor( - private rangeUpdater: RangeUpdater, - private snippets: Snippets, - private modifierStageFactory: ModifierStageFactory, - ) { - this.run = this.run.bind(this); - } - - getFinalStages(snippet: NamedWrapWithSnippetArg) { - const defaultScopeType = this.getScopeType(snippet); - - if (defaultScopeType == null) { - return []; - } - - return [ - new ModifyIfUntypedStage(this.modifierStageFactory, { - type: "modifyIfUntyped", - modifier: { - type: "containingScope", - scopeType: defaultScopeType, - }, - }), - ]; - } - - private getScopeType( - snippetDescription: NamedWrapWithSnippetArg, - ): ScopeType | undefined { - const { name, variableName } = snippetDescription; - - const snippet = this.snippets.getSnippetStrict(name); - - const variables = snippet.variables ?? {}; - const scopeTypeType = variables[variableName]?.wrapperScopeType; - return scopeTypeType == null - ? undefined - : { - type: scopeTypeType, - }; - } - - private getBody( - snippetDescription: NamedWrapWithSnippetArg, - targets: Target[], - ): string { - const { name } = snippetDescription; - - const snippet = this.snippets.getSnippetStrict(name); - - const definition = findMatchingSnippetDefinitionStrict( - this.modifierStageFactory, - targets, - snippet.definitions, - ); - - return definition.body.join("\n"); - } - - async run( - targets: Target[], - snippetDescription: NamedWrapWithSnippetArg, - ): Promise { - showLegacySnippetsNotification(); - - const editor = ide().getEditableTextEditor(ensureSingleEditor(targets)); - - const body = this.getBody(snippetDescription, targets); - - const parsedSnippet = this.snippetParser.parse(body); - - transformSnippetVariables(parsedSnippet, snippetDescription.variableName); - - const snippetString = parsedSnippet.toTextmateString(); - - await flashTargets(ide(), targets, FlashStyle.pendingModification0); - - const targetSelections = targets.map((target) => target.contentSelection); - - const callback = () => - editor.insertSnippet(snippetString, targetSelections); - - const { targetSelections: updatedTargetSelections } = - await performEditsAndUpdateSelections({ - rangeUpdater: this.rangeUpdater, - editor, - callback, - preserveCursorSelections: true, - selections: { - targetSelections, - }, - }); - - return { - thatSelections: updatedTargetSelections.map((selection) => ({ - editor, - selection, - })), - }; - } -} diff --git a/packages/cursorless-engine/src/actions/snippetsLegacy/legacySnippetsNotification.ts b/packages/cursorless-engine/src/actions/snippetsLegacy/legacySnippetsNotification.ts deleted file mode 100644 index 45873ff07a..0000000000 --- a/packages/cursorless-engine/src/actions/snippetsLegacy/legacySnippetsNotification.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { showWarning } from "@cursorless/common"; -import { ide } from "../../singletons/ide.singleton"; - -// Show only once per vscode instance -let wasShown = false; - -export function showLegacySnippetsNotification() { - if (wasShown) { - return; - } - - void showWarning( - ide().messages, - "legacySnippets", - "Talon community snippets are now fully supported in Cursorless! Cursorless's experimental snippets are now deprecated, but in most cases we can help you migrate automatically. Update cursorless-talon and say 'cursorless migrate snippets'.", - ); - - wasShown = true; -} diff --git a/packages/cursorless-engine/src/actions/snippetsLegacy/snippet.ts b/packages/cursorless-engine/src/actions/snippetsLegacy/snippet.ts deleted file mode 100644 index 7989759b97..0000000000 --- a/packages/cursorless-engine/src/actions/snippetsLegacy/snippet.ts +++ /dev/null @@ -1,212 +0,0 @@ -import type { - SimpleScopeTypeType, - SnippetDefinition, -} from "@cursorless/common"; -import type { ModifierStageFactory } from "../../processTargets/ModifierStageFactory"; -import type { ModifierStateOptions } from "../../processTargets/PipelineStages.types"; -import { - Placeholder, - Text, - Variable, - type TextmateSnippet, -} from "../../snippets/vendor/vscodeSnippet/snippetParser"; -import { KnownSnippetVariableNames } from "../../snippets/vendor/vscodeSnippet/snippetVariables"; -import type { Target } from "../../typings/target.types"; - -/** - * Replaces the snippet variable with name `placeholderName` with - * TM_SELECTED_TEXT - * - * Also replaces any unknown variables with placeholders. We do this so it's - * easier to leave one of the placeholders blank. We may make it so that you can - * disable this with a setting in the future - * @param parsedSnippet The parsed textmate snippet to operate on - * @param placeholderName The variable name to replace with TM_SELECTED_TEXT - * @param substitutions A map from variable names to text values that will be - * substituted and the given variable will no longer be a placeholder in the - * final snippet - */ -export function transformSnippetVariables( - parsedSnippet: TextmateSnippet, - placeholderName?: string | null, - substitutions?: Record, -): void { - let nextPlaceholderIndex = getMaxPlaceholderIndex(parsedSnippet) + 1; - const placeholderIndexMap: Record = {}; - - parsedSnippet.walk((candidate) => { - if (candidate instanceof Variable) { - if (candidate.name === placeholderName) { - candidate.name = "TM_SELECTED_TEXT"; - } else if ( - substitutions != null && - Object.prototype.hasOwnProperty.call(substitutions, candidate.name) - ) { - candidate.parent.replace(candidate, [ - new Text(substitutions[candidate.name]), - ]); - } else if (!KnownSnippetVariableNames[candidate.name]) { - let placeholderIndex: number; - if (candidate.name in placeholderIndexMap) { - placeholderIndex = placeholderIndexMap[candidate.name]; - } else { - placeholderIndex = nextPlaceholderIndex++; - placeholderIndexMap[candidate.name] = placeholderIndex; - } - const placeholder = new Placeholder(placeholderIndex); - candidate.children.forEach((child) => placeholder.appendChild(child)); - candidate.parent.replace(candidate, [placeholder]); - } - } else if (candidate instanceof Placeholder) { - if (candidate.index.toString() === placeholderName) { - candidate.parent.replace(candidate, [new Variable("TM_SELECTED_TEXT")]); - } - } - return true; - }); -} - -/** - * Returns the highest placeholder index in the given snippet - * @param parsedSnippet The parsed textmate snippet - * @returns The highest placeholder index in the given snippet - */ -function getMaxPlaceholderIndex(parsedSnippet: TextmateSnippet): number { - let placeholderIndex = 0; - parsedSnippet.walk((candidate) => { - if (candidate instanceof Placeholder) { - placeholderIndex = Math.max(placeholderIndex, candidate.index); - } - return true; - }); - return placeholderIndex; -} - -/** - * Based on the context determined by {@link targets} (eg the file's language - * id and containing scope), finds the first snippet definition that matches the - * given context. Throws an error if different snippet definitions match for - * different targets or if matching snippet definition could not be found - * @param targets The target that defines the context to use for finding the - * right snippet definition - * @param definitions The list of snippet definitions to search - * @returns The snippet definition that matches the given context - */ -export function findMatchingSnippetDefinitionStrict( - modifierStageFactory: ModifierStageFactory, - targets: Target[], - definitions: SnippetDefinition[], -): SnippetDefinition { - const definitionIndices = targets.map((target) => - findMatchingSnippetDefinitionForSingleTarget( - modifierStageFactory, - target, - definitions, - ), - ); - - const definitionIndex = definitionIndices[0]; - - if (!definitionIndices.every((index) => index === definitionIndex)) { - throw new Error("Multiple snippet definitions match the given context"); - } - - if (definitionIndex === -1) { - throw new Error("Couldn't find matching snippet definition"); - } - - return definitions[definitionIndex]; -} - -/** - * Based on the context determined by {@link target} (eg the file's language id - * and containing scope), finds the best snippet definition that matches the - * given context. Returns -1 if no matching snippet definition could be found. - * - * We assume that the definitions are sorted in precedence order, so we just - * return the first match we find. - * - * @param modifierStageFactory For creating containing scope modifiers - * @param target The target to find a matching snippet definition for - * @param definitions The list of snippet definitions to search - * @returns The index of the best snippet definition that matches the given - * target, or -1 if no matching snippet definition could be found - */ -function findMatchingSnippetDefinitionForSingleTarget( - modifierStageFactory: ModifierStageFactory, - target: Target, - definitions: SnippetDefinition[], -): number { - const languageId = target.editor.document.languageId; - - const options: ModifierStateOptions = { - multipleTargets: false, - }; - - // We want to find the first definition that matches the given context. - // Note that we just use the first match we find because the definitions are - // guaranteed to come sorted in precedence order. - return definitions.findIndex(({ scope }) => { - if (scope == null) { - return true; - } - - const { langIds, scopeTypes, excludeDescendantScopeTypes } = scope; - - if (langIds != null && !langIds.includes(languageId)) { - return false; - } - - if (scopeTypes != null) { - const allScopeTypes = scopeTypes.concat( - excludeDescendantScopeTypes ?? [], - ); - let matchingTarget: Target | undefined = undefined; - let matchingScopeType: SimpleScopeTypeType | undefined = undefined; - for (const scopeTypeType of allScopeTypes) { - try { - let containingTarget = modifierStageFactory - .create({ - type: "containingScope", - scopeType: { type: scopeTypeType }, - }) - .run(target, options)[0]; - - if (target.contentRange.isRangeEqual(containingTarget.contentRange)) { - // Skip this scope if the target is exactly the same as the - // containing scope, otherwise wrapping won't work, because we're - // really outside the containing scope when we're wrapping - containingTarget = modifierStageFactory - .create({ - type: "containingScope", - scopeType: { type: scopeTypeType }, - ancestorIndex: 1, - }) - .run(target, options)[0]; - } - - if ( - matchingTarget == null || - matchingTarget.contentRange.contains(containingTarget.contentRange) - ) { - matchingTarget = containingTarget; - matchingScopeType = scopeTypeType; - } - } catch (_e) { - continue; - } - } - - if (matchingScopeType == null) { - return false; - } - - return ( - matchingTarget != null && - !(excludeDescendantScopeTypes ?? []).includes(matchingScopeType) - ); - } - - return true; - }); -} diff --git a/packages/cursorless-engine/src/actions/snippetsLegacy/textFormatters.ts b/packages/cursorless-engine/src/actions/snippetsLegacy/textFormatters.ts deleted file mode 100644 index d5e6dc9581..0000000000 --- a/packages/cursorless-engine/src/actions/snippetsLegacy/textFormatters.ts +++ /dev/null @@ -1,35 +0,0 @@ -type TextFormatter = (tokens: string[]) => string; - -export const textFormatters: Record = { - camelCase(tokens: string[]) { - if (tokens.length === 0) { - return ""; - } - - const [first, ...rest] = tokens; - - return first + rest.map(capitalizeToken).join(""); - }, - - snakeCase(tokens: string[]) { - return tokens.join("_"); - }, - - upperSnakeCase(tokens: string[]) { - return tokens.map((token) => token.toUpperCase()).join("_"); - }, - - pascalCase(tokens: string[]) { - return tokens.map(capitalizeToken).join(""); - }, -}; - -function capitalizeToken(token: string): string { - return token.length === 0 ? "" : token[0].toUpperCase() + token.substr(1); -} - -export type TextFormatterName = - | "camelCase" - | "pascalCase" - | "snakeCase" - | "upperSnakeCase"; diff --git a/packages/cursorless-engine/src/core/Snippets.ts b/packages/cursorless-engine/src/core/Snippets.ts index dd09026c44..f6309c84c1 100644 --- a/packages/cursorless-engine/src/core/Snippets.ts +++ b/packages/cursorless-engine/src/core/Snippets.ts @@ -1,20 +1,6 @@ -import type { Snippet, TextEditor } from "@cursorless/common"; +import type { TextEditor } from "@cursorless/common"; -/** - * Handles all cursorless snippets, including core, third-party and - * user-defined. Merges these collections and allows looking up snippets by - * name. - */ export interface Snippets { - /** - * Looks in merged collection of snippets for a snippet with key - * `snippetName`. Throws an exception if the snippet of the given name could - * not be found - * @param snippetName The name of the snippet to look up - * @returns The named snippet - */ - getSnippetStrict(snippetName: string): Snippet; - /** * Opens a new snippet file * @param snippetName The name of the snippet diff --git a/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts b/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts index b145222719..cf40f8f75f 100644 --- a/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts +++ b/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts @@ -1,11 +1,7 @@ -import type { Snippet, TextEditor } from "@cursorless/common"; +import type { TextEditor } from "@cursorless/common"; import type { Snippets } from "../core/Snippets"; export class DisabledSnippets implements Snippets { - getSnippetStrict(_snippetName: string): Snippet { - throw new Error("Snippets are not implemented."); - } - openNewSnippetFile( _snippetName: string, _directory: string, diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index 9fe4df0027..9a056ff736 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -10,10 +10,6 @@ import type { InsertionMode, Range, Selection, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - Snippet, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - SnippetVariable, TargetPlainObject, TextEditor, } from "@cursorless/common"; diff --git a/packages/cursorless-neovim/src/registerCommands.ts b/packages/cursorless-neovim/src/registerCommands.ts index e25bbc7af7..8a75c5894f 100644 --- a/packages/cursorless-neovim/src/registerCommands.ts +++ b/packages/cursorless-neovim/src/registerCommands.ts @@ -88,7 +88,6 @@ export async function registerCommands( ["cursorless.showQuickPick"]: dummyCommandHandler, ["cursorless.showDocumentation"]: dummyCommandHandler, ["cursorless.showInstallationDependencies"]: dummyCommandHandler, - ["cursorless.migrateSnippets"]: dummyCommandHandler, ["cursorless.private.logQuickActions"]: dummyCommandHandler, // Hats diff --git a/packages/cursorless-org-docs/src/docs/user/customization.md b/packages/cursorless-org-docs/src/docs/user/customization.md index ca9e1f088a..2b203edc18 100644 --- a/packages/cursorless-org-docs/src/docs/user/customization.md +++ b/packages/cursorless-org-docs/src/docs/user/customization.md @@ -176,11 +176,7 @@ Cursorless exposes a couple talon actions and captures that you can use to defin #### Snippet actions -See [snippets](./experimental/snippets.md) for more information about Cursorless snippets. - -- `user.cursorless_insert_snippet_by_name(name: str)`: Insert a snippet with the given name, eg `functionDeclaration` - `user.cursorless_insert_snippet(body: str, destination: Optional[CursorlessDestination], scope_type: Optional[Union[str, list[str]]])`: Insert a snippet with the given body defined using our snippet body syntax (see the [snippet format docs](./experimental/snippet-format.md)). The body should be a single string, which could contain newline `\n` characters, rather than a list of strings as is expected in our snippet json representation. Destination is where the snippet will be inserted. If omitted will default to current selection. An optional scope type can be provided for the target to expand to. `"snip if after air"` for example could be desired to go after the statement containing `air` instead of the token. -- `user.cursorless_wrap_with_snippet_by_name(name: str, variable_name: str, target: CursorlessTarget)`: Wrap the given target with a snippet with the given name, eg `functionDeclaration`. Note that `variable_name` should be one of the variables defined in the named snippet. Eg, if the named snippet has a variable `$foo`, you can pass in `"foo"` for `variable_name`, and `target` will be inserted into the position of `$foo` in the given named snippet. - `user.cursorless_wrap_with_snippet(body, target, variable_name, scope)`: Wrap the given target with a snippet with the given body defined using our snippet body syntax (see the [snippet format docs](./experimental/snippet-format.md)). The body should be a single string, which could contain newline `\n` characters, rather than a list of strings as is expected in our snippet json representation. Note that `variable_name` should be one of the variables defined in `body`. Eg, if `body` has a variable `$foo`, you can pass in `"foo"` for `variable_name`, and `target` will be inserted into the position of `$foo` in the given named snippet. The `scope` variable can be used to automatically expand the target to the given scope type, eg `"line"`. ### Example of combining capture and action diff --git a/packages/cursorless-org-docs/src/docs/user/experimental/snippets.md b/packages/cursorless-org-docs/src/docs/user/experimental/snippets.md deleted file mode 100644 index 850cd68927..0000000000 --- a/packages/cursorless-org-docs/src/docs/user/experimental/snippets.md +++ /dev/null @@ -1,166 +0,0 @@ -# Snippets - -![Wrapper snippet demo](images/tryWrapFine.gif) -![Link wrap](images/linkWrap.gif) - -## Using community snippets (RECOMMENDED) - -The community Talon files now support their own snippet format, which is now preferred. - -If you'd like to use these snippets for wrapping and inserting with Cursorless instead of the legacy Cursorless snippets, add the following line to your `settings.talon` file: - -```talon -tag(): user.cursorless_use_community_snippets -``` - -Note that this line will also disable any Cursorless snippets defined in your Cursorless customization CSVs. You will need to migrate your Cursorless snippets to the new community snippet format [described in community](https://github.com/talonhub/community/blob/main/core/snippets/README.md). If you'd be interested in a tool to help with this migration, please leave a comment on [cursorless-dev/cursorless#2149](https://github.com/cursorless-dev/cursorless/issues/2149), ideally with a link to your custom snippets for us to look at. - -## Cursorless experimental snippets (DEPRECATED) - -Cursorless has its own experimental snippet engine that allows you to both insert snippets and wrap targets with snippets. Cursorless ships with a few built-in snippets, but users can also use their own snippets. - -## Migrate Cursorless snippet to community - -Say `"Cursorless migrate snippets"` to convert your existing experimental Cursorless snippet JSON files (which are now deprecated) to the new community snippet format. - -## Using snippets - -### Wrapping a target with snippets - -#### Command syntax - -The command syntax is as follows: - -``` -" wrap " -``` - -#### Examples - -- `"try wrap air"`: Wrap the statement containing the marked `a` in a try-catch statement -- `"try wrap air past bat"`: Wrap the sequence of statements from the marked `a` to the marked `b` in a try-catch statement - -#### Default scope types - -Each snippet wrapper has a default scope type. When you refer to a target, by default it will expand to the given scope type. This way, for example, when you say `"try wrap air"`, it will refer to the statement containing `a` rather than just the token. - -### Built-in wrapper snippets - -| Default spoken form | Snippet | Default target scope type | -| ------------------- | --------------------------------------------- | ------------------------- | -| `"if wrap"` | If statement | Statement | -| `"else wrap"` | If-else statement; target goes in else branch | Statement | -| `"if else wrap"` | If-else statement; target goes in if branch | Statement | -| `"try wrap"` | Try-catch statement | Statement | -| `"link wrap"` | Markdown link | | -| `"funk wrap"` | Function | Statement | - -### Inserting a snippet - -The same snippet definitions that allow for wrapping targets can also be used for insertion. You can either insert a snippet at the current cursor position, or use a positional target to insert before / after / replace something. - -#### Command syntax - -The command syntax options are as follows. In its simplest form, you can just say - -``` -"snippet " -``` - -This command will insert a snippet at the current position. For example: - -- `"snippet funk"` -- `"snippet if"` - -For some snippets, you can include a phrase, that will automatically fill a particular snippet variable with the given phrase, formatted properly: - -``` -"snippet " -``` - -For example: - -- `"snippet funk hello world"`: Insert function with name `helloWorld` - -Finally, we support inserting a snippet onto, before or after a Cursorless target: - -``` -"snippet before " -"snippet after " -"snippet to " -``` - -For example: - -- `"snippet if after air"`: Insert `if` statement after the statement with a hat over the `a` - -Note that each snippet can use `insertionScopeTypes` to indicate that it will auto-expand the target. So, for example, `"snippet if after this"` will insert an `if` statement after the current statement. - -### Built-in insertion snippets - -| Default spoken form | Snippet | Default insertion scope type | Accepts optional phrase? | -| ------------------- | --------------------------------------- | ---------------------------- | ------------------------ | -| `"snippet if"` | If statement | Statement | ❌ | -| `"snippet if else"` | If-else statement | Statement | ❌ | -| `"snippet try"` | Try-catch statement | Statement | ❌ | -| `"snippet funk"` | Function; phrase becomes name | Function | ✅ | -| `"snippet link"` | Markdown link; phrase becomes link text | | ✅ | - -## Customizing spoken forms - -As usual, the spoken forms for these snippets can be [customized by csv](../customization.md). The csvs are in the files in `cursorless-settings/experimental` with `snippet` in their name. - -In addition, you can change the term `"snippet"` (for snippet insertion) using actions.csv. Keep in mind that if you change it to `"snip"`, you may want to turn off the built-in community `"snip"` commands to avoid conflicts. - -## Adding your own snippets - -To define your own snippets, proceed as follows: - -### Define snippets in vscode - -1. In your VSCode Cursorless settings (say `"cursorless settings"`), set the `cursorless.experimental.snippetsDir` setting to a directory in which you'd like to create your snippets. You can use the `${userHome}` vairable to refer to your user home directory. -2. Add snippets to the directory in files ending in `.cursorless-snippets`. See the [documentation](snippet-format.md) for the cursorless snippet format. - -### 2. Add snippet to spoken forms csvs - -Snippets can be used for wrapping or insertion or both. - -#### For wrapping - -For each snippet that you'd like to be able to use as a wrapper snippet, add a line to the `cursorless-settings/experimental/wrapper_snippets.csv` csv overrides file. The first column is the desired spoken form, and the second column is of the form `.`, where `name` is the name of the snippet (ie the key in your snippet json file), and `variable` is one of the placeholder variables in your snippet where the target should go. - -#### For insertion - -For each snippet that you'd like to be able to use for insertion, add a line to one of the following files: - -- Use `cursorless-settings/experimental/insertion_snippets.csv` if you **don't** need an optional trailing phrase (eg for `"snippet funk hello world"` to provide a function name). In this case, the first column is the spoken form, and the second column is the snippet name. -- Use `cursorless-settings/experimental/insertion_snippets_single_phrase.csv` if you want to be able to include an optional extra phrase. In this csv, the first column is the desired spoken form, and the second column is of the form `.`, where `name` is the name of the snippet (ie the key in your snippet json file), and `variable` is one of the placeholder variables in your snippet where the extra phrase should go. - -## Customizing built-in snippets - -To customize a built-in snippet, just define a custom snippet (as above), but -use the same name as the cursorless core snippet you'd like to change, and give -definitions along with scopes where you'd like your override to be active. Here -is an example: - -```json -{ - "tryCatchStatement": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript", - "typescriptreact", - "javascript", - "javascriptreact" - ] - }, - "body": ["try {", "\t$body", "} catch (err) {", "\t$exceptBody", "}"] - } - ] - } -} -``` - -The above will change the definition of the try-catch statement in typescript. diff --git a/packages/cursorless-vscode/src/VscodeSnippets.ts b/packages/cursorless-vscode/src/VscodeSnippets.ts index 3a87a3f96c..01618b4464 100644 --- a/packages/cursorless-vscode/src/VscodeSnippets.ts +++ b/packages/cursorless-vscode/src/VscodeSnippets.ts @@ -1,232 +1,10 @@ -import type { IDE, Snippet, SnippetMap, TextEditor } from "@cursorless/common"; -import { mergeStrict, showError } from "@cursorless/common"; +import type { IDE, TextEditor } from "@cursorless/common"; import { type Snippets } from "@cursorless/cursorless-engine"; -import { walkFiles } from "@cursorless/node-common"; -import { max } from "lodash-es"; -import { open, readFile, stat } from "node:fs/promises"; +import { open } from "node:fs/promises"; import { join } from "node:path"; -import { mergeSnippets } from "./snippetsLegacy/mergeSnippets"; -// DEPRECATED @ 2025-02-01 -export const CURSORLESS_SNIPPETS_SUFFIX = ".cursorless-snippets"; -const SNIPPET_DIR_REFRESH_INTERVAL_MS = 1000; - -interface DirectoryErrorMessage { - directory: string; - errorMessage: string; -} - -/** - * Handles all cursorless snippets, including core, third-party and - * user-defined. Merges these collections and allows looking up snippets by - * name. - */ export class VscodeSnippets implements Snippets { - private coreSnippets!: SnippetMap; - private userSnippets!: SnippetMap[]; - - private mergedSnippets!: SnippetMap; - - private userSnippetsDir?: string; - - /** - * The maximum modification time of any snippet in user snippets dir. - * - * This variable will be set to -1 if no user snippets have yet been read or - * if the user snippets path has changed. - * - * This variable will be set to 0 if the user has no snippets dir configured and - * we've already set userSnippets to {}. - */ - private maxSnippetMtimeMs: number = -1; - - /** - * If the user has misconfigured their snippet dir, then we keep track of it - * so that we can show them the error message if we can't find a snippet - * later, and so that we don't show them the same error message every time - * we try to poll the directory. - */ - private directoryErrorMessage: DirectoryErrorMessage | null | undefined = - null; - - constructor(private ide: IDE) { - this.updateUserSnippetsPath(); - - this.updateUserSnippetsPath(); - - this.ide.disposeOnExit( - this.ide.configuration.onDidChangeConfiguration(() => - this.updateUserSnippetsPath(), - ), - ); - - this.updateUserSnippets = this.updateUserSnippets.bind(this); - - const timer = setInterval( - this.updateUserSnippets, - SNIPPET_DIR_REFRESH_INTERVAL_MS, - ); - - this.ide.disposeOnExit( - this.ide.configuration.onDidChangeConfiguration(() => { - if (this.updateUserSnippetsPath()) { - void this.updateUserSnippets(); - } - }), - { - dispose() { - clearInterval(timer); - }, - }, - ); - } - - async init() { - const extensionPath = this.ide.assetsRoot; - const snippetsDir = join(extensionPath, "cursorless-snippets"); - const snippetFiles = await getSnippetPaths(snippetsDir); - this.coreSnippets = mergeStrict( - ...(await Promise.all( - snippetFiles.map(async (path) => - JSON.parse(await readFile(path, "utf8")), - ), - )), - ); - await this.updateUserSnippets(); - } - - /** - * Updates the userSnippetsDir field if it has change, returning a boolean - * indicating whether there was an update. If there was an update, resets the - * maxSnippetMtime to -1 to ensure snippet update. - * @returns Boolean indicating whether path has changed - */ - private updateUserSnippetsPath(): boolean { - const newUserSnippetsDir = this.ide.configuration.getOwnConfiguration( - "experimental.snippetsDir", - ); - - if (newUserSnippetsDir === this.userSnippetsDir) { - return false; - } - - // Reset mtime to -1 so that next time we'll update the snippets - this.maxSnippetMtimeMs = -1; - - this.userSnippetsDir = newUserSnippetsDir; - - return true; - } - - async updateUserSnippets() { - let snippetFiles: string[]; - try { - snippetFiles = this.userSnippetsDir - ? await getSnippetPaths(this.userSnippetsDir) - : []; - } catch (err) { - if (this.directoryErrorMessage?.directory !== this.userSnippetsDir) { - // NB: We suppress error messages once we've shown it the first time - // because we poll the directory every second and want to make sure we - // don't show the same error message repeatedly - const errorMessage = `Error with cursorless snippets dir "${ - this.userSnippetsDir - }": ${(err as Error).message}`; - - void showError(this.ide.messages, "snippetsDirError", errorMessage); - - this.directoryErrorMessage = { - directory: this.userSnippetsDir!, - errorMessage, - }; - } - - this.userSnippets = []; - this.mergeSnippets(); - - return; - } - - this.directoryErrorMessage = null; - - const maxSnippetMtime = - max( - (await Promise.all(snippetFiles.map((file) => stat(file)))).map( - (stat) => stat.mtimeMs, - ), - ) ?? 0; - - if (maxSnippetMtime <= this.maxSnippetMtimeMs) { - return; - } - - this.maxSnippetMtimeMs = maxSnippetMtime; - - this.userSnippets = await Promise.all( - snippetFiles.map(async (path) => { - try { - const content = await readFile(path, "utf8"); - - if (content.length === 0) { - // Gracefully handle an empty file - return {}; - } - - return JSON.parse(content); - } catch (err) { - void showError( - this.ide.messages, - "snippetsFileError", - `Error with cursorless snippets file "${path}": ${ - (err as Error).message - }`, - ); - - // We don't want snippets from all files to stop working if there is - // a parse error in one file, so we just effectively ignore this file - // once we've shown an error message - return {}; - } - }), - ); - - this.mergeSnippets(); - } - - /** - * Merge core, third-party, and user snippets, with precedence user > third - * party > core. - */ - private mergeSnippets() { - this.mergedSnippets = mergeSnippets( - this.coreSnippets, - {}, - this.userSnippets, - ); - } - - /** - * Looks in merged collection of snippets for a snippet with key - * `snippetName`. Throws an exception if the snippet of the given name could - * not be found - * @param snippetName The name of the snippet to look up - * @returns The named snippet - */ - getSnippetStrict(snippetName: string): Snippet { - const snippet = this.mergedSnippets[snippetName]; - - if (snippet == null) { - let errorMessage = `Couldn't find snippet ${snippetName}. `; - - if (this.directoryErrorMessage != null) { - errorMessage += `This could be due to: ${this.directoryErrorMessage.errorMessage}.`; - } - - throw Error(errorMessage); - } - - return snippet; - } + constructor(private ide: IDE) {} async openNewSnippetFile( snippetName: string, @@ -236,26 +14,6 @@ export class VscodeSnippets implements Snippets { await touch(path); return this.ide.openTextDocument(path); } - - getUserDirectoryStrict() { - const userSnippetsDir = this.ide.configuration.getOwnConfiguration( - "experimental.snippetsDir", - ); - - if (!userSnippetsDir) { - throw new Error("User snippets dir not configured."); - } - - return userSnippetsDir; - } - - getSnippetPaths(snippetsDir: string) { - return walkFiles(snippetsDir, CURSORLESS_SNIPPETS_SUFFIX); - } -} - -function getSnippetPaths(snippetsDir: string) { - return walkFiles(snippetsDir, CURSORLESS_SNIPPETS_SUFFIX); } async function touch(path: string) { diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 481a47c74d..042c54f8b0 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -97,10 +97,7 @@ export async function activate( const treeSitter = createTreeSitter(parseTreeApi); const talonSpokenForms = new FileSystemTalonSpokenForms(fileSystem); - // NOTE: do not await on snippet loading and hats initialization because we don't want to - // block extension activation const snippets = new VscodeSnippets(normalizedIde); - void snippets.init(); const treeSitterQueryProvider = new FileSystemRawTreeSitterQueryProvider( normalizedIde, @@ -193,7 +190,6 @@ export async function activate( vscodeTutorial, installationDependencies, storedTargets, - snippets, ); void new ReleaseNotes(vscodeApi, context, normalizedIde.messages).maybeShow(); diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts deleted file mode 100644 index dc6fe24da0..0000000000 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ /dev/null @@ -1,351 +0,0 @@ -import type { - SnippetMap, - SnippetVariable as SnippetVariableLegacy, -} from "@cursorless/common"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import { - serializeSnippetFile, - type SnippetFile, - type SnippetVariable, -} from "talon-snippets"; -import * as vscode from "vscode"; -import { - CURSORLESS_SNIPPETS_SUFFIX, - type VscodeSnippets, -} from "./VscodeSnippets"; - -interface Result { - migrated: Record; - migratedPartially: Record; - skipped: string[]; -} - -export interface SpokenForms { - insertion: Record; - insertionWithPhrase: Record; - wrapper: Record; -} - -export async function migrateSnippets( - snippets: VscodeSnippets, - targetDirectory: string, - spokenForms: SpokenForms, -) { - const sourceDirectory = snippets.getUserDirectoryStrict(); - const files = await snippets.getSnippetPaths(sourceDirectory); - - const spokenFormsInverted: SpokenForms = { - insertion: swapKeyValue(spokenForms.insertion), - insertionWithPhrase: swapKeyValue(spokenForms.insertionWithPhrase), - wrapper: swapKeyValue(spokenForms.wrapper), - }; - - const result: Result = { - migrated: {}, - migratedPartially: {}, - skipped: [], - }; - - for (const file of files) { - await migrateFile(result, spokenFormsInverted, targetDirectory, file); - } - - await openResultDocument(result, sourceDirectory, targetDirectory); -} - -async function migrateFile( - result: Result, - spokenForms: SpokenForms, - targetDirectory: string, - filePath: string, -) { - const fileName = path.basename(filePath, CURSORLESS_SNIPPETS_SUFFIX); - const legacySnippetFile = await readLegacyFile(filePath); - - const [communitySnippetFile, hasSkippedSnippet] = migrateLegacySnippet( - spokenForms, - legacySnippetFile, - ); - - if (communitySnippetFile.snippets.length === 0) { - result.skipped.push(fileName); - return; - } - - const destinationName = await saveSnippetFile( - communitySnippetFile, - targetDirectory, - fileName, - ); - - if (hasSkippedSnippet) { - result.migratedPartially[fileName] = destinationName; - } else { - result.migrated[fileName] = destinationName; - } -} - -export function migrateLegacySnippet( - spokenForms: SpokenForms, - legacySnippetFile: SnippetMap, -): [SnippetFile, boolean] { - const communitySnippetFile: SnippetFile = { snippets: [] }; - const snippetNames = Object.keys(legacySnippetFile); - const useHeader = snippetNames.length === 1; - let hasSkippedSnippet = false; - - for (const snippetName of snippetNames) { - const snippet = legacySnippetFile[snippetName]; - let phrase = spokenForms.insertion[snippetName]; - - if (!phrase) { - const key = Object.keys(spokenForms.insertionWithPhrase).find((key) => - key.startsWith(`${snippetName}.`), - ); - if (key) { - phrase = spokenForms.insertionWithPhrase[key]; - } - } - - const phrases = phrase ? [phrase] : undefined; - - if (useHeader) { - communitySnippetFile.header = { - name: snippetName, - description: snippet.description, - phrases: phrases, - variables: parseVariables({ - spokenForms, - snippetName, - snippetVariables: snippet.variables, - defVariables: undefined, - addMissingPhrases: true, - addMissingInsertionFormatters: false, - }), - insertionScopes: snippet.insertionScopeTypes, - }; - } - - for (const def of snippet.definitions) { - if ( - def.scope?.scopeTypes?.length || - def.scope?.excludeDescendantScopeTypes?.length - ) { - hasSkippedSnippet = true; - continue; - } - communitySnippetFile.snippets.push({ - name: useHeader ? undefined : snippetName, - description: useHeader ? undefined : snippet.description, - phrases: useHeader ? undefined : phrases, - insertionScopes: useHeader ? undefined : snippet.insertionScopeTypes, - languages: def.scope?.langIds, - variables: parseVariables({ - spokenForms, - snippetName, - snippetVariables: useHeader ? undefined : snippet.variables, - defVariables: def.variables, - addMissingPhrases: !useHeader, - addMissingInsertionFormatters: true, - }), - // SKIP: def.scope?.scopeTypes - // SKIP: def.scope?.excludeDescendantScopeTypes - body: def.body.map((line) => line.replaceAll("\t", " ")), - }); - } - } - - return [communitySnippetFile, hasSkippedSnippet]; -} - -interface ParseVariablesOpts { - spokenForms: SpokenForms; - snippetName: string; - snippetVariables: Record | undefined; - defVariables: Record | undefined; - addMissingPhrases: boolean; - addMissingInsertionFormatters: boolean; -} - -function parseVariables({ - spokenForms, - snippetName, - snippetVariables, - defVariables, - addMissingPhrases, - addMissingInsertionFormatters, -}: ParseVariablesOpts): SnippetVariable[] { - const map: Record = {}; - - const add = (name: string, variable: SnippetVariableLegacy) => { - if (!map[name]) { - const phrase = spokenForms.wrapper[`${snippetName}.${name}`]; - map[name] = { - name, - wrapperPhrases: phrase ? [phrase] : undefined, - }; - } - if (variable.wrapperScopeType) { - map[name].wrapperScope = variable.wrapperScopeType; - } - if (variable.formatter) { - map[name].insertionFormatters = getFormatter(variable.formatter); - } - // SKIP: variable.description - }; - - Object.entries(snippetVariables ?? {}).forEach(([name, variable]) => - add(name, variable), - ); - Object.entries(defVariables ?? {}).forEach(([name, variable]) => - add(name, variable), - ); - - if (addMissingPhrases) { - for (const key in spokenForms.wrapper) { - const [snipName, variableName] = key.split("."); - if (snipName === snippetName && !map[variableName]) { - map[variableName] = { - name: variableName, - wrapperPhrases: [spokenForms.wrapper[key]], - }; - } - } - } - - if (addMissingInsertionFormatters) { - for (const key in spokenForms.insertionWithPhrase) { - const [snipName, variableName] = key.split("."); - if (snipName === snippetName) { - if (!map[variableName]) { - map[variableName] = { name: variableName }; - } - if (!map[variableName].insertionFormatters) { - map[variableName].insertionFormatters = ["NOOP"]; - } - } - } - } - - return Object.values(map); -} - -// Convert Cursorless formatters to Talon community formatters -function getFormatter(formatter: string): string[] { - switch (formatter) { - case "camelCase": - return ["PRIVATE_CAMEL_CASE"]; - case "pascalCase": - return ["PUBLIC_CAMEL_CASE"]; - case "snakeCase": - return ["SNAKE_CASE"]; - case "upperSnakeCase": - return ["ALL_CAPS", "SNAKE_CASE"]; - default: - return [formatter]; - } -} - -async function saveSnippetFile( - communitySnippetFile: SnippetFile, - targetDirectory: string, - fileName: string, -) { - let destinationName: string; - - try { - destinationName = `${fileName}.snippet`; - const destinationPath = path.join(targetDirectory, destinationName); - await writeCommunityFile(communitySnippetFile, destinationPath, "wx"); - } catch (error: any) { - if (error.code === "EEXIST") { - destinationName = `${fileName}_CONFLICT.snippet`; - const destinationPath = path.join(targetDirectory, destinationName); - await writeCommunityFile(communitySnippetFile, destinationPath, "w"); - } else { - throw error; - } - } - - return destinationName; -} - -async function openResultDocument( - result: Result, - sourceDirectory: string, - targetDirectory: string, -) { - const migratedKeys = Object.keys(result.migrated).sort(); - const migratedPartiallyKeys = Object.keys(result.migratedPartially).sort(); - const skipMessage = - "(Snippets containing `scopeTypes` and/or `excludeDescendantScopeTypes` attributes are not supported by community snippets.)"; - - const content: string[] = [ - `# Snippets migrated from Cursorless`, - "", - `From: ${sourceDirectory}`, - `To: ${targetDirectory}`, - "", - ]; - - if (result.skipped.length > 0) { - content.push( - `## Skipped ${result.skipped.length} snippet files:`, - ...result.skipped.map((key) => `- ${key}`), - skipMessage, - "", - ); - } - - if (migratedPartiallyKeys.length > 0) { - content.push( - `## Migrated ${migratedPartiallyKeys.length} snippet files partially:`, - ...migratedPartiallyKeys.map( - (key) => `- ${key} -> ${result.migratedPartially[key]}`, - ), - skipMessage, - "", - ); - } - - content.push( - `## Migrated ${migratedKeys.length} snippet files:`, - ...migratedKeys.map((key) => `- ${key} -> ${result.migrated[key]}`), - "", - ); - - const textDocument = await vscode.workspace.openTextDocument({ - content: content.join("\n"), - language: "markdown", - }); - await vscode.window.showTextDocument(textDocument); -} - -async function readLegacyFile(filePath: string): Promise { - const content = await fs.readFile(filePath, "utf8"); - if (content.length === 0) { - return {}; - } - return JSON.parse(content); -} - -async function writeCommunityFile( - snippetFile: SnippetFile, - filePath: string, - flags: string, -) { - const snippetText = serializeSnippetFile(snippetFile); - const file = await fs.open(filePath, flags); - try { - await file.write(snippetText); - } finally { - await file.close(); - } -} - -function swapKeyValue(obj: Record): Record { - return Object.fromEntries( - Object.entries(obj).map(([key, value]) => [value, key]), - ); -} diff --git a/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts b/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts deleted file mode 100644 index ea32077bbb..0000000000 --- a/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -import type { SnippetMap } from "@cursorless/common"; -import assert from "node:assert"; -import { serializeSnippetFile } from "talon-snippets"; -import { migrateLegacySnippet, type SpokenForms } from "./migrateSnippets"; - -interface Fixture { - name: string; - input: SnippetMap; - output: string; -} - -const spokenForms: SpokenForms = { - insertion: { - mySnippet: "snip", - myPythonSnippet: "snip py", - }, - insertionWithPhrase: { - "myPhraseSnippet.foo": "phrase", - }, - wrapper: { - "myWrapperSnippet.foo": "foo", - }, -}; - -const fixtures: Fixture[] = [ - { - name: "Empty map", - input: {}, - output: "", - }, - - { - name: "Empty definitions", - input: { - mySnippet: { - definitions: [], - }, - }, - output: `\ -name: mySnippet -phrase: snip ---- -`, - }, - - { - name: "Basic", - input: { - mySnippet: { - description: "Example description", - definitions: [ - { - scope: { langIds: ["plaintext"] }, - body: ["Hello, $0, world!"], - }, - ], - }, - }, - output: `\ -name: mySnippet -description: Example description -phrase: snip ---- - -language: plaintext -- -Hello, $0, world! ---- -`, - }, - - { - name: "Insertion phrase", - input: { - myPhraseSnippet: { - description: "Example description", - definitions: [ - { - scope: { langIds: ["plaintext"] }, - body: ["Hello, $foo, world!"], - variables: { - foo: { formatter: "snakeCase" }, - }, - }, - ], - }, - }, - output: `\ -name: myPhraseSnippet -description: Example description -phrase: phrase ---- - -language: plaintext - -$foo.insertionFormatter: SNAKE_CASE -- -Hello, $foo, world! ---- -`, - }, - - { - name: "Insertion phrase noop", - input: { - myPhraseSnippet: { - description: "Example description", - definitions: [ - { - scope: { langIds: ["plaintext"] }, - body: ["Hello, $foo, world!"], - }, - ], - }, - }, - output: `\ -name: myPhraseSnippet -description: Example description -phrase: phrase ---- - -language: plaintext - -$foo.insertionFormatter: NOOP -- -Hello, $foo, world! ---- -`, - }, - - { - name: "Wrapper phrase", - input: { - myWrapperSnippet: { - definitions: [ - { - scope: { langIds: ["plaintext"] }, - body: ["Hello, $foo, world!"], - }, - ], - }, - }, - output: `\ -name: myWrapperSnippet - -$foo.wrapperPhrase: foo ---- - -language: plaintext -- -Hello, $foo, world! ---- -`, - }, - - { - name: "Multiple definitions", - input: { - mySnippet: { - definitions: [ - { - scope: { langIds: ["plaintext"] }, - body: ["Hello, $0 plain world!"], - }, - { - scope: { langIds: ["python"] }, - body: ["Hello, $0 python world!"], - }, - ], - }, - }, - output: `\ -name: mySnippet -phrase: snip ---- - -language: plaintext -- -Hello, $0 plain world! ---- - -language: python -- -Hello, $0 python world! ---- -`, - }, - - { - name: "Multiple snippets", - input: { - mySnippet: { - definitions: [ - { - scope: { langIds: ["plaintext"] }, - body: ["Hello, $0 plain world!"], - }, - ], - }, - myPythonSnippet: { - definitions: [ - { - scope: { langIds: ["python"] }, - body: ["Hello, $0 python world!"], - }, - ], - }, - }, - output: `\ -name: mySnippet -language: plaintext -phrase: snip -- -Hello, $0 plain world! ---- - -name: myPythonSnippet -language: python -phrase: snip py -- -Hello, $0 python world! ---- -`, - }, -]; - -suite("Migrate snippets", async function () { - fixtures.forEach((fixture) => { - test(fixture.name, () => runTest(fixture.input, fixture.output)); - }); -}); - -function runTest(input: SnippetMap, expectedOutput: string) { - const [snippetFile] = migrateLegacySnippet(spokenForms, input); - const actualOutput = serializeSnippetFile(snippetFile); - - assert.equal(actualOutput, expectedOutput); -} diff --git a/packages/cursorless-vscode/src/registerCommands.ts b/packages/cursorless-vscode/src/registerCommands.ts index 4f0c8d8e03..ba436f32a5 100644 --- a/packages/cursorless-vscode/src/registerCommands.ts +++ b/packages/cursorless-vscode/src/registerCommands.ts @@ -23,7 +23,6 @@ import type { ScopeVisualizer, VisualizationType, } from "./ScopeVisualizerCommandApi"; -import type { VscodeSnippets } from "./VscodeSnippets"; import type { VscodeTutorial } from "./VscodeTutorial"; import { showDocumentation, @@ -34,7 +33,6 @@ import type { VscodeIDE } from "./ide/vscode/VscodeIDE"; import type { VscodeHats } from "./ide/vscode/hats/VscodeHats"; import type { KeyboardCommands } from "./keyboard/KeyboardCommands"; import { logQuickActions } from "./logQuickActions"; -import { migrateSnippets } from "./migrateSnippets"; export function registerCommands( extensionContext: vscode.ExtensionContext, @@ -49,7 +47,6 @@ export function registerCommands( tutorial: VscodeTutorial, installationDependencies: InstallationDependencies, storedTargets: StoredTargetMap, - snippets: VscodeSnippets, ): void { const runCommandWrapper = async (run: () => Promise) => { try { @@ -97,8 +94,6 @@ export function registerCommands( ["cursorless.showDocumentation"]: showDocumentation, ["cursorless.showInstallationDependencies"]: installationDependencies.show, - ["cursorless.migrateSnippets"]: migrateSnippets.bind(null, snippets), - ["cursorless.private.logQuickActions"]: logQuickActions, // Hats diff --git a/packages/cursorless-vscode/src/snippetsLegacy/compareSnippetDefinitions.ts b/packages/cursorless-vscode/src/snippetsLegacy/compareSnippetDefinitions.ts deleted file mode 100644 index b10f5e8c56..0000000000 --- a/packages/cursorless-vscode/src/snippetsLegacy/compareSnippetDefinitions.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { - SimpleScopeTypeType, - SnippetDefinition, - SnippetScope, -} from "@cursorless/common"; -import type { SnippetOrigin } from "./mergeSnippets"; - -/** - * Compares two snippet definitions by how specific their scope, breaking - * ties by origin. - * @param a One of the snippet definitions to compare - * @param b The other snippet definition to compare - * @returns A negative number if a should come before b, a positive number if b - */ -export function compareSnippetDefinitions( - a: SnippetDefinitionWithOrigin, - b: SnippetDefinitionWithOrigin, -): number { - const scopeComparision = compareSnippetScopes( - a.definition.scope, - b.definition.scope, - ); - - // Prefer the more specific snippet definition, no matter the origin - if (scopeComparision !== 0) { - return scopeComparision; - } - - // If the scopes are the same, prefer the snippet from the higher priority - // origin - return a.origin - b.origin; -} - -function compareSnippetScopes( - a: SnippetScope | undefined, - b: SnippetScope | undefined, -): number { - if (a == null && b == null) { - return 0; - } - - // Prefer the snippet that has a scope at all - if (a == null) { - return -1; - } - - if (b == null) { - return 1; - } - - // Prefer the snippet that is language-specific, regardless of scope type - if (a.langIds == null && b.langIds != null) { - return -1; - } - - if (b.langIds == null && a.langIds != null) { - return 1; - } - - // If both snippets are language-specific, prefer the snippet that specifies - // scope types. Note that this holds even if one snippet specifies more - // languages than the other. The motivating use case is if you have a snippet - // for functions in js and ts, and a snippet for methods in js and ts. If you - // override the function snippet for ts, you still want the method snippet to - // be used for ts methods. - const scopeTypesComparision = compareScopeTypes(a.scopeTypes, b.scopeTypes); - - if (scopeTypesComparision !== 0) { - return scopeTypesComparision; - } - - // If snippets both have scope types or both don't have scope types, prefer - // the snippet that specifies fewer languages - return a.langIds == null ? 0 : b.langIds!.length - a.langIds.length; -} - -function compareScopeTypes( - a: SimpleScopeTypeType[] | undefined, - b: SimpleScopeTypeType[] | undefined, -): number { - if (a == null && b != null) { - return -1; - } - - if (b == null && a != null) { - return 1; - } - - return 0; -} - -interface SnippetDefinitionWithOrigin { - origin: SnippetOrigin; - definition: SnippetDefinition; -} diff --git a/packages/cursorless-vscode/src/snippetsLegacy/mergeSnippets.ts b/packages/cursorless-vscode/src/snippetsLegacy/mergeSnippets.ts deleted file mode 100644 index e00bdb088e..0000000000 --- a/packages/cursorless-vscode/src/snippetsLegacy/mergeSnippets.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Snippet, SnippetMap } from "@cursorless/common"; -import { cloneDeep, groupBy, mapValues, merge } from "lodash-es"; -import { compareSnippetDefinitions } from "./compareSnippetDefinitions"; - -export function mergeSnippets( - coreSnippets: SnippetMap, - thirdPartySnippets: Record, - userSnippets: SnippetMap[], -): SnippetMap { - const mergedSnippets: SnippetMap = {}; - - // We make a merged map where we map every key to an array of all snippets - // with that key, whether they are core, third-party, or user snippets. - const mergedMap = mapValues( - groupBy( - [ - ...prepareSnippetsFromOrigin(SnippetOrigin.core, coreSnippets), - ...prepareSnippetsFromOrigin( - SnippetOrigin.thirdParty, - ...Object.values(thirdPartySnippets), - ), - ...prepareSnippetsFromOrigin(SnippetOrigin.user, ...userSnippets), - ], - ([key]) => key, - ), - (entries) => entries.map(([, value]) => value), - ); - - Object.entries(mergedMap).forEach(([key, snippets]) => { - const mergedSnippet: Snippet = merge( - {}, - // We sort the snippets by origin as (core, third-party, user) so that - // when we merge them, the user snippets will override the third-party - // snippets, which will override the core snippets. - ...snippets - .sort((a, b) => a.origin - b.origin) - .map(({ snippet }) => snippet), - ); - - // We sort the definitions by decreasing precedence, so that earlier - // definitions will be chosen before later definitions when we're choosing a - // definition for a given target context. - mergedSnippet.definitions = snippets - .flatMap(({ origin, snippet }) => - snippet.definitions.map((definition) => ({ origin, definition })), - ) - .sort((a, b) => -compareSnippetDefinitions(a, b)) - .map(({ definition }) => definition); - - mergedSnippets[key] = mergedSnippet; - }); - - return mergedSnippets; -} - -/** - * Prepares the given snippet maps for merging by adding the given origin to - * each snippet. - * @param origin The origin of the snippets - * @param snippetMaps The snippet maps from the given origin - * @returns An array of entries of the form [key, {origin, snippet}] - */ -function prepareSnippetsFromOrigin( - origin: SnippetOrigin, - ...snippetMaps: SnippetMap[] -) { - return snippetMaps - .map((snippetMap) => - mapValues(cloneDeep(snippetMap), (snippet) => ({ - origin, - snippet, - })), - ) - .flatMap((snippetMap) => Object.entries(snippetMap)); -} - -export enum SnippetOrigin { - core = 0, - thirdParty = 1, - user = 2, -} diff --git a/packages/neovim-common/src/getExtensionApi.ts b/packages/neovim-common/src/getExtensionApi.ts index 3349fc007f..4602f2ed91 100644 --- a/packages/neovim-common/src/getExtensionApi.ts +++ b/packages/neovim-common/src/getExtensionApi.ts @@ -1,17 +1,8 @@ -import type { SnippetMap } from "@cursorless/common"; -//import * as vscode from "vscode"; import { getNeovimRegistry } from "@cursorless/neovim-registry"; import type { NeovimTestHelpers } from "./TestHelpers"; export interface CursorlessApi { testHelpers: NeovimTestHelpers | undefined; - - experimental: { - registerThirdPartySnippets: ( - extensionId: string, - snippets: SnippetMap, - ) => void; - }; } // See packages\cursorless-neovim\src\extension.ts:createTreeSitter() for neovim From 94afaa8c144695073bb1cb6a95e51464240f2dc9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 18:37:09 +0100 Subject: [PATCH 02/11] More clean up --- packages/common/src/errors.ts | 4 +- .../src/ide/normalized/NormalizedIDE.ts | 2 - .../common/src/ide/types/Configuration.ts | 4 +- .../src/types/command/ActionDescriptor.ts | 14 +- .../cursorless-engine/src/CommandHistory.ts | 2 - .../src/actions/InsertSnippet.ts | 4 - .../src/actions/WrapWithSnippet.ts | 10 +- .../src/core/getPreferredSnippet.test.ts | 16 +- .../src/core/getPreferredSnippet.ts | 15 +- .../src/typings/target.types.ts | 8 +- .../src/ide/TalonJsConfiguration.ts | 1 - packages/cursorless-neovim/src/extension.ts | 1 - packages/cursorless-vscode/src/extension.ts | 1 - .../src/ide/vscode/VscodeConfiguration.ts | 5 +- schemas/cursorless-snippets.json | 149 ------------------ 15 files changed, 26 insertions(+), 210 deletions(-) delete mode 100644 schemas/cursorless-snippets.json diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts index 7941b6c92e..019930e5c5 100644 --- a/packages/common/src/errors.ts +++ b/packages/common/src/errors.ts @@ -43,7 +43,9 @@ export class NoContainingScopeError extends Error { export class NamedSnippetsDeprecationError extends Error { constructor() { - super("Named snippets are deprecated and not supported anymore"); + super( + "Cursorless snippets are deprecated and have been removed. Please use community snippets.", + ); this.name = "NamedSnippetsDeprecationError"; } } diff --git a/packages/common/src/ide/normalized/NormalizedIDE.ts b/packages/common/src/ide/normalized/NormalizedIDE.ts index 0813e7e5a0..08e9107eac 100644 --- a/packages/common/src/ide/normalized/NormalizedIDE.ts +++ b/packages/common/src/ide/normalized/NormalizedIDE.ts @@ -16,7 +16,6 @@ export class NormalizedIDE extends PassthroughIDEBase { original: IDE, public fakeIde: FakeIDE, private isSilent: boolean, - private cursorlessSnippetsDir?: string, ) { super(original); @@ -46,7 +45,6 @@ export class NormalizedIDE extends PassthroughIDEBase { hatStability: this.configuration.getOwnConfiguration( "experimental.hatStability", ), - snippetsDir: this.cursorlessSnippetsDir, keyboardTargetFollowsSelection: false, }); } diff --git a/packages/common/src/ide/types/Configuration.ts b/packages/common/src/ide/types/Configuration.ts index 10c0ad8c47..dd9cf32d69 100644 --- a/packages/common/src/ide/types/Configuration.ts +++ b/packages/common/src/ide/types/Configuration.ts @@ -7,7 +7,6 @@ export type CursorlessConfiguration = { tokenHatSplittingMode: TokenHatSplittingMode; wordSeparators: string[]; experimental: { - snippetsDir: string | undefined; hatStability: HatStability; keyboardTargetFollowsSelection: boolean; }; @@ -28,7 +27,6 @@ export const CONFIGURATION_DEFAULTS: CursorlessConfiguration = { wordSeparators: ["_"], decorationDebounceDelayMs: 50, experimental: { - snippetsDir: undefined, hatStability: HatStability.balanced, keyboardTargetFollowsSelection: false, }, @@ -40,7 +38,7 @@ export interface Configuration { /** * Returns a Cursorless configuration value. Dots are accepted in * {@link path}, and are interpreted as child access, eg - * `experimental.snippetsDir`. + * `experimental.hatStability`. * * @param path A configuration key or path. Dots are interpreted as child * access diff --git a/packages/common/src/types/command/ActionDescriptor.ts b/packages/common/src/types/command/ActionDescriptor.ts index 868aa7a4db..5f17322672 100644 --- a/packages/common/src/types/command/ActionDescriptor.ts +++ b/packages/common/src/types/command/ActionDescriptor.ts @@ -145,10 +145,8 @@ export interface GenerateSnippetActionDescriptor { target: PartialTargetDescriptor; } -export interface NamedInsertSnippetArg { +export interface DeprecatedNamedSnippetArg { type: "named"; - name: string; - substitutions?: Record; } export interface CustomInsertSnippetArg { @@ -167,7 +165,7 @@ export interface ListInsertSnippetArg { } export type InsertSnippetArg = - | NamedInsertSnippetArg + | DeprecatedNamedSnippetArg | CustomInsertSnippetArg | ListInsertSnippetArg; @@ -177,12 +175,6 @@ export interface InsertSnippetActionDescriptor { destination: DestinationDescriptor; } -export interface NamedWrapWithSnippetArg { - type: "named"; - name: string; - variableName: string; -} - export interface CustomWrapWithSnippetArg { type: "custom"; body: string; @@ -198,7 +190,7 @@ export interface ListWrapWithSnippetArg { } export type WrapWithSnippetArg = - | NamedWrapWithSnippetArg + | DeprecatedNamedSnippetArg | CustomWrapWithSnippetArg | ListWrapWithSnippetArg; diff --git a/packages/cursorless-engine/src/CommandHistory.ts b/packages/cursorless-engine/src/CommandHistory.ts index 79fea9ed7d..4b5ff72af8 100644 --- a/packages/cursorless-engine/src/CommandHistory.ts +++ b/packages/cursorless-engine/src/CommandHistory.ts @@ -122,8 +122,6 @@ function sanitizeActionInPlace(action: ActionDescriptor): void { snippet.body = ""; delete snippet.substitutions; } - } else { - delete action.snippetDescription.substitutions; } break; diff --git a/packages/cursorless-engine/src/actions/InsertSnippet.ts b/packages/cursorless-engine/src/actions/InsertSnippet.ts index f4756c0828..1d25c1dd6a 100644 --- a/packages/cursorless-engine/src/actions/InsertSnippet.ts +++ b/packages/cursorless-engine/src/actions/InsertSnippet.ts @@ -32,10 +32,6 @@ export default class InsertSnippet { destinations: Destination[], snippetDescription: InsertSnippetArg, ): ModifierStage[] { - if (snippetDescription.type === "named") { - throw new NamedSnippetsDeprecationError(); - } - const editor = ensureSingleEditor(destinations); const snippet = getPreferredSnippet( snippetDescription, diff --git a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts index bf847d9e58..c546de61ee 100644 --- a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts +++ b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts @@ -1,5 +1,5 @@ import type { WrapWithSnippetArg } from "@cursorless/common"; -import { FlashStyle, NamedSnippetsDeprecationError } from "@cursorless/common"; +import { FlashStyle } from "@cursorless/common"; import { getPreferredSnippet } from "../core/getPreferredSnippet"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; @@ -27,10 +27,6 @@ export default class WrapWithSnippet { targets: Target[], snippetDescription: WrapWithSnippetArg, ): ModifierStage[] { - if (snippetDescription.type === "named") { - throw new NamedSnippetsDeprecationError(); - } - const editor = ensureSingleEditor(targets); const snippet = getPreferredSnippet( snippetDescription, @@ -56,10 +52,6 @@ export default class WrapWithSnippet { targets: Target[], snippetDescription: WrapWithSnippetArg, ): Promise { - if (snippetDescription.type === "named") { - throw new NamedSnippetsDeprecationError(); - } - const editor = ide().getEditableTextEditor(ensureSingleEditor(targets)); const snippet = getPreferredSnippet( snippetDescription, diff --git a/packages/cursorless-engine/src/core/getPreferredSnippet.test.ts b/packages/cursorless-engine/src/core/getPreferredSnippet.test.ts index ccd6d83684..84bcc9eae6 100644 --- a/packages/cursorless-engine/src/core/getPreferredSnippet.test.ts +++ b/packages/cursorless-engine/src/core/getPreferredSnippet.test.ts @@ -1,17 +1,15 @@ import type { CustomInsertSnippetArg, CustomWrapWithSnippetArg, + DeprecatedNamedSnippetArg, ListInsertSnippetArg, ListWrapWithSnippetArg, - NamedInsertSnippetArg, - NamedWrapWithSnippetArg, } from "@cursorless/common"; import assert from "node:assert"; import { getPreferredSnippet } from "./getPreferredSnippet"; -const insertNamed: NamedInsertSnippetArg = { +const deprecatedNamed: DeprecatedNamedSnippetArg = { type: "named", - name: "named snippet", }; const insertSingleLanguage: CustomInsertSnippetArg = { @@ -37,12 +35,6 @@ const insertionSnippets = [ insertSingleLanguage, ]; -const wrapNamed: NamedWrapWithSnippetArg = { - type: "named", - name: "named wrap snippet", - variableName: "var", -}; - const wrapSingleLanguage: CustomWrapWithSnippetArg = { type: "custom", body: "custom body", @@ -68,7 +60,7 @@ const wrapSnippets = [wrapGlobal, wrapMultiLanguage, wrapSingleLanguage]; suite("getPreferredSnippet", () => { test("Insert named", () => { assert.throws(() => { - getPreferredSnippet(insertNamed, "a"); + getPreferredSnippet(deprecatedNamed, "a"); }); }); @@ -122,7 +114,7 @@ suite("getPreferredSnippet", () => { test("Wrap named", () => { assert.throws(() => { - getPreferredSnippet(wrapNamed, "a"); + getPreferredSnippet(deprecatedNamed, "a"); }); }); diff --git a/packages/cursorless-engine/src/core/getPreferredSnippet.ts b/packages/cursorless-engine/src/core/getPreferredSnippet.ts index 5dbc092de4..0e21c14dcb 100644 --- a/packages/cursorless-engine/src/core/getPreferredSnippet.ts +++ b/packages/cursorless-engine/src/core/getPreferredSnippet.ts @@ -1,8 +1,9 @@ -import type { - CustomInsertSnippetArg, - CustomWrapWithSnippetArg, - InsertSnippetArg, - WrapWithSnippetArg, +import { + NamedSnippetsDeprecationError, + type CustomInsertSnippetArg, + type CustomWrapWithSnippetArg, + type InsertSnippetArg, + type WrapWithSnippetArg, } from "@cursorless/common"; export function getPreferredSnippet( @@ -20,9 +21,7 @@ export function getPreferredSnippet( languageId: string, ) { if (snippetDescription.type === "named") { - throw new Error( - "Cursorless snippets are deprecated. Please use community snippets. Update to latest cursorless-talon and say 'cursorless migrate snippets'.", - ); + throw new NamedSnippetsDeprecationError(); } if (snippetDescription.type === "custom") { return snippetDescription; diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index 9a056ff736..ffdceb4b0e 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -10,6 +10,10 @@ import type { InsertionMode, Range, Selection, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + CustomInsertSnippetArg, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + CustomWrapWithSnippetArg, TargetPlainObject, TextEditor, } from "@cursorless/common"; @@ -71,9 +75,9 @@ export interface Target { * - To expand to `"token"` for `"leading"` and `"trailing"` * - To expand to nearest containing pair for `"inside"`, `"bounds"`, and * `"rewrap"` - * - To expand to {@link SnippetVariable.wrapperScopeType} for snippet + * - To expand to {@link CustomWrapWithSnippetArg.scopeType} for snippet * wrapping - * - To expand to {@link Snippet.insertionScopeTypes} for snippet insertion + * - To expand to {@link CustomInsertSnippetArg.scopeType} for snippet insertion * * For example, when the user says `"pour air"`, the * {@link DecoratedSymbolStage} will return an {@link UntypedTarget}, which diff --git a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsConfiguration.ts b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsConfiguration.ts index 91d4b68654..47f530068a 100644 --- a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsConfiguration.ts +++ b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsConfiguration.ts @@ -19,7 +19,6 @@ const CONFIGURATION_DEFAULTS: CursorlessConfiguration = { wordSeparators: ["_"], decorationDebounceDelayMs: 50, experimental: { - snippetsDir: undefined, hatStability: HatStability.balanced, keyboardTargetFollowsSelection: false, }, diff --git a/packages/cursorless-neovim/src/extension.ts b/packages/cursorless-neovim/src/extension.ts index 9d557b68e6..2bfb8e2c91 100644 --- a/packages/cursorless-neovim/src/extension.ts +++ b/packages/cursorless-neovim/src/extension.ts @@ -30,7 +30,6 @@ export async function activate(plugin: NvimPlugin) { neovimIDE, new FakeIDE(), neovimIDE.runMode === "test", - undefined, ); const fakeCommandServerApi = new FakeCommandServerApi(); diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 042c54f8b0..0644065857 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -85,7 +85,6 @@ export async function activate( vscodeIDE, new FakeIDE(), vscodeIDE.runMode === "test", - getFixturePath("cursorless-snippets"), ); const fakeCommandServerApi = new FakeCommandServerApi(); diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeConfiguration.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeConfiguration.ts index 6640df042f..d9c571d2aa 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeConfiguration.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeConfiguration.ts @@ -20,9 +20,6 @@ const translators: TranslatorMap = { ["experimental.hatStability"]: (value: string) => { return HatStability[value as keyof typeof HatStability]; }, - ["experimental.snippetsDir"]: (value?: string) => { - return value != null ? evaluateStringVariables(value) : undefined; - }, }; export default class VscodeConfiguration implements Configuration { @@ -56,7 +53,7 @@ export default class VscodeConfiguration implements Configuration { * * We currently only support `${userHome}`. * - * @param path The path to the configuration value, eg `cursorless.snippetsDir` + * @param path The path to the configuration value, eg `cursorless.experimental.hatStability` * @returns The configuration value, with variables expanded, or undefined if * the value is not set */ diff --git a/schemas/cursorless-snippets.json b/schemas/cursorless-snippets.json deleted file mode 100644 index 2782ea5444..0000000000 --- a/schemas/cursorless-snippets.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Snippets for use in cursorless", - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "definitions": { - "type": "array", - "descriptions": "List of possible definitions for this snippet", - "items": { - "type": "object", - "properties": { - "scope": { - "type": "object", - "description": "Scopes where this snippet is active", - "properties": { - "langIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "VSCode language ids where this snippet definition should be active" - }, - "scopeTypes": { - "type": "array", - "items": { - "$ref": "#/$defs/scopeType" - }, - "description": "Cursorless scopes in which this snippet is active. Allows, for example, to have different snippets to define a function if you're in a class or at global scope." - }, - "excludeDescendantScopeTypes": { - "type": "array", - "items": { - "$ref": "#/$defs/scopeType" - }, - "description": "Exclude regions of scopeTypes that are descendants of these scope types. For example, if you have a snippet that should be active in a class but not in a function within the class, you can specify `scopeTypes: [\"class\"], excludeDescendantScopeTypes: [\"namedFunction\"]`" - } - }, - "additionalProperties": false - }, - "body": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Inline snippet text using VSCode snippet syntax; entries joined by newline. Named variables of the form `$foo` can be used as placeholders" - }, - "variables": { - "$ref": "#/$defs/variables", - "description": "Scope-specific overrides for the variables defined in the snippet" - } - }, - "additionalProperties": false, - "required": ["body"] - } - }, - "variables": { - "$ref": "#/$defs/variables", - "description": "For each named variable in the snippet, provides extra information about the variable." - }, - "description": { - "type": "string", - "description": "Description of the snippet" - }, - "insertionScopeTypes": { - "type": "array", - "items": { - "$ref": "#/$defs/scopeType" - }, - "description": "Try to expand target to this scope type when inserting this snippet before/after a target without scope type specified. If multiple scope types are specified try them each in order until one of them matches." - } - }, - "additionalProperties": false - }, - "$defs": { - "scopeType": { - "type": "string", - "enum": [ - "argumentOrParameter", - "anonymousFunction", - "attribute", - "branch", - "class", - "className", - "collectionItem", - "collectionKey", - "comment", - "functionCall", - "functionName", - "ifStatement", - "list", - "map", - "name", - "namedFunction", - "regularExpression", - "statement", - "string", - "type", - "value", - "condition", - "section", - "sectionLevelOne", - "sectionLevelTwo", - "sectionLevelThree", - "sectionLevelFour", - "sectionLevelFive", - "sectionLevelSix", - "selector", - "xmlBothTags", - "xmlElement", - "xmlEndTag", - "xmlStartTag", - "token", - "line", - "notebookCell", - "paragraph", - "document", - "character", - "word", - "nonWhitespaceSequence", - "url" - ] - }, - "variables": { - "type": "object", - "description": "For each named variable in the snippet, provides extra information about the variable.", - "additionalProperties": { - "type": "object", - "properties": { - "wrapperScopeType": { - "$ref": "#/$defs/scopeType", - "description": "Default to this scope type when wrapping a target without scope type specified" - }, - "description": { - "type": "string", - "description": "Description of the snippet variable" - }, - "formatter": { - "type": "string", - "description": "Format text inserted into this variable using the given formatter", - "enum": ["camelCase", "pascalCase", "snakeCase", "upperSnakeCase"] - } - }, - "additionalProperties": false - } - } - } -} From f63f271f7369dcf56da6924596b21dc92aa2deba Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 18:42:46 +0100 Subject: [PATCH 03/11] changelog --- changelog/2026-02-removeCursorlessSnippets.md | 6 ++++++ .../src/actions/generate_snippet.py | 18 +----------------- cursorless-talon/src/cursorless.talon | 3 --- .../docs/user/experimental/snippet-format.md | 7 ------- packages/cursorless-vscode/package.json | 5 ----- 5 files changed, 7 insertions(+), 32 deletions(-) create mode 100644 changelog/2026-02-removeCursorlessSnippets.md delete mode 100644 packages/cursorless-org-docs/src/docs/user/experimental/snippet-format.md diff --git a/changelog/2026-02-removeCursorlessSnippets.md b/changelog/2026-02-removeCursorlessSnippets.md new file mode 100644 index 0000000000..54fec42cb6 --- /dev/null +++ b/changelog/2026-02-removeCursorlessSnippets.md @@ -0,0 +1,6 @@ +--- +tags: [enhancement] +pullRequest: 3151 +--- + +- Remove deprecated Cursorless snippets. Cursorless now fully relies on Talon community for snippet definitions. diff --git a/cursorless-talon/src/actions/generate_snippet.py b/cursorless-talon/src/actions/generate_snippet.py index 7d9b66a4f3..754717bbe4 100644 --- a/cursorless-talon/src/actions/generate_snippet.py +++ b/cursorless-talon/src/actions/generate_snippet.py @@ -1,7 +1,7 @@ import glob from pathlib import Path -from talon import Module, actions, registry, settings +from talon import Module, actions, settings from ..targets.target_types import CursorlessExplicitTarget @@ -10,22 +10,6 @@ @mod.action_class class Actions: - def private_cursorless_migrate_snippets(): - """Migrate snippets from Cursorless to community format""" - actions.user.private_cursorless_run_rpc_command_no_wait( - "cursorless.migrateSnippets", - str(get_directory_path()), - { - "insertion": registry.lists[ - "user.cursorless_insertion_snippet_no_phrase" - ][-1], - "insertionWithPhrase": registry.lists[ - "user.cursorless_insertion_snippet_single_phrase" - ][-1], - "wrapper": registry.lists["user.cursorless_wrapper_snippet"][-1], - }, - ) - 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( diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon index 08d0594a02..416b9fc074 100644 --- a/cursorless-talon/src/cursorless.talon +++ b/cursorless-talon/src/cursorless.talon @@ -53,6 +53,3 @@ tutorial resume: user.private_cursorless_tutorial_resume() tutorial (list | close): user.private_cursorless_tutorial_list() tutorial : user.private_cursorless_tutorial_start_by_number(number_small) - -{user.cursorless_homophone} migrate snippets: - user.private_cursorless_migrate_snippets() diff --git a/packages/cursorless-org-docs/src/docs/user/experimental/snippet-format.md b/packages/cursorless-org-docs/src/docs/user/experimental/snippet-format.md deleted file mode 100644 index 7539881bca..0000000000 --- a/packages/cursorless-org-docs/src/docs/user/experimental/snippet-format.md +++ /dev/null @@ -1,7 +0,0 @@ -# Cursorless snippet format - -Cursorless has experimental support for snippets. Currently these snippets are just used for wrapping targets. - -The best place to start is to look at the [core cursorless snippets](../../../cursorless-snippets). Additionally, there is autocomplete with documentation as you're writing a snippet. - -Note that for `body`, we support [the full textmate syntax supported by VSCode](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax), but we prefer to use variable names (eg `$foo`) instead of placeholders (eg `$1`) so that it is easy to use snippets for wrapping. diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index 775b940870..e69097180c 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -246,11 +246,6 @@ "command": "cursorless.keyboard.redoTarget", "title": "Cursorless: Redo keyboard targeting changes", "enablement": "false" - }, - { - "command": "cursorless.migrateSnippets", - "title": "Cursorless: Migrate snippets from the old Cursorless format to the new community format", - "enablement": "false" } ], "colors": [ From 775c55247f57b76e2451f6b45c68cc9cfcb39b03 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 18:48:52 +0100 Subject: [PATCH 04/11] Lint fix --- packages/cursorless-vscode/src/extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 0644065857..4adf1b23bb 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -22,7 +22,6 @@ import { FileSystemCommandHistoryStorage, FileSystemRawTreeSitterQueryProvider, FileSystemTalonSpokenForms, - getFixturePath, } from "@cursorless/node-common"; import { ScopeTestRecorder, From d8ac5ff579b1553fb8b4d3f5fac7e4e894a35aad Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 18:50:59 +0100 Subject: [PATCH 05/11] Remove legacy tests --- .../snippets/snipFunkBeforeClassLegacy.yml | 45 ------------------- .../actions/snippets/tryWrapThisLegacy.yml | 35 --------------- 2 files changed, 80 deletions(-) delete mode 100644 data/fixtures/recorded/actions/snippets/snipFunkBeforeClassLegacy.yml delete mode 100644 data/fixtures/recorded/actions/snippets/tryWrapThisLegacy.yml diff --git a/data/fixtures/recorded/actions/snippets/snipFunkBeforeClassLegacy.yml b/data/fixtures/recorded/actions/snippets/snipFunkBeforeClassLegacy.yml deleted file mode 100644 index 35cb6755e3..0000000000 --- a/data/fixtures/recorded/actions/snippets/snipFunkBeforeClassLegacy.yml +++ /dev/null @@ -1,45 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: snippet funk before class - action: - name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} - destination: - type: primitive - insertionMode: before - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: class} - usePrePhraseSnapshot: true -spokenFormError: named insertion snippet -initialState: - documentContents: |- - class Aaa { - - } - selections: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} - marks: {} -finalState: - documentContents: |- - function () { - - } - - class Aaa { - - } - selections: - - anchor: {line: 0, character: 9} - active: {line: 0, character: 9} - thatMark: - - type: UntypedTarget - contentRange: - start: {line: 0, character: 0} - end: {line: 2, character: 1} - isReversed: false - hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/tryWrapThisLegacy.yml b/data/fixtures/recorded/actions/snippets/tryWrapThisLegacy.yml deleted file mode 100644 index 3b3212b41d..0000000000 --- a/data/fixtures/recorded/actions/snippets/tryWrapThisLegacy.yml +++ /dev/null @@ -1,35 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: try wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: tryCatchStatement, variableName: body} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: true -spokenFormError: named wrap with snippet -initialState: - documentContents: const foo = "bar"; - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: |- - try { - const foo = "bar"; - } catch (err) { - - } - selections: - - anchor: {line: 3, character: 4} - active: {line: 3, character: 4} - thatMark: - - type: UntypedTarget - contentRange: - start: {line: 0, character: 0} - end: {line: 4, character: 1} - isReversed: false - hasExplicitRange: true From 3804ead487af79ec1a62bed97c4713d37f84a0fc Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 18:58:49 +0100 Subject: [PATCH 06/11] Remove dead links --- packages/cursorless-org-docs/src/docs/user/README.md | 4 ---- packages/cursorless-org-docs/src/docs/user/customization.md | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/cursorless-org-docs/src/docs/user/README.md b/packages/cursorless-org-docs/src/docs/user/README.md index e0a9712518..6f03d1782f 100644 --- a/packages/cursorless-org-docs/src/docs/user/README.md +++ b/packages/cursorless-org-docs/src/docs/user/README.md @@ -711,10 +711,6 @@ The rewrap command, mapped to `"repack"` by default, can be used to swap a given See [paired delimiters](#paired-delimiters) for a list of possible wrappers. -### \[experimental\] Snippets - -See [experimental documentation](experimental/snippets.md). - ### Show definition/reference/quick fix Each of these commands performs a vscode action of the same or a similar name on the target. diff --git a/packages/cursorless-org-docs/src/docs/user/customization.md b/packages/cursorless-org-docs/src/docs/user/customization.md index 2b203edc18..9f2c489554 100644 --- a/packages/cursorless-org-docs/src/docs/user/customization.md +++ b/packages/cursorless-org-docs/src/docs/user/customization.md @@ -176,8 +176,8 @@ Cursorless exposes a couple talon actions and captures that you can use to defin #### Snippet actions -- `user.cursorless_insert_snippet(body: str, destination: Optional[CursorlessDestination], scope_type: Optional[Union[str, list[str]]])`: Insert a snippet with the given body defined using our snippet body syntax (see the [snippet format docs](./experimental/snippet-format.md)). The body should be a single string, which could contain newline `\n` characters, rather than a list of strings as is expected in our snippet json representation. Destination is where the snippet will be inserted. If omitted will default to current selection. An optional scope type can be provided for the target to expand to. `"snip if after air"` for example could be desired to go after the statement containing `air` instead of the token. -- `user.cursorless_wrap_with_snippet(body, target, variable_name, scope)`: Wrap the given target with a snippet with the given body defined using our snippet body syntax (see the [snippet format docs](./experimental/snippet-format.md)). The body should be a single string, which could contain newline `\n` characters, rather than a list of strings as is expected in our snippet json representation. Note that `variable_name` should be one of the variables defined in `body`. Eg, if `body` has a variable `$foo`, you can pass in `"foo"` for `variable_name`, and `target` will be inserted into the position of `$foo` in the given named snippet. The `scope` variable can be used to automatically expand the target to the given scope type, eg `"line"`. +- `user.cursorless_insert_snippet(body: str, destination: Optional[CursorlessDestination], scope_type: Optional[Union[str, list[str]]])`: Insert a snippet with the given body. The body should be a single string, which could contain newline `\n` characters, rather than a list of strings as is expected in our snippet json representation. Destination is where the snippet will be inserted. If omitted will default to current selection. An optional scope type can be provided for the target to expand to. `"snip if after air"` for example could be desired to go after the statement containing `air` instead of the token. +- `user.cursorless_wrap_with_snippet(body, target, variable_name, scope)`: Wrap the given target with a snippet with the given body. The body should be a single string, which could contain newline `\n` characters, rather than a list of strings as is expected in our snippet json representation. Note that `variable_name` should be one of the variables defined in `body`. Eg, if `body` has a variable `$foo`, you can pass in `"foo"` for `variable_name`, and `target` will be inserted into the position of `$foo` in the given named snippet. The `scope` variable can be used to automatically expand the target to the given scope type, eg `"line"`. ### Example of combining capture and action From 24faf185a678121e3fa2a8d7a58fb4df2800d8a8 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 20:06:40 +0100 Subject: [PATCH 07/11] Empty commit From 1ca476d0e62a2b885cf2e433c156f570d919dcca Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 20:14:27 +0100 Subject: [PATCH 08/11] Remove assets and fixtures --- ...duplicatedVariableTest.cursorless-snippets | 25 --- .../elseBranch.cursorless-snippets | 22 --- .../link.cursorless-snippets | 30 ---- .../multipleSnippets.cursorless-snippets | 149 ------------------ .../spaghetti.cursorless-snippets | 22 --- .../tryCatchStatement.cursorless-snippets | 23 --- packages/cursorless-vscode/package.json | 14 -- .../src/scripts/populateDist/assets.ts | 1 - 8 files changed, 286 deletions(-) delete mode 100644 data/fixtures/cursorless-snippets/duplicatedVariableTest.cursorless-snippets delete mode 100644 data/fixtures/cursorless-snippets/elseBranch.cursorless-snippets delete mode 100644 data/fixtures/cursorless-snippets/link.cursorless-snippets delete mode 100644 data/fixtures/cursorless-snippets/multipleSnippets.cursorless-snippets delete mode 100644 data/fixtures/cursorless-snippets/spaghetti.cursorless-snippets delete mode 100644 data/fixtures/cursorless-snippets/tryCatchStatement.cursorless-snippets diff --git a/data/fixtures/cursorless-snippets/duplicatedVariableTest.cursorless-snippets b/data/fixtures/cursorless-snippets/duplicatedVariableTest.cursorless-snippets deleted file mode 100644 index 351f37c0ca..0000000000 --- a/data/fixtures/cursorless-snippets/duplicatedVariableTest.cursorless-snippets +++ /dev/null @@ -1,25 +0,0 @@ -{ - "duplicatedVariableTest": { - "definitions": [ - { - "scope": { - "langIds": [ - "plaintext" - ] - }, - "body": [ - "This variable: '$duplicated' is duplicated here: '$duplicated', but '$unique' is unique!" - ], - "variables": { - "duplicated": { - "formatter": "snakeCase" - }, - "unique": { - "formatter": "camelCase" - } - } - } - ], - "description": "Snippet for testing snippets with duplicated variables" - } -} diff --git a/data/fixtures/cursorless-snippets/elseBranch.cursorless-snippets b/data/fixtures/cursorless-snippets/elseBranch.cursorless-snippets deleted file mode 100644 index e5e0e3c56d..0000000000 --- a/data/fixtures/cursorless-snippets/elseBranch.cursorless-snippets +++ /dev/null @@ -1,22 +0,0 @@ -{ - "elseStatement": { - "definitions": [ - { - "scope": { - "langIds": [ - "python" - ] - }, - "body": [ - "else:", - "\t$body" - ] - } - ], - "description": "Else branch", - "variables": { - "body": {} - }, - "insertionScopeTypes": ["branch"] - } -} diff --git a/data/fixtures/cursorless-snippets/link.cursorless-snippets b/data/fixtures/cursorless-snippets/link.cursorless-snippets deleted file mode 100644 index 01a4eca0d7..0000000000 --- a/data/fixtures/cursorless-snippets/link.cursorless-snippets +++ /dev/null @@ -1,30 +0,0 @@ -{ - "link": { - "definitions": [ - { - "scope": { - "langIds": [ - "markdown" - ] - }, - "body": [ - "[$text](${url:$CLIPBOARD})" - ] - }, - { - "scope": { - "langIds": [ - "typescript", - "javascript", - "typescriptreact", - "javascriptreact" - ] - }, - "body": [ - "{@link $text}" - ] - } - ], - "description": "Link" - } -} diff --git a/data/fixtures/cursorless-snippets/multipleSnippets.cursorless-snippets b/data/fixtures/cursorless-snippets/multipleSnippets.cursorless-snippets deleted file mode 100644 index 54765bff11..0000000000 --- a/data/fixtures/cursorless-snippets/multipleSnippets.cursorless-snippets +++ /dev/null @@ -1,149 +0,0 @@ -{ - "mobxConstructor": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript", - "javascript", - "typescriptreact", - "javascriptreact" - ] - }, - "body": [ - "constructor($parameters) {", - "\tmakeAutoObservable(this);", - "}" - ] - } - ], - "description": "Constructor using makeAutoObservable", - "insertionScopeTypes": [ - "namedFunction" - ] - }, - "constantDeclaration": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript", - "javascript", - "typescriptreact", - "javascriptreact" - ] - }, - "body": [ - "const $name = ${value/^([^;]*);?$/$1/};" - ], - "variables": { - "name": { - "formatter": "camelCase" - } - } - } - ], - "description": "Constant variable declaration", - "insertionScopeTypes": [ - "statement" - ], - "variables": { - "value": { - "wrapperScopeType": "statement" - } - } - }, - "constantDeclarationWithType": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript", - "javascript", - "typescriptreact", - "javascriptreact" - ] - }, - "body": [ - "const $name: $type = ${value/^([^;]*);?$/$1/};" - ], - "variables": { - "name": { - "formatter": "camelCase" - } - } - } - ], - "description": "Constant variable declaration with type", - "insertionScopeTypes": [ - "statement" - ], - "variables": { - "value": { - "wrapperScopeType": "statement" - } - } - }, - "letDeclaration": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript", - "javascript", - "typescriptreact", - "javascriptreact" - ] - }, - "body": [ - "let $name = ${value/^([^;]*);?$/$1/};" - ], - "variables": { - "name": { - "formatter": "camelCase" - } - } - } - ], - "description": "Let variable declaration", - "insertionScopeTypes": [ - "statement" - ], - "variables": { - "value": { - "wrapperScopeType": "statement" - } - } - }, - "letDeclarationWithType": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript", - "javascript", - "typescriptreact", - "javascriptreact" - ] - }, - "body": [ - "let $name: $type = ${value/^([^;]*);?$/$1/};" - ], - "variables": { - "name": { - "formatter": "camelCase" - } - } - } - ], - "description": "Let variable declaration with type", - "insertionScopeTypes": [ - "statement" - ], - "variables": { - "value": { - "wrapperScopeType": "statement" - } - } - } -} diff --git a/data/fixtures/cursorless-snippets/spaghetti.cursorless-snippets b/data/fixtures/cursorless-snippets/spaghetti.cursorless-snippets deleted file mode 100644 index d31e9e2fed..0000000000 --- a/data/fixtures/cursorless-snippets/spaghetti.cursorless-snippets +++ /dev/null @@ -1,22 +0,0 @@ -{ - "spaghetti": { - "definitions": [ - { - "scope": { - "langIds": [ - "plaintext" - ] - }, - "body": [ - "My friend $foo likes to eat spaghetti!" - ], - "variables": { - "foo": { - "formatter": "snakeCase" - } - } - } - ], - "description": "Snippet just for testing user adding snippets" - } -} diff --git a/data/fixtures/cursorless-snippets/tryCatchStatement.cursorless-snippets b/data/fixtures/cursorless-snippets/tryCatchStatement.cursorless-snippets deleted file mode 100644 index 8b578745da..0000000000 --- a/data/fixtures/cursorless-snippets/tryCatchStatement.cursorless-snippets +++ /dev/null @@ -1,23 +0,0 @@ -{ - "tryCatchStatement": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript", - "typescriptreact", - "javascript", - "javascriptreact" - ] - }, - "body": [ - "try {", - "\t$body", - "} catch (err) {", - "\t$exceptBody", - "}" - ] - } - ] - } -} diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index e69097180c..5a500c02d5 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -1201,20 +1201,6 @@ } } }, - "languages": [ - { - "id": "json", - "extensions": [ - ".cursorless-snippets" - ] - } - ], - "jsonValidation": [ - { - "fileMatch": "*.cursorless-snippets", - "url": "./schemas/cursorless-snippets.json" - } - ], "icons": { "cursorless-icon": { "description": "Cursorless icon", diff --git a/packages/cursorless-vscode/src/scripts/populateDist/assets.ts b/packages/cursorless-vscode/src/scripts/populateDist/assets.ts index 5ae3c8d9ff..5ccc19cdba 100644 --- a/packages/cursorless-vscode/src/scripts/populateDist/assets.ts +++ b/packages/cursorless-vscode/src/scripts/populateDist/assets.ts @@ -14,7 +14,6 @@ export const assets: Asset[] = [ // build, and is only used when they say "cursorless cheatsheet". optionalInDev: true, }, - { source: "../../cursorless-snippets", destination: "cursorless-snippets" }, { source: "../../fonts/cursorless-glyph.svg", destination: "fonts/cursorless-glyph.svg", From 2184fbceb1778c6b442a6e316fcea32ba9ecdda0 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 20:18:47 +0100 Subject: [PATCH 09/11] Remove schemas assets --- packages/cursorless-vscode/src/scripts/populateDist/assets.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cursorless-vscode/src/scripts/populateDist/assets.ts b/packages/cursorless-vscode/src/scripts/populateDist/assets.ts index 5ccc19cdba..50d7a1027b 100644 --- a/packages/cursorless-vscode/src/scripts/populateDist/assets.ts +++ b/packages/cursorless-vscode/src/scripts/populateDist/assets.ts @@ -49,7 +49,6 @@ export const assets: Asset[] = [ source: "resources/installationDependencies.js", destination: "resources/installationDependencies.js", }, - { source: "../../schemas", destination: "schemas" }, { source: "../../third-party-licenses.csv", destination: "third-party-licenses.csv", From 8ee8f6a0bbe7f1c2a62fb25bf8c3dbdd92c726df Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 31 Jan 2026 23:59:39 +0100 Subject: [PATCH 10/11] More clean up --- cursorless-talon/src/actions/wrap.py | 32 ------------------- .../src/core/getPreferredSnippet.ts | 12 +++---- 2 files changed, 6 insertions(+), 38 deletions(-) diff --git a/cursorless-talon/src/actions/wrap.py b/cursorless-talon/src/actions/wrap.py index f9e846fc56..37e38966fa 100644 --- a/cursorless-talon/src/actions/wrap.py +++ b/cursorless-talon/src/actions/wrap.py @@ -26,35 +26,3 @@ def private_cursorless_wrap_with_paired_delimiter( "target": target, } ) - - def private_cursorless_wrap_with_snippet( - action_name: str, # pyright: ignore [reportGeneralTypeIssues] - target: CursorlessTarget, - snippet_location: str, - ): - """Execute Cursorless wrap with snippet action""" - if action_name == "wrapWithPairedDelimiter": - action_name = "wrapWithSnippet" - elif action_name == "rewrap": - raise Exception("Rewrapping with snippet not supported") - - snippet_name, variable_name = parse_snippet_location(snippet_location) - - actions.user.private_cursorless_command_and_wait( - { - "name": action_name, - "snippetDescription": { - "type": "named", - "name": snippet_name, - "variableName": variable_name, - }, - "target": target, - } - ) - - -def parse_snippet_location(snippet_location: str) -> tuple[str, str]: - [snippet_name, variable_name] = snippet_location.split(".") - if snippet_name is None or variable_name is None: - raise Exception("Snippet location missing '.'") - return (snippet_name, variable_name) diff --git a/packages/cursorless-engine/src/core/getPreferredSnippet.ts b/packages/cursorless-engine/src/core/getPreferredSnippet.ts index 0e21c14dcb..a3f518c90f 100644 --- a/packages/cursorless-engine/src/core/getPreferredSnippet.ts +++ b/packages/cursorless-engine/src/core/getPreferredSnippet.ts @@ -1,9 +1,9 @@ -import { - NamedSnippetsDeprecationError, - type CustomInsertSnippetArg, - type CustomWrapWithSnippetArg, - type InsertSnippetArg, - type WrapWithSnippetArg, +import { NamedSnippetsDeprecationError } from "@cursorless/common"; +import type { + CustomInsertSnippetArg, + CustomWrapWithSnippetArg, + InsertSnippetArg, + WrapWithSnippetArg, } from "@cursorless/common"; export function getPreferredSnippet( From a130c7fb389a25787aea86314ddcdb20e3533b1f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 1 Feb 2026 08:39:16 +0100 Subject: [PATCH 11/11] Fix failing grammar test --- .../test/fixtures/communitySnippets.fixture.ts | 2 ++ .../src/test/fixtures/spokenFormTest.ts | 15 ++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts b/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts index e7d1350895..bd3f585bb3 100644 --- a/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts +++ b/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts @@ -17,6 +17,8 @@ const snippetAfterAction: ActionDescriptor = { }, snippetDescription: { type: "list", + // This will be the current active language + fallbackLanguage: "typescript", snippets: [ { type: "custom", diff --git a/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts b/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts index 4394cf0a66..d66ae96eb3 100644 --- a/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts +++ b/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts @@ -1,7 +1,8 @@ -import type { - ActionDescriptor, - CommandResponse, - CommandV6, +import { + LATEST_VERSION, + type ActionDescriptor, + type CommandLatest, + type CommandResponse, } from "@cursorless/common"; export interface SpokenFormTest { @@ -22,7 +23,7 @@ export interface SpokenFormTest { * The sequence of Cursorless commands that should be executed when * {@link spokenForm} is spoken. */ - commands: CommandV6[]; + commands: CommandLatest[]; } export function spokenFormTest( @@ -55,9 +56,9 @@ function wrapMockedGetValue( return mockedGetValue == null ? undefined : { returnValue: mockedGetValue }; } -function command(spokenForm: string, action: ActionDescriptor): CommandV6 { +function command(spokenForm: string, action: ActionDescriptor): CommandLatest { return { - version: 6, + version: LATEST_VERSION, spokenForm, usePrePhraseSnapshot: true, action,