Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/vitest-dev/vscode. VS Code extension for Vitest vitest.dev/vscode
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288import type { VitestProcessAPI } from './apiProcess'import type { TransformSchemaProvider } from './schemaProvider'import type { TestTree } from './testTree'import { relative } from 'node:path'import { normalize } from 'pathe'import * as vscode from 'vscode'import { getConfig } from './config'import { log } from './log'
const DEBOUNCE_DELAY = 300
export class ExtensionWatcher extends vscode.Disposable { private watcherByFolder = new Map<vscode.WorkspaceFolder, vscode.FileSystemWatcher>() private apisByFolder = new WeakMap<vscode.WorkspaceFolder, VitestProcessAPI[]>() private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
constructor( private readonly testTree: TestTree, private readonly transformSchemaProvider: TransformSchemaProvider, ) { super(() => { this.reset() log.verbose?.('[VSCODE] Watcher disposed') }) }
reset() { this.watcherByFolder.forEach((x) => x.dispose()) this.watcherByFolder.clear() this.apisByFolder = new WeakMap() this.debounceTimers.forEach((timer) => clearTimeout(timer)) this.debounceTimers.clear() }
watchTestFilesInWorkspace(api: VitestProcessAPI) { const folder = api.workspaceFolder const apis = this.apisByFolder.get(folder) ?? [] if (!apis.includes(api)) { log.info('[API] Watching', relative(folder.uri.fsPath, api.id)) apis.push(api) } this.apisByFolder.set(folder, apis) if (this.watcherByFolder.has(folder)) { return }
const pattern = new vscode.RelativePattern(folder, getConfig(folder).filesWatcherInclude) log.info('[VSCODE] Watching', folder.name, 'with pattern', pattern.pattern) const watcher = vscode.workspace.createFileSystemWatcher(pattern) this.watcherByFolder.set(folder, watcher)
const deleteQueue = new Map<string, vscode.Uri>() const changeQueue = new Map<string, vscode.Uri>() const createQueue = new Map<string, vscode.Uri>()
const scheduleCreateDeleteFlush = () => { this.scheduleFlush(`create-delete:${folder.index}`, async () => { const deleteBatch = new Map(deleteQueue) const createBatch = new Map(createQueue) deleteQueue.clear() createQueue.clear()
// Process creates first so new items exist in the tree // before deletes remove old ones (prevents parent folders // from being orphaned during renames) if (createBatch.size) { log.verbose?.(`[VSCODE] Flushing ${createBatch.size} created items`)
const apis = this.apisByFolder.get(folder) || [] const roots = apis.flatMap((api) => api.config.projects.map((p) => normalize(p.dir || p.root)), ) const openedFiles = vscode.workspace.textDocuments.map((d) => normalize(d.uri.fsPath))
const entries = [...createBatch.entries()] const types = await Promise.allSettled( entries.map(([path, uri]) => this.getFsType(path, uri)), )
const files: string[] = [] const folders: [string, vscode.Uri][] = [] for (let i = 0; i < entries.length; i++) { const result = types[i] if (result.status !== 'fulfilled' || !result.value) { continue } if (result.value === 'file') { files.push(entries[i][0]) } else { folders.push(entries[i]) } }
const folderFiles = await Promise.allSettled( folders.map(([, uri]) => this.readFilesRecursively(uri, roots)), )
const seen = new Set(files) for (const result of folderFiles) { if (result.status !== 'fulfilled') { continue } for (const file of result.value) { seen.add(file) } }
seen.forEach((file) => { apis.forEach((api) => { const metadata = api.getPotentialTestFileMetadata(file) metadata.forEach((meta) => { this.testTree.getOrCreateFileTestItem(api, meta, file)
// If file is open and not a continuous run, // Collect its tests immidetly, otherwise ignore if ( openedFiles.includes(file) && !api.getPersistentProcessMeta() && !api.isSpawningPersistentProcess ) { api.collectTests(meta.project, file) } }) }) }) }
if (deleteBatch.size) { log.verbose?.(`[VSCODE] Flushing ${deleteBatch.size} deleted items`) for (const [path, uri] of deleteBatch) { this.transformSchemaProvider.emitChange(uri) // We don't know if it was a file or a folder this.testTree.removeFile(path) this.testTree.removeFolder(path) } } }) }
watcher.onDidDelete((uri) => { const path = normalize(uri.fsPath) if (this.isCommondIgnore(path)) { return } deleteQueue.set(path, uri) scheduleCreateDeleteFlush() })
watcher.onDidChange((uri) => { const path = normalize(uri.fsPath) if (this.isCommondIgnore(path)) { return } changeQueue.set(path, uri) this.scheduleFlush(`change:${folder.index}`, async () => { const batch = new Map(changeQueue) changeQueue.clear() log.verbose?.(`[VSCODE] Flushing ${batch.size} changed items`) const apis = this.apisByFolder.get(folder) || []
const entries = [...batch.entries()] const types = await Promise.allSettled( entries.map(([path, uri]) => this.getFsType(path, uri)), )
for (let i = 0; i < entries.length; i++) { const result = types[i] if (result.status !== 'fulfilled' || result.value !== 'file') { continue } const [path, uri] = entries[i] this.transformSchemaProvider.emitChange(uri)
apis.forEach((api) => { api.onFileChanged(path) const fileItems = this.testTree.getFileTestItems(path)
// Ignore changed to never opened files if (fileItems.every((item) => item.children.size === 0 && !item.error)) { return }
// If runner is persistent, the tests will be collected at runtime if (api.getPersistentProcessMeta() || api.isSpawningPersistentProcess) { return }
const metadata = api.getPotentialTestFileMetadata(path) metadata.forEach((meta) => { api.collectTests(meta.project, path) }) }) } }) })
watcher.onDidCreate((uri) => { const path = normalize(uri.fsPath) if (this.isCommondIgnore(path)) { return } createQueue.set(path, uri) scheduleCreateDeleteFlush() }) }
private scheduleFlush(key: string, flush: () => void) { const existing = this.debounceTimers.get(key) if (existing) { clearTimeout(existing) } this.debounceTimers.set( key, setTimeout(() => { this.debounceTimers.delete(key) flush() }, DEBOUNCE_DELAY), ) }
private isCommondIgnore(path: string) { return ( path.includes('/node_modules/') || path.includes('\\node_modules\\') || path.includes('/.git/') || path.includes('\\.git\\') || path.endsWith('.git') ) }
private async readFilesRecursively(uri: vscode.Uri, roots: string[]): Promise<string[]> { const dirPath = normalize(uri.fsPath) // skip if this directory is not inside any project root and no root is inside it if (!roots.some((root) => dirPath.startsWith(root) || root.startsWith(dirPath))) { return [] } const entries = await vscode.workspace.fs.readDirectory(uri) const files: string[] = [] const subdirs: vscode.Uri[] = [] for (const [name, type] of entries) { const childUri = vscode.Uri.joinPath(uri, name) if ( type === vscode.FileType.Directory || type === (vscode.FileType.Directory | vscode.FileType.SymbolicLink) ) { if (this.isCommondIgnore(normalize(childUri.fsPath))) { continue } subdirs.push(childUri) } else if ( type === vscode.FileType.File || type === (vscode.FileType.File | vscode.FileType.SymbolicLink) ) { files.push(normalize(childUri.fsPath)) } } const results = await Promise.allSettled( subdirs.map((child) => this.readFilesRecursively(child, roots)), ) for (const result of results) { if (result.status === 'fulfilled') { files.push(...result.value) } } return files }
private async getFsType(path: string, uri: vscode.Uri) { if (this.isCommondIgnore(path)) { return null } try { const stats = await vscode.workspace.fs.stat(uri) if ( // if not a file stats.type !== vscode.FileType.File && // if not a symlinked file stats.type !== (vscode.FileType.File | vscode.FileType.SymbolicLink) ) { return 'folder' } return 'file' } catch { return null } }}