@@ -7,9 +7,12 @@ import * as vscode from 'vscode'
77import { getConfig } from './config'
88import { log } from './log'
99
10+ const DEBOUNCE_DELAY = 300
11+
1012export class ExtensionWatcher extends vscode . Disposable {
1113 private watcherByFolder = new Map < vscode . WorkspaceFolder , vscode . FileSystemWatcher > ( )
1214 private apisByFolder = new WeakMap < vscode . WorkspaceFolder , VitestProcessAPI [ ] > ( )
15+ private debounceTimers = new Map < string , ReturnType < typeof setTimeout > > ( )
1316
1417 constructor (
1518 private readonly testTree : TestTree ,
@@ -25,6 +28,8 @@ export class ExtensionWatcher extends vscode.Disposable {
2528 this . watcherByFolder . forEach ( ( x ) => x . dispose ( ) )
2629 this . watcherByFolder . clear ( )
2730 this . apisByFolder = new WeakMap ( )
31+ this . debounceTimers . forEach ( ( timer ) => clearTimeout ( timer ) )
32+ this . debounceTimers . clear ( )
2833 }
2934
3035 watchTestFilesInWorkspace ( api : VitestProcessAPI ) {
@@ -44,82 +49,136 @@ export class ExtensionWatcher extends vscode.Disposable {
4449 const watcher = vscode . workspace . createFileSystemWatcher ( pattern )
4550 this . watcherByFolder . set ( folder , watcher )
4651
47- watcher . onDidDelete ( async ( uri ) => {
52+ const deleteQueue = new Map < string , vscode . Uri > ( )
53+ const changeQueue = new Map < string , vscode . Uri > ( )
54+ const createQueue = new Map < string , vscode . Uri > ( )
55+
56+ watcher . onDidDelete ( ( uri ) => {
4857 const path = normalize ( uri . fsPath )
4958 if ( this . isCommondIgnore ( path ) ) {
5059 return
5160 }
52-
53- log . verbose ?.( '[VSCODE] Item deleted:' , this . relative ( api , uri ) )
54-
55- this . transformSchemaProvider . emitChange ( uri )
56-
57- // We don't know if it is a file or a folder
58- this . testTree . removeFile ( path )
59- this . testTree . removeFolder ( path )
61+ deleteQueue . set ( path , uri )
62+ this . scheduleFlush ( `delete:${ folder . name } ` , ( ) => {
63+ const batch = new Map ( deleteQueue )
64+ deleteQueue . clear ( )
65+ log . verbose ?.( `[VSCODE] Flushing ${ batch . size } deleted items` )
66+ for ( const [ path , uri ] of batch ) {
67+ this . transformSchemaProvider . emitChange ( uri )
68+ // We don't know if it is a file or a folder
69+ this . testTree . removeFile ( path )
70+ this . testTree . removeFolder ( path )
71+ }
72+ } )
6073 } )
6174
62- watcher . onDidChange ( async ( uri ) => {
75+ watcher . onDidChange ( ( uri ) => {
6376 const path = normalize ( uri . fsPath )
64- const type = await this . getFsType ( api , path , uri )
65- if ( type !== 'file' ) {
77+ if ( this . isCommondIgnore ( path ) ) {
6678 return
6779 }
80+ changeQueue . set ( path , uri )
81+ this . scheduleFlush ( `change:${ folder . name } ` , async ( ) => {
82+ const batch = new Map ( changeQueue )
83+ changeQueue . clear ( )
84+ log . verbose ?.( `[VSCODE] Flushing ${ batch . size } changed items` )
85+ const apis = this . apisByFolder . get ( folder ) || [ ]
86+ for ( const [ path , uri ] of batch ) {
87+ const type = await this . getFsType ( api , path , uri )
88+ if ( type !== 'file' ) {
89+ continue
90+ }
91+ this . transformSchemaProvider . emitChange ( uri )
92+
93+ apis . forEach ( ( api ) => {
94+ api . onFileChanged ( path )
95+ const fileItems = this . testTree . getFileTestItems ( path )
96+
97+ // Ignore changed to never opened files
98+ if ( fileItems . every ( ( item ) => item . children . size === 0 && ! item . error ) ) {
99+ return
100+ }
101+
102+ if ( api . getPersistentProcessMeta ( ) || api . isSpawningPersistentProcess ) {
103+ return
104+ }
68105
69- this . transformSchemaProvider . emitChange ( uri )
70- log . verbose ?.( '[VSCODE] File changed:' , this . relative ( api , uri ) )
71- const apis = this . apisByFolder . get ( folder ) || [ ]
72- apis . forEach ( ( api ) => api . onFileChanged ( path ) )
73- apis . forEach ( ( api ) => {
74- if ( api . getPersistentProcessMeta ( ) || api . isSpawningPersistentProcess ) {
75- return
106+ const metadata = api . getPotentialTestFileMetadata ( path )
107+ metadata . forEach ( ( meta ) => {
108+ api . collectTests ( meta . project , path )
109+ } )
110+ } )
76111 }
77- const metadata = api . getPotentialTestFileMetadata ( path )
78- metadata . forEach ( ( meta ) => {
79- api . collectTests ( meta . project , path )
80- } )
81112 } )
82113 } )
83114
84- watcher . onDidCreate ( async ( uri ) => {
115+ watcher . onDidCreate ( ( uri ) => {
85116 const path = normalize ( uri . fsPath )
86- const type = await this . getFsType ( api , path , uri )
87-
88- if ( ! type ) {
117+ if ( this . isCommondIgnore ( path ) ) {
89118 return
90119 }
120+ createQueue . set ( path , uri )
121+ this . scheduleFlush ( `create:${ folder . name } ` , async ( ) => {
122+ const batch = new Map ( createQueue )
123+ createQueue . clear ( )
124+ log . verbose ?.( `[VSCODE] Flushing ${ batch . size } created items` )
125+
126+ const apis = this . apisByFolder . get ( folder ) || [ ]
127+ const roots = apis . flatMap ( ( api ) =>
128+ // TODO: resolve should be done on the worker side
129+ api . config . projects . map ( ( p ) => normalize ( resolve ( api . config . cwd , p . dir || p . root ) ) ) ,
130+ )
131+ const openedFiles = vscode . workspace . textDocuments . map ( ( d ) => normalize ( d . uri . fsPath ) )
132+
133+ const allFiles : string [ ] = [ ]
134+ for ( const [ path , uri ] of batch ) {
135+ const type = await this . getFsType ( api , path , uri )
136+ if ( ! type ) {
137+ continue
138+ }
139+ if ( type === 'file' ) {
140+ allFiles . push ( path )
141+ } else {
142+ allFiles . push ( ...( await this . readFilesRecursively ( uri , roots ) ) )
143+ }
144+ }
91145
92- log . verbose ?.( '[VSCODE]' , 'New' , type , 'created:' , this . relative ( api , uri ) )
93-
94- const apis = this . apisByFolder . get ( folder ) || [ ]
95- const roots = apis . flatMap ( ( api ) =>
96- // TODO: resolve should be done on the worker side
97- api . config . projects . map ( ( p ) => normalize ( resolve ( api . config . cwd , p . dir || p . root ) ) ) ,
98- )
99- const files = type === 'file' ? [ path ] : await this . readFilesRecursively ( uri , roots )
100- const openedFiles = vscode . workspace . textDocuments . map ( ( d ) => normalize ( d . uri . fsPath ) )
101-
102- files . forEach ( ( file ) => {
103- apis . forEach ( ( api ) => {
104- const metadata = api . getPotentialTestFileMetadata ( file )
105- metadata . forEach ( ( meta ) => {
106- this . testTree . getOrCreateFileTestItem ( api , meta , file )
107-
108- // If file is open and not a continuous run,
109- // Collect its tests immidetly, otherwise ignore
110- if (
111- openedFiles . includes ( file ) &&
112- ! api . getPersistentProcessMeta ( ) &&
113- ! api . isSpawningPersistentProcess
114- ) {
115- api . collectTests ( meta . project , file )
116- }
146+ allFiles . forEach ( ( file ) => {
147+ apis . forEach ( ( api ) => {
148+ const metadata = api . getPotentialTestFileMetadata ( file )
149+ metadata . forEach ( ( meta ) => {
150+ this . testTree . getOrCreateFileTestItem ( api , meta , file )
151+
152+ // If file is open and not a continuous run,
153+ // Collect its tests immidetly, otherwise ignore
154+ if (
155+ openedFiles . includes ( file ) &&
156+ ! api . getPersistentProcessMeta ( ) &&
157+ ! api . isSpawningPersistentProcess
158+ ) {
159+ api . collectTests ( meta . project , file )
160+ }
161+ } )
117162 } )
118163 } )
119164 } )
120165 } )
121166 }
122167
168+ private scheduleFlush ( key : string , flush : ( ) => void ) {
169+ const existing = this . debounceTimers . get ( key )
170+ if ( existing ) {
171+ clearTimeout ( existing )
172+ }
173+ this . debounceTimers . set (
174+ key ,
175+ setTimeout ( ( ) => {
176+ this . debounceTimers . delete ( key )
177+ flush ( )
178+ } , DEBOUNCE_DELAY ) ,
179+ )
180+ }
181+
123182 private relative ( api : VitestProcessAPI , uri : vscode . Uri ) {
124183 return relative ( api . workspaceFolder . uri . fsPath , uri . fsPath )
125184 }
0 commit comments