From 0572b15adc2b3c19ff2912ff7510a0d700d85f06 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Sat, 15 Jul 2017 19:11:27 -0700 Subject: [PATCH] Instead of watching directories, watch tsconfig files of inferred project root --- .../unittests/tsserverProjectSystem.ts | 15 +- src/server/editorServices.ts | 288 ++++++++++++------ src/server/project.ts | 13 +- 3 files changed, 215 insertions(+), 101 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 2272102924..22bf06bef6 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -281,7 +281,7 @@ namespace ts.projectSystem { } export function checkMapKeys(caption: string, map: Map, expectedKeys: string[]) { - assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map`); + assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}`); for (const name of expectedKeys) { assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`); } @@ -800,7 +800,7 @@ namespace ts.projectSystem { const project = projectService.inferredProjects[0]; checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); - checkWatchedDirectories(host, ["/a/b/c", "/a/b", "/a"]); + checkWatchedFiles(host, ["/a/b/c/tsconfig.json", "/a/b/tsconfig.json", "/a/tsconfig.json", libFile.path, moduleFile.path]); }); it("can handle tsconfig file name with difference casing", () => { @@ -890,15 +890,15 @@ namespace ts.projectSystem { projectService.openClientFile(commonFile2.path); checkNumberOfInferredProjects(projectService, 2); - checkWatchedDirectories(host, ["/a/b", "/a"]); + checkWatchedFiles(host, [configFile.path, "/a/tsconfig.json", libFile.path]); // Add a tsconfig file host.reloadFS(filesWithConfig); - + host.checkTimeoutQueueLengthAndRun(1); checkNumberOfInferredProjects(projectService, 1); checkNumberOfConfiguredProjects(projectService, 1); // watching all files except one that was open - checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedFiles(host, [libFile.path, configFile.path, "/a/tsconfig.json"]); // remove the tsconfig file host.reloadFS(filesWithoutConfig); @@ -908,7 +908,7 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, 2); checkNumberOfConfiguredProjects(projectService, 0); - checkWatchedDirectories(host, ["/a/b", "/a"]); + checkWatchedFiles(host, ["/a/b/tsconfig.json", "/a/tsconfig.json", libFile.path]); }); it("add new files to a configured project without file list", () => { @@ -1315,7 +1315,7 @@ namespace ts.projectSystem { host.reloadFS([file1, configFile, file2, file3, libFile]); - + host.checkTimeoutQueueLengthAndRun(1); checkNumberOfConfiguredProjects(projectService, 1); checkNumberOfInferredProjects(projectService, 1); checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); @@ -1728,6 +1728,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); host.reloadFS([file1, file2, file3, configFile]); + host.checkTimeoutQueueLengthAndRun(1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, file3.path, configFile.path]); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 410872e369..698dbe3ffd 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -245,57 +245,14 @@ namespace ts.server { } } - class DirectoryWatchers { - /** - * a path to directory watcher map that detects added tsconfig files - */ - private readonly directoryWatchersForTsconfig: Map = createMap(); - /** - * count of how many projects are using the directory watcher. - * If the number becomes 0 for a watcher, then we should close it. - */ - private readonly directoryWatchersRefCount: Map = createMap(); - - constructor(private readonly projectService: ProjectService) { - } - - stopWatchingDirectory(directory: string) { - // if the ref count for this directory watcher drops to 0, it's time to close it - const refCount = this.directoryWatchersRefCount.get(directory) - 1; - this.directoryWatchersRefCount.set(directory, refCount); - if (refCount === 0) { - this.projectService.logger.info(`Close directory watcher for: ${directory}`); - this.directoryWatchersForTsconfig.get(directory).close(); - this.directoryWatchersForTsconfig.delete(directory); - } - } - - startWatchingContainingDirectoriesForFile(fileName: string, project: InferredProject, callback: (fileName: string) => void) { - let currentPath = getDirectoryPath(fileName); - let parentPath = getDirectoryPath(currentPath); - while (currentPath !== parentPath) { - if (!this.directoryWatchersForTsconfig.has(currentPath)) { - this.projectService.logger.info(`Add watcher for: ${currentPath}`); - this.directoryWatchersForTsconfig.set(currentPath, this.projectService.host.watchDirectory(currentPath, callback)); - this.directoryWatchersRefCount.set(currentPath, 1); - } - else { - this.directoryWatchersRefCount.set(currentPath, this.directoryWatchersRefCount.get(currentPath) + 1); - } - project.directoriesWatchedForTsconfig.push(currentPath); - currentPath = parentPath; - parentPath = getDirectoryPath(parentPath); - } - } - } - /* @internal */ export const enum WatchType { ConfigFilePath = "Config file for the program", MissingFilePath = "Missing file from program", WildCardDirectories = "Wild card directory", TypeRoot = "Type root of the project", - ClosedScriptInfo = "Closed Script info" + ClosedScriptInfo = "Closed Script info", + ConfigFileForInferredRoot = "Config file for the root script info of the inferred project" } /* @internal */ @@ -308,12 +265,27 @@ namespace ts.server { OrphanScriptInfoWithChange = "Orphan script info, Detected change in file thats not needed any more", OrphanScriptInfo = "Removing Orphan script info as part of cleanup", FileDeleted = "File was deleted", - FileOpened = "File opened" + FileOpened = "File opened", + ConfigProjectCreated = "Config file project created" + } + + const enum ConfigFileWatcherStatus { + ReloadingFiles = "Reloading configured projects files", + NoAction = "No action on files", + UpdatedCallback = "Updated the callback", + TrackingFileAdded = "Tracking file added", + TrackingFileRemoved = "Tracking file removed" } /* @internal */ export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; + type ConfigFileExistence = { + exists: boolean; + trackingOpenFiles?: ScriptInfo[]; + configFileWatcher?: FileWatcher; + }; + export interface ProjectServiceOptions { host: ServerHost; logger: Logger; @@ -362,8 +334,7 @@ namespace ts.server { private compilerOptionsForInferredProjects: CompilerOptions; private compileOnSaveForInferredProjects: boolean; private readonly projectToSizeMap: Map = createMap(); - private readonly directoryWatchers: DirectoryWatchers; - private readonly mapOfTsConfigPresence: Map; + private readonly mapOfConfigFilePresence: Map; private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; @@ -407,8 +378,7 @@ namespace ts.server { this.currentDirectory = this.host.getCurrentDirectory(); this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); - this.directoryWatchers = new DirectoryWatchers(this); - this.mapOfTsConfigPresence = createMap(); + this.mapOfConfigFilePresence = createMap(); this.throttledOperations = new ThrottledOperations(this.host); this.typingsInstaller.attach(this); @@ -519,10 +489,6 @@ namespace ts.server { this.delayUpdateProjectGraphs(this.inferredProjects); } - stopWatchingDirectory(directory: string) { - this.directoryWatchers.stopWatchingDirectory(directory); - } - findProject(projectName: string): Project { if (projectName === undefined) { return undefined; @@ -701,36 +667,53 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); if (eventKind === FileWatcherEventKind.Deleted) { + // Update the cached status + // No action needed on tracking open files since the existing config file anyways didnt affect the tracking file + configFilePresenceInfo.exists = false; + this.logTrackingFiles(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.NoAction); this.removeProject(project); + // Reload the configured projects for these open files in the project as // they could be held up by another config file somewhere in the parent directory - const openFilesInProject = filter(this.openFiles, file => file.containingProjects.length === 0); - this.reloadConfiguredProjectForFiles(openFilesInProject, project => { project.pendingReload = true; this.delayUpdateProjectGraph(project); }); - this.delayInferredProjectsRefresh(); + const orphanFiles = filter(this.openFiles, file => file.containingProjects.length === 0); + this.delayReloadConfiguredProjectForFiles(orphanFiles); } else { project.pendingReload = true; - this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + this.logTrackingFiles(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + if (configFilePresenceInfo.trackingOpenFiles) { + this.delayUpdateProjectGraph(project); + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFiles); + } + else { + this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + } } } /** - * This is the callback function when a watched directory has an added tsconfig file. + * This is the callback function for the config file add/remove/change for the root in the inferred project */ - private onConfigFileAddedForInferredProject(fileName: string) { - // TODO: check directory separators - if (getBaseFileName(fileName) !== "tsconfig.json") { - this.logger.info(`${fileName} is not tsconfig.json`); - return; - } + private onConfigFileAddedForInferredProject(configFileName: NormalizedPath, eventKind: FileWatcherEventKind) { + // This callback is called only if we dont have config file project for this config file + const cononicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(cononicalConfigPath); - // TODO: (sheetalkamat) - // 1. We should only watch tsconfig/jsconfig file here instead of watching directory - // 2. We should try reloading projects with open files in Inferred project only - // 3. We should use this watcher to answer questions to findConfigFile rather than calling host everytime - this.logger.info(`Detected newly added tsconfig file: ${fileName}`); - this.reloadProjects(); + if (eventKind === FileWatcherEventKind.Deleted) { + // No action needed if the event was for deletion of the file + // - because the existing config file didnt affect the inferred project roots anyways + configFilePresenceInfo.exists = false; + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.NoAction); + } + else { + // Either the config file was created or changed + // Reload the projects + configFilePresenceInfo.exists = true; + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFiles); + } } private removeProject(project: Project) { @@ -882,13 +865,131 @@ namespace ts.server { private configFileExists(configFileName: NormalizedPath) { const canonicalConfigFilePath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); - const cachedResult = this.mapOfTsConfigPresence.get(canonicalConfigFilePath); - if (cachedResult) { - // Use the information here to answer the question + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + if (configFilePresenceInfo) { + return configFilePresenceInfo.exists; } return this.host.fileExists(configFileName); } + private setConfigFilePresenceByNewConfiguredProject(project: ConfiguredProject) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); + if (configFilePresenceInfo) { + configFilePresenceInfo.exists = true; + // close existing watcher + if (configFilePresenceInfo.configFileWatcher) { + const configFileName = project.getConfigFilePath(); + this.closeFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + configFilePresenceInfo.configFileWatcher, WatcherCloseReason.ConfigProjectCreated + ); + configFilePresenceInfo.configFileWatcher = undefined; + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } + } + else { + // Mark existence of the config file with the project creation + this.mapOfConfigFilePresence.set(project.canonicalConfigFilePath, { exists: true }); + } + } + + /* @internal */ + setConfigFilePresenceByClosedConfigFile(closedProject: ConfiguredProject) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(closedProject.canonicalConfigFilePath); + Debug.assert(!!configFilePresenceInfo); + if (configFilePresenceInfo.trackingOpenFiles) { + const configFileName = closedProject.getConfigFilePath(); + configFilePresenceInfo.configFileWatcher = this.addFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + (_filename, eventKind) => this.onConfigFileAddedForInferredProject(configFileName, eventKind) + ); + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } + else { + // There is no one tracking anymore. Remove the status + this.mapOfConfigFilePresence.delete(closedProject.canonicalConfigFilePath); + } + } + + private logTrackingFiles(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { + const watchType = configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForInferredRoot : WatchType.ConfigFilePath; + const files = map(configFilePresenceInfo.trackingOpenFiles, info => info.fileName); + this.logger.info(`FileWatcher:: ${watchType}: File: ${configFileName} Currently Tracking for files: ${files} Status: ${status}`); + } + + private watchConfigFileForInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo) { + let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + if (configFilePresenceInfo) { + // Existing information - just add to tracking files + (configFilePresenceInfo.trackingOpenFiles || (configFilePresenceInfo.trackingOpenFiles = [])).push(root); + } + else { + // Add new callback + configFilePresenceInfo = { + exists: this.host.fileExists(configFileName), + trackingOpenFiles: [root], + configFileWatcher: this.addFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + (_fileName, eventKind) => this.onConfigFileAddedForInferredProject(configFileName, eventKind) + ) + }; + this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); + } + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + } + + private closeWatchConfigFileForInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo, reason: WatcherCloseReason) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + Debug.assert(!!configFilePresenceInfo); + if (configFilePresenceInfo.trackingOpenFiles.length === 1) { + configFilePresenceInfo.trackingOpenFiles = undefined; + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); + if (configFilePresenceInfo.configFileWatcher) { + this.closeFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, + configFileName, configFilePresenceInfo.configFileWatcher, reason + ); + this.mapOfConfigFilePresence.delete(canonicalConfigFilePath); + } + } + else { + removeItemFromSet(configFilePresenceInfo.trackingOpenFiles, root); + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); + } + } + + private enumerateWatchingRootOfInferredProject(root: ScriptInfo, + action: (configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo) => void) { + let current = root.fileName; + let currentPath = getDirectoryPath(root.path); + let parentPath = getDirectoryPath(currentPath); + while (currentPath !== parentPath) { + current = asNormalizedPath(getDirectoryPath(current)); + action(asNormalizedPath(combinePaths(current, "tsconfig.json")), combinePaths(currentPath, "tsconfig.json"), root); + //if (root.isJavaScript()) { + // this.watchConfigFileForInferredRoot(asNormalizedPath(combinePaths(current, "jsconfig.json")), combinePaths(currentPath, "jsconfig.json"), root); + //} + currentPath = parentPath; + parentPath = getDirectoryPath(parentPath); + } + } + + /*@internal*/ + startWatchingRootOfInferredProject(root: ScriptInfo) { + this.enumerateWatchingRootOfInferredProject(root, + (configFileName, canonicalConfigFilePath, root) => + this.watchConfigFileForInferredRoot(configFileName, canonicalConfigFilePath, root) + ); + } + + /*@internal*/ + stopWatchingRootOfInferredProject(root: ScriptInfo, reason: WatcherCloseReason) { + this.enumerateWatchingRootOfInferredProject(root, + (configFileName, canonicalConfigFilePath, root) => + this.closeWatchConfigFileForInferredRoot(configFileName, canonicalConfigFilePath, root, reason) + ); + } + /** * This function tries to search for a tsconfig.json for the given file. * This is different from the method the compiler uses because @@ -1124,6 +1225,7 @@ namespace ts.server { this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); this.configuredProjects.push(project); + this.setConfigFilePresenceByNewConfiguredProject(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; } @@ -1172,7 +1274,7 @@ namespace ts.server { if (scriptInfo.isScriptOpen()) { // if file is already root in some inferred project // - remove the file from that project and delete the project if necessary - this.removRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo); + this.removeRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo); } } } @@ -1261,11 +1363,7 @@ namespace ts.server { : new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects); project.addRoot(root); - - this.directoryWatchers.startWatchingContainingDirectoriesForFile( - root.fileName, - project, - fileName => this.onConfigFileAddedForInferredProject(fileName)); + this.startWatchingRootOfInferredProject(root); project.updateGraph(); if (!useExistingProject) { @@ -1415,11 +1513,22 @@ namespace ts.server { */ reloadProjects() { this.logger.info("reload projects."); - this.reloadConfiguredProjectForFiles(this.openFiles, project => this.reloadConfiguredProject(project)); + this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false); this.refreshInferredProjects(); } - reloadConfiguredProjectForFiles(openFiles: ScriptInfo[], reload: (project: ConfiguredProject) => void) { + delayReloadConfiguredProjectForFiles(openFiles: ScriptInfo[]) { + this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); + this.delayInferredProjectsRefresh(); + } + + /** + * This function goes through all the openFiles and tries to file the config file for them. + * If the config file is found and it refers to existing project, it reloads it either immediately + * or schedules it for reload depending on delayedReload option + * If the there is no existing project it just opens the configured project for the config file + */ + reloadConfiguredProjectForFiles(openFiles: ScriptInfo[], delayReload: boolean) { const mapUpdatedProjects = createMap(); // try to reload config file for all open files for (const info of openFiles) { @@ -1435,7 +1544,13 @@ namespace ts.server { mapUpdatedProjects.set(configFileName, true); } else if (!mapUpdatedProjects.has(configFileName)) { - reload(project); + if (delayReload) { + project.pendingReload = true; + this.delayUpdateProjectGraph(project); + } + else { + this.reloadConfiguredProject(project); + } mapUpdatedProjects.set(configFileName, true); } } @@ -1448,7 +1563,7 @@ namespace ts.server { * - references in inferred project supercede the root part * - root/reference in non-inferred project beats root in inferred project */ - private removRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { + private removeRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { if (info.containingProjects.length > 1 && info.containingProjects[0].projectKind === ProjectKind.Inferred && info.containingProjects[0].isRoot(info)) { @@ -1478,7 +1593,7 @@ namespace ts.server { } // Or remove the root of inferred project if is referenced in more than one projects else { - this.removRootOfInferredProjectIfNowPartOfOtherProject(info); + this.removeRootOfInferredProjectIfNowPartOfOtherProject(info); } } @@ -1766,7 +1881,8 @@ namespace ts.server { const rootFiles: protocol.ExternalFile[] = []; for (const file of proj.rootFiles) { const normalized = toNormalizedPath(file.fileName); - if (getBaseFileName(normalized) === "tsconfig.json") { + const baseFileName = getBaseFileName(normalized); + if (baseFileName === "tsconfig.json" || baseFileName === "jsconfig.json") { if (this.host.fileExists(normalized)) { (tsConfigFiles || (tsConfigFiles = [])).push(normalized); } diff --git a/src/server/project.ts b/src/server/project.ts index 1b99e76545..2e14c75efc 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -648,7 +648,7 @@ namespace ts.server { missingFilePath => { const fileWatcher = this.projectService.addFileWatcher( WatchType.MissingFilePath, this, missingFilePath, - (filename: string, eventKind: FileWatcherEventKind) => { + (filename, eventKind) => { if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { this.missingFilesMap.delete(missingFilePath); this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); @@ -911,9 +911,6 @@ namespace ts.server { super.setCompilerOptions(newOptions); } - // Used to keep track of what directories are watched for this project - directoriesWatchedForTsconfig: string[] = []; - constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions) { super(InferredProject.newName(), ProjectKind.Inferred, @@ -927,6 +924,7 @@ namespace ts.server { } addRoot(info: ScriptInfo) { + this.projectService.startWatchingRootOfInferredProject(info); if (!this._isJsInferredProject && info.isJavaScript()) { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } @@ -934,6 +932,7 @@ namespace ts.server { } removeRoot(info: ScriptInfo) { + this.projectService.stopWatchingRootOfInferredProject(info, WatcherCloseReason.NotNeeded); super.removeRoot(info); if (this._isJsInferredProject && info.isJavaScript()) { if (!some(this.getRootScriptInfos(), info => info.isJavaScript())) { @@ -958,11 +957,8 @@ namespace ts.server { } close() { + forEach(this.getRootScriptInfos(), root => this.projectService.stopWatchingRootOfInferredProject(root, WatcherCloseReason.ProjectClose)); super.close(); - - for (const directory of this.directoriesWatchedForTsconfig) { - this.projectService.stopWatchingDirectory(directory); - } } getTypeAcquisition(): TypeAcquisition { @@ -1212,6 +1208,7 @@ namespace ts.server { if (this.configFileWatcher) { this.projectService.closeFileWatcher(WatchType.ConfigFilePath, this, this.getConfigFilePath(), this.configFileWatcher, WatcherCloseReason.ProjectClose); this.configFileWatcher = undefined; + this.projectService.setConfigFilePresenceByClosedConfigFile(this); } this.stopWatchingTypeRoots(WatcherCloseReason.ProjectClose);