243 lines
8.6 KiB
TypeScript
243 lines
8.6 KiB
TypeScript
/*
|
|
* sourceEnumerator.ts
|
|
* Copyright (c) Microsoft Corporation.
|
|
* Licensed under the MIT license.
|
|
*
|
|
* Logic for enumerating all of the Python source files in
|
|
* a project.
|
|
*/
|
|
|
|
import { ConsoleInterface } from '../common/console';
|
|
import { FileSystem } from '../common/fileSystem';
|
|
import { Uri } from '../common/uri/uri';
|
|
import { FileSpec, getFileSystemEntriesWithSymlinkedDirectories, tryRealpath, tryStat } from '../common/uri/uriUtils';
|
|
|
|
export interface SourceEnumerateResult {
|
|
matches: Map<string, Uri>;
|
|
autoExcludedDirs: Uri[];
|
|
isComplete: boolean;
|
|
}
|
|
|
|
const envMarkers = [['bin', 'activate'], ['Scripts', 'activate'], ['pyvenv.cfg'], ['conda-meta']];
|
|
|
|
interface DirToExplore {
|
|
uri: Uri;
|
|
includeRegExp: RegExp;
|
|
hasDirectoryWildcard: boolean;
|
|
}
|
|
|
|
export class SourceEnumerator {
|
|
private _elapsedTimeInMs = 0;
|
|
private _includesToExplore: FileSpec[];
|
|
private _dirsToExplore: DirToExplore[] = [];
|
|
private _matches = new Map<string, Uri>();
|
|
private _autoExcludeDirs: Uri[] = [];
|
|
private _isComplete = false;
|
|
private _numFilesVisited = 0;
|
|
private _loggedLongOperationError = false;
|
|
private _seenDirs = new Set<string>();
|
|
// This tracks symlinked directory roots across the entire enumeration cycle,
|
|
// potentially spanning multiple include roots, so Pylance can later filter
|
|
// workspace indexing against the full discovered set.
|
|
private readonly _symlinkedDirectoryRoots = new Map<string, Uri>();
|
|
|
|
constructor(
|
|
include: FileSpec[],
|
|
private _excludes: FileSpec[],
|
|
private _autoExcludeVenv: boolean,
|
|
private _fs: FileSystem,
|
|
private _console: ConsoleInterface
|
|
) {
|
|
this._includesToExplore = include.slice(0).reverse();
|
|
|
|
this._console.log(`Searching for source files`);
|
|
}
|
|
|
|
getSymlinkedDirectoryRoots(): Uri[] {
|
|
return Array.from(this._symlinkedDirectoryRoots.values());
|
|
}
|
|
|
|
// Enumerates as many files as possible within the specified
|
|
// time limit and returns all matching files.
|
|
enumerate(timeLimitInMs: number): SourceEnumerateResult {
|
|
const startTime = Date.now();
|
|
|
|
while (!this._isComplete) {
|
|
if (this._doNext()) {
|
|
if (!this._isComplete) {
|
|
this._finish();
|
|
}
|
|
}
|
|
|
|
const elapsedTime = Date.now() - startTime;
|
|
if (timeLimitInMs > 0 && elapsedTime > timeLimitInMs) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
this._elapsedTimeInMs += Date.now() - startTime;
|
|
|
|
if (!this._loggedLongOperationError) {
|
|
const longOperationLimitInMs = 10000;
|
|
const nFilesToSuggestSubfolder = 50;
|
|
|
|
// If this is taking a long time, log an error to help the user
|
|
// diagnose and mitigate the problem.
|
|
if (this._elapsedTimeInMs >= longOperationLimitInMs && this._numFilesVisited >= nFilesToSuggestSubfolder) {
|
|
this._console.error(
|
|
`Enumeration of workspace source files is taking longer than ${
|
|
longOperationLimitInMs * 0.001
|
|
} seconds.\n` +
|
|
'This may be because:\n' +
|
|
'* You have opened your home directory or entire hard drive as a workspace\n' +
|
|
'* Your workspace contains a very large number of directories and files\n' +
|
|
'* Your workspace contains a symlink to a directory with many files\n' +
|
|
'* Your workspace is remote, and file enumeration is slow\n' +
|
|
'To reduce this time, open a workspace directory with fewer files ' +
|
|
'or add a viperconfig.json configuration file with an "exclude" section to exclude ' +
|
|
'subdirectories from your workspace. For more details, refer to ' +
|
|
'https://github.com/TermiNexus/TransPyC/wiki/viper-configuration.'
|
|
);
|
|
|
|
this._loggedLongOperationError = true;
|
|
}
|
|
}
|
|
|
|
return {
|
|
matches: this._matches,
|
|
autoExcludedDirs: this._autoExcludeDirs,
|
|
isComplete: this._isComplete,
|
|
};
|
|
}
|
|
|
|
private _recordSymlinkedDirectoryRoot(root: Uri): void {
|
|
for (const existingRoot of this._symlinkedDirectoryRoots.values()) {
|
|
if (root.isChild(existingRoot)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (const [key, existingRoot] of this._symlinkedDirectoryRoots.entries()) {
|
|
if (existingRoot.isChild(root)) {
|
|
this._symlinkedDirectoryRoots.delete(key);
|
|
}
|
|
}
|
|
|
|
this._symlinkedDirectoryRoots.set(root.key, root);
|
|
}
|
|
|
|
// Performs the next enumeration action. Returns true if complete.
|
|
private _doNext(): boolean {
|
|
const dirToExplore = this._dirsToExplore.pop();
|
|
if (dirToExplore) {
|
|
this._exploreDir(dirToExplore);
|
|
return false;
|
|
}
|
|
|
|
const includeToExplore = this._includesToExplore.pop();
|
|
if (includeToExplore) {
|
|
this._exploreInclude(includeToExplore);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private _exploreDir(dir: DirToExplore) {
|
|
const realDirPath = tryRealpath(this._fs, dir.uri);
|
|
if (!realDirPath) {
|
|
this._console.warn(`Skipping broken link "${dir.uri}"`);
|
|
return;
|
|
}
|
|
|
|
if (realDirPath.key !== dir.uri.key) {
|
|
this._recordSymlinkedDirectoryRoot(dir.uri);
|
|
}
|
|
|
|
if (this._seenDirs.has(realDirPath.key)) {
|
|
this._console.info(`Skipping recursive symlink "${dir.uri}" -> "${realDirPath}"`);
|
|
return;
|
|
}
|
|
this._seenDirs.add(realDirPath.key);
|
|
|
|
if (this._autoExcludeVenv) {
|
|
if (envMarkers.some((f) => this._fs.existsSync(dir.uri.resolvePaths(...f)))) {
|
|
this._autoExcludeDirs.push(dir.uri);
|
|
this._console.info(`Auto-excluding ${dir.uri.toUserVisibleString()}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const { files, directories, symlinkedDirectories } = getFileSystemEntriesWithSymlinkedDirectories(
|
|
this._fs,
|
|
dir.uri
|
|
);
|
|
|
|
for (const symlinkedDir of symlinkedDirectories) {
|
|
this._recordSymlinkedDirectoryRoot(symlinkedDir);
|
|
}
|
|
|
|
for (const file of files) {
|
|
if (FileSpec.matchIncludeFileSpec(dir.includeRegExp, this._excludes, file)) {
|
|
this._numFilesVisited++;
|
|
this._matches.set(file.key, file);
|
|
}
|
|
}
|
|
|
|
for (const subDir of directories.slice().reverse()) {
|
|
if (subDir.matchesRegex(dir.includeRegExp) || dir.hasDirectoryWildcard) {
|
|
if (!FileSpec.isInPath(subDir, this._excludes)) {
|
|
this._dirsToExplore.push({
|
|
uri: subDir,
|
|
includeRegExp: dir.includeRegExp,
|
|
hasDirectoryWildcard: dir.hasDirectoryWildcard,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private _exploreInclude(includeSpec: FileSpec) {
|
|
if (FileSpec.isInPath(includeSpec.wildcardRoot, this._excludes)) {
|
|
return;
|
|
}
|
|
|
|
this._seenDirs.clear();
|
|
|
|
// Skip enumeration for non-file URI schemes (e.g., memfs:, zowe-uss:).
|
|
// These require async file system access that isn't available here.
|
|
if (includeSpec.wildcardRoot.scheme !== 'file' && includeSpec.wildcardRoot.scheme !== '') {
|
|
this._console.info(
|
|
`Skipping file enumeration for non-file URI scheme "${includeSpec.wildcardRoot.scheme}".`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const stat = tryStat(this._fs, includeSpec.wildcardRoot);
|
|
if (stat?.isFile()) {
|
|
this._matches.set(includeSpec.wildcardRoot.key, includeSpec.wildcardRoot);
|
|
} else if (stat?.isDirectory()) {
|
|
this._dirsToExplore.push({
|
|
uri: includeSpec.wildcardRoot,
|
|
includeRegExp: includeSpec.regExp,
|
|
hasDirectoryWildcard: includeSpec.hasDirectoryWildcard,
|
|
});
|
|
} else {
|
|
this._console.error(
|
|
`File or directory "${includeSpec.wildcardRoot.toUserVisibleString()}" does not exist.`
|
|
);
|
|
}
|
|
}
|
|
|
|
private _finish() {
|
|
this._isComplete = true;
|
|
|
|
const fileCount = this._matches.size;
|
|
if (fileCount === 0) {
|
|
this._console.info(`No source files found.`);
|
|
} else {
|
|
this._console.info(`Found ${fileCount} ` + `source ${fileCount === 1 ? 'file' : 'files'}`);
|
|
}
|
|
}
|
|
}
|