Skip to content

Commit 2c76430

Browse files
committed
fix(watcher): renaming/deleting/creating folders correctly updates the test tree
1 parent a3abb96 commit 2c76430

3 files changed

Lines changed: 92 additions & 22 deletions

File tree

packages/extension/src/apiProcess.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export class VitestProjectConfig {
3333
return this.pkg.prefix
3434
}
3535

36+
get cwd() {
37+
return this.pkg.cwd
38+
}
39+
3640
get configs() {
3741
return this.projects.map((p) => p.config).filter((n) => n != null)
3842
}
@@ -62,6 +66,7 @@ export class VitestProjectConfig {
6266
return metadata
6367
}
6468

69+
// TODO: if dir is set, this doesn't seem to work properly
6570
matchesTestGlob(project: SerializedProject, moduleId: string, source: () => string) {
6671
const relativeId = relative(project.dir || project.root, moduleId)
6772
if (pm.isMatch(relativeId, project.exclude)) {

packages/extension/src/testTree.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,13 @@ export class TestTree extends vscode.Disposable {
213213
items?.forEach((item) => this.recursiveDelete(item))
214214
}
215215

216+
public removeFolder(folderPath: string) {
217+
const folderItem = this.folderItems.get(normalize(folderPath))
218+
if (folderItem) {
219+
this.recursiveDelete(folderItem)
220+
}
221+
}
222+
216223
private recursiveDelete(item: vscode.TestItem) {
217224
if (!item.parent) return
218225
item.parent.children.delete(item.id)

packages/extension/src/watcher.ts

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { VitestProcessAPI } from './apiProcess'
22
import type { TransformSchemaProvider } from './schemaProvider'
33
import type { TestTree } from './testTree'
44
import { relative } from 'node:path'
5-
import { normalize } from 'pathe'
5+
import { normalize, resolve } from 'pathe'
66
import * as vscode from 'vscode'
77
import { getConfig } from './config'
88
import { 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

Comments
 (0)