diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 56dd48aaebf..3a989eaa138 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -30,6 +30,10 @@ namespace ts { isInvalidated?: boolean; } + interface ResolutionWithResolvedFileName { + resolvedFileName: string | undefined; + } + export interface ResolutionCacheHost extends ModuleResolutionHost { toPath(fileName: string): Path; getCompilationSettings(): CompilerOptions; @@ -59,10 +63,15 @@ namespace ts { // The values are Map of resolutions with key being name lookedup. const resolvedModuleNames = createMap>(); const perDirectoryResolvedModuleNames = createMap>(); + const resolvedTypeReferenceDirectives = createMap>(); const perDirectoryResolvedTypeReferenceDirectives = createMap>(); + const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory()); + const failedLookupDefaultExtensions = [Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx, Extension.Json]; + const customFailedLookupPaths = createMap(); + const directoryWatchesOfFailedLookups = createMap(); let rootDir: string; let rootPath: Path; @@ -85,6 +94,14 @@ namespace ts { clear }; + function getResolvedModule(resolution: ResolvedModuleWithFailedLookupLocations) { + return resolution.resolvedModule; + } + + function getResolvedTypeReferenceDirective(resolution: ResolvedTypeReferenceDirectiveWithFailedLookupLocations) { + return resolution.resolvedTypeReferenceDirective; + } + function setRootDirectory(dir: string) { Debug.assert(!resolvedModuleNames.size && !resolvedTypeReferenceDirectives.size && !directoryWatchesOfFailedLookups.size); rootDir = removeTrailingDirectorySeparator(getNormalizedAbsolutePath(dir, getCurrentDirectory())); @@ -100,6 +117,7 @@ namespace ts { function clear() { clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); + customFailedLookupPaths.clear(); closeTypeRootsWatch(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); @@ -160,14 +178,13 @@ namespace ts { return primaryResult; } - function resolveNamesWithLocalCache( + function resolveNamesWithLocalCache( names: string[], containingFile: string, cache: Map>, perDirectoryCache: Map>, loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, - getResult: (s: T) => R, - getResultFileName: (result: R) => string | undefined, + getResolutionFromNameResolutionWithFailedLookupLocations: (s: T) => R, logChanges: boolean): R[] { const path = resolutionHost.toPath(containingFile); @@ -208,7 +225,7 @@ namespace ts { } Debug.assert(resolution !== undefined && !resolution.isInvalidated); seenNamesInFile.set(name, true); - resolvedModules.push(getResult(resolution)); + resolvedModules.push(getResolutionFromNameResolutionWithFailedLookupLocations(resolution)); } // Stop watching and remove the unused name @@ -228,15 +245,15 @@ namespace ts { if (!oldResolution || !newResolution || oldResolution.isInvalidated) { return false; } - const oldResult = getResult(oldResolution); - const newResult = getResult(newResolution); + const oldResult = getResolutionFromNameResolutionWithFailedLookupLocations(oldResolution); + const newResult = getResolutionFromNameResolutionWithFailedLookupLocations(newResolution); if (oldResult === newResult) { return true; } if (!oldResult || !newResult) { return false; } - return getResultFileName(oldResult) === getResultFileName(newResult); + return oldResult.resolvedFileName === newResult.resolvedFileName; } } @@ -244,7 +261,7 @@ namespace ts { return resolveNamesWithLocalCache( typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, - resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, + resolveTypeReferenceDirective, getResolvedTypeReferenceDirective, /*logChanges*/ false ); } @@ -253,7 +270,7 @@ namespace ts { return resolveNamesWithLocalCache( moduleNames, containingFile, resolvedModuleNames, perDirectoryResolvedModuleNames, - resolveModuleName, m => m.resolvedModule, r => r.resolvedFileName, + resolveModuleName, getResolvedModule, logChanges ); } @@ -300,13 +317,24 @@ namespace ts { return { dir, dirPath }; } + function isPathWithDefaultFailedLookupExtension(path: Path) { + return fileExtensionIsOneOf(path, failedLookupDefaultExtensions); + } + function watchFailedLookupLocationOfResolution( resolution: T, startIndex?: number ) { if (resolution && resolution.failedLookupLocations) { for (let i = startIndex || 0; i < resolution.failedLookupLocations.length; i++) { const failedLookupLocation = resolution.failedLookupLocations[i]; - const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, resolutionHost.toPath(failedLookupLocation)); + const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); + // If the failed lookup location path is not one of the supported extensions, + // store it in the custom path + if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) { + const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0; + customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); + } + const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); if (dirWatcher) { dirWatcher.refCount++; @@ -330,7 +358,17 @@ namespace ts { if (resolution && resolution.failedLookupLocations) { for (let i = startIndex; i < resolution.failedLookupLocations.length; i++) { const failedLookupLocation = resolution.failedLookupLocations[i]; - const { dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, resolutionHost.toPath(failedLookupLocation)); + const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); + const refCount = customFailedLookupPaths.get(failedLookupLocationPath); + if (refCount) { + if (refCount === 1) { + customFailedLookupPaths.delete(failedLookupLocationPath); + } + else { + customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); + } + } + const { dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); // Do not close the watcher yet since it might be needed by other failed lookup locations. dirWatcher.refCount--; @@ -349,72 +387,106 @@ namespace ts { // If the files are added to project root or node_modules directory, always run through the invalidation process // Otherwise run through invalidation only if adding to the immediate directory if (dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrFolderPath) === dirPath) { - const isChangedFailedLookupLocation: (location: string) => boolean = dirPath === fileOrFolderPath ? - // If the file watched directory is created/deleted invalidate any resolution has failed lookup in this directory - location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)) : - // Otherwise only the resolutions referencing the file or folder added - location => resolutionHost.toPath(location) === fileOrFolderPath; - if (invalidateResolutionOfFailedLookupLocation(isChangedFailedLookupLocation)) { + let isChangedFailedLookupLocation: (location: string) => boolean; + if (dirPath === fileOrFolderPath) { + // Watching directory is created + // Invalidate any resolution has failed lookup in this directory + isChangedFailedLookupLocation = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)); + } + else { + // Some file or folder in the watching directory is created + // Return early if it does not have any of the watching extension or not the custom failed lookup path + if (!isPathWithDefaultFailedLookupExtension(fileOrFolderPath) && !customFailedLookupPaths.has(fileOrFolderPath)) { + return; + } + // Resolution need to be invalidated if failed lookup location is same as the file or folder getting created + isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrFolderPath; + } + const hasChangedFailedLookupLocation = (resolution: NameResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation); + if (invalidateResolutionOfFailedLookupLocation(hasChangedFailedLookupLocation)) { resolutionHost.onInvalidatedResolution(); } } }, WatchDirectoryFlags.Recursive); } - function invalidateResolutionCacheOfDeletedFile( - deletedFilePath: Path, + function invalidateResolutionCache( cache: Map>, - getResult: (s: T) => R, - getResultFileName: (result: R) => string | undefined) { - cache.forEach((value, path) => { - if (path === deletedFilePath) { - cache.delete(path); - if (value) { - value.forEach(stopWatchFailedLookupLocationOfResolution); + ignoreFile: (resolutions: Map, containingFilePath: Path) => boolean, + isInvalidatedResolution: (resolution: T) => boolean + ) { + const seen = createMap>(); + cache.forEach((resolutions, containingFilePath) => { + if (!ignoreFile(resolutions, containingFilePath as Path) && resolutions) { + const dirPath = getDirectoryPath(containingFilePath); + let seenInDir = seen.get(dirPath); + if (!seenInDir) { + seenInDir = createMap(); + seen.set(dirPath, seenInDir); } - } - else if (value) { - value.forEach(resolution => { - if (resolution && !resolution.isInvalidated) { - const result = getResult(resolution); - if (result) { - if (resolutionHost.toPath(getResultFileName(result)) === deletedFilePath) { - resolution.isInvalidated = true; - (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(path, true); - } - } + resolutions.forEach((resolution, name) => { + if (seenInDir.has(name)) { + return; + } + seenInDir.set(name, true); + if (!resolution.isInvalidated && isInvalidatedResolution(resolution)) { + // Mark the file as needing re-evaluation of module resolution instead of using it blindly. + resolution.isInvalidated = true; + (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFilePath, true); } }); } }); } + function invalidateResolutionCacheOfDeletedFile( + deletedFilePath: Path, + cache: Map>, + getResolutionFromNameResolutionWithFailedLookupLocations: (s: T) => R, + ) { + invalidateResolutionCache( + cache, + // Ignore file thats same as deleted file path, and handle it here + (resolutions, containingFilePath) => { + if (containingFilePath !== deletedFilePath) { + return false; + } + + // Deleted file, stop watching failed lookups for all the resolutions in the file + cache.delete(containingFilePath); + resolutions.forEach(stopWatchFailedLookupLocationOfResolution); + return true; + }, + // Resolution is invalidated if the resulting file name is same as the deleted file path + resolution => { + const result = getResolutionFromNameResolutionWithFailedLookupLocations(resolution); + return result && resolutionHost.toPath(result.resolvedFileName) === deletedFilePath; + } + ); + } + function invalidateResolutionOfFile(filePath: Path) { - invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); - invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); + invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, getResolvedModule); + invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, getResolvedTypeReferenceDirective); } function invalidateResolutionCacheOfFailedLookupLocation( cache: Map>, - isChangedFailedLookupLocation: (location: string) => boolean + hasChangedFailedLookupLocation: (resolution: T) => boolean ) { - cache.forEach((value, containingFile) => { - if (value) { - value.forEach(resolution => { - if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, isChangedFailedLookupLocation)) { - // Mark the file as needing re-evaluation of module resolution instead of using it blindly. - resolution.isInvalidated = true; - (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFile, true); - } - }); - } - }); + invalidateResolutionCache( + cache, + // Do not ignore any file + returnFalse, + // Resolution is invalidated if the resulting file name is same as the deleted file path + hasChangedFailedLookupLocation + ); } - function invalidateResolutionOfFailedLookupLocation(isChangedFailedLookupLocation: (location: string) => boolean) { + function invalidateResolutionOfFailedLookupLocation(hasChangedFailedLookupLocation: (resolution: NameResolutionWithFailedLookupLocations) => boolean) { const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; - invalidateResolutionCacheOfFailedLookupLocation(resolvedModuleNames, isChangedFailedLookupLocation); - invalidateResolutionCacheOfFailedLookupLocation(resolvedTypeReferenceDirectives, isChangedFailedLookupLocation); + invalidateResolutionCacheOfFailedLookupLocation(resolvedModuleNames, hasChangedFailedLookupLocation); + invalidateResolutionCacheOfFailedLookupLocation(resolvedTypeReferenceDirectives, hasChangedFailedLookupLocation); return filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a105e5fcbfe..d033de6a65c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4010,7 +4010,8 @@ namespace ts { Tsx = ".tsx", Dts = ".d.ts", Js = ".js", - Jsx = ".jsx" + Jsx = ".jsx", + Json = ".json" } export interface ResolvedModuleWithFailedLookupLocations { @@ -4025,7 +4026,7 @@ namespace ts { // True if the type declaration file was found in a primary lookup location primary: boolean; // The location of the .d.ts file we located, or undefined if resolution failed - resolvedFileName?: string; + resolvedFileName: string | undefined; } export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations { diff --git a/src/server/session.ts b/src/server/session.ts index 92f4b34a46e..6bcfef554d3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -469,12 +469,14 @@ namespace ts.server { index++; if (checkSpec.project.containsFile(checkSpec.fileName, requireOpen)) { this.syntacticCheck(checkSpec.fileName, checkSpec.project); - next.immediate(() => { - this.semanticCheck(checkSpec.fileName, checkSpec.project); - if (checkList.length > index) { - next.delay(followMs, checkOne); - } - }); + if (this.changeSeq === seq) { + next.immediate(() => { + this.semanticCheck(checkSpec.fileName, checkSpec.project); + if (checkList.length > index) { + next.delay(followMs, checkOne); + } + }); + } } } }; @@ -1287,11 +1289,11 @@ namespace ts.server { const start = scriptInfo.lineOffsetToPosition(args.line, args.offset); const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); if (start >= 0) { + this.changeSeq++; this.projectService.applyChangesToFile(scriptInfo, [{ span: { start, length: end - start }, newText: args.insertString }]); - this.changeSeq++; } } @@ -1698,8 +1700,8 @@ namespace ts.server { return this.requiredResponse(converted); }, [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { - this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles); this.changeSeq++; + this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles); // TODO: report errors return this.requiredResponse(/*response*/ true); },