@@ -15,71 +15,130 @@ import {
1515 type VscodeSnippets ,
1616} from "./VscodeSnippets" ;
1717
18+ interface Result {
19+ migrated : Record < string , string > ;
20+ migratedPartially : Record < string , string > ;
21+ skipped : string [ ] ;
22+ }
23+
24+ interface SpokenForms {
25+ insertion : Record < string , string > ;
26+ insertionWithPhrase : Record < string , string > ;
27+ wrapper : Record < string , string > ;
28+ }
29+
1830export async function migrateSnippets (
1931 snippets : VscodeSnippets ,
2032 targetDirectory : string ,
33+ spokenForms : SpokenForms ,
2134) {
22- const userSnippetsDir = snippets . getUserDirectoryStrict ( ) ;
23- const files = await snippets . getSnippetPaths ( userSnippetsDir ) ;
35+ const sourceDirectory = snippets . getUserDirectoryStrict ( ) ;
36+ const files = await snippets . getSnippetPaths ( sourceDirectory ) ;
37+
38+ const spokenFormsInverted : SpokenForms = {
39+ insertion : swapKeyValue ( spokenForms . insertion ) ,
40+ insertionWithPhrase : swapKeyValue (
41+ spokenForms . insertionWithPhrase ,
42+ ( name ) => name . split ( "." ) [ 0 ] ,
43+ ) ,
44+ wrapper : swapKeyValue ( spokenForms . wrapper ) ,
45+ } ;
46+
47+ const result : Result = {
48+ migrated : { } ,
49+ migratedPartially : { } ,
50+ skipped : [ ] ,
51+ } ;
2452
2553 for ( const file of files ) {
26- await migrateFile ( targetDirectory , file ) ;
54+ await migrateFile ( result , spokenFormsInverted , targetDirectory , file ) ;
2755 }
2856
29- await vscode . window . showInformationMessage (
30- `${ files . length } snippet files migrated successfully!` ,
31- ) ;
57+ await openResultDocument ( result , sourceDirectory , targetDirectory ) ;
3258}
3359
34- async function migrateFile ( targetDirectory : string , filePath : string ) {
60+ async function migrateFile (
61+ result : Result ,
62+ spokenForms : SpokenForms ,
63+ targetDirectory : string ,
64+ filePath : string ,
65+ ) {
3566 const fileName = path . basename ( filePath , CURSORLESS_SNIPPETS_SUFFIX ) ;
3667 const snippetFile = await readLegacyFile ( filePath ) ;
3768 const communitySnippetFile : SnippetFile = { snippets : [ ] } ;
69+ let hasSkippedSnippet = false ;
3870
3971 for ( const snippetName in snippetFile ) {
4072 const snippet = snippetFile [ snippetName ] ;
73+ const phrase =
74+ spokenForms . insertion [ snippetName ] ??
75+ spokenForms . insertionWithPhrase [ snippetName ] ;
4176
4277 communitySnippetFile . header = {
4378 name : snippetName ,
4479 description : snippet . description ,
45- variables : parseVariables ( snippet . variables ) ,
80+ phrases : phrase ? [ phrase ] : undefined ,
81+ variables : parseVariables ( spokenForms , snippetName , snippet . variables ) ,
4682 insertionScopes : snippet . insertionScopeTypes ,
4783 } ;
4884
4985 for ( const def of snippet . definitions ) {
86+ if (
87+ def . scope ?. scopeTypes ?. length ||
88+ def . scope ?. excludeDescendantScopeTypes ?. length
89+ ) {
90+ hasSkippedSnippet = true ;
91+ continue ;
92+ }
5093 communitySnippetFile . snippets . push ( {
5194 body : def . body . map ( ( line ) => line . replaceAll ( "\t" , " " ) ) ,
5295 languages : def . scope ?. langIds ,
53- variables : parseVariables ( def . variables ) ,
96+ variables : parseVariables ( spokenForms , snippetName , def . variables ) ,
5497 // SKIP: def.scope?.scopeTypes
5598 // SKIP: def.scope?.excludeDescendantScopeTypes
5699 } ) ;
57100 }
58101 }
59102
103+ if ( communitySnippetFile . snippets . length === 0 ) {
104+ result . skipped . push ( fileName ) ;
105+ return ;
106+ }
107+
108+ let destinationName : string ;
109+
60110 try {
61- const destinationPath = path . join ( targetDirectory , `${ fileName } .snippet` ) ;
62- await writeCommunityFile ( communitySnippetFile , destinationPath ) ;
111+ destinationName = `${ fileName } .snippet` ;
112+ const destinationPath = path . join ( targetDirectory , destinationName ) ;
113+ await writeCommunityFile ( communitySnippetFile , destinationPath , "wx" ) ;
63114 } catch ( error : any ) {
64115 if ( error . code === "EEXIST" ) {
65- const destinationPath = path . join (
66- targetDirectory ,
67- `${ fileName } _CONFLICT.snippet` ,
68- ) ;
69- await writeCommunityFile ( communitySnippetFile , destinationPath ) ;
116+ destinationName = `${ fileName } _CONFLICT.snippet` ;
117+ const destinationPath = path . join ( targetDirectory , destinationName ) ;
118+ await writeCommunityFile ( communitySnippetFile , destinationPath , "w" ) ;
70119 } else {
71120 throw error ;
72121 }
73122 }
123+
124+ if ( hasSkippedSnippet ) {
125+ result . migratedPartially [ fileName ] = destinationName ;
126+ } else {
127+ result . migrated [ fileName ] = destinationName ;
128+ }
74129}
75130
76131function parseVariables (
132+ spokenForms : SpokenForms ,
133+ snippetName : string ,
77134 variables ?: Record < string , SnippetVariableLegacy > ,
78135) : SnippetVariable [ ] {
79136 return Object . entries ( variables ?? { } ) . map (
80137 ( [ name , variable ] ) : SnippetVariable => {
138+ const phrase = spokenForms . wrapper [ `${ snippetName } .${ name } ` ] ;
81139 return {
82140 name,
141+ wrapperPhrases : phrase ? [ phrase ] : undefined ,
83142 wrapperScope : variable . wrapperScopeType ,
84143 insertionFormatters : variable . formatter
85144 ? [ variable . formatter ]
@@ -90,6 +149,52 @@ function parseVariables(
90149 ) ;
91150}
92151
152+ async function openResultDocument (
153+ result : Result ,
154+ sourceDirectory : string ,
155+ targetDirectory : string ,
156+ ) {
157+ const migratedKeys = Object . keys ( result . migrated ) . sort ( ) ;
158+ const migratedPartiallyKeys = Object . keys ( result . migratedPartially ) . sort ( ) ;
159+ const skipMessage =
160+ "(Snippets containing `scopeTypes` and/or `excludeDescendantScopeTypes` attributes are not supported by community snippets.)" ;
161+
162+ const content : string [ ] = [
163+ `# Snippets migrated from Cursorless` ,
164+ "" ,
165+ `From: ${ sourceDirectory } ` ,
166+ `To: ${ targetDirectory } ` ,
167+ "" ,
168+ `## Migrated ${ migratedKeys . length } snippet files:` ,
169+ ...migratedKeys . map ( ( key ) => `- ${ key } -> ${ result . migrated [ key ] } ` ) ,
170+ "" ,
171+ ] ;
172+
173+ if ( migratedPartiallyKeys . length > 0 ) {
174+ content . push (
175+ `## Migrated ${ migratedPartiallyKeys . length } snippet files partially:` ,
176+ ...migratedPartiallyKeys . map (
177+ ( key ) => `- ${ key } -> ${ result . migratedPartially [ key ] } ` ,
178+ ) ,
179+ skipMessage ,
180+ ) ;
181+ }
182+
183+ if ( result . skipped . length > 0 ) {
184+ content . push (
185+ `## Skipped ${ result . skipped . length } snippet files:` ,
186+ ...result . skipped . map ( ( key ) => `- ${ key } ` ) ,
187+ skipMessage ,
188+ ) ;
189+ }
190+
191+ const textDocument = await vscode . workspace . openTextDocument ( {
192+ content : content . join ( "\n" ) ,
193+ language : "markdown" ,
194+ } ) ;
195+ await vscode . window . showTextDocument ( textDocument ) ;
196+ }
197+
93198async function readLegacyFile ( filePath : string ) : Promise < SnippetMap > {
94199 const content = await fs . readFile ( filePath , "utf8" ) ;
95200 if ( content . length === 0 ) {
@@ -98,12 +203,25 @@ async function readLegacyFile(filePath: string): Promise<SnippetMap> {
98203 return JSON . parse ( content ) ;
99204}
100205
101- async function writeCommunityFile ( snippetFile : SnippetFile , filePath : string ) {
206+ async function writeCommunityFile (
207+ snippetFile : SnippetFile ,
208+ filePath : string ,
209+ flags : string ,
210+ ) {
102211 const snippetText = serializeSnippetFile ( snippetFile ) ;
103- const file = await fs . open ( filePath , "wx" ) ;
212+ const file = await fs . open ( filePath , flags ) ;
104213 try {
105214 await file . write ( snippetText ) ;
106215 } finally {
107216 await file . close ( ) ;
108217 }
109218}
219+
220+ function swapKeyValue (
221+ obj : Record < string , string > ,
222+ map ?: ( value : string ) => string ,
223+ ) : Record < string , string > {
224+ return Object . fromEntries (
225+ Object . entries ( obj ) . map ( ( [ key , value ] ) => [ map ?.( value ) ?? value , key ] ) ,
226+ ) ;
227+ }
0 commit comments