@@ -2,7 +2,7 @@ import type { VitestProcessAPI } from './apiProcess'
22import type { TransformSchemaProvider } from './schemaProvider'
33import type { TestTree } from './testTree'
44import { relative } from 'node:path'
5- import { normalize } from 'pathe'
5+ import { normalize , resolve } from 'pathe'
66import * as vscode from 'vscode'
77import { getConfig } from './config'
88import { log } from './log'
@@ -46,19 +46,26 @@ export class ExtensionWatcher extends vscode.Disposable {
4646
4747 watcher . onDidDelete ( async ( uri ) => {
4848 const path = normalize ( uri . fsPath )
49- if ( await this . shouldIgnoreFile ( api , path , uri ) ) {
49+ if ( this . isCommondIgnore ( path ) ) {
5050 return
5151 }
52- log . verbose ?.( '[VSCODE] File deleted:' , this . relative ( api , uri ) )
53- this . testTree . removeFile ( normalize ( uri . fsPath ) )
52+
53+ log . verbose ?.( '[VSCODE] Item deleted:' , this . relative ( api , uri ) )
54+
5455 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 )
5560 } )
5661
5762 watcher . onDidChange ( async ( uri ) => {
5863 const path = normalize ( uri . fsPath )
59- if ( await this . shouldIgnoreFile ( api , path , uri ) ) {
64+ const type = await this . getFsType ( api , path , uri )
65+ if ( type !== 'file' ) {
6066 return
6167 }
68+
6269 this . transformSchemaProvider . emitChange ( uri )
6370 log . verbose ?.( '[VSCODE] File changed:' , this . relative ( api , uri ) )
6471 const apis = this . apisByFolder . get ( folder ) || [ ]
@@ -76,18 +83,38 @@ export class ExtensionWatcher extends vscode.Disposable {
7683
7784 watcher . onDidCreate ( async ( uri ) => {
7885 const path = normalize ( uri . fsPath )
79- if ( await this . shouldIgnoreFile ( api , path , uri ) ) {
86+ const type = await this . getFsType ( api , path , uri )
87+
88+ if ( ! type ) {
8089 return
8190 }
82- log . verbose ?.( '[VSCODE] File created:' , this . relative ( api , uri ) )
91+
92+ log . verbose ?.( '[VSCODE]' , 'New' , type , 'created:' , this . relative ( api , uri ) )
93+
8394 const apis = this . apisByFolder . get ( folder ) || [ ]
84- apis . forEach ( ( api ) => {
85- const metadata = api . getPotentialTestFileMetadata ( path )
86- metadata . forEach ( ( meta ) => {
87- this . testTree . getOrCreateFileTestItem ( api , meta , path )
88- if ( ! api . getPersistentProcessMeta ( ) && ! api . isSpawningPersistentProcess ) {
89- api . collectTests ( meta . project , path )
90- }
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+ }
117+ } )
91118 } )
92119 } )
93120 } )
@@ -97,15 +124,47 @@ export class ExtensionWatcher extends vscode.Disposable {
97124 return relative ( api . workspaceFolder . uri . fsPath , uri . fsPath )
98125 }
99126
100- private async shouldIgnoreFile ( api : VitestProcessAPI , path : string , uri : vscode . Uri ) {
101- if (
127+ private isCommondIgnore ( path : string ) {
128+ return (
102129 path . includes ( '/node_modules/' ) ||
103130 path . includes ( '\\node_modules\\' ) ||
104131 path . includes ( '/.git/' ) ||
105132 path . includes ( '\\.git\\' ) ||
106133 path . endsWith ( '.git' )
107- ) {
108- return true
134+ )
135+ }
136+
137+ private async readFilesRecursively ( uri : vscode . Uri , roots : string [ ] ) : Promise < string [ ] > {
138+ const dirPath = normalize ( uri . fsPath )
139+ // skip if this directory is not inside any project root and no root is inside it
140+ if ( ! roots . some ( ( root ) => dirPath . startsWith ( root ) || root . startsWith ( dirPath ) ) ) {
141+ return [ ]
142+ }
143+ const entries = await vscode . workspace . fs . readDirectory ( uri )
144+ const files : string [ ] = [ ]
145+ for ( const [ name , type ] of entries ) {
146+ const childUri = vscode . Uri . joinPath ( uri , name )
147+ if (
148+ type === vscode . FileType . Directory ||
149+ type === ( vscode . FileType . Directory | vscode . FileType . SymbolicLink )
150+ ) {
151+ if ( this . isCommondIgnore ( normalize ( childUri . fsPath ) ) ) {
152+ continue
153+ }
154+ files . push ( ...( await this . readFilesRecursively ( childUri , roots ) ) )
155+ } else if (
156+ type === vscode . FileType . File ||
157+ type === ( vscode . FileType . File | vscode . FileType . SymbolicLink )
158+ ) {
159+ files . push ( normalize ( childUri . fsPath ) )
160+ }
161+ }
162+ return files
163+ }
164+
165+ private async getFsType ( api : VitestProcessAPI , path : string , uri : vscode . Uri ) {
166+ if ( this . isCommondIgnore ( path ) ) {
167+ return null
109168 }
110169 try {
111170 const stats = await vscode . workspace . fs . stat ( uri )
@@ -115,12 +174,11 @@ export class ExtensionWatcher extends vscode.Disposable {
115174 // if not a symlinked file
116175 stats . type !== ( vscode . FileType . File | vscode . FileType . SymbolicLink )
117176 ) {
118- log . verbose ?.( '[VSCODE]' , this . relative ( api , uri ) , 'is not a file. Ignoring.' )
119- return true
177+ return 'folder'
120178 }
121- return false
179+ return 'file'
122180 } catch {
123- return true
181+ return null
124182 }
125183 }
126184}
0 commit comments