@@ -19,12 +19,12 @@ const FILE_DELETE_TIMEOUT_IN_MS = 500
1919/**
2020 * Event emitted by the file watcher
2121 *
22- * Includes the type of the event, the path of the file that triggered the event and the extension path that contains the file.
23- * path and extensionPath could be the same if the event is at the extension level (create, delete extension)
22+ * Includes the type of the event, the path of the file that triggered the event and the extension handle that owns the file.
23+ * For folder-level events (create, delete), extensionHandle is undefined since the extension may not exist yet.
2424 *
2525 * @typeParam type - The type of the event
2626 * @typeParam path - The path of the file that triggered the event
27- * @typeParam extensionPath - The path of the extension that contains the file
27+ * @typeParam extensionHandle - The unique handle of the extension that owns the file
2828 * @typeParam startTime - The time when the event was triggered
2929 */
3030export interface WatcherEvent {
@@ -37,6 +37,9 @@ export interface WatcherEvent {
3737 | 'extensions_config_updated'
3838 | 'app_config_deleted'
3939 path : string
40+ /** The unique handle of the extension that owns this file. Undefined for folder-level events. */
41+ extensionHandle ?: string
42+ /** The directory path of the extension. Used for folder-level events (create/delete) where no extension handle exists yet. */
4043 extensionPath : string
4144 startTime : StartTime
4245}
@@ -56,7 +59,7 @@ export class FileWatcher {
5659 private watcher ?: FSWatcher
5760 private readonly debouncedEmit : ( ) => void
5861 private readonly ignored : { [ key : string ] : ignore . Ignore | undefined } = { }
59- // Map of file paths to the extensions that watch them
62+ // Map of file paths to the extension handles that watch them
6063 private readonly extensionWatchedFiles = new Map < string , Set < string > > ( )
6164
6265 constructor (
@@ -155,22 +158,21 @@ export class FileWatcher {
155158 private getAllWatchedFiles ( ) : string [ ] {
156159 this . extensionWatchedFiles . clear ( )
157160
158- const extensionResults = this . app . nonConfigExtensions . map ( ( extension ) => ( {
161+ const extensionResults = this . app . realExtensions . map ( ( extension ) => ( {
159162 extension,
160163 watchedFiles : extension . watchedFiles ( ) ,
161164 } ) )
162165
163166 const allFiles = new Set < string > ( )
164167 for ( const { extension, watchedFiles} of extensionResults ) {
165- const extensionDir = normalizePath ( extension . directory )
166168 for ( const file of watchedFiles ) {
167169 const normalizedPath = normalizePath ( file )
168170 allFiles . add ( normalizedPath )
169171
170- // Track which extensions watch this file
171- const extensionsSet = this . extensionWatchedFiles . get ( normalizedPath ) ?? new Set ( )
172- extensionsSet . add ( extensionDir )
173- this . extensionWatchedFiles . set ( normalizedPath , extensionsSet )
172+ // Track which extension handles watch this file
173+ const handlesSet = this . extensionWatchedFiles . get ( normalizedPath ) ?? new Set ( )
174+ handlesSet . add ( extension . handle )
175+ this . extensionWatchedFiles . set ( normalizedPath , handlesSet )
174176 }
175177 }
176178
@@ -204,13 +206,13 @@ export class FileWatcher {
204206 }
205207
206208 // If the event is already in the list, don't push it again
207- // Check path, type, AND extensionPath to properly handle shared files
209+ // Check path, type, AND extensionHandle to properly handle shared files
208210 if (
209211 this . currentEvents . some (
210212 ( extEvent ) =>
211213 extEvent . path === event . path &&
212214 extEvent . type === event . type &&
213- extEvent . extensionPath === event . extensionPath ,
215+ extEvent . extensionHandle === event . extensionHandle ,
214216 )
215217 )
216218 return
@@ -229,15 +231,17 @@ export class FileWatcher {
229231 private shouldIgnoreEvent ( event : WatcherEvent ) {
230232 if ( event . type === 'extension_folder_deleted' || event . type === 'extension_folder_created' ) return false
231233
232- const extension = this . app . realExtensions . find ( ( ext ) => ext . directory === event . extensionPath )
234+ const extension = event . extensionHandle
235+ ? this . app . realExtensions . find ( ( ext ) => ext . handle === event . extensionHandle )
236+ : undefined
233237 const watchPaths = extension ?. watchedFiles ( )
234- const ignoreInstance = this . ignored [ event . extensionPath ]
238+ const ignoreInstance = extension ? this . ignored [ extension . directory ] : undefined
235239
236240 if ( watchPaths ) {
237241 const isAValidWatchedPath = watchPaths . some ( ( pattern ) => matchGlob ( event . path , pattern ) )
238242 return ! isAValidWatchedPath
239243 } else if ( ignoreInstance ) {
240- const relative = relativePath ( event . extensionPath , event . path )
244+ const relative = relativePath ( extension ! . directory , event . path )
241245 return ignoreInstance . ignores ( relative )
242246 }
243247
@@ -255,8 +259,8 @@ export class FileWatcher {
255259 if ( isConfigAppPath ) {
256260 this . handleEventForExtension ( event , path , this . app . directory , startTime , false )
257261 } else {
258- const affectedExtensions = this . extensionWatchedFiles . get ( normalizedPath )
259- const isUnknownExtension = affectedExtensions === undefined || affectedExtensions . size === 0
262+ const affectedHandles = this . extensionWatchedFiles . get ( normalizedPath )
263+ const isUnknownExtension = affectedHandles === undefined || affectedHandles . size === 0
260264
261265 if ( isUnknownExtension && ! isExtensionToml && ! isConfigAppPath ) {
262266 // Ignore an event if it's not part of an existing extension
@@ -265,8 +269,10 @@ export class FileWatcher {
265269 return
266270 }
267271
268- for ( const extensionPath of affectedExtensions ?? [ ] ) {
269- this . handleEventForExtension ( event , path , extensionPath , startTime , false )
272+ for ( const handle of affectedHandles ?? [ ] ) {
273+ const extension = this . app . realExtensions . find ( ( ext ) => ext . handle === handle )
274+ const extensionPath = extension ? normalizePath ( extension . directory ) : this . app . directory
275+ this . handleEventForExtension ( event , path , extensionPath , startTime , false , handle )
270276 }
271277 if ( isUnknownExtension ) {
272278 this . handleEventForExtension ( event , path , this . app . directory , startTime , true )
@@ -281,6 +287,7 @@ export class FileWatcher {
281287 extensionPath : string ,
282288 startTime : StartTime ,
283289 isUnknownExtension : boolean ,
290+ extensionHandle ?: string ,
284291 ) {
285292 const isExtensionToml = path . endsWith ( '.extension.toml' )
286293 const isConfigAppPath = path === this . app . configPath
@@ -293,17 +300,17 @@ export class FileWatcher {
293300 break
294301 }
295302 if ( isExtensionToml || isConfigAppPath ) {
296- this . pushEvent ( { type : 'extensions_config_updated' , path, extensionPath, startTime} )
303+ this . pushEvent ( { type : 'extensions_config_updated' , path, extensionPath, extensionHandle , startTime} )
297304 } else {
298- this . pushEvent ( { type : 'file_updated' , path, extensionPath, startTime} )
305+ this . pushEvent ( { type : 'file_updated' , path, extensionPath, extensionHandle , startTime} )
299306 }
300307 break
301308 case 'add' :
302309 // If it's a normal non-toml file, just report a file_created event.
303310 // If a toml file was added, a new extension(s) is being created.
304311 // We need to wait for the lock file to disappear before triggering the event.
305312 if ( ! isExtensionToml ) {
306- this . pushEvent ( { type : 'file_created' , path, extensionPath, startTime} )
313+ this . pushEvent ( { type : 'file_created' , path, extensionPath, extensionHandle , startTime} )
307314 break
308315 }
309316 let totalWaitedTime = 0
@@ -339,7 +346,7 @@ export class FileWatcher {
339346 setTimeout ( ( ) => {
340347 // If the extensionPath is not longer in the list, the extension was deleted while the timeout was running.
341348 if ( ! this . extensionPaths . includes ( extensionPath ) ) return
342- this . pushEvent ( { type : 'file_deleted' , path, extensionPath, startTime} )
349+ this . pushEvent ( { type : 'file_deleted' , path, extensionPath, extensionHandle , startTime} )
343350 // Force an emit because we are inside a timeout callback
344351 this . debouncedEmit ( )
345352 } , FILE_DELETE_TIMEOUT_IN_MS )
0 commit comments