Skip to content

Commit

Permalink
On linux or editor with canUseEvents to prefer immediate directory if…
Browse files Browse the repository at this point in the history
… its not in root or node_modules (#58866)
  • Loading branch information
sheetalkamat authored Jun 27, 2024
1 parent fe0bdc8 commit f7833b2
Show file tree
Hide file tree
Showing 86 changed files with 135,375 additions and 230 deletions.
22 changes: 17 additions & 5 deletions src/compiler/resolutionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,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 @@ -346,6 +347,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 @@ -385,6 +387,7 @@ export function getDirectoryToWatchFailedLookupLocation(
nodeModulesIndex,
rootPathComponents,
lastNodeModulesIndex,
preferNonRecursiveWatch,
);
}

Expand All @@ -396,6 +399,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 @@ -407,14 +411,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 @@ -458,6 +465,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 @@ -474,6 +482,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 @@ -1120,6 +1129,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 @@ -1334,6 +1344,7 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD
rootPath,
rootPathComponents,
getCurrentDirectory,
resolutionHost.preferNonRecursiveWatch,
);
if (toWatch) {
const { dirPath, packageDirPath } = toWatch;
Expand Down Expand Up @@ -1640,6 +1651,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 @@ -511,6 +511,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 @@ -1083,13 +1083,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 @@ -1327,6 +1331,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 @@ -1396,6 +1401,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 @@ -555,6 +555,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 @@ -615,6 +616,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 @@ -29,6 +29,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

0 comments on commit f7833b2

Please sign in to comment.