diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index e9f15b6fa84ca..0fa06d9d2670f 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -332,9 +332,9 @@ namespace ts { /*@internal*/ export interface RecursiveDirectoryWatcherHost { watchDirectory: HostWatchDirectory; + useCaseSensitiveFileNames: boolean; getAccessibleSortedChildDirectories(path: string): ReadonlyArray; directoryExists(dir: string): boolean; - filePathComparer: Comparer; realpath(s: string): string; } @@ -345,60 +345,94 @@ namespace ts { */ /*@internal*/ export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher { - type ChildWatches = ReadonlyArray; - interface DirectoryWatcher extends FileWatcher { - childWatches: ChildWatches; + interface ChildDirectoryWatcher extends FileWatcher { dirName: string; } + type ChildWatches = ReadonlyArray; + interface HostDirectoryWatcher { + watcher: FileWatcher; + childWatches: ChildWatches; + refCount: number; + } + + const cache = createMap(); + const callbackCache = createMultiMap(); + const filePathComparer = getStringComparer(!host.useCaseSensitiveFileNames); + const toCanonicalFilePath = createGetCanonicalFileName(host.useCaseSensitiveFileNames); return createDirectoryWatcher; /** * Create the directory watcher for the dirPath. */ - function createDirectoryWatcher(dirName: string, callback: DirectoryWatcherCallback): DirectoryWatcher { - const watcher = host.watchDirectory(dirName, fileName => { - // Call the actual callback - callback(fileName); + function createDirectoryWatcher(dirName: string, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher { + const dirPath = toCanonicalFilePath(dirName) as Path; + let directoryWatcher = cache.get(dirPath); + if (directoryWatcher) { + directoryWatcher.refCount++; + } + else { + directoryWatcher = { + watcher: host.watchDirectory(dirName, fileName => { + // Call the actual callback + callbackCache.forEach((callbacks, rootDirName) => { + if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) { + callbacks.forEach(callback => callback(fileName)); + } + }); - // Iterate through existing children and update the watches if needed - updateChildWatches(result, callback); - }); + // Iterate through existing children and update the watches if needed + updateChildWatches(dirName, dirPath); + }), + refCount: 1, + childWatches: emptyArray + }; + cache.set(dirPath, directoryWatcher); + updateChildWatches(dirName, dirPath); + } - let result: DirectoryWatcher = { - close: () => { - watcher.close(); - result.childWatches.forEach(closeFileWatcher); - result = undefined!; - }, + if (callback) { + callbackCache.add(dirPath, callback); + } + + return { dirName, - childWatches: emptyArray + close: () => { + const directoryWatcher = Debug.assertDefined(cache.get(dirPath)); + if (callback) callbackCache.remove(dirPath, callback); + directoryWatcher.refCount--; + + if (directoryWatcher.refCount) return; + + cache.delete(dirPath); + closeFileWatcherOf(directoryWatcher); + directoryWatcher.childWatches.forEach(closeFileWatcher); + } }; - updateChildWatches(result, callback); - return result; } - function updateChildWatches(watcher: DirectoryWatcher, callback: DirectoryWatcherCallback) { + function updateChildWatches(dirName: string, dirPath: Path) { // Iterate through existing children and update the watches if needed - if (watcher) { - watcher.childWatches = watchChildDirectories(watcher.dirName, watcher.childWatches, callback); + const parentWatcher = cache.get(dirPath); + if (parentWatcher) { + parentWatcher.childWatches = watchChildDirectories(dirName, parentWatcher.childWatches); } } /** * Watch the directories in the parentDir */ - function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches { - let newChildWatches: DirectoryWatcher[] | undefined; - enumerateInsertsAndDeletes( + function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches): ChildWatches { + let newChildWatches: ChildDirectoryWatcher[] | undefined; + enumerateInsertsAndDeletes( host.directoryExists(parentDir) ? mapDefined(host.getAccessibleSortedChildDirectories(parentDir), child => { const childFullName = getNormalizedAbsolutePath(child, parentDir); // Filter our the symbolic link directories since those arent included in recursive watch // which is same behaviour when recursive: true is passed to fs.watch - return host.filePathComparer(childFullName, host.realpath(childFullName)) === Comparison.EqualTo ? childFullName : undefined; + return filePathComparer(childFullName, normalizePath(host.realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined; }) : emptyArray, existingChildWatches, - (child, childWatcher) => host.filePathComparer(child, childWatcher.dirName), + (child, childWatcher) => filePathComparer(child, childWatcher.dirName), createAndAddChildDirectoryWatcher, closeFileWatcher, addChildDirectoryWatcher @@ -410,14 +444,14 @@ namespace ts { * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list */ function createAndAddChildDirectoryWatcher(childName: string) { - const result = createDirectoryWatcher(childName, callback); + const result = createDirectoryWatcher(childName); addChildDirectoryWatcher(result); } /** * Add child directory watcher to the new ChildDirectoryWatcher list */ - function addChildDirectoryWatcher(childWatcher: DirectoryWatcher) { + function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) { (newChildWatches || (newChildWatches = [])).push(childWatcher); } } @@ -710,7 +744,7 @@ namespace ts { createWatchDirectoryUsing(dynamicPollingWatchFile || createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })) : watchDirectoryUsingFsWatch; const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ - filePathComparer: getStringComparer(!useCaseSensitiveFileNames), + useCaseSensitiveFileNames, directoryExists, getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, watchDirectory, diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 45ae2e3547828..502e66dac1a60 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -398,7 +398,7 @@ namespace ts { case WatchLogLevel.TriggerOnly: return createFileWatcherWithTriggerLogging; case WatchLogLevel.Verbose: - return createFileWatcherWithLogging; + return addWatch === watchDirectory ? createDirectoryWatcherWithLogging : createFileWatcherWithLogging; } } @@ -413,6 +413,25 @@ namespace ts { }; } + function createDirectoryWatcherWithLogging(host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { + const watchInfo = `${watchCaption}:: Added:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`; + log(watchInfo); + const start = timestamp(); + const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo); + const elapsed = timestamp() - start; + log(`Elapsed:: ${elapsed}ms ${watchInfo}`); + return { + close: () => { + const watchInfo = `${watchCaption}:: Close:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`; + log(watchInfo); + const start = timestamp(); + watcher.close(); + const elapsed = timestamp() - start; + log(`Elapsed:: ${elapsed}ms ${watchInfo}`); + } + }; + } + function createFileWatcherWithTriggerLogging(host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { return addWatch(host, file, (fileName, cbOptional) => { const triggerredInfo = `${watchCaption}:: Triggered with ${fileName}${cbOptional !== undefined ? cbOptional : ""}:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`; diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 8f3fbe1ad2d4c..572c59bb305fd 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -352,9 +352,9 @@ interface Array {}` if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, directoryExists: path => this.directoryExists(path), getAccessibleSortedChildDirectories: path => this.getDirectories(path), - filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, watchDirectory, realpath: s => this.realpath(s) }); @@ -362,9 +362,9 @@ interface Array {}` else if (tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory) { const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, directoryExists: path => this.directoryExists(path), getAccessibleSortedChildDirectories: path => this.getDirectories(path), - filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, watchDirectory, realpath: s => this.realpath(s) }); @@ -373,9 +373,9 @@ interface Array {}` const watchFile = createDynamicPriorityPollingWatchFile(this); const watchDirectory: HostWatchDirectory = (directory, cb) => watchFile(directory, () => cb(directory), PollingInterval.Medium); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, directoryExists: path => this.directoryExists(path), getAccessibleSortedChildDirectories: path => this.getDirectories(path), - filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, watchDirectory, realpath: s => this.realpath(s) }); diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 7e09bae555bab..d2a398849c967 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -8589,10 +8589,10 @@ export const x = 10;` tscWatchDirectory === Tsc_WatchDirectory.WatchFile ? expectedWatchedFiles : createMap(); - // For failed resolution lookup and tsconfig files - mapOfDirectories.set(projectFolder, 2); + // For failed resolution lookup and tsconfig files => cached so only watched only once + mapOfDirectories.set(projectFolder, 1); // Through above recursive watches - mapOfDirectories.set(projectSrcFolder, 2); + mapOfDirectories.set(projectSrcFolder, 1); // node_modules/@types folder mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1); const expectedCompletions = ["file1"]; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index c9267db9a4b7f..840ebc1f259cc 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1933,9 +1933,6 @@ declare namespace ts { getAmbientModules(): Symbol[]; tryGetMemberInModuleExports(memberName: string, moduleSymbol: Symbol): Symbol | undefined; getApparentType(type: Type): Type; - getSuggestionForNonexistentProperty(name: Identifier | string, containingType: Type): string | undefined; - getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string | undefined; - getSuggestionForNonexistentExport(node: Identifier, target: Symbol): string | undefined; getBaseConstraintOfType(type: Type): Type | undefined; getDefaultFromTypeParameter(type: Type): Type | undefined; /**