From 2a92a04d303d3f2b8020ec77b897df3a40094ffa Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 7 Feb 2025 05:49:22 +0100 Subject: [PATCH 1/6] Properly handle snippet migration with multiple different snippets in one file --- .../multipleSnippets.cursorless-snippets | 149 ++++++++++++++++++ .../cursorless-vscode/src/migrateSnippets.ts | 68 +++++--- 2 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 data/fixtures/cursorless-snippets/multipleSnippets.cursorless-snippets diff --git a/data/fixtures/cursorless-snippets/multipleSnippets.cursorless-snippets b/data/fixtures/cursorless-snippets/multipleSnippets.cursorless-snippets new file mode 100644 index 0000000000..54765bff11 --- /dev/null +++ b/data/fixtures/cursorless-snippets/multipleSnippets.cursorless-snippets @@ -0,0 +1,149 @@ +{ + "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/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index b4e7f31c90..d0f89b5ce5 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -66,21 +66,26 @@ async function migrateFile( const fileName = path.basename(filePath, CURSORLESS_SNIPPETS_SUFFIX); const snippetFile = await readLegacyFile(filePath); const communitySnippetFile: SnippetFile = { snippets: [] }; + const snippetNames = Object.keys(snippetFile); + const useHeader = snippetNames.length === 1; let hasSkippedSnippet = false; - for (const snippetName in snippetFile) { + for (const snippetName of snippetNames) { const snippet = snippetFile[snippetName]; const phrase = spokenForms.insertion[snippetName] ?? spokenForms.insertionWithPhrase[snippetName]; + const phrases = phrase ? [phrase] : undefined; - communitySnippetFile.header = { - name: snippetName, - description: snippet.description, - phrases: phrase ? [phrase] : undefined, - variables: parseVariables(spokenForms, snippetName, snippet.variables), - insertionScopes: snippet.insertionScopeTypes, - }; + if (useHeader) { + communitySnippetFile.header = { + name: snippetName, + description: snippet.description, + phrases: phrases, + variables: parseVariables(spokenForms, snippetName, snippet.variables), + insertionScopes: snippet.insertionScopeTypes, + }; + } for (const def of snippet.definitions) { if ( @@ -91,11 +96,20 @@ async function migrateFile( continue; } communitySnippetFile.snippets.push({ - body: def.body.map((line) => line.replaceAll("\t", " ")), + 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, def.variables), + variables: parseVariables( + spokenForms, + snippetName, + useHeader ? undefined : snippet.variables, + def.variables, + ), // SKIP: def.scope?.scopeTypes // SKIP: def.scope?.excludeDescendantScopeTypes + body: def.body.map((line) => line.replaceAll("\t", " ")), }); } } @@ -131,22 +145,36 @@ async function migrateFile( function parseVariables( spokenForms: SpokenForms, snippetName: string, - variables?: Record, + snippetVariables?: Record, + defVariables?: Record, ): SnippetVariable[] { - return Object.entries(variables ?? {}).map( - ([name, variable]): SnippetVariable => { + const map: Record = {}; + + const add = (name: string, variable: SnippetVariableLegacy) => { + if (!map[name]) { const phrase = spokenForms.wrapper[`${snippetName}.${name}`]; - return { + map[name] = { name, wrapperPhrases: phrase ? [phrase] : undefined, - wrapperScope: variable.wrapperScopeType, - insertionFormatters: variable.formatter - ? getFormatter(variable.formatter) - : undefined, - // SKIP: variable.description }; - }, + } + 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), ); + + return Object.values(map); } // Convert Cursorless formatters to Talon community formatters From 1c994c6964a62b784dc4151eca021ca324392d04 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 9 Feb 2025 10:53:07 +0100 Subject: [PATCH 2/6] Added migration tests --- .../src/migrateSnippets.test.ts | 267 ++++++++++++++++++ .../cursorless-vscode/src/migrateSnippets.ts | 113 +++++--- 2 files changed, 347 insertions(+), 33 deletions(-) create mode 100644 packages/cursorless-vscode/src/migrateSnippets.test.ts diff --git a/packages/cursorless-vscode/src/migrateSnippets.test.ts b/packages/cursorless-vscode/src/migrateSnippets.test.ts new file mode 100644 index 0000000000..e3e7968f41 --- /dev/null +++ b/packages/cursorless-vscode/src/migrateSnippets.test.ts @@ -0,0 +1,267 @@ +import type { SnippetMap } from "@cursorless/common"; +import assert from "node:assert"; +import type { SnippetFile } from "talon-snippets"; +import { migrateLegacySnippet, type SpokenForms } from "./migrateSnippets"; + +interface Fixture { + name: string; + input: SnippetMap; + output: SnippetFile; +} + +const spokenForms: SpokenForms = { + insertion: { + mySnippet: "snip", + myPythonSnippet: "snip py", + }, + insertionWithPhrase: { + myPhraseSnippet: "phrase", + }, + wrapper: { + "myWrapperSnippet.foo": "foo", + }, +}; + +const fixtures: Fixture[] = [ + { + name: "Empty map", + input: {}, + output: { + snippets: [], + }, + }, + { + name: "Empty definitions", + input: { + mySnippet: { + definitions: [], + }, + }, + output: { + header: { + name: "mySnippet", + phrases: ["snip"], + description: undefined, + insertionScopes: undefined, + variables: [], + }, + snippets: [], + }, + }, + { + name: "Basic", + input: { + mySnippet: { + description: "Example description", + definitions: [ + { + scope: { langIds: ["plaintext"] }, + body: ["Hello, $0, world!"], + }, + ], + }, + }, + output: { + header: { + name: "mySnippet", + description: "Example description", + phrases: ["snip"], + insertionScopes: undefined, + variables: [], + }, + snippets: [ + { + name: undefined, + description: undefined, + phrases: undefined, + languages: ["plaintext"], + insertionScopes: undefined, + variables: [], + body: ["Hello, $0, world!"], + }, + ], + }, + }, + { + name: "Insertion phrase", + input: { + myPhraseSnippet: { + description: "Example description", + definitions: [ + { + scope: { langIds: ["plaintext"] }, + body: ["Hello, $foo, world!"], + variables: { + foo: { formatter: "snakeCase" }, + }, + }, + ], + }, + }, + output: { + header: { + name: "myPhraseSnippet", + description: "Example description", + phrases: ["phrase"], + insertionScopes: undefined, + variables: [], + }, + snippets: [ + { + name: undefined, + description: undefined, + phrases: undefined, + languages: ["plaintext"], + insertionScopes: undefined, + variables: [ + { + name: "foo", + insertionFormatters: ["SNAKE_CASE"], + wrapperPhrases: undefined, + }, + ], + body: ["Hello, $foo, world!"], + }, + ], + }, + }, + { + name: "Wrapper phrase", + input: { + myWrapperSnippet: { + definitions: [ + { + scope: { langIds: ["plaintext"] }, + body: ["Hello, $foo, world!"], + }, + ], + }, + }, + output: { + header: { + name: "myWrapperSnippet", + description: undefined, + phrases: undefined, + insertionScopes: undefined, + variables: [ + { + name: "foo", + wrapperPhrases: ["foo"], + }, + ], + }, + snippets: [ + { + name: undefined, + description: undefined, + phrases: undefined, + languages: ["plaintext"], + insertionScopes: undefined, + variables: [], + body: ["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: { + header: { + name: "mySnippet", + description: undefined, + phrases: ["snip"], + insertionScopes: undefined, + variables: [], + }, + snippets: [ + { + name: undefined, + description: undefined, + phrases: undefined, + languages: ["plaintext"], + insertionScopes: undefined, + variables: [], + body: ["Hello, $0 plain world!"], + }, + { + name: undefined, + description: undefined, + phrases: undefined, + languages: ["python"], + insertionScopes: undefined, + variables: [], + body: ["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: { + snippets: [ + { + name: "mySnippet", + description: undefined, + phrases: ["snip"], + languages: ["plaintext"], + insertionScopes: undefined, + variables: [], + body: ["Hello, $0 plain world!"], + }, + { + name: "myPythonSnippet", + description: undefined, + phrases: ["snip py"], + languages: ["python"], + insertionScopes: undefined, + variables: [], + body: ["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: SnippetFile) { + const [actualOutput] = migrateLegacySnippet(spokenForms, input); + + assert.deepStrictEqual(actualOutput, expectedOutput); +} diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index d0f89b5ce5..3ac4b69094 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -21,7 +21,7 @@ interface Result { skipped: string[]; } -interface SpokenForms { +export interface SpokenForms { insertion: Record; insertionWithPhrase: Record; wrapper: Record; @@ -64,14 +64,42 @@ async function migrateFile( filePath: string, ) { const fileName = path.basename(filePath, CURSORLESS_SNIPPETS_SUFFIX); - const snippetFile = await readLegacyFile(filePath); + 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(snippetFile); + const snippetNames = Object.keys(legacySnippetFile); const useHeader = snippetNames.length === 1; let hasSkippedSnippet = false; for (const snippetName of snippetNames) { - const snippet = snippetFile[snippetName]; + const snippet = legacySnippetFile[snippetName]; const phrase = spokenForms.insertion[snippetName] ?? spokenForms.insertionWithPhrase[snippetName]; @@ -82,7 +110,13 @@ async function migrateFile( name: snippetName, description: snippet.description, phrases: phrases, - variables: parseVariables(spokenForms, snippetName, snippet.variables), + variables: parseVariables( + spokenForms, + snippetName, + snippet.variables, + undefined, + true, + ), insertionScopes: snippet.insertionScopeTypes, }; } @@ -106,6 +140,7 @@ async function migrateFile( snippetName, useHeader ? undefined : snippet.variables, def.variables, + !useHeader, ), // SKIP: def.scope?.scopeTypes // SKIP: def.scope?.excludeDescendantScopeTypes @@ -114,39 +149,15 @@ async function migrateFile( } } - if (communitySnippetFile.snippets.length === 0) { - result.skipped.push(fileName); - return; - } - - 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; - } - } - - if (hasSkippedSnippet) { - result.migratedPartially[fileName] = destinationName; - } else { - result.migrated[fileName] = destinationName; - } + return [communitySnippetFile, hasSkippedSnippet]; } function parseVariables( spokenForms: SpokenForms, snippetName: string, - snippetVariables?: Record, - defVariables?: Record, + snippetVariables: Record | undefined, + defVariables: Record | undefined, + addMissingPhrases: boolean, ): SnippetVariable[] { const map: Record = {}; @@ -174,6 +185,18 @@ function parseVariables( 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]], + }; + } + } + } + return Object.values(map); } @@ -193,6 +216,30 @@ function getFormatter(formatter: string): string[] { } } +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, From f315e6bac62a6c40c3be5a566d878ca69bc7ab52 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 9 Feb 2025 11:15:49 +0100 Subject: [PATCH 3/6] Rename file --- .../{migrateSnippets.test.ts => migrateSnippets.vscode.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/cursorless-vscode/src/{migrateSnippets.test.ts => migrateSnippets.vscode.test.ts} (100%) diff --git a/packages/cursorless-vscode/src/migrateSnippets.test.ts b/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts similarity index 100% rename from packages/cursorless-vscode/src/migrateSnippets.test.ts rename to packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts From e6c70a48c061933786ee3cdce6448c618d8f4850 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 10 Feb 2025 09:13:00 +0100 Subject: [PATCH 4/6] Update format --- .../link.cursorless-snippets | 30 +++ .../cursorless-vscode/src/migrateSnippets.ts | 43 ++- .../src/migrateSnippets.vscode.test.ts | 247 ++++++++---------- 3 files changed, 170 insertions(+), 150 deletions(-) create mode 100644 data/fixtures/cursorless-snippets/link.cursorless-snippets diff --git a/data/fixtures/cursorless-snippets/link.cursorless-snippets b/data/fixtures/cursorless-snippets/link.cursorless-snippets new file mode 100644 index 0000000000..01a4eca0d7 --- /dev/null +++ b/data/fixtures/cursorless-snippets/link.cursorless-snippets @@ -0,0 +1,30 @@ +{ + "link": { + "definitions": [ + { + "scope": { + "langIds": [ + "markdown" + ] + }, + "body": [ + "[$text](${url:$CLIPBOARD})" + ] + }, + { + "scope": { + "langIds": [ + "typescript", + "javascript", + "typescriptreact", + "javascriptreact" + ] + }, + "body": [ + "{@link $text}" + ] + } + ], + "description": "Link" + } +} diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index 3ac4b69094..1cc182c3ea 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -37,10 +37,7 @@ export async function migrateSnippets( const spokenFormsInverted: SpokenForms = { insertion: swapKeyValue(spokenForms.insertion), - insertionWithPhrase: swapKeyValue( - spokenForms.insertionWithPhrase, - (name) => name.split(".")[0], - ), + insertionWithPhrase: swapKeyValue(spokenForms.insertionWithPhrase), wrapper: swapKeyValue(spokenForms.wrapper), }; @@ -100,9 +97,17 @@ export function migrateLegacySnippet( for (const snippetName of snippetNames) { const snippet = legacySnippetFile[snippetName]; - const phrase = - spokenForms.insertion[snippetName] ?? - spokenForms.insertionWithPhrase[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) { @@ -116,6 +121,7 @@ export function migrateLegacySnippet( snippet.variables, undefined, true, + false, ), insertionScopes: snippet.insertionScopeTypes, }; @@ -141,6 +147,7 @@ export function migrateLegacySnippet( useHeader ? undefined : snippet.variables, def.variables, !useHeader, + true, ), // SKIP: def.scope?.scopeTypes // SKIP: def.scope?.excludeDescendantScopeTypes @@ -158,6 +165,7 @@ function parseVariables( snippetVariables: Record | undefined, defVariables: Record | undefined, addMissingPhrases: boolean, + addMissingInsertionFormatters: boolean, ): SnippetVariable[] { const map: Record = {}; @@ -197,6 +205,20 @@ function parseVariables( } } + 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); } @@ -313,11 +335,8 @@ async function writeCommunityFile( } } -function swapKeyValue( - obj: Record, - map?: (value: string) => string, -): Record { +function swapKeyValue(obj: Record): Record { return Object.fromEntries( - Object.entries(obj).map(([key, value]) => [map?.(value) ?? value, key]), + 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 index e3e7968f41..919e0dd083 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts @@ -1,12 +1,12 @@ import type { SnippetMap } from "@cursorless/common"; import assert from "node:assert"; -import type { SnippetFile } from "talon-snippets"; +import { serializeSnippetFile, type SnippetFile } from "talon-snippets"; import { migrateLegacySnippet, type SpokenForms } from "./migrateSnippets"; interface Fixture { name: string; input: SnippetMap; - output: SnippetFile; + output: string; } const spokenForms: SpokenForms = { @@ -15,7 +15,7 @@ const spokenForms: SpokenForms = { myPythonSnippet: "snip py", }, insertionWithPhrase: { - myPhraseSnippet: "phrase", + "myPhraseSnippet.foo": "phrase", }, wrapper: { "myWrapperSnippet.foo": "foo", @@ -26,10 +26,9 @@ const fixtures: Fixture[] = [ { name: "Empty map", input: {}, - output: { - snippets: [], - }, + output: "", }, + { name: "Empty definitions", input: { @@ -37,17 +36,13 @@ const fixtures: Fixture[] = [ definitions: [], }, }, - output: { - header: { - name: "mySnippet", - phrases: ["snip"], - description: undefined, - insertionScopes: undefined, - variables: [], - }, - snippets: [], - }, + output: `\ +name: mySnippet +phrase: snip +--- +`, }, + { name: "Basic", input: { @@ -61,27 +56,19 @@ const fixtures: Fixture[] = [ ], }, }, - output: { - header: { - name: "mySnippet", - description: "Example description", - phrases: ["snip"], - insertionScopes: undefined, - variables: [], - }, - snippets: [ - { - name: undefined, - description: undefined, - phrases: undefined, - languages: ["plaintext"], - insertionScopes: undefined, - variables: [], - body: ["Hello, $0, world!"], - }, - ], - }, + output: `\ +name: mySnippet +description: Example description +phrase: snip +--- + +language: plaintext +- +Hello, $0, world! +--- +`, }, + { name: "Insertion phrase", input: { @@ -98,37 +85,26 @@ const fixtures: Fixture[] = [ ], }, }, - output: { - header: { - name: "myPhraseSnippet", - description: "Example description", - phrases: ["phrase"], - insertionScopes: undefined, - variables: [], - }, - snippets: [ - { - name: undefined, - description: undefined, - phrases: undefined, - languages: ["plaintext"], - insertionScopes: undefined, - variables: [ - { - name: "foo", - insertionFormatters: ["SNAKE_CASE"], - wrapperPhrases: undefined, - }, - ], - body: ["Hello, $foo, world!"], - }, - ], - }, + output: `\ +name: myPhraseSnippet +description: Example description +phrase: phrase +--- + +language: plaintext + +$foo.insertionFormatter: SNAKE_CASE +- +Hello, $foo, world! +--- +`, }, + { - name: "Wrapper phrase", + name: "Insertion phrase noop", input: { - myWrapperSnippet: { + myPhraseSnippet: { + description: "Example description", definitions: [ { scope: { langIds: ["plaintext"] }, @@ -137,32 +113,46 @@ const fixtures: Fixture[] = [ ], }, }, - output: { - header: { - name: "myWrapperSnippet", - description: undefined, - phrases: undefined, - insertionScopes: undefined, - variables: [ + output: `\ +name: myPhraseSnippet +description: Example description +phrase: phrase +--- + +language: plaintext + +$foo.insertionFormatter: NOOP +- +Hello, $foo, world! +--- +`, + }, + + { + name: "Wrapper phrase", + input: { + myWrapperSnippet: { + definitions: [ { - name: "foo", - wrapperPhrases: ["foo"], + scope: { langIds: ["plaintext"] }, + body: ["Hello, $foo, world!"], }, ], }, - snippets: [ - { - name: undefined, - description: undefined, - phrases: undefined, - languages: ["plaintext"], - insertionScopes: undefined, - variables: [], - body: ["Hello, $foo, world!"], - }, - ], }, + output: `\ +name: myWrapperSnippet + +$foo.wrapperPhrase: foo +--- + +language: plaintext +- +Hello, $foo, world! +--- +`, }, + { name: "Multiple definitions", input: { @@ -179,36 +169,23 @@ const fixtures: Fixture[] = [ ], }, }, - output: { - header: { - name: "mySnippet", - description: undefined, - phrases: ["snip"], - insertionScopes: undefined, - variables: [], - }, - snippets: [ - { - name: undefined, - description: undefined, - phrases: undefined, - languages: ["plaintext"], - insertionScopes: undefined, - variables: [], - body: ["Hello, $0 plain world!"], - }, - { - name: undefined, - description: undefined, - phrases: undefined, - languages: ["python"], - insertionScopes: undefined, - variables: [], - 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: { @@ -229,28 +206,21 @@ const fixtures: Fixture[] = [ ], }, }, - output: { - snippets: [ - { - name: "mySnippet", - description: undefined, - phrases: ["snip"], - languages: ["plaintext"], - insertionScopes: undefined, - variables: [], - body: ["Hello, $0 plain world!"], - }, - { - name: "myPythonSnippet", - description: undefined, - phrases: ["snip py"], - languages: ["python"], - insertionScopes: undefined, - variables: [], - 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! +--- +`, }, ]; @@ -260,8 +230,9 @@ suite("Migrate snippets", async function () { }); }); -function runTest(input: SnippetMap, expectedOutput: SnippetFile) { - const [actualOutput] = migrateLegacySnippet(spokenForms, input); +function runTest(input: SnippetMap, expectedOutput: string) { + const [snippetFile] = migrateLegacySnippet(spokenForms, input); + const actualOutput = serializeSnippetFile(snippetFile); - assert.deepStrictEqual(actualOutput, expectedOutput); + assert.equal(actualOutput, expectedOutput); } From 6b98bbe9312819f910f82abdd0005d1cb29016f1 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 10 Feb 2025 09:16:31 +0100 Subject: [PATCH 5/6] Remove unused import --- packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts b/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts index 919e0dd083..ea32077bbb 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.vscode.test.ts @@ -1,6 +1,6 @@ import type { SnippetMap } from "@cursorless/common"; import assert from "node:assert"; -import { serializeSnippetFile, type SnippetFile } from "talon-snippets"; +import { serializeSnippetFile } from "talon-snippets"; import { migrateLegacySnippet, type SpokenForms } from "./migrateSnippets"; interface Fixture { From 2b4d271eeade5c576f1faefb9b61bb8c11d21f7f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:33:28 +0000 Subject: [PATCH 6/6] minor readability --- .../cursorless-vscode/src/migrateSnippets.ts | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index 1cc182c3ea..dc6fe24da0 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -115,14 +115,14 @@ export function migrateLegacySnippet( name: snippetName, description: snippet.description, phrases: phrases, - variables: parseVariables( + variables: parseVariables({ spokenForms, snippetName, - snippet.variables, - undefined, - true, - false, - ), + snippetVariables: snippet.variables, + defVariables: undefined, + addMissingPhrases: true, + addMissingInsertionFormatters: false, + }), insertionScopes: snippet.insertionScopeTypes, }; } @@ -141,14 +141,14 @@ export function migrateLegacySnippet( phrases: useHeader ? undefined : phrases, insertionScopes: useHeader ? undefined : snippet.insertionScopeTypes, languages: def.scope?.langIds, - variables: parseVariables( + variables: parseVariables({ spokenForms, snippetName, - useHeader ? undefined : snippet.variables, - def.variables, - !useHeader, - true, - ), + 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", " ")), @@ -159,14 +159,23 @@ export function migrateLegacySnippet( return [communitySnippetFile, hasSkippedSnippet]; } -function parseVariables( - spokenForms: SpokenForms, - snippetName: string, - snippetVariables: Record | undefined, - defVariables: Record | undefined, - addMissingPhrases: boolean, - addMissingInsertionFormatters: boolean, -): SnippetVariable[] { +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) => {