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" ;
162
173/**
184 * Handles all cursorless snippets, including core, third-party and
195 * user-defined. Merges these collections and allows looking up snippets by
206 * name.
217 */
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 > ;
18810
18911 /**
19012 * Allows extensions to register third-party snippets. Calling this function
@@ -195,22 +17,7 @@ export class Snippets {
19517 * @param extensionId The id of the extension registering the snippets.
19618 * @param snippets The snippets to be registered.
19719 */
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 ;
21421
21522 /**
21623 * Looks in merged collection of snippets for a snippet with key
@@ -219,23 +26,11 @@ export class Snippets {
21926 * @param snippetName The name of the snippet to look up
22027 * @returns The named snippet
22128 */
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 ;
22730
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 > ;
24136}
0 commit comments