From f6f4dab492f8c4e695f032ff69f8f6e94fb5d240 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jan 2025 07:51:49 +0100 Subject: [PATCH 1/5] Open markdown result page after snippet migration --- .../cursorless-vscode/src/migrateSnippets.ts | 112 +++++++++++++++--- 1 file changed, 98 insertions(+), 14 deletions(-) diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index f85b3bf7f6..6c70ba6d70 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -15,6 +15,12 @@ import { type VscodeSnippets, } from "./VscodeSnippets"; +interface Result { + migrated: Record; + migratedPartially: Record; + skipped: string[]; +} + export async function migrateSnippets( snippets: VscodeSnippets, targetDirectory: string, @@ -22,19 +28,74 @@ export async function migrateSnippets( const userSnippetsDir = snippets.getUserDirectoryStrict(); const files = await snippets.getSnippetPaths(userSnippetsDir); + const result: Result = { + migrated: {}, + migratedPartially: {}, + skipped: [], + }; + for (const file of files) { - await migrateFile(targetDirectory, file); + await migrateFile(result, targetDirectory, file); } - await vscode.window.showInformationMessage( - `${files.length} snippet files migrated successfully!`, - ); + await openResultDocument(result, userSnippetsDir, targetDirectory); +} + +async function openResultDocument( + result: Result, + userSnippetsDir: 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 contentParts: string[] = [ + `# Snippets migrated from Cursorless`, + "", + `From: ${userSnippetsDir}`, + `To: ${targetDirectory}`, + "", + `## Migrated ${migratedKeys.length} snippet files`, + ...migratedKeys.map((key) => `- ${key} -> ${result.migrated[key]}`), + "", + ]; + + if (migratedPartiallyKeys.length > 0) { + contentParts.push( + `## Migrated ${migratedPartiallyKeys.length} snippet files partially`, + skipMessage, + ...migratedPartiallyKeys.map( + (key) => `- ${key} -> ${result.migratedPartially[key]}`, + ), + ); + } + + if (result.skipped.length > 0) { + contentParts.push( + `## Skipped ${result.skipped.length} snippet files`, + skipMessage, + ...result.skipped.map((key) => `- ${key}`), + ); + } + + const textDocument = await vscode.workspace.openTextDocument({ + content: contentParts.join("\n"), + language: "markdown", + }); + await vscode.window.showTextDocument(textDocument); } -async function migrateFile(targetDirectory: string, filePath: string) { +async function migrateFile( + result: Result, + targetDirectory: string, + filePath: string, +) { const fileName = path.basename(filePath, CURSORLESS_SNIPPETS_SUFFIX); const snippetFile = await readLegacyFile(filePath); const communitySnippetFile: SnippetFile = { snippets: [] }; + let hasSkippedSnippet = false; for (const snippetName in snippetFile) { const snippet = snippetFile[snippetName]; @@ -47,6 +108,13 @@ async function migrateFile(targetDirectory: string, filePath: string) { }; for (const def of snippet.definitions) { + if ( + def.scope?.scopeTypes?.length || + def.scope?.excludeDescendantScopeTypes?.length + ) { + hasSkippedSnippet = true; + continue; + } communitySnippetFile.snippets.push({ body: def.body.map((line) => line.replaceAll("\t", " ")), languages: def.scope?.langIds, @@ -57,20 +125,32 @@ async function migrateFile(targetDirectory: string, filePath: string) { } } + if (communitySnippetFile.snippets.length === 0) { + result.skipped.push(fileName); + return; + } + + let destinationName: string; + try { - const destinationPath = path.join(targetDirectory, `${fileName}.snippet`); - await writeCommunityFile(communitySnippetFile, destinationPath); + destinationName = `${fileName}.snippet`; + const destinationPath = path.join(targetDirectory, destinationName); + await writeCommunityFile(communitySnippetFile, destinationPath, "wx"); } catch (error: any) { if (error.code === "EEXIST") { - const destinationPath = path.join( - targetDirectory, - `${fileName}_CONFLICT.snippet`, - ); - await writeCommunityFile(communitySnippetFile, destinationPath); + 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; + } } function parseVariables( @@ -98,9 +178,13 @@ async function readLegacyFile(filePath: string): Promise { return JSON.parse(content); } -async function writeCommunityFile(snippetFile: SnippetFile, filePath: string) { +async function writeCommunityFile( + snippetFile: SnippetFile, + filePath: string, + flags: string, +) { const snippetText = serializeSnippetFile(snippetFile); - const file = await fs.open(filePath, "wx"); + const file = await fs.open(filePath, flags); try { await file.write(snippetText); } finally { From f738b6434fef83090a9e7e2995158a63981c2142 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jan 2025 08:58:54 +0100 Subject: [PATCH 2/5] Use spoken forms in migration --- .../src/actions/generate_snippet.py | 11 +- .../cursorless-vscode/src/migrateSnippets.ts | 138 +++++++++++------- .../cursorless-vscode/src/registerCommands.ts | 2 +- 3 files changed, 97 insertions(+), 54 deletions(-) diff --git a/cursorless-talon/src/actions/generate_snippet.py b/cursorless-talon/src/actions/generate_snippet.py index 4f85521e71..54835bd66f 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 Context, Module, actions, settings +from talon import Context, Module, actions, registry, settings from ..targets.target_types import CursorlessExplicitTarget @@ -20,6 +20,15 @@ def private_cursorless_migrate_snippets(): 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] diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index 6c70ba6d70..7c66a17c88 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -21,12 +21,28 @@ interface Result { skipped: string[]; } +interface SpokenForms { + insertion: Record; + insertionWithPhrase: Record; + wrapper: Record; +} + export async function migrateSnippets( snippets: VscodeSnippets, targetDirectory: string, + spokenForms: SpokenForms, ) { - const userSnippetsDir = snippets.getUserDirectoryStrict(); - const files = await snippets.getSnippetPaths(userSnippetsDir); + const sourceDirectory = snippets.getUserDirectoryStrict(); + const files = await snippets.getSnippetPaths(sourceDirectory); + + const spokenFormsInverted: SpokenForms = { + insertion: swapKeyValue(spokenForms.insertion), + insertionWithPhrase: swapKeyValue( + spokenForms.insertionWithPhrase, + (name) => name.split(".")[0], + ), + wrapper: swapKeyValue(spokenForms.wrapper), + }; const result: Result = { migrated: {}, @@ -35,60 +51,15 @@ export async function migrateSnippets( }; for (const file of files) { - await migrateFile(result, targetDirectory, file); + await migrateFile(result, spokenFormsInverted, targetDirectory, file); } - await openResultDocument(result, userSnippetsDir, targetDirectory); -} - -async function openResultDocument( - result: Result, - userSnippetsDir: 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 contentParts: string[] = [ - `# Snippets migrated from Cursorless`, - "", - `From: ${userSnippetsDir}`, - `To: ${targetDirectory}`, - "", - `## Migrated ${migratedKeys.length} snippet files`, - ...migratedKeys.map((key) => `- ${key} -> ${result.migrated[key]}`), - "", - ]; - - if (migratedPartiallyKeys.length > 0) { - contentParts.push( - `## Migrated ${migratedPartiallyKeys.length} snippet files partially`, - skipMessage, - ...migratedPartiallyKeys.map( - (key) => `- ${key} -> ${result.migratedPartially[key]}`, - ), - ); - } - - if (result.skipped.length > 0) { - contentParts.push( - `## Skipped ${result.skipped.length} snippet files`, - skipMessage, - ...result.skipped.map((key) => `- ${key}`), - ); - } - - const textDocument = await vscode.workspace.openTextDocument({ - content: contentParts.join("\n"), - language: "markdown", - }); - await vscode.window.showTextDocument(textDocument); + await openResultDocument(result, sourceDirectory, targetDirectory); } async function migrateFile( result: Result, + spokenForms: SpokenForms, targetDirectory: string, filePath: string, ) { @@ -99,11 +70,15 @@ async function migrateFile( for (const snippetName in snippetFile) { const snippet = snippetFile[snippetName]; + const phrase = + spokenForms.insertion[snippetName] ?? + spokenForms.insertionWithPhrase[snippetName]; communitySnippetFile.header = { name: snippetName, description: snippet.description, - variables: parseVariables(snippet.variables), + phrases: phrase ? [phrase] : undefined, + variables: parseVariables(spokenForms, snippetName, snippet.variables), insertionScopes: snippet.insertionScopeTypes, }; @@ -118,7 +93,7 @@ async function migrateFile( communitySnippetFile.snippets.push({ body: def.body.map((line) => line.replaceAll("\t", " ")), languages: def.scope?.langIds, - variables: parseVariables(def.variables), + variables: parseVariables(spokenForms, snippetName, def.variables), // SKIP: def.scope?.scopeTypes // SKIP: def.scope?.excludeDescendantScopeTypes }); @@ -154,12 +129,16 @@ async function migrateFile( } function parseVariables( + spokenForms: SpokenForms, + snippetName: string, variables?: Record, ): SnippetVariable[] { return Object.entries(variables ?? {}).map( ([name, variable]): SnippetVariable => { + const phrase = spokenForms.wrapper[`${snippetName}.${name}`]; return { name, + wrapperPhrases: phrase ? [phrase] : undefined, wrapperScope: variable.wrapperScopeType, insertionFormatters: variable.formatter ? [variable.formatter] @@ -170,6 +149,52 @@ function parseVariables( ); } +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 contentParts: string[] = [ + `# Snippets migrated from Cursorless`, + "", + `From: ${sourceDirectory}`, + `To: ${targetDirectory}`, + "", + `## Migrated ${migratedKeys.length} snippet files`, + ...migratedKeys.map((key) => `- ${key} -> ${result.migrated[key]}`), + "", + ]; + + if (migratedPartiallyKeys.length > 0) { + contentParts.push( + `## Migrated ${migratedPartiallyKeys.length} snippet files partially`, + skipMessage, + ...migratedPartiallyKeys.map( + (key) => `- ${key} -> ${result.migratedPartially[key]}`, + ), + ); + } + + if (result.skipped.length > 0) { + contentParts.push( + `## Skipped ${result.skipped.length} snippet files`, + skipMessage, + ...result.skipped.map((key) => `- ${key}`), + ); + } + + const textDocument = await vscode.workspace.openTextDocument({ + content: contentParts.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) { @@ -191,3 +216,12 @@ async function writeCommunityFile( await file.close(); } } + +function swapKeyValue( + obj: Record, + map?: (value: string) => string, +): Record { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [map?.(value) ?? value, key]), + ); +} diff --git a/packages/cursorless-vscode/src/registerCommands.ts b/packages/cursorless-vscode/src/registerCommands.ts index 21faece1a9..4675bf9d4f 100644 --- a/packages/cursorless-vscode/src/registerCommands.ts +++ b/packages/cursorless-vscode/src/registerCommands.ts @@ -89,7 +89,7 @@ export function registerCommands( ["cursorless.showDocumentation"]: showDocumentation, ["cursorless.showInstallationDependencies"]: installationDependencies.show, - ["cursorless.migrateSnippets"]: (dir) => migrateSnippets(snippets, dir), + ["cursorless.migrateSnippets"]: migrateSnippets.bind(null, snippets), ["cursorless.private.logQuickActions"]: logQuickActions, From 0d0981b49a70f47d6b09acd18b499b43e9b5f6bf Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jan 2025 09:06:49 +0100 Subject: [PATCH 3/5] rename --- packages/cursorless-vscode/src/migrateSnippets.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index 7c66a17c88..37af9ea399 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -159,7 +159,7 @@ async function openResultDocument( const skipMessage = "Snippets containing `scopeTypes` and/or `excludeDescendantScopeTypes` attributes are not supported by community snippets."; - const contentParts: string[] = [ + const content: string[] = [ `# Snippets migrated from Cursorless`, "", `From: ${sourceDirectory}`, @@ -171,7 +171,7 @@ async function openResultDocument( ]; if (migratedPartiallyKeys.length > 0) { - contentParts.push( + content.push( `## Migrated ${migratedPartiallyKeys.length} snippet files partially`, skipMessage, ...migratedPartiallyKeys.map( @@ -181,7 +181,7 @@ async function openResultDocument( } if (result.skipped.length > 0) { - contentParts.push( + content.push( `## Skipped ${result.skipped.length} snippet files`, skipMessage, ...result.skipped.map((key) => `- ${key}`), @@ -189,7 +189,7 @@ async function openResultDocument( } const textDocument = await vscode.workspace.openTextDocument({ - content: contentParts.join("\n"), + content: content.join("\n"), language: "markdown", }); await vscode.window.showTextDocument(textDocument); From 07fedc4cfee089a15649500467d223bf875dd2e5 Mon Sep 17 00:00:00 2001 From: Phil Cohen Date: Thu, 30 Jan 2025 08:49:12 -0800 Subject: [PATCH 4/5] Apply suggestions from code review --- packages/cursorless-vscode/src/migrateSnippets.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index 37af9ea399..7dc3653b99 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -165,14 +165,14 @@ async function openResultDocument( `From: ${sourceDirectory}`, `To: ${targetDirectory}`, "", - `## Migrated ${migratedKeys.length} snippet files`, + `## Migrated ${migratedKeys.length} snippet files:`, ...migratedKeys.map((key) => `- ${key} -> ${result.migrated[key]}`), "", ]; if (migratedPartiallyKeys.length > 0) { content.push( - `## Migrated ${migratedPartiallyKeys.length} snippet files partially`, + `## Migrated ${migratedPartiallyKeys.length} snippet files partially:`, skipMessage, ...migratedPartiallyKeys.map( (key) => `- ${key} -> ${result.migratedPartially[key]}`, @@ -182,7 +182,7 @@ async function openResultDocument( if (result.skipped.length > 0) { content.push( - `## Skipped ${result.skipped.length} snippet files`, + `## Skipped ${result.skipped.length} snippet files:`, skipMessage, ...result.skipped.map((key) => `- ${key}`), ); From a98b37b90b358507cdcb24929aa10f98cc5c36d3 Mon Sep 17 00:00:00 2001 From: Phil Cohen Date: Thu, 30 Jan 2025 08:50:45 -0800 Subject: [PATCH 5/5] Apply suggestions from code review --- packages/cursorless-vscode/src/migrateSnippets.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cursorless-vscode/src/migrateSnippets.ts b/packages/cursorless-vscode/src/migrateSnippets.ts index 7dc3653b99..429b43c4f7 100644 --- a/packages/cursorless-vscode/src/migrateSnippets.ts +++ b/packages/cursorless-vscode/src/migrateSnippets.ts @@ -157,7 +157,7 @@ async function openResultDocument( 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."; + "(Snippets containing `scopeTypes` and/or `excludeDescendantScopeTypes` attributes are not supported by community snippets.)"; const content: string[] = [ `# Snippets migrated from Cursorless`, @@ -173,18 +173,18 @@ async function openResultDocument( if (migratedPartiallyKeys.length > 0) { content.push( `## Migrated ${migratedPartiallyKeys.length} snippet files partially:`, - skipMessage, ...migratedPartiallyKeys.map( (key) => `- ${key} -> ${result.migratedPartially[key]}`, ), + skipMessage, ); } if (result.skipped.length > 0) { content.push( `## Skipped ${result.skipped.length} snippet files:`, - skipMessage, ...result.skipped.map((key) => `- ${key}`), + skipMessage, ); }