Skip to content

On linux or editor with canUseEvents to prefer immediate directory if its not in root or node_modules #58866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/compiler/resolutionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export interface ResolutionCacheHost extends MinimalResolutionCacheHost {
toPath(fileName: string): Path;
getCanonicalFileName: GetCanonicalFileName;
getCompilationSettings(): CompilerOptions;
preferNonRecursiveWatch: boolean | undefined;
watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher;
watchAffectingFileLocation(file: string, cb: FileWatcherCallback): FileWatcher;
onInvalidatedResolution(): void;
Expand Down Expand Up @@ -351,6 +352,7 @@ export function getDirectoryToWatchFailedLookupLocation(
rootPath: Path,
rootPathComponents: Readonly<PathPathComponents>,
getCurrentDirectory: () => string | undefined,
preferNonRecursiveWatch: boolean | undefined,
): DirectoryOfFailedLookupWatch | undefined {
const failedLookupPathComponents: Readonly<PathPathComponents> = getPathComponents(failedLookupLocationPath);
// Ensure failed look up is normalized path
Expand Down Expand Up @@ -390,6 +392,7 @@ export function getDirectoryToWatchFailedLookupLocation(
nodeModulesIndex,
rootPathComponents,
lastNodeModulesIndex,
preferNonRecursiveWatch,
);
}

Expand All @@ -401,6 +404,7 @@ function getDirectoryToWatchFromFailedLookupLocationDirectory(
nodeModulesIndex: number,
rootPathComponents: Readonly<PathPathComponents>,
lastNodeModulesIndex: number,
preferNonRecursiveWatch: boolean | undefined,
): DirectoryOfFailedLookupWatch | undefined {
// If directory path contains node module, get the most parent node_modules directory for watching
if (nodeModulesIndex !== -1) {
Expand All @@ -412,14 +416,17 @@ function getDirectoryToWatchFromFailedLookupLocationDirectory(
lastNodeModulesIndex,
);
}

// Use some ancestor of the root directory
let nonRecursive = true;
let length = dirPathComponentsLength;
for (let i = 0; i < dirPathComponentsLength; i++) {
if (dirPathComponents[i] !== rootPathComponents[i]) {
nonRecursive = false;
length = Math.max(i + 1, perceivedOsRootLength + 1);
break;
if (!preferNonRecursiveWatch) {
for (let i = 0; i < dirPathComponentsLength; i++) {
if (dirPathComponents[i] !== rootPathComponents[i]) {
nonRecursive = false;
length = Math.max(i + 1, perceivedOsRootLength + 1);
break;
}
}
}
return getDirectoryOfFailedLookupWatch(
Expand Down Expand Up @@ -463,6 +470,7 @@ export function getDirectoryToWatchFailedLookupLocationFromTypeRoot(
rootPath: Path,
rootPathComponents: Readonly<PathPathComponents>,
getCurrentDirectory: () => string | undefined,
preferNonRecursiveWatch: boolean | undefined,
filterCustomPath: (path: Path) => boolean, // Return true if this path can be used
): Path | undefined {
const typeRootPathComponents = getPathComponents(typeRootPath);
Expand All @@ -479,6 +487,7 @@ export function getDirectoryToWatchFailedLookupLocationFromTypeRoot(
typeRootPathComponents.indexOf("node_modules" as Path),
rootPathComponents,
typeRootPathComponents.lastIndexOf("node_modules" as Path),
preferNonRecursiveWatch,
);
return toWatch && filterCustomPath(toWatch.dirPath) ? toWatch.dirPath : undefined;
}
Expand Down Expand Up @@ -1140,6 +1149,7 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD
rootPath,
rootPathComponents,
getCurrentDirectory,
resolutionHost.preferNonRecursiveWatch,
);
if (toWatch) {
const { dir, dirPath, nonRecursive, packageDir, packageDirPath } = toWatch;
Expand Down Expand Up @@ -1354,6 +1364,7 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD
rootPath,
rootPathComponents,
getCurrentDirectory,
resolutionHost.preferNonRecursiveWatch,
);
if (toWatch) {
const { dirPath, packageDirPath } = toWatch;
Expand Down Expand Up @@ -1662,6 +1673,7 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD
rootPath,
rootPathComponents,
getCurrentDirectory,
resolutionHost.preferNonRecursiveWatch,
dirPath => directoryWatchesOfFailedLookups.has(dirPath) || dirPathToSymlinkPackageRefCount.has(dirPath),
);
if (dirPath) {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,7 @@ export interface System {
*/
watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher;
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher;
/**@internal */ preferNonRecursiveWatch?: boolean;
resolvePath(path: string): string;
fileExists(path: string): boolean;
directoryExists(path: string): boolean;
Expand Down Expand Up @@ -1534,6 +1535,7 @@ export let sys: System = (() => {
writeFile,
watchFile,
watchDirectory,
preferNonRecursiveWatch: !fsSupportsRecursiveFsWatch,
resolvePath: path => _path.resolve(path),
fileExists,
directoryExists,
Expand Down
1 change: 1 addition & 0 deletions src/compiler/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ export function createWatchHost(system = sys, reportWatchStatus?: WatchStatusRep
watchDirectory: maybeBind(system, system.watchDirectory) || returnNoopFileWatcher,
setTimeout: maybeBind(system, system.setTimeout) || noop,
clearTimeout: maybeBind(system, system.clearTimeout) || noop,
preferNonRecursiveWatch: system.preferNonRecursiveWatch,
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/compiler/watchPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export interface WatchHost {
setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any;
/** If provided, will be used to reset existing delayed compilation */
clearTimeout?(timeoutId: any): void;
preferNonRecursiveWatch?: boolean;
}
export interface ProgramHost<T extends BuilderProgram> {
/**
Expand Down Expand Up @@ -498,6 +499,7 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
compilerHost.toPath = toPath;
compilerHost.getCompilationSettings = () => compilerOptions!;
compilerHost.useSourceOfProjectReferenceRedirect = maybeBind(host, host.useSourceOfProjectReferenceRedirect);
compilerHost.preferNonRecursiveWatch = host.preferNonRecursiveWatch;
compilerHost.watchDirectoryOfFailedLookupLocation = (dir, cb, flags) => watchDirectory(dir, cb, flags, watchOptions, WatchType.FailedLookupLocations);
compilerHost.watchAffectingFileLocation = (file, cb) => watchFile(file, cb, PollingInterval.High, watchOptions, WatchType.AffectingFileLocation);
compilerHost.watchTypeRootsDirectory = (dir, cb, flags) => watchDirectory(dir, cb, flags, watchOptions, WatchType.TypeRoots);
Expand Down
1 change: 1 addition & 0 deletions src/harness/incrementalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ function verifyProgram(service: ts.server.ProjectService, project: ts.server.Pro
fileIsOpen: project.fileIsOpen.bind(project),
getCurrentProgram: () => project.getCurrentProgram(),

preferNonRecursiveWatch: project.preferNonRecursiveWatch,
watchDirectoryOfFailedLookupLocation: ts.returnNoopFileWatcher,
watchAffectingFileLocation: ts.returnNoopFileWatcher,
onInvalidatedResolution: ts.noop,
Expand Down
10 changes: 8 additions & 2 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,13 +1074,17 @@ function getHostWatcherMap<T>(): HostWatcherMap<T> {
return { idToCallbacks: new Map(), pathToId: new Map() };
}

function getCanUseWatchEvents(service: ProjectService, canUseWatchEvents: boolean | undefined) {
return !!canUseWatchEvents && !!service.eventHandler && !!service.session;
}

function createWatchFactoryHostUsingWatchEvents(service: ProjectService, canUseWatchEvents: boolean | undefined): WatchFactoryHost | undefined {
if (!canUseWatchEvents || !service.eventHandler || !service.session) return undefined;
if (!getCanUseWatchEvents(service, canUseWatchEvents)) return undefined;
const watchedFiles = getHostWatcherMap<FileWatcherCallback>();
const watchedDirectories = getHostWatcherMap<DirectoryWatcherCallback>();
const watchedDirectoriesRecursive = getHostWatcherMap<DirectoryWatcherCallback>();
let ids = 1;
service.session.addProtocolHandler(protocol.CommandTypes.WatchChange, req => {
service.session!.addProtocolHandler(protocol.CommandTypes.WatchChange, req => {
onWatchChange((req as protocol.WatchChangeRequest).arguments);
return { responseRequired: false };
});
Expand Down Expand Up @@ -1321,6 +1325,7 @@ export class ProjectService {
/** @internal */ verifyDocumentRegistry = noop;
/** @internal */ verifyProgram: (project: Project) => void = noop;
/** @internal */ onProjectCreation: (project: Project) => void = noop;
/** @internal */ canUseWatchEvents: boolean;

readonly jsDocParsingMode: JSDocParsingMode | undefined;

Expand Down Expand Up @@ -1392,6 +1397,7 @@ export class ProjectService {
log,
getDetailWatchInfo,
);
this.canUseWatchEvents = getCanUseWatchEvents(this, opts.canUseWatchEvents);
opts.incrementalVerifier?.(this);
}

Expand Down
2 changes: 2 additions & 0 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
protected typeAcquisition: TypeAcquisition | undefined;
/** @internal */
createHash = maybeBind(this.projectService.host, this.projectService.host.createHash);
/** @internal*/ preferNonRecursiveWatch: boolean | undefined;

readonly jsDocParsingMode: JSDocParsingMode | undefined;

Expand Down Expand Up @@ -559,6 +560,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
this.trace = s => host.trace!(s);
}
this.realpath = maybeBind(host, host.realpath);
this.preferNonRecursiveWatch = this.projectService.canUseWatchEvents || host.preferNonRecursiveWatch;

// Use the current directory as resolution root only if the project created using current directory string
this.resolutionCache = createResolutionCache(
Expand Down
1 change: 1 addition & 0 deletions src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type RequireResult = ModuleImportResult;
export interface ServerHost extends System {
watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher;
watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher;
preferNonRecursiveWatch?: boolean;
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any;
clearTimeout(timeoutId: any): void;
setImmediate(callback: (...args: any[]) => void, ...args: any[]): any;
Expand Down
126 changes: 66 additions & 60 deletions src/testRunner/unittests/canWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,71 +55,77 @@ describe("unittests:: canWatch::", () => {
scenario: string,
forPath: "node_modules" | "node_modules/@types" | "",
) {
["file", "dir", "subDir"].forEach(type => {
baselineCanWatch(
`${scenario}In${type}`,
() => `Determines whether to watch given failed lookup location (file that didnt exist) when resolving module.\r\nIt also determines the directory to watch and whether to watch it recursively or not.`,
(paths, longestPathLength, baseline) => {
const recursive = "Recursive";
const maxLength = longestPathLength + ts.combinePaths(forPath, "dir/subdir/somefile.d.ts").length;
const maxLengths = [maxLength, maxLength, recursive.length, maxLength] as const;
baselineCanWatchForRoot(paths, baseline, (rootPathCompoments, root) => {
pushHeader(baseline, ["Location", "getDirectoryToWatchFailedLookupLocation", recursive, "Location if not symlink"], maxLengths);
paths.forEach(path => {
let subPath;
switch (type) {
case "file":
subPath = "somefile.d.ts";
break;
case "dir":
subPath = "dir/somefile.d.ts";
break;
case "subDir":
subPath = "dir/subdir/somefile.d.ts";
break;
}
const testPath = combinePaths(path, forPath, subPath);
const result = ts.getDirectoryToWatchFailedLookupLocation(
testPath,
testPath,
root,
root,
rootPathCompoments,
ts.returnUndefined,
);
pushRow(baseline, [testPath, result ? result.packageDir ?? result.dir : "", result ? `${!result.nonRecursive}` : "", result?.packageDir ? result.dir : ""], maxLengths);
[undefined, true].forEach(preferNonRecursiveWatch => {
["file", "dir", "subDir"].forEach(type => {
baselineCanWatch(
`${scenario}In${type}${preferNonRecursiveWatch ? "NonRecursive" : ""}`,
() => `Determines whether to watch given failed lookup location (file that didnt exist) when resolving module.\r\nIt also determines the directory to watch and whether to watch it recursively or not.`,
(paths, longestPathLength, baseline) => {
const recursive = "Recursive";
const maxLength = longestPathLength + ts.combinePaths(forPath, "dir/subdir/somefile.d.ts").length;
const maxLengths = [maxLength, maxLength, recursive.length, maxLength] as const;
baselineCanWatchForRoot(paths, baseline, (rootPathCompoments, root) => {
pushHeader(baseline, ["Location", "getDirectoryToWatchFailedLookupLocation", recursive, "Location if not symlink"], maxLengths);
paths.forEach(path => {
let subPath;
switch (type) {
case "file":
subPath = "somefile.d.ts";
break;
case "dir":
subPath = "dir/somefile.d.ts";
break;
case "subDir":
subPath = "dir/subdir/somefile.d.ts";
break;
}
const testPath = combinePaths(path, forPath, subPath);
const result = ts.getDirectoryToWatchFailedLookupLocation(
testPath,
testPath,
root,
root,
rootPathCompoments,
ts.returnUndefined,
preferNonRecursiveWatch,
);
pushRow(baseline, [testPath, result ? result.packageDir ?? result.dir : "", result ? `${!result.nonRecursive}` : "", result?.packageDir ? result.dir : ""], maxLengths);
});
});
});
},
);
},
);
});
});
}

baselineCanWatch(
"getDirectoryToWatchFailedLookupLocationFromTypeRoot",
() => `When watched typeRoot handler is invoked, this method determines the directory for which the failedLookupLocation would need to be invalidated.\r\nSince this is invoked only when watching default typeRoot and is used to handle flaky directory watchers, this is used as a fail safe where if failed lookup starts with returned directory we will invalidate that resolution.`,
(paths, longestPathLength, baseline) => {
const maxLength = longestPathLength + "/node_modules/@types".length;
const maxLengths = [maxLength, maxLength] as const;
baselineCanWatchForRoot(paths, baseline, (rootPathCompoments, root) => {
pushHeader(baseline, ["Directory", "getDirectoryToWatchFailedLookupLocationFromTypeRoot"], maxLengths);
paths.forEach(path => {
path = combinePaths(path, "node_modules/@types");
// This is invoked only on paths that are watched
if (!ts.canWatchAtTypes(path)) return;
const result = ts.getDirectoryToWatchFailedLookupLocationFromTypeRoot(
path,
path,
root,
rootPathCompoments,
ts.returnUndefined,
ts.returnTrue,
);
pushRow(baseline, [path, result !== undefined ? result : ""], maxLengths);
[undefined, true].forEach(preferNonRecursiveWatch => {
baselineCanWatch(
`getDirectoryToWatchFailedLookupLocationFromTypeRoot${preferNonRecursiveWatch ? "NonRecursive" : ""}`,
() => `When watched typeRoot handler is invoked, this method determines the directory for which the failedLookupLocation would need to be invalidated.\r\nSince this is invoked only when watching default typeRoot and is used to handle flaky directory watchers, this is used as a fail safe where if failed lookup starts with returned directory we will invalidate that resolution.`,
(paths, longestPathLength, baseline) => {
const maxLength = longestPathLength + "/node_modules/@types".length;
const maxLengths = [maxLength, maxLength] as const;
baselineCanWatchForRoot(paths, baseline, (rootPathCompoments, root) => {
pushHeader(baseline, ["Directory", "getDirectoryToWatchFailedLookupLocationFromTypeRoot"], maxLengths);
paths.forEach(path => {
path = combinePaths(path, "node_modules/@types");
// This is invoked only on paths that are watched
if (!ts.canWatchAtTypes(path)) return;
const result = ts.getDirectoryToWatchFailedLookupLocationFromTypeRoot(
path,
path,
root,
rootPathCompoments,
ts.returnUndefined,
preferNonRecursiveWatch,
ts.returnTrue,
);
pushRow(baseline, [path, result !== undefined ? result : ""], maxLengths);
});
});
});
},
);
},
);
});

function baselineCanWatchForRoot(paths: readonly ts.Path[], baseline: string[], baselineForRoot: (rootPathCompoments: Readonly<ts.PathPathComponents>, root: ts.Path) => void) {
paths.forEach(rootDirForResolution => {
Expand Down
Loading