From 587309d029964d17398bf74de99a49e5451957d3 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Mon, 19 Jun 2017 15:45:47 -0700 Subject: [PATCH 1/7] Update error case check `getTouchingWord` indicates failure by returning the sourceFile node, rather than `undefined`. --- src/services/documentHighlights.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/documentHighlights.ts b/src/services/documentHighlights.ts index 7ea8c519646..fc84b60cc1f 100644 --- a/src/services/documentHighlights.ts +++ b/src/services/documentHighlights.ts @@ -2,7 +2,10 @@ namespace ts.DocumentHighlights { export function getDocumentHighlights(program: Program, cancellationToken: CancellationToken, sourceFile: SourceFile, position: number, sourceFilesToSearch: SourceFile[]): DocumentHighlights[] | undefined { const node = getTouchingWord(sourceFile, position, /*includeJsDocComment*/ true); - if (!node) return undefined; + // Note that getTouchingWord indicates failure by returning the sourceFile node. + if (node === sourceFile) return undefined; + + Debug.assert(node.parent !== undefined); if (isJsxOpeningElement(node.parent) && node.parent.tagName === node || isJsxClosingElement(node.parent)) { // For a JSX element, just highlight the matching tag, not all references. From 4863ada22cdbf7278bcf5a6013b637db85d7d87c Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 20 Jun 2017 15:54:43 -0700 Subject: [PATCH 2/7] Track missing files 1. Expose missing files from the `Program`. 2. In `tsc --watch` and `tsserver`, add file watchers to missing files. 3. When missing files are created, schedule compilation (tsc) or refresh the containing projects (tsserver). --- src/compiler/program.ts | 23 +++++++++++++++++++++++ src/compiler/sys.ts | 13 +++++++++++-- src/compiler/tsc.ts | 13 +++++++++++++ src/compiler/types.ts | 6 ++++++ src/server/lsHost.ts | 7 +++++-- src/server/project.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/server/server.ts | 7 ++++++- 7 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 1d66eeaf34a..b292ff3965a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -513,6 +513,8 @@ namespace ts { } } + const missingFilePaths = filesByName.getKeys().filter(p => !filesByName.get(p)); + // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; @@ -524,6 +526,7 @@ namespace ts { getSourceFile, getSourceFileByPath, getSourceFiles: () => files, + getMissingFilePaths: () => missingFilePaths, getCompilerOptions: () => options, getSyntacticDiagnostics, getOptionsDiagnostics, @@ -862,11 +865,31 @@ namespace ts { return oldProgram.structureIsReused; } + // If a file has ceased to be missing, then we need to discard some of the old + // structure in order to pick it up. + // Caution: if the file has created and then deleted between since it was discovered to + // be missing, then the corresponding file watcher will have been closed and no new one + // will be created until we encounter a change that prevents complete structure reuse. + // During this interval, creation of the file will go unnoticed. We expect this to be + // both rare and low-impact. + if (oldProgram.getMissingFilePaths) { + const missingFilePaths: Path[] = oldProgram.getMissingFilePaths() || emptyArray; + for (const missingFilePath of missingFilePaths) { + if (host.fileExists(missingFilePath)) { + return oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + } + } + // update fileName -> file mapping for (let i = 0; i < newSourceFiles.length; i++) { filesByName.set(filePaths[i], newSourceFiles[i]); } + for (const p of oldProgram.getMissingFilePaths()) { + filesByName.set(p, undefined); + } + files = newSourceFiles; fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics(); diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index cd14f3b2801..8448c9b3615 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -340,11 +340,20 @@ namespace ts { } function fileChanged(curr: any, prev: any) { - if (+curr.mtime <= +prev.mtime) { + const isCurrZero = +curr.mtime === 0; + const isPrevZero = +prev.mtime === 0; + const added = !isCurrZero && isPrevZero; + const deleted = isCurrZero && !isPrevZero; + + // This value is consistent with poll() in createPollingWatchedFileSet() + // and depended upon by the file watchers created in Project.updateGraphWorker. + const removed = deleted ? true : (added ? false : undefined); + + if (!added && !deleted && +curr.mtime <= +prev.mtime) { return; } - callback(fileName); + callback(fileName, removed); } }, watchDirectory: (directoryName, callback, recursive) => { diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 3b2422bfe08..8264282cba9 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -285,6 +285,19 @@ namespace ts { setCachedProgram(compileResult.program); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); + + if (compileResult.program.getMissingFilePaths) { + const missingPaths = compileResult.program.getMissingFilePaths() || []; + missingPaths.forEach((path: Path): void => { + const fileWatcher = sys.watchFile(path, (_fileName: string, removed?: boolean) => { + // removed = deleted ? true : (added ? false : undefined) + if (removed === false) { + fileWatcher.close(); + startTimerForRecompilation(); + } + }); + }); + } } function cachedFileExists(fileName: string): boolean { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5c959d3736e..99d62091ed5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2426,6 +2426,12 @@ namespace ts { */ getSourceFiles(): SourceFile[]; + /** + * Get a list of file names that were passed to 'createProgram' or referenced in a + * program source file but could not be located. + */ + getMissingFilePaths?(): Path[]; + /** * Emits the JavaScript and declaration files. If targetSourceFile is not specified, then * the JavaScript and declaration files will be produced for all the files in this program. diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index b8f9807139f..1cccaf979cf 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -210,8 +210,11 @@ namespace ts.server { return this.host.resolvePath(path); } - fileExists(path: string): boolean { - return this.host.fileExists(path); + fileExists(file: string): boolean { + // As an optimization, don't hit the disks for files we already know don't exist + // (because we're watching for their creation). + const path = toPath(file, this.host.getCurrentDirectory(), this.getCanonicalFileName); + return !this.project.isWatchedMissingFile(path) && this.host.fileExists(file); } readFile(fileName: string): string { diff --git a/src/server/project.ts b/src/server/project.ts index d4544af5e59..d1b4a627fac 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -107,6 +107,7 @@ namespace ts.server { private rootFilesMap: Map = createMap(); private program: ts.Program; private externalFiles: SortedReadonlyArray; + private missingFilesMap: FileMap = createFileMap(); private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap(); private lastCachedUnresolvedImportsList: SortedReadonlyArray; @@ -606,6 +607,39 @@ namespace ts.server { } } + if (hasChanges && this.program.getMissingFilePaths) { + const missingFilePaths = this.program.getMissingFilePaths() || emptyArray; + const missingFilePathsSet = createMap(); + missingFilePaths.forEach(p => missingFilePathsSet.set(p, true)); + + // Files that are no longer missing (e.g. because they are no longer required) + // should no longer be watched. + this.missingFilesMap.getKeys().forEach(p => { + if (!missingFilePathsSet.has(p)) { + this.missingFilesMap.get(p).close(); + this.missingFilesMap.remove(p); + } + }); + + // Missing files that are not yet watched should be added to the map. + missingFilePaths.forEach(p => { + if (!this.missingFilesMap.contains(p)) { + const fileWatcher = ts.sys.watchFile(p, (_filename: string, removed?: boolean) => { + // removed = deleted ? true : (added ? false : undefined) + if (removed === false && this.missingFilesMap.contains(p)) { + fileWatcher.close(); + this.missingFilesMap.remove(p); + + // When a missing file is created, we should update the graph. + this.markAsDirty(); + this.updateGraph(); + } + }); + this.missingFilesMap.set(p, fileWatcher); + } + }); + } + const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; this.externalFiles = this.getExternalFiles(); enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, @@ -626,6 +660,10 @@ namespace ts.server { return hasChanges; } + isWatchedMissingFile(path: Path) { + return this.missingFilesMap.contains(path); + } + getScriptInfoLSHost(fileName: string) { const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); if (scriptInfo) { diff --git a/src/server/server.ts b/src/server/server.ts index 5f2b7054755..9874969064e 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -522,6 +522,9 @@ namespace ts.server { return; } + // removed = deleted ? true : (added ? false : undefined) + // This value is consistent with sys.watchFile() + // and depended upon by the file watchers created in performCompilation() in tsc's executeCommandLine(). fs.stat(watchedFile.fileName, (err: any, stats: any) => { if (err) { watchedFile.callback(watchedFile.fileName); @@ -560,7 +563,9 @@ namespace ts.server { const file: WatchedFile = { fileName, callback, - mtime: getModifiedTime(fileName) + mtime: sys.fileExists(fileName) + ? getModifiedTime(fileName) + : new Date(0) // Any subsequent modification will occur after this time }; watchedFiles.push(file); From a39e969338801423f79aa7263f8b86d8997e4a79 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 20 Jun 2017 16:30:33 -0700 Subject: [PATCH 3/7] Clean up file watchers on project close --- src/server/project.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/server/project.ts b/src/server/project.ts index d1b4a627fac..18c203c194e 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -311,6 +311,13 @@ namespace ts.server { this.lsHost.dispose(); this.lsHost = undefined; + // Clean up file watchers waiting for missing files + for (const p of this.missingFilesMap.getKeys()) { + this.missingFilesMap.get(p).close(); + this.missingFilesMap.remove(p); + } + this.missingFilesMap = undefined; + // signal language service to release source files acquired from document registry this.languageService.dispose(); this.languageService = undefined; From 4652fc491f692bac282365e5e165ed2da646006d Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 20 Jun 2017 16:31:47 -0700 Subject: [PATCH 4/7] Confirm method is defined before calling --- src/compiler/program.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index b292ff3965a..ac82fbba02c 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -879,6 +879,10 @@ namespace ts { return oldProgram.structureIsReused = StructureIsReused.SafeModules; } } + + for (const p of oldProgram.getMissingFilePaths()) { + filesByName.set(p, undefined); + } } // update fileName -> file mapping @@ -886,10 +890,6 @@ namespace ts { filesByName.set(filePaths[i], newSourceFiles[i]); } - for (const p of oldProgram.getMissingFilePaths()) { - filesByName.set(p, undefined); - } - files = newSourceFiles; fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics(); From 6d200bffbd36bf10dcc009135a4e3ec775e6e888 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 21 Jun 2017 10:49:17 -0700 Subject: [PATCH 5/7] Watch files through the host Call `this.projectService.host.watchFile`, rather than `ts.sys.watchFile` so that it gets mocked correctly in the unit tests. Repair two failing tests. --- src/harness/unittests/tsserverProjectSystem.ts | 2 +- src/harness/unittests/typingsInstaller.ts | 2 +- src/server/project.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 2f6ba7aa3bb..d098c91eecf 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -2028,7 +2028,7 @@ namespace ts.projectSystem { projectService.openExternalProject({ projectFileName, options: {}, rootFiles: [{ fileName: file1.path, scriptKind: ScriptKind.JS, hasMixedContent: true }] }); checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkWatchedFiles(host, []); + checkWatchedFiles(host, [libFile.path]); // watching the "missing" lib file const project = projectService.externalProjects[0]; diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 9483308287f..6c43ea1c034 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -731,7 +731,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); const p = projectService.configuredProjects[0]; checkProjectActualFiles(p, [app.path, jsconfig.path]); - checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules"]); + checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", libFile.path]); installer.installAll(/*expectedCount*/ 1); diff --git a/src/server/project.ts b/src/server/project.ts index 18c203c194e..163510bad27 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -631,7 +631,7 @@ namespace ts.server { // Missing files that are not yet watched should be added to the map. missingFilePaths.forEach(p => { if (!this.missingFilesMap.contains(p)) { - const fileWatcher = ts.sys.watchFile(p, (_filename: string, removed?: boolean) => { + const fileWatcher = this.projectService.host.watchFile(p, (_filename: string, removed?: boolean) => { // removed = deleted ? true : (added ? false : undefined) if (removed === false && this.missingFilesMap.contains(p)) { fileWatcher.close(); From 0f683ac2adafc657256029a2268262cbc35e0a20 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 6 Jun 2017 16:39:41 -0700 Subject: [PATCH 6/7] Add missing file unit tests 1. Test `Program.getMissingFilePaths` 2. Test program structure reuse (i.e. that the appearance of a missing file prevents complete reuse) --- Jakefile.js | 1 + src/harness/tsconfig.json | 3 +- .../unittests/cachingInServerLSHost.ts | 2 +- src/harness/unittests/programMissingFiles.ts | 109 ++++++++++++++++++ .../unittests/reuseProgramStructure.ts | 25 ++++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/harness/unittests/programMissingFiles.ts diff --git a/Jakefile.js b/Jakefile.js index e425a8767c4..58779a3d8dc 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -138,6 +138,7 @@ var harnessSources = harnessCoreSources.concat([ "telemetry.ts", "transform.ts", "customTransforms.ts", + "programMissingFiles.ts", ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index eee6473f77f..8d52791f539 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -129,6 +129,7 @@ "./unittests/transform.ts", "./unittests/customTransforms.ts", "./unittests/textChanges.ts", - "./unittests/telemetry.ts" + "./unittests/telemetry.ts", + "./unittests/programMissingFiles.ts" ] } diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index 46d9aa462ce..eb2907e89de 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -75,7 +75,7 @@ namespace ts { const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, /*containingProject*/ undefined); const project = projectService.createInferredProjectWithRootFileIfNecessary(rootScriptInfo); - project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); + project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } ); return { project, rootScriptInfo diff --git a/src/harness/unittests/programMissingFiles.ts b/src/harness/unittests/programMissingFiles.ts new file mode 100644 index 00000000000..26cc63590bd --- /dev/null +++ b/src/harness/unittests/programMissingFiles.ts @@ -0,0 +1,109 @@ +/// + +namespace ts { + describe("Program.getMissingFilePaths", () => { + + const options: CompilerOptions = { + noLib: true, + }; + + const emptyFileName = "empty.ts"; + const emptyFileRelativePath = "./" + emptyFileName; + + const emptyFile: Harness.Compiler.TestFile = { + unitName: emptyFileName, + content: "" + }; + + const referenceFileName = "reference.ts"; + const referenceFileRelativePath = "./" + referenceFileName; + + const referenceFile: Harness.Compiler.TestFile = { + unitName: referenceFileName, + content: + "/// \n" + // Absolute + "/// \n" + // Relative + "/// \n" + // Unqualified + "/// \n" // No extension + }; + + const testCompilerHost = Harness.Compiler.createCompilerHost( + /*inputFiles*/ [emptyFile, referenceFile], + /*writeFile*/ undefined, + /*scriptTarget*/ undefined, + /*useCaseSensitiveFileNames*/ false, + /*currentDirectory*/ "d:\\pretend\\", + /*newLineKind*/ NewLineKind.LineFeed, + /*libFiles*/ undefined + ); + + it("handles no missing root files", () => { + const program = createProgram([emptyFileRelativePath], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + assert.isDefined(missing); + assert.equal(missing.length, 0); + }); + + it("handles missing root file", () => { + const program = createProgram(["./nonexistent.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + assert.isDefined(missing); + assert.equal(missing.length, 1); + assert.equal(missing[0].toString(), "d:/pretend/nonexistent.ts"); // Absolute path + }); + + it("handles multiple missing root files", () => { + const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths().sort(); + assert.equal(missing.length, 2); + assert.equal(missing[0].toString(), "d:/pretend/nonexistent0.ts"); + assert.equal(missing[1].toString(), "d:/pretend/nonexistent1.ts"); + }); + + it("handles a mix of present and missing root files", () => { + const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths().sort(); + assert.equal(missing.length, 2); + assert.equal(missing[0].toString(), "d:/pretend/nonexistent0.ts"); + assert.equal(missing[1].toString(), "d:/pretend/nonexistent1.ts"); + }); + + it("handles repeatedly specified root files", () => { + const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + assert.isDefined(missing); + assert.equal(missing.length, 1); + assert.equal(missing[0].toString(), "d:/pretend/nonexistent.ts"); + }); + + it("normalizes file paths", () => { + const program0 = createProgram(["./nonexistent.ts", "./NONEXISTENT.ts"], options, testCompilerHost); + const program1 = createProgram(["./NONEXISTENT.ts", "./nonexistent.ts"], options, testCompilerHost); + const missing0 = program0.getMissingFilePaths(); + const missing1 = program1.getMissingFilePaths(); + assert.equal(missing0.length, 1); + assert.deepEqual(missing0, missing1); + }); + + it("handles missing triple slash references", () => { + const program = createProgram([referenceFileRelativePath], options, testCompilerHost); + const missing = program.getMissingFilePaths().sort(); + assert.isDefined(missing); + assert.equal(missing.length, 6); + + // From absolute reference + assert.equal(missing[0].toString(), "d:/imaginary/nonexistent1.ts"); + + // From relative reference + assert.equal(missing[1].toString(), "d:/pretend/nonexistent2.ts"); + + // From unqualified reference + assert.equal(missing[2].toString(), "d:/pretend/nonexistent3.ts"); + + // From no-extension reference + assert.equal(missing[3].toString(), "d:/pretend/nonexistent4.d.ts"); + assert.equal(missing[4].toString(), "d:/pretend/nonexistent4.ts"); + assert.equal(missing[5].toString(), "d:/pretend/nonexistent4.tsx"); + }); + }); +} \ No newline at end of file diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index 2eb3d1f0890..de5f5a756d8 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -316,6 +316,31 @@ namespace ts { assert.isTrue(program_1.structureIsReused === StructureIsReused.Not); }); + it("succeeds if missing files remain missing", () => { + const options: CompilerOptions = { target, noLib: true }; + + const program_1 = newProgram(files, ["a.ts"], options); + assert.notDeepEqual(emptyArray, program_1.getMissingFilePaths()); + + const program_2 = updateProgram(program_1, ["a.ts"], options, noop); + assert.deepEqual(program_1.getMissingFilePaths(), program_2.getMissingFilePaths()); + + assert.equal(StructureIsReused.Completely, program_1.structureIsReused); + }); + + it("fails if missing file is created", () => { + const options: CompilerOptions = { target, noLib: true }; + + const program_1 = newProgram(files, ["a.ts"], options); + assert.notDeepEqual(emptyArray, program_1.getMissingFilePaths()); + + const newTexts: NamedSourceText[] = files.concat([{ name: "non-existing-file.ts", text: SourceText.New("", "", `var x = 1`) }]); + const program_2 = updateProgram(program_1, ["a.ts"], options, noop, newTexts); + assert.deepEqual(emptyArray, program_2.getMissingFilePaths()); + + assert.equal(StructureIsReused.SafeModules, program_1.structureIsReused); + }); + it("resolution cache follows imports", () => { (Error).stackTraceLimit = Infinity; From 569ecabb0a6c83f8c0eb9249dd79cfa3e4e91231 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 22 Jun 2017 13:47:54 -0700 Subject: [PATCH 7/7] Address PR feedback Make Program.getMissingFilePaths required Assume getMissingFilePaths always returns a defined value Make getMissingFilePaths internal Replace nullable-bool with enum Update type to reflect possibility of undefined Use deepEqual to simplify tests Make condition const Don't bother cleaning up map before freeing it Switch from foreach to for-of to simplify debugging Use a Map, rather than a FileMap, to track open FileWatchers Fix compilation errors Introduce and consume arrayToSet Fix lint warnings about misplaced braces Delete incorrect comment Delete from map during iteration Eliminate unnecessary type annotations --- src/compiler/core.ts | 9 ++++ src/compiler/program.ts | 19 +++----- src/compiler/sys.ts | 24 ++++++---- src/compiler/tsc.ts | 25 +++++------ src/compiler/types.ts | 3 +- src/harness/unittests/compileOnSave.ts | 6 +-- src/harness/unittests/programMissingFiles.ts | 40 +++++++---------- src/harness/unittests/projectErrors.ts | 4 +- .../unittests/tsserverProjectSystem.ts | 40 ++++++++--------- src/harness/unittests/typingsInstaller.ts | 2 +- src/server/project.ts | 45 ++++++++----------- src/server/server.ts | 10 ++--- 12 files changed, 112 insertions(+), 115 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 86ad483ccc3..93c7eed7aca 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1097,6 +1097,15 @@ namespace ts { return result; } + /** + * Creates a set from the elements of an array. + * + * @param array the array of input elements. + */ + export function arrayToSet(array: T[], makeKey: (value: T) => string): Map { + return arrayToMap(array, makeKey, () => true); + } + export function cloneMap(map: Map) { const clone = createMap(); copyEntries(map, clone); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index ac82fbba02c..7b62fde8570 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -472,7 +472,7 @@ namespace ts { resolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile) => loadWithLocalCache(typeReferenceDirectiveNames, containingFile, loader); } - const filesByName = createMap(); + const filesByName = createMap(); // stores 'filename -> file association' ignoring case // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap(fileName => fileName.toLowerCase()) : undefined; @@ -513,7 +513,7 @@ namespace ts { } } - const missingFilePaths = filesByName.getKeys().filter(p => !filesByName.get(p)); + const missingFilePaths = arrayFrom(filesByName.keys(), p => p).filter(p => !filesByName.get(p)); // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; @@ -872,17 +872,12 @@ namespace ts { // will be created until we encounter a change that prevents complete structure reuse. // During this interval, creation of the file will go unnoticed. We expect this to be // both rare and low-impact. - if (oldProgram.getMissingFilePaths) { - const missingFilePaths: Path[] = oldProgram.getMissingFilePaths() || emptyArray; - for (const missingFilePath of missingFilePaths) { - if (host.fileExists(missingFilePath)) { - return oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - } + if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) { + return oldProgram.structureIsReused = StructureIsReused.SafeModules; + } - for (const p of oldProgram.getMissingFilePaths()) { - filesByName.set(p, undefined); - } + for (const p of oldProgram.getMissingFilePaths()) { + filesByName.set(p, undefined); } // update fileName -> file mapping diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 8448c9b3615..ca6f85cf0dd 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -4,7 +4,13 @@ declare function setTimeout(handler: (...args: any[]) => void, timeout: number): declare function clearTimeout(handle: any): void; namespace ts { - export type FileWatcherCallback = (fileName: string, removed?: boolean) => void; + export enum FileWatcherEventKind { + Created, + Changed, + Deleted + } + + export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; export type DirectoryWatcherCallback = (fileName: string) => void; export interface WatchedFile { fileName: string; @@ -174,7 +180,7 @@ namespace ts { const callbacks = fileWatcherCallbacks.get(fileName); if (callbacks) { for (const fileCallback of callbacks) { - fileCallback(fileName); + fileCallback(fileName, FileWatcherEventKind.Changed); } } } @@ -342,18 +348,20 @@ namespace ts { function fileChanged(curr: any, prev: any) { const isCurrZero = +curr.mtime === 0; const isPrevZero = +prev.mtime === 0; - const added = !isCurrZero && isPrevZero; + const created = !isCurrZero && isPrevZero; const deleted = isCurrZero && !isPrevZero; - // This value is consistent with poll() in createPollingWatchedFileSet() - // and depended upon by the file watchers created in Project.updateGraphWorker. - const removed = deleted ? true : (added ? false : undefined); + const eventKind = created + ? FileWatcherEventKind.Created + : deleted + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; - if (!added && !deleted && +curr.mtime <= +prev.mtime) { + if (eventKind === FileWatcherEventKind.Changed && +curr.mtime <= +prev.mtime) { return; } - callback(fileName, removed); + callback(fileName, eventKind); } }, watchDirectory: (directoryName, callback, recursive) => { diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 8264282cba9..6e0aefd99a0 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -286,18 +286,15 @@ namespace ts { setCachedProgram(compileResult.program); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); - if (compileResult.program.getMissingFilePaths) { - const missingPaths = compileResult.program.getMissingFilePaths() || []; - missingPaths.forEach((path: Path): void => { - const fileWatcher = sys.watchFile(path, (_fileName: string, removed?: boolean) => { - // removed = deleted ? true : (added ? false : undefined) - if (removed === false) { - fileWatcher.close(); - startTimerForRecompilation(); - } - }); + const missingPaths = compileResult.program.getMissingFilePaths(); + missingPaths.forEach(path => { + const fileWatcher = sys.watchFile(path, (_fileName, eventKind) => { + if (eventKind === FileWatcherEventKind.Created) { + fileWatcher.close(); + startTimerForRecompilation(); + } }); - } + }); } function cachedFileExists(fileName: string): boolean { @@ -321,7 +318,7 @@ namespace ts { const sourceFile = hostGetSourceFile(fileName, languageVersion, onError); if (sourceFile && isWatchSet(compilerOptions) && sys.watchFile) { // Attach a file watcher - sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (_fileName: string, removed?: boolean) => sourceFileChanged(sourceFile, removed)); + sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (_fileName, eventKind) => sourceFileChanged(sourceFile, eventKind)); } return sourceFile; } @@ -343,10 +340,10 @@ namespace ts { } // If a source file changes, mark it as unwatched and start the recompilation timer - function sourceFileChanged(sourceFile: SourceFile, removed?: boolean) { + function sourceFileChanged(sourceFile: SourceFile, eventKind: FileWatcherEventKind) { sourceFile.fileWatcher.close(); sourceFile.fileWatcher = undefined; - if (removed) { + if (eventKind === FileWatcherEventKind.Deleted) { unorderedRemoveItem(rootFileNames, sourceFile.fileName); } startTimerForRecompilation(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 99d62091ed5..bb2994a487f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2430,7 +2430,8 @@ namespace ts { * Get a list of file names that were passed to 'createProgram' or referenced in a * program source file but could not be located. */ - getMissingFilePaths?(): Path[]; + /* @internal */ + getMissingFilePaths(): Path[]; /** * Emits the JavaScript and declaration files. If targetSourceFile is not specified, then diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index 7e262a1b257..79e1f921dcb 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -208,7 +208,7 @@ namespace ts.projectSystem { file1Consumer1.content = `let y = 10;`; host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); - host.triggerFileWatcherCallback(file1Consumer1.path, /*removed*/ false); + host.triggerFileWatcherCallback(file1Consumer1.path, FileWatcherEventKind.Changed); session.executeCommand(changeModuleFile1ShapeRequest1); sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); @@ -225,7 +225,7 @@ namespace ts.projectSystem { session.executeCommand(changeModuleFile1ShapeRequest1); // Delete file1Consumer2 host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]); - host.triggerFileWatcherCallback(file1Consumer2.path, /*removed*/ true); + host.triggerFileWatcherCallback(file1Consumer2.path, FileWatcherEventKind.Deleted); sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); }); @@ -475,7 +475,7 @@ namespace ts.projectSystem { openFilesForSession([referenceFile1], session); host.reloadFS([referenceFile1, configFile]); - host.triggerFileWatcherCallback(moduleFile1.path, /*removed*/ true); + host.triggerFileWatcherCallback(moduleFile1.path, FileWatcherEventKind.Deleted); const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); sendAffectedFileRequestAndCheckResult(session, request, [ diff --git a/src/harness/unittests/programMissingFiles.ts b/src/harness/unittests/programMissingFiles.ts index 26cc63590bd..668b684a250 100644 --- a/src/harness/unittests/programMissingFiles.ts +++ b/src/harness/unittests/programMissingFiles.ts @@ -41,39 +41,33 @@ namespace ts { const program = createProgram([emptyFileRelativePath], options, testCompilerHost); const missing = program.getMissingFilePaths(); assert.isDefined(missing); - assert.equal(missing.length, 0); + assert.deepEqual(missing, []); }); it("handles missing root file", () => { const program = createProgram(["./nonexistent.ts"], options, testCompilerHost); const missing = program.getMissingFilePaths(); assert.isDefined(missing); - assert.equal(missing.length, 1); - assert.equal(missing[0].toString(), "d:/pretend/nonexistent.ts"); // Absolute path + assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path }); it("handles multiple missing root files", () => { const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost); const missing = program.getMissingFilePaths().sort(); - assert.equal(missing.length, 2); - assert.equal(missing[0].toString(), "d:/pretend/nonexistent0.ts"); - assert.equal(missing[1].toString(), "d:/pretend/nonexistent1.ts"); + assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); }); it("handles a mix of present and missing root files", () => { const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost); const missing = program.getMissingFilePaths().sort(); - assert.equal(missing.length, 2); - assert.equal(missing[0].toString(), "d:/pretend/nonexistent0.ts"); - assert.equal(missing[1].toString(), "d:/pretend/nonexistent1.ts"); + assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); }); it("handles repeatedly specified root files", () => { const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost); const missing = program.getMissingFilePaths(); assert.isDefined(missing); - assert.equal(missing.length, 1); - assert.equal(missing[0].toString(), "d:/pretend/nonexistent.ts"); + assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); }); it("normalizes file paths", () => { @@ -89,21 +83,21 @@ namespace ts { const program = createProgram([referenceFileRelativePath], options, testCompilerHost); const missing = program.getMissingFilePaths().sort(); assert.isDefined(missing); - assert.equal(missing.length, 6); + assert.deepEqual(missing, [ + // From absolute reference + "d:/imaginary/nonexistent1.ts", - // From absolute reference - assert.equal(missing[0].toString(), "d:/imaginary/nonexistent1.ts"); + // From relative reference + "d:/pretend/nonexistent2.ts", - // From relative reference - assert.equal(missing[1].toString(), "d:/pretend/nonexistent2.ts"); + // From unqualified reference + "d:/pretend/nonexistent3.ts", - // From unqualified reference - assert.equal(missing[2].toString(), "d:/pretend/nonexistent3.ts"); - - // From no-extension reference - assert.equal(missing[3].toString(), "d:/pretend/nonexistent4.d.ts"); - assert.equal(missing[4].toString(), "d:/pretend/nonexistent4.ts"); - assert.equal(missing[5].toString(), "d:/pretend/nonexistent4.tsx"); + // From no-extension reference + "d:/pretend/nonexistent4.d.ts", + "d:/pretend/nonexistent4.ts", + "d:/pretend/nonexistent4.tsx" + ]); }); }); } \ No newline at end of file diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index 30942fb2fdc..f250d4d9b36 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -135,7 +135,7 @@ namespace ts.projectSystem { } // fix config and trigger watcher host.reloadFS([file1, file2, correctConfig]); - host.triggerFileWatcherCallback(correctConfig.path, /*false*/); + host.triggerFileWatcherCallback(correctConfig.path, FileWatcherEventKind.Changed); { projectService.checkNumberOfProjects({ configuredProjects: 1 }); const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); @@ -177,7 +177,7 @@ namespace ts.projectSystem { } // break config and trigger watcher host.reloadFS([file1, file2, corruptedConfig]); - host.triggerFileWatcherCallback(corruptedConfig.path, /*false*/); + host.triggerFileWatcherCallback(corruptedConfig.path, FileWatcherEventKind.Changed); { projectService.checkNumberOfProjects({ configuredProjects: 1 }); const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index d098c91eecf..66873ee2411 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -485,12 +485,12 @@ namespace ts.projectSystem { } } - triggerFileWatcherCallback(fileName: string, removed?: boolean): void { + triggerFileWatcherCallback(fileName: string, eventKind: FileWatcherEventKind): void { const path = this.toPath(fileName); const callbacks = this.watchedFiles.get(path); if (callbacks) { for (const callback of callbacks) { - callback(path, removed); + callback(path, eventKind); } } } @@ -771,7 +771,7 @@ namespace ts.projectSystem { // remove the tsconfig file host.reloadFS(filesWithoutConfig); - host.triggerFileWatcherCallback(configFile.path); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkNumberOfInferredProjects(projectService, 2); checkNumberOfConfiguredProjects(projectService, 0); @@ -928,7 +928,7 @@ namespace ts.projectSystem { "files": ["${commonFile1.path}"] }`; host.reloadFS(files); - host.triggerFileWatcherCallback(configFile.path); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkNumberOfConfiguredProjects(projectService, 1); checkProjectRootFiles(project, [commonFile1.path]); @@ -1002,7 +1002,7 @@ namespace ts.projectSystem { "files": ["${file1.path}"] }`; host.reloadFS(files); - host.triggerFileWatcherCallback(configFile.path); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); checkNumberOfInferredProjects(projectService, 1); }); @@ -1433,7 +1433,7 @@ namespace ts.projectSystem { }; host.reloadFS([file1, modifiedFile2, file3]); - host.triggerFileWatcherCallback(modifiedFile2.path, /*removed*/ false); + host.triggerFileWatcherCallback(modifiedFile2.path, FileWatcherEventKind.Changed); checkNumberOfInferredProjects(projectService, 1); checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); @@ -1465,7 +1465,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { inferredProjects: 1 }); host.reloadFS([file1, file3]); - host.triggerFileWatcherCallback(file2.path, /*removed*/ true); + host.triggerFileWatcherCallback(file2.path, FileWatcherEventKind.Deleted); checkNumberOfProjects(projectService, { inferredProjects: 2 }); @@ -1663,7 +1663,7 @@ namespace ts.projectSystem { }; host.reloadFS([file1, file2, modifiedConfigFile]); - host.triggerFileWatcherCallback(configFile.path, /*removed*/ false); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); @@ -1696,7 +1696,7 @@ namespace ts.projectSystem { }; host.reloadFS([file1, file2, modifiedConfigFile]); - host.triggerFileWatcherCallback(configFile.path, /*removed*/ false); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); @@ -1776,7 +1776,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); host.reloadFS([file1, file2]); - host.triggerFileWatcherCallback(config.path, /*removed*/ true); + host.triggerFileWatcherCallback(config.path, FileWatcherEventKind.Deleted); checkNumberOfProjects(projectService, { inferredProjects: 2 }); checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); @@ -2258,7 +2258,7 @@ namespace ts.projectSystem { assert.isFalse(lastEvent.data.languageServiceEnabled, "Language service state"); host.reloadFS([f1, f2, configWithExclude]); - host.triggerFileWatcherCallback(config.path, /*removed*/ false); + host.triggerFileWatcherCallback(config.path, FileWatcherEventKind.Changed); checkNumberOfProjects(projectService, { configuredProjects: 1 }); assert.isTrue(project.languageServiceEnabled, "Language service enabled"); @@ -2492,7 +2492,7 @@ namespace ts.projectSystem { // rename tsconfig.json back to lib.ts host.reloadFS([f1, f2]); - host.triggerFileWatcherCallback(tsconfig.path, /*removed*/ true); + host.triggerFileWatcherCallback(tsconfig.path, FileWatcherEventKind.Deleted); projectService.openExternalProject({ projectFileName: projectName, rootFiles: toExternalFiles([f1.path, f2.path]), @@ -2637,7 +2637,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [libES5.path, app.path, config1.path]); host.reloadFS([libES5, libES2015Promise, app, config2]); - host.triggerFileWatcherCallback(config1.path); + host.triggerFileWatcherCallback(config1.path, FileWatcherEventKind.Changed); projectService.checkNumberOfProjects({ configuredProjects: 1 }); checkProjectActualFiles(projectService.configuredProjects[0], [libES5.path, libES2015Promise.path, app.path, config2.path]); @@ -2860,7 +2860,7 @@ namespace ts.projectSystem { const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1]); - host.triggerFileWatcherCallback(moduleFileOldPath); + host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Changed); host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; @@ -2868,7 +2868,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1]); - host.triggerFileWatcherCallback(moduleFileNewPath); + host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Changed); host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); @@ -2912,7 +2912,7 @@ namespace ts.projectSystem { const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, configFile]); - host.triggerFileWatcherCallback(moduleFileOldPath); + host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Changed); host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; @@ -2920,7 +2920,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, configFile]); - host.triggerFileWatcherCallback(moduleFileNewPath); + host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Changed); host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; @@ -3085,7 +3085,7 @@ namespace ts.projectSystem { } }`; host.reloadFS([file, configFile]); - host.triggerFileWatcherCallback(configFile.path); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); host.runQueuedTimeoutCallbacks(); serverEventManager.checkEventCountOfType("configFileDiag", 2); @@ -3093,7 +3093,7 @@ namespace ts.projectSystem { "compilerOptions": {} }`; host.reloadFS([file, configFile]); - host.triggerFileWatcherCallback(configFile.path); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); host.runQueuedTimeoutCallbacks(); serverEventManager.checkEventCountOfType("configFileDiag", 3); }); @@ -4021,7 +4021,7 @@ namespace ts.projectSystem { configFile.content = configFileContentWithoutCommentLine; host.reloadFS([file, configFile]); - host.triggerFileWatcherCallback(configFile.path); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); const diagsAfterEdit = session.executeCommand({ type: "request", diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 6c43ea1c034..728eb057fc3 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -820,7 +820,7 @@ namespace ts.projectSystem { installer.checkPendingCommands(/*expectedCount*/ 0); host.reloadFS([f, fixedPackageJson]); - host.triggerFileWatcherCallback(fixedPackageJson.path, /*removed*/ false); + host.triggerFileWatcherCallback(fixedPackageJson.path, FileWatcherEventKind.Changed); // expected install request installer.installAll(/*expectedCount*/ 1); diff --git a/src/server/project.ts b/src/server/project.ts index 163510bad27..25573216b1c 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -107,7 +107,7 @@ namespace ts.server { private rootFilesMap: Map = createMap(); private program: ts.Program; private externalFiles: SortedReadonlyArray; - private missingFilesMap: FileMap = createFileMap(); + private missingFilesMap: Map = createMap(); private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap(); private lastCachedUnresolvedImportsList: SortedReadonlyArray; @@ -312,10 +312,7 @@ namespace ts.server { this.lsHost = undefined; // Clean up file watchers waiting for missing files - for (const p of this.missingFilesMap.getKeys()) { - this.missingFilesMap.get(p).close(); - this.missingFilesMap.remove(p); - } + this.missingFilesMap.forEach(fileWatcher => fileWatcher.close()); this.missingFilesMap = undefined; // signal language service to release source files acquired from document registry @@ -594,12 +591,12 @@ namespace ts.server { const oldProgram = this.program; this.program = this.languageService.getProgram(); - let hasChanges = false; // bump up the version if // - oldProgram is not set - this is a first time updateGraph is called // - newProgram is different from the old program and structure of the old program was not reused. - if (!oldProgram || (this.program !== oldProgram && !(oldProgram.structureIsReused & StructureIsReused.Completely))) { - hasChanges = true; + const hasChanges = !oldProgram || (this.program !== oldProgram && !(oldProgram.structureIsReused & StructureIsReused.Completely)); + + if (hasChanges) { if (oldProgram) { for (const f of oldProgram.getSourceFiles()) { if (this.program.getSourceFileByPath(f.path)) { @@ -612,39 +609,35 @@ namespace ts.server { } } } - } - if (hasChanges && this.program.getMissingFilePaths) { - const missingFilePaths = this.program.getMissingFilePaths() || emptyArray; - const missingFilePathsSet = createMap(); - missingFilePaths.forEach(p => missingFilePathsSet.set(p, true)); + const missingFilePaths = this.program.getMissingFilePaths(); + const missingFilePathsSet = arrayToSet(missingFilePaths, p => p); // Files that are no longer missing (e.g. because they are no longer required) // should no longer be watched. - this.missingFilesMap.getKeys().forEach(p => { - if (!missingFilePathsSet.has(p)) { - this.missingFilesMap.get(p).close(); - this.missingFilesMap.remove(p); + this.missingFilesMap.forEach((fileWatcher, missingFilePath) => { + if (!missingFilePathsSet.has(missingFilePath)) { + this.missingFilesMap.delete(missingFilePath); + fileWatcher.close(); } }); // Missing files that are not yet watched should be added to the map. - missingFilePaths.forEach(p => { - if (!this.missingFilesMap.contains(p)) { - const fileWatcher = this.projectService.host.watchFile(p, (_filename: string, removed?: boolean) => { - // removed = deleted ? true : (added ? false : undefined) - if (removed === false && this.missingFilesMap.contains(p)) { + for (const missingFilePath of missingFilePaths) { + if (!this.missingFilesMap.has(missingFilePath)) { + const fileWatcher = this.projectService.host.watchFile(missingFilePath, (_filename: string, eventKind: FileWatcherEventKind) => { + if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { fileWatcher.close(); - this.missingFilesMap.remove(p); + this.missingFilesMap.delete(missingFilePath); // When a missing file is created, we should update the graph. this.markAsDirty(); this.updateGraph(); } }); - this.missingFilesMap.set(p, fileWatcher); + this.missingFilesMap.set(missingFilePath, fileWatcher); } - }); + } } const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; @@ -668,7 +661,7 @@ namespace ts.server { } isWatchedMissingFile(path: Path) { - return this.missingFilesMap.contains(path); + return this.missingFilesMap.has(path); } getScriptInfoLSHost(fileName: string) { diff --git a/src/server/server.ts b/src/server/server.ts index 9874969064e..dda87d16b6a 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -522,16 +522,16 @@ namespace ts.server { return; } - // removed = deleted ? true : (added ? false : undefined) - // This value is consistent with sys.watchFile() - // and depended upon by the file watchers created in performCompilation() in tsc's executeCommandLine(). fs.stat(watchedFile.fileName, (err: any, stats: any) => { if (err) { - watchedFile.callback(watchedFile.fileName); + watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Changed); } else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { watchedFile.mtime = stats.mtime; - watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0); + const eventKind = watchedFile.mtime.getTime() === 0 + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; + watchedFile.callback(watchedFile.fileName, eventKind); } }); }