1
- import { showError , Snippet , SnippetMap , walkFiles } from "@cursorless/common" ;
2
- import { readFile , stat } from "fs/promises" ;
3
- import { max } from "lodash-es" ;
4
- import { join } from "path" ;
5
- import { ide } from "../singletons/ide.singleton" ;
6
- import { mergeStrict } from "../util/object" ;
7
- import { mergeSnippets } from "./mergeSnippets" ;
8
-
9
- const CURSORLESS_SNIPPETS_SUFFIX = ".cursorless-snippets" ;
10
- const SNIPPET_DIR_REFRESH_INTERVAL_MS = 1000 ;
11
-
12
- interface DirectoryErrorMessage {
13
- directory : string ;
14
- errorMessage : string ;
15
- }
1
+ import { Snippet , SnippetMap } from "@cursorless/common" ;
16
2
17
3
/**
18
4
* Handles all cursorless snippets, including core, third-party and
19
5
* user-defined. Merges these collections and allows looking up snippets by
20
6
* name.
21
7
*/
22
- export class Snippets {
23
- private coreSnippets ! : SnippetMap ;
24
- private thirdPartySnippets : Record < string , SnippetMap > = { } ;
25
- private userSnippets ! : SnippetMap [ ] ;
26
-
27
- private mergedSnippets ! : SnippetMap ;
28
-
29
- private userSnippetsDir ?: string ;
30
-
31
- /**
32
- * The maximum modification time of any snippet in user snippets dir.
33
- *
34
- * This variable will be set to -1 if no user snippets have yet been read or
35
- * if the user snippets path has changed.
36
- *
37
- * This variable will be set to 0 if the user has no snippets dir configured and
38
- * we've already set userSnippets to {}.
39
- */
40
- private maxSnippetMtimeMs : number = - 1 ;
41
-
42
- /**
43
- * If the user has misconfigured their snippet dir, then we keep track of it
44
- * so that we can show them the error message if we can't find a snippet
45
- * later, and so that we don't show them the same error message every time
46
- * we try to poll the directory.
47
- */
48
- private directoryErrorMessage : DirectoryErrorMessage | null | undefined =
49
- null ;
50
-
51
- constructor ( ) {
52
- this . updateUserSnippetsPath ( ) ;
53
-
54
- this . updateUserSnippets = this . updateUserSnippets . bind ( this ) ;
55
- this . registerThirdPartySnippets =
56
- this . registerThirdPartySnippets . bind ( this ) ;
57
-
58
- const timer = setInterval (
59
- this . updateUserSnippets ,
60
- SNIPPET_DIR_REFRESH_INTERVAL_MS ,
61
- ) ;
62
-
63
- ide ( ) . disposeOnExit (
64
- ide ( ) . configuration . onDidChangeConfiguration ( ( ) => {
65
- if ( this . updateUserSnippetsPath ( ) ) {
66
- this . updateUserSnippets ( ) ;
67
- }
68
- } ) ,
69
- {
70
- dispose ( ) {
71
- clearInterval ( timer ) ;
72
- } ,
73
- } ,
74
- ) ;
75
- }
76
-
77
- async init ( ) {
78
- const extensionPath = ide ( ) . assetsRoot ;
79
- const snippetsDir = join ( extensionPath , "cursorless-snippets" ) ;
80
- const snippetFiles = await getSnippetPaths ( snippetsDir ) ;
81
- this . coreSnippets = mergeStrict (
82
- ...( await Promise . all (
83
- snippetFiles . map ( async ( path ) =>
84
- JSON . parse ( await readFile ( path , "utf8" ) ) ,
85
- ) ,
86
- ) ) ,
87
- ) ;
88
- await this . updateUserSnippets ( ) ;
89
- }
90
-
91
- /**
92
- * Updates the userSnippetsDir field if it has change, returning a boolean
93
- * indicating whether there was an update. If there was an update, resets the
94
- * maxSnippetMtime to -1 to ensure snippet update.
95
- * @returns Boolean indicating whether path has changed
96
- */
97
- private updateUserSnippetsPath ( ) : boolean {
98
- const newUserSnippetsDir = ide ( ) . configuration . getOwnConfiguration (
99
- "experimental.snippetsDir" ,
100
- ) ;
101
-
102
- if ( newUserSnippetsDir === this . userSnippetsDir ) {
103
- return false ;
104
- }
105
-
106
- // Reset mtime to -1 so that next time we'll update the snippets
107
- this . maxSnippetMtimeMs = - 1 ;
108
-
109
- this . userSnippetsDir = newUserSnippetsDir ;
110
-
111
- return true ;
112
- }
113
-
114
- async updateUserSnippets ( ) {
115
- let snippetFiles : string [ ] ;
116
- try {
117
- snippetFiles = this . userSnippetsDir
118
- ? await getSnippetPaths ( this . userSnippetsDir )
119
- : [ ] ;
120
- } catch ( err ) {
121
- if ( this . directoryErrorMessage ?. directory !== this . userSnippetsDir ) {
122
- // NB: We suppress error messages once we've shown it the first time
123
- // because we poll the directory every second and want to make sure we
124
- // don't show the same error message repeatedly
125
- const errorMessage = `Error with cursorless snippets dir "${
126
- this . userSnippetsDir
127
- } ": ${ ( err as Error ) . message } `;
128
-
129
- showError ( ide ( ) . messages , "snippetsDirError" , errorMessage ) ;
130
-
131
- this . directoryErrorMessage = {
132
- directory : this . userSnippetsDir ! ,
133
- errorMessage,
134
- } ;
135
- }
136
-
137
- this . userSnippets = [ ] ;
138
- this . mergeSnippets ( ) ;
139
-
140
- return ;
141
- }
142
-
143
- this . directoryErrorMessage = null ;
144
-
145
- const maxSnippetMtime =
146
- max (
147
- ( await Promise . all ( snippetFiles . map ( ( file ) => stat ( file ) ) ) ) . map (
148
- ( stat ) => stat . mtimeMs ,
149
- ) ,
150
- ) ?? 0 ;
151
-
152
- if ( maxSnippetMtime <= this . maxSnippetMtimeMs ) {
153
- return ;
154
- }
155
-
156
- this . maxSnippetMtimeMs = maxSnippetMtime ;
157
-
158
- this . userSnippets = await Promise . all (
159
- snippetFiles . map ( async ( path ) => {
160
- try {
161
- const content = await readFile ( path , "utf8" ) ;
162
-
163
- if ( content . length === 0 ) {
164
- // Gracefully handle an empty file
165
- return { } ;
166
- }
167
-
168
- return JSON . parse ( content ) ;
169
- } catch ( err ) {
170
- showError (
171
- ide ( ) . messages ,
172
- "snippetsFileError" ,
173
- `Error with cursorless snippets file "${ path } ": ${
174
- ( err as Error ) . message
175
- } `,
176
- ) ;
177
-
178
- // We don't want snippets from all files to stop working if there is
179
- // a parse error in one file, so we just effectively ignore this file
180
- // once we've shown an error message
181
- return { } ;
182
- }
183
- } ) ,
184
- ) ;
185
-
186
- this . mergeSnippets ( ) ;
187
- }
8
+ export interface Snippets {
9
+ updateUserSnippets ( ) : Promise < void > ;
188
10
189
11
/**
190
12
* Allows extensions to register third-party snippets. Calling this function
@@ -195,22 +17,7 @@ export class Snippets {
195
17
* @param extensionId The id of the extension registering the snippets.
196
18
* @param snippets The snippets to be registered.
197
19
*/
198
- registerThirdPartySnippets ( extensionId : string , snippets : SnippetMap ) {
199
- this . thirdPartySnippets [ extensionId ] = snippets ;
200
- this . mergeSnippets ( ) ;
201
- }
202
-
203
- /**
204
- * Merge core, third-party, and user snippets, with precedence user > third
205
- * party > core.
206
- */
207
- private mergeSnippets ( ) {
208
- this . mergedSnippets = mergeSnippets (
209
- this . coreSnippets ,
210
- this . thirdPartySnippets ,
211
- this . userSnippets ,
212
- ) ;
213
- }
20
+ registerThirdPartySnippets ( extensionId : string , snippets : SnippetMap ) : void ;
214
21
215
22
/**
216
23
* Looks in merged collection of snippets for a snippet with key
@@ -219,23 +26,11 @@ export class Snippets {
219
26
* @param snippetName The name of the snippet to look up
220
27
* @returns The named snippet
221
28
*/
222
- getSnippetStrict ( snippetName : string ) : Snippet {
223
- const snippet = this . mergedSnippets [ snippetName ] ;
224
-
225
- if ( snippet == null ) {
226
- let errorMessage = `Couldn't find snippet ${ snippetName } . ` ;
29
+ getSnippetStrict ( snippetName : string ) : Snippet ;
227
30
228
- if ( this . directoryErrorMessage != null ) {
229
- errorMessage += `This could be due to: ${ this . directoryErrorMessage . errorMessage } .` ;
230
- }
231
-
232
- throw Error ( errorMessage ) ;
233
- }
234
-
235
- return snippet ;
236
- }
237
- }
238
-
239
- function getSnippetPaths ( snippetsDir : string ) {
240
- return walkFiles ( snippetsDir , CURSORLESS_SNIPPETS_SUFFIX ) ;
31
+ /**
32
+ * Opens a new snippet file in the users snippet directory.
33
+ * @param snippetName The name of the snippet
34
+ */
35
+ openNewSnippetFile ( snippetName : string ) : Promise < void > ;
241
36
}
0 commit comments