From f723beb24491e51aff667a31663c11e24c8a889c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 7 Aug 2017 14:47:32 -0700 Subject: [PATCH] More updates per PR feedback --- src/compiler/core.ts | 13 + .../unittests/cachingInServerLSHost.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 2 + src/server/editorServices.ts | 519 +++++++++--------- src/services/refactorProvider.ts | 2 +- 5 files changed, 284 insertions(+), 254 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 54b772cc29c..829a6afa4b7 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -534,6 +534,19 @@ namespace ts { return result; } + export function mapDefinedIter(iter: Iterator, mapFn: (x: T) => U | undefined): U[] { + const result: U[] = []; + while (true) { + const { value, done } = iter.next(); + if (done) break; + const res = mapFn(value); + if (res !== undefined) { + result.push(res); + } + } + return result; + } + /** * Computes the first matching span of elements and returns a tuple of the first span * and the remaining elements. diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index eb2907e89de..3498030142a 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -74,7 +74,7 @@ namespace ts { const projectService = new server.ProjectService(svcOpts); const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, /*containingProject*/ undefined); - const project = projectService.createInferredProjectWithRootFileIfNecessary(rootScriptInfo); + const project = projectService.assignScriptInfoToInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } ); return { project, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 7d70aa74fdf..deaa17c2afe 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -263,6 +263,8 @@ namespace ts.projectSystem { function invokeWatcherCallbacks(callbacks: T[], invokeCallback: (cb: T) => void): void { if (callbacks) { + // The array copy is made to ensure that even if one of the callback removes the callbacks, + // we dont miss any callbacks following it const cbs = callbacks.slice(); for (const cb of cbs) { invokeCallback(cb); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5ca8a54d71f..4d704a365b5 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -271,16 +271,16 @@ namespace ts.server { ReloadingFiles = "Reloading configured projects for files", ReloadingInferredRootFiles = "Reloading configured projects for only inferred root files", UpdatedCallback = "Updated the callback", - TrackingFileAdded = "Tracking file added", - TrackingFileRemoved = "Tracking file removed", - InferredRootAdded = "Inferred Root file added", - InferredRootRemoved = "Inferred Root file removed", + OpenFilesImpactedByConfigFileAdd = "File added to open files impacted by this config file", + OpenFilesImpactedByConfigFileRemove = "File removed from open files impacted by this config file", + RootOfInferredProjectTrue = "Open file was set as Inferred root", + RootOfInferredProjectFalse = "Open file was set as not inferred root", } /* @internal */ export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; - interface ConfigFileExistence { + interface ConfigFileExistenceInfo { /** * Cached value of existence of config file * It is true if there is configured project open for this file. @@ -290,18 +290,19 @@ namespace ts.server { */ exists: boolean; /** - * The value in the trackingOpenFilesMap is true if the open file is inferred project root - * and hence tracking changes to this config file - * (either config file is already present but doesnt include the open file in the project structure or config file doesnt exist) - * - * Otherwise its false + * openFilesImpactedByConfigFiles is a map of open files that would be impacted by this config file + * because these are the paths being looked up for their default configured project location + * The value in the map is true if the open file is root of the inferred project + * It is false when the open file that would still be impacted by existance of + * this config file but it is not the root of inferred project */ - trackingOpenFilesMap: Map; + openFilesImpactedByConfigFile: Map; /** - * The file watcher corresponding to this config file for the inferred project root - * The watcher is present only when there is no open configured project for this config file + * The file watcher watching the config file because there is open script info that is root of + * inferred project and will be impacted by change in the status of the config file + * The watcher is present only when there is no open configured project for the config file */ - configFileWatcher?: FileWatcher; + configFileWatcherForRootOfInferredProject?: FileWatcher; } export interface ProjectServiceOptions { @@ -351,6 +352,9 @@ namespace ts.server { private compilerOptionsForInferredProjects: CompilerOptions; private compileOnSaveForInferredProjects: boolean; + /** + * Project size for configured or external projects + */ private readonly projectToSizeMap: Map = createMap(); /** * This is a map of config file paths existance that doesnt need query to disk @@ -359,7 +363,7 @@ namespace ts.server { * - Or it is present if we have configured project open with config file at that location * In this case the exists property is always true */ - private readonly mapOfConfigFilePresence = createMap(); + private readonly mapOfConfigFileExistenceInfo = createMap(); private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; @@ -427,14 +431,17 @@ namespace ts.server { return this.changedFiles; } + /* @internal */ ensureInferredProjectsUpToDate_TestOnly() { this.ensureProjectStructuresUptoDate(); } + /* @internal */ getCompilerOptionsForInferredProjects() { return this.compilerOptionsForInferredProjects; } + /* @internal */ onUpdateLanguageServiceStateForProject(project: Project, languageServiceEnabled: boolean) { if (!this.eventHandler) { return; @@ -479,14 +486,13 @@ namespace ts.server { const projectName = project.getProjectName(); this.pendingProjectUpdates.set(projectName, project); this.throttledOperations.schedule(projectName, /*delay*/ 250, () => { - const project = this.pendingProjectUpdates.get(projectName); - if (project) { - this.pendingProjectUpdates.delete(projectName); + if (this.pendingProjectUpdates.delete(projectName)) { project.updateGraph(); } }); } + /* @internal */ delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) { this.delayUpdateProjectGraph(project); this.delayInferredProjectsRefresh(); @@ -513,7 +519,7 @@ namespace ts.server { this.delayUpdateProjectGraphs(this.inferredProjects); } - findProject(projectName: string): Project { + findProject(projectName: string): Project | undefined { if (projectName === undefined) { return undefined; } @@ -687,39 +693,46 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.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; + // We arent updating or removing the cached config file presence info as that will be taken care of by + // setConfigFilePresenceByClosedConfigFile when the project is closed (depending on tracking open files) + configFileExistenceInfo.exists = false; this.removeProject(project); // Reload the configured projects for the open files in the map as they are affectected by this config file - this.logConfigFileWatchUpdate(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); - // Since the configured project was deleted, we want to reload projects for all the open files - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ false); + // Since the configured project was deleted, we want to reload projects for all the open files including files + // that are not root of the inferred project + this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ false); } else { - this.logConfigFileWatchUpdate(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingInferredRootFiles); + this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingInferredRootFiles); project.pendingReload = true; this.delayUpdateProjectGraph(project); - // As we scheduled the updated project graph, we would need to only schedule the project reload for the inferred project roots - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ true); + // As we scheduled the update on configured project graph, + // we would need to schedule the project reload for only the root of inferred projects + this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ true); } } /** - * This is the callback function for the config file add/remove/change at any location that matters to open - * script info but doesnt have configured project open for the config file + * This is the callback function for the config file add/remove/change at any location + * that matters to open script info but doesnt have configured project open + * for the config file */ private onConfigFileChangeForOpenScriptInfo(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); - configFilePresenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); - // The tracking opens files would only contaion the inferred root so no need to check - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ false); + const canonicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigPath); + configFileExistenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigPath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + + // Because there is no configured project open for the config file, the tracking open files map + // will only have open files that need the re-detection of the project and hence + // reload projects for all the tracking open files in the map + this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ false); } private removeProject(project: Project) { @@ -737,7 +750,7 @@ namespace ts.server { case ProjectKind.Configured: this.configuredProjects.delete((project).canonicalConfigFilePath); this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath); - this.setConfigFilePresenceByClosedConfigFile(project); + this.setConfigFileExistenceInfoByClosedConfiguredProject(project); break; case ProjectKind.Inferred: unorderedRemoveItem(this.inferredProjects, project); @@ -745,47 +758,16 @@ namespace ts.server { } } - private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void { - if (info.containingProjects.length === 0) { - // create new inferred project p with the newly opened file as root - // or add root to existing inferred project if 'useOneInferredProject' is true - this.createInferredProjectWithRootFileIfNecessary(info); - - // if useOneInferredProject is not set then try to fixup ownership of open files - // check 'defaultProject !== inferredProject' is necessary to handle cases - // when creation inferred project for some file has added other open files into this project - // (i.e.as referenced files) - // we definitely don't want to delete the project that was just created - // Also note that we need to create a copy of the array since the list of project will change - for (const inferredProject of this.inferredProjects.slice(0, this.inferredProjects.length - 1)) { - Debug.assert(!this.useSingleInferredProject); - // Remove this file from the root of inferred project if its part of more than 2 projects - // This logic is same as iterating over all open files and calling - // this.removRootOfInferredProjectIfNowPartOfOtherProject(f); - // Since this is also called from refreshInferredProject and closeOpen file - // to update inferred projects of the open file, this iteration might be faster - // instead of scanning all open files - const root = inferredProject.getRootScriptInfos(); - Debug.assert(root.length === 1); - if (root[0].containingProjects.length > 1) { - this.removeProject(inferredProject); - } - } - } - else { - for (const p of info.containingProjects) { - // file is the part of configured project - if (p.projectKind === ProjectKind.Configured) { - if (addToListOfOpenFiles) { - ((p)).addOpenRef(); - } - } + private addToListOfOpenFiles(info: ScriptInfo) { + Debug.assert(info.containingProjects.length !== 0); + for (const p of info.containingProjects) { + // file is the part of configured project, addref the project + if (p.projectKind === ProjectKind.Configured) { + ((p)).addOpenRef(); } } - if (addToListOfOpenFiles) { - this.openFiles.push(info); - } + this.openFiles.push(info); } /** @@ -837,17 +819,16 @@ namespace ts.server { this.removeProject(project); } - // collect orphaned files and try to re-add them as newly opened - // treat orphaned files as newly opened - // for all open files + // collect orphaned files and assign them to inferred project just like we treat open of a file for (const f of this.openFiles) { if (f.containingProjects.length === 0) { - this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false); + this.assignScriptInfoToInferredProject(f); } } - // Cleanup script infos that arent part of any project is postponed to - // next file open so that if file from same project is opened we wont end up creating same script infos + // Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project) + // is postponed to next file open so that if file from same project is opened, + // we wont end up creating same script infos } // If the current info is being just closed - add the watcher file to track changes @@ -871,15 +852,15 @@ namespace ts.server { } private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { - let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (configFilePresenceInfo) { - // By default the info is belong to the config file. - // Only adding the info as a root to inferred project will make it the root - if (!configFilePresenceInfo.trackingOpenFilesMap.has(info.path)) { - configFilePresenceInfo.trackingOpenFilesMap.set(info.path, false); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + let configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + if (configFileExistenceInfo) { + // By default the info would get impacted by presence of config file since its in the detection path + // Only adding the info as a root to inferred project will need the existence to be watched by file watcher + if (!configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) { + configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd); } - return configFilePresenceInfo.exists; + return configFileExistenceInfo.exists; } // Theoretically we should be adding watch for the directory here itself. @@ -887,162 +868,148 @@ namespace ts.server { // somewhere inside the another config file directory. // And technically we could handle that case in configFile's directory watcher in some cases // But given that its a rare scenario it seems like too much overhead. (we werent watching those directories earlier either) - // So what we are now watching is: configFile if the project is open - // And the whole chain of config files only for the inferred project roots - // Cache the host value of file exists and add the info tio to the tracked root - const trackingOpenFilesMap = createMap(); - trackingOpenFilesMap.set(info.path, false); + // So what we are now watching is: configFile if the configured project corresponding to it is open + // Or the whole chain of config files for the roots of the inferred projects + + // Cache the host value of file exists and add the info to map of open files impacted by this config file + const openFilesImpactedByConfigFile = createMap(); + openFilesImpactedByConfigFile.set(info.path, false); const exists = this.host.fileExists(configFileName); - configFilePresenceInfo = { exists, trackingOpenFilesMap }; - this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + configFileExistenceInfo = { exists, openFilesImpactedByConfigFile }; + this.mapOfConfigFileExistenceInfo.set(canonicalConfigFilePath, configFileExistenceInfo); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd); return exists; } - private setConfigFilePresenceByNewConfiguredProject(project: ConfiguredProject) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); - if (configFilePresenceInfo) { - Debug.assert(configFilePresenceInfo.exists); + private setConfigFileExistenceByNewConfiguredProject(project: ConfiguredProject) { + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(project.canonicalConfigFilePath); + if (configFileExistenceInfo) { + Debug.assert(configFileExistenceInfo.exists); // close existing watcher - if (configFilePresenceInfo.configFileWatcher) { + if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) { const configFileName = project.getConfigFilePath(); this.closeFileWatcher( WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - configFilePresenceInfo.configFileWatcher, WatcherCloseReason.ConfigProjectCreated + configFileExistenceInfo.configFileWatcherForRootOfInferredProject, WatcherCloseReason.ConfigProjectCreated ); - configFilePresenceInfo.configFileWatcher = undefined; - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined; + this.logConfigFileWatchUpdate(configFileName, project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); } } else { - // We could be in this scenario if it is the external project tracked configured file + // We could be in this scenario if project is the configured project tracked by external project // Since that route doesnt check if the config file is present or not - this.mapOfConfigFilePresence.set(project.canonicalConfigFilePath, { + this.mapOfConfigFileExistenceInfo.set(project.canonicalConfigFilePath, { exists: true, - trackingOpenFilesMap: createMap() + openFilesImpactedByConfigFile: createMap() }); } } - private configFileExistenceTracksInferredRoot(configFilePresenceInfo: ConfigFileExistence) { - return forEachEntry(configFilePresenceInfo.trackingOpenFilesMap, (value, __key) => value); + /** + * Returns true if the configFileExistenceInfo is needed/impacted by open files that are root of inferred project + */ + private configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo: ConfigFileExistenceInfo) { + return forEachEntry(configFileExistenceInfo.openFilesImpactedByConfigFile, (isRootOfInferredProject, __key) => isRootOfInferredProject); } - private setConfigFilePresenceByClosedConfigFile(closedProject: ConfiguredProject) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(closedProject.canonicalConfigFilePath); - Debug.assert(!!configFilePresenceInfo); - const trackingOpenFilesMap = configFilePresenceInfo.trackingOpenFilesMap; - if (trackingOpenFilesMap.size) { + private setConfigFileExistenceInfoByClosedConfiguredProject(closedProject: ConfiguredProject) { + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(closedProject.canonicalConfigFilePath); + Debug.assert(!!configFileExistenceInfo); + if (configFileExistenceInfo.openFilesImpactedByConfigFile.size) { const configFileName = closedProject.getConfigFilePath(); - if (this.configFileExistenceTracksInferredRoot(configFilePresenceInfo)) { - Debug.assert(!configFilePresenceInfo.configFileWatcher); - configFilePresenceInfo.configFileWatcher = this.addFileWatcher( + // If there are open files that are impacted by this config file existence + // but none of them are root of inferred project, the config file watcher will be + // created when any of the script infos are added as root of inferred project + if (this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { + Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject); + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = this.addFileWatcher( WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) ); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + this.logConfigFileWatchUpdate(configFileName, closedProject.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); } } else { - // There is no one tracking anymore. Remove the status - this.mapOfConfigFilePresence.delete(closedProject.canonicalConfigFilePath); + // There is not a single file open thats tracking the status of this config file. Remove from cache + this.mapOfConfigFileExistenceInfo.delete(closedProject.canonicalConfigFilePath); } } - private logConfigFileWatchUpdate(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { + private logConfigFileWatchUpdate(configFileName: NormalizedPath, canonicalConfigFilePath: string, configFileExistenceInfo: ConfigFileExistenceInfo, status: ConfigFileWatcherStatus) { if (!this.logger.loggingEnabled()) { return; } const inferredRoots: string[] = []; const otherFiles: string[] = []; - configFilePresenceInfo.trackingOpenFilesMap.forEach((value, key: Path) => { - const info = this.getScriptInfoForPath(key); - if (value) { - inferredRoots.push(info.fileName); - } - else { - otherFiles.push(info.fileName); - } + configFileExistenceInfo.openFilesImpactedByConfigFile.forEach((isRootOfInferredProject, key) => { + const info = this.getScriptInfoForPath(key as Path); + (isRootOfInferredProject ? inferredRoots : otherFiles).push(info.fileName); }); - const watchType = status === ConfigFileWatcherStatus.UpdatedCallback || - status === ConfigFileWatcherStatus.ReloadingFiles || - status === ConfigFileWatcherStatus.ReloadingInferredRootFiles ? - (configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForInferredRoot : WatchType.ConfigFilePath) : - ""; - this.logger.info(`ConfigFilePresence ${watchType}:: File: ${configFileName} Currently Tracking: InferredRootFiles: ${inferredRoots} OtherFiles: ${otherFiles} Status: ${status}`); + + const watches: WatchType[] = []; + if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) { + watches.push(WatchType.ConfigFileForInferredRoot); + } + if (this.configuredProjects.has(canonicalConfigFilePath)) { + watches.push(WatchType.ConfigFilePath); + } + this.logger.info(`ConfigFilePresence:: Current Watches: ['${watches.join("','")}']:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`); } - private closeConfigFileWatcherIfInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, - configFilePresenceInfo: ConfigFileExistence, infoIsInferredRoot: boolean, reason: WatcherCloseReason) { - // Close the config file watcher if it was the last inferred root - if (infoIsInferredRoot && - configFilePresenceInfo.configFileWatcher && - !this.configFileExistenceTracksInferredRoot(configFilePresenceInfo)) { + /** + * Close the config file watcher in the cached ConfigFileExistenceInfo + * if there arent any open files that are root of inferred project + */ + private closeConfigFileWatcherOfConfigFileExistenceInfo( + configFileName: NormalizedPath, configFileExistenceInfo: ConfigFileExistenceInfo, + reason: WatcherCloseReason + ) { + // Close the config file watcher if there are no more open files that are root of inferred project + if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject && + !this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { this.closeFileWatcher( WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - configFilePresenceInfo.configFileWatcher, reason - ); - configFilePresenceInfo.configFileWatcher = undefined; - } - - // If this was the last tracking file open for this config file, remove the cached value - if (!configFilePresenceInfo.trackingOpenFilesMap.size && - !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { - this.mapOfConfigFilePresence.delete(canonicalConfigFilePath); - } - } - - private closeConfigFileWatchForClosedScriptInfo(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (configFilePresenceInfo) { - const isInferredRoot = configFilePresenceInfo.trackingOpenFilesMap.get(info.path); - - // Delete the info from tracking - configFilePresenceInfo.trackingOpenFilesMap.delete(info.path); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); - - // Close the config file watcher if it was the last inferred root - this.closeConfigFileWatcherIfInferredRoot(configFileName, canonicalConfigFilePath, - configFilePresenceInfo, isInferredRoot, WatcherCloseReason.FileClosed + configFileExistenceInfo.configFileWatcherForRootOfInferredProject, reason ); + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined; } } /** * This is called on file close, so that we stop watching the config file for this script info - * @param info */ private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) { Debug.assert(!info.isScriptOpen()); - this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => - this.closeConfigFileWatchForClosedScriptInfo(configFileName, canonicalConfigFilePath, info) - ); - } + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + if (configFileExistenceInfo) { + const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile.get(info.path); - private watchConfigFileForInferredProjectRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { - let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (!configFilePresenceInfo) { - // Create the cache - configFilePresenceInfo = { - exists: this.host.fileExists(configFileName), - trackingOpenFilesMap: createMap() - }; - this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); - } + // Delete the info from map, since this file is no more open + configFileExistenceInfo.openFilesImpactedByConfigFile.delete(info.path); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileRemove); - // Set this file as inferred root - configFilePresenceInfo.trackingOpenFilesMap.set(info.path, true); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.InferredRootAdded); + // If the script info was not root of inferred project, + // there wont be config file watch open because of this script info + if (infoIsRootOfInferredProject) { + // But if it is a root, it could be the last script info that is root of inferred project + // and hence we would need to close the config file watcher + this.closeConfigFileWatcherOfConfigFileExistenceInfo( + configFileName, configFileExistenceInfo, WatcherCloseReason.FileClosed + ); + } - // If there is no configured project for this config file, create the watcher - if (!configFilePresenceInfo.configFileWatcher && - !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { - configFilePresenceInfo.configFileWatcher = this.addFileWatcher(WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - (_fileName, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) - ); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); - } + // If there are no open files that are impacted by configFileExistenceInfo after closing this script info + // there is no configured project present, remove the cached existence info + if (!configFileExistenceInfo.openFilesImpactedByConfigFile.size && + !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { + Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject); + this.mapOfConfigFileExistenceInfo.delete(canonicalConfigFilePath); + } + } + }); } /** @@ -1051,25 +1018,30 @@ namespace ts.server { /* @internal */ startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); - this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => - this.watchConfigFileForInferredProjectRoot(configFileName, canonicalConfigFilePath, info) - ); - } - - private closeWatchConfigFileForInferredProjectRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo, reason: WatcherCloseReason) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (configFilePresenceInfo) { - // Set this as not inferred root - if (configFilePresenceInfo.trackingOpenFilesMap.has(info.path)) { - configFilePresenceInfo.trackingOpenFilesMap.set(info.path, false); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.InferredRootRemoved); + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { + let configFilePresenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + if (!configFilePresenceInfo) { + // Create the cache + configFilePresenceInfo = { + exists: this.host.fileExists(configFileName), + openFilesImpactedByConfigFile: createMap() + }; + this.mapOfConfigFileExistenceInfo.set(canonicalConfigFilePath, configFilePresenceInfo); } - // Close the watcher if present - this.closeConfigFileWatcherIfInferredRoot(configFileName, canonicalConfigFilePath, - configFilePresenceInfo, /*infoIsInferredRoot*/ true, reason - ); - } + // Set this file as the root of inferred project + configFilePresenceInfo.openFilesImpactedByConfigFile.set(info.path, true); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFilePresenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectTrue); + + // If there is no configured project for this config file, add the file watcher + if (!configFilePresenceInfo.configFileWatcherForRootOfInferredProject && + !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { + configFilePresenceInfo.configFileWatcherForRootOfInferredProject = this.addFileWatcher(WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + (_fileName, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) + ); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } + }); } /** @@ -1077,9 +1049,21 @@ namespace ts.server { */ /* @internal */ stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, reason: WatcherCloseReason) { - this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => - this.closeWatchConfigFileForInferredProjectRoot(configFileName, canonicalConfigFilePath, info, reason) - ); + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + if (configFileExistenceInfo && configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) { + Debug.assert(info.isScriptOpen()); + + // Info is not root of inferred project any more + configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectFalse); + + // Close the config file watcher + this.closeConfigFileWatcherOfConfigFileExistenceInfo( + configFileName, configFileExistenceInfo, reason + ); + } + }); } /** @@ -1165,7 +1149,6 @@ namespace ts.server { function printProjects(logger: Logger, projects: Project[], counter: number) { for (const project of projects) { - // Print shouldnt update the graph. It should emit whatever state the project is currently in logger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`); logger.info(project.filesToString()); logger.info("-----------------------------------------------"); @@ -1175,13 +1158,13 @@ namespace ts.server { } } - private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { + private findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined { // make sure that casing of config file name is consistent const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath); } - private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string) { + private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string): ConfiguredProject | undefined { return this.configuredProjects.get(canonicalConfigFilePath); } @@ -1259,7 +1242,7 @@ namespace ts.server { return false; } - private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition) { + private createExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition) { const compilerOptions = convertCompilerOptions(options); const project = new ExternalProject( projectFileName, @@ -1319,7 +1302,18 @@ namespace ts.server { } } - private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedServerHost: CachedServerHost, clientFileName?: string) { + private addFilesToNonInferredProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void { + project.setProjectErrors(configFileErrors); + this.updateNonInferredProjectFiles(project, files, propertyReader, clientFileName); + project.setTypeAcquisition(typeAcquisition); + // This doesnt need scheduling since its either creation or reload of the project + project.updateGraph(); + } + + private createConfiguredProject(configFileName: NormalizedPath, clientFileName?: string) { + const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName); + const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); + this.logger.info(`Opened configuration file ${configFileName}`); const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, @@ -1332,7 +1326,7 @@ namespace ts.server { cachedServerHost); project.configFileSpecs = configFileSpecs; - // TODO: (sheetalkamat) We should also watch the configFiles that are extended + // TODO: We probably should also watch the configFiles that are extended project.configFileWatcher = this.addFileWatcher(WatchType.ConfigFilePath, project, configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind) ); @@ -1343,26 +1337,11 @@ namespace ts.server { this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); this.configuredProjects.set(project.canonicalConfigFilePath, project); - this.setConfigFilePresenceByNewConfiguredProject(project); + this.setConfigFileExistenceByNewConfiguredProject(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; } - private addFilesToNonInferredProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void { - project.setProjectErrors(configFileErrors); - this.updateNonInferredProjectFiles(project, files, propertyReader, clientFileName); - project.setTypeAcquisition(typeAcquisition); - // This doesnt need scheduling since its either creation or reload of the project - project.updateGraph(); - } - - private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName); - const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); - this.logger.info(`Opened configuration file ${configFileName}`); - return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedServerHost, clientFileName); - } - private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, clientFileName?: string) { const projectRootFilesMap = project.getRootFilesMap(); const newRootScriptInfoMap: Map = createMap(); @@ -1474,18 +1453,44 @@ namespace ts.server { this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } - createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) { + /*@internal*/ + assignScriptInfoToInferredProject(info: ScriptInfo) { + Debug.assert(info.containingProjects.length === 0); + + // create new inferred project p with the newly opened file as root + // or add root to existing inferred project if 'useSingleInferredProject' is true const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length; const project = useExistingProject ? this.inferredProjects[0] : new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects); - project.addRoot(root); + project.addRoot(info); project.updateGraph(); if (!useExistingProject) { + // Add the new project to inferred projects list this.inferredProjects.push(project); + + for (const inferredProject of this.inferredProjects.slice(0, this.inferredProjects.length - 1)) { + // Note that we need to create a copy of the array since the list of project can change + Debug.assert(inferredProject !== project); + + // Remove the inferred project if the root of it is now part of newly created inferred project + // e.g through references + // Which means if any root of inferred project is part of more than 1 project can be removed + // This logic is same as iterating over all open files and calling + // this.removeRootOfInferredProjectIfNowPartOfOtherProject(f); + // Since this is also called from refreshInferredProject and closeOpen file + // to update inferred projects of the open file, this iteration might be faster + // instead of scanning all open files + const root = inferredProject.getRootScriptInfos(); + Debug.assert(root.length === 1); + if (root[0].containingProjects.length > 1) { + this.removeProject(inferredProject); + } + } } + return project; } @@ -1632,28 +1637,33 @@ namespace ts.server { */ reloadProjects() { this.logger.info("reload projects."); - this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false); + this.reloadConfiguredsProjectForFiles(this.openFiles, /*delayReload*/ false); this.refreshInferredProjects(); } - delayReloadConfiguredProjectForFiles(openFilesMap: Map, ignoreIfNotInferredProjectRoot: boolean) { + private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) { // Get open files to reload projects for - const openFiles = flatMapIter(openFilesMap.keys(), path => { - if (!ignoreIfNotInferredProjectRoot || openFilesMap.get(path)) { - return this.getScriptInfoForPath(path as Path); + const openFiles = mapDefinedIter( + configFileExistenceInfo.openFilesImpactedByConfigFile.entries(), + ([path, isRootOfInferredProject]) => { + if (!ignoreIfNotRootOfInferredProject || isRootOfInferredProject) { + const info = this.getScriptInfoForPath(path as Path); + Debug.assert(!!info); + return info; + } } - }); - this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); + ); + this.reloadConfiguredsProjectForFiles(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 + * or schedules it for reload depending on delayReload option * If the there is no existing project it just opens the configured project for the config file */ - reloadConfiguredProjectForFiles(openFiles: ScriptInfo[], delayReload: boolean) { + private reloadConfiguredsProjectForFiles(openFiles: ScriptInfo[], delayReload: boolean) { const mapUpdatedProjects = createMap(); // try to reload config file for all open files for (const info of openFiles) { @@ -1665,7 +1675,7 @@ namespace ts.server { if (configFileName) { let project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { - project = this.openConfigFile(configFileName, info.fileName); + project = this.createConfiguredProject(configFileName, info.fileName); mapUpdatedProjects.set(configFileName, true); } else if (!mapUpdatedProjects.has(configFileName)) { @@ -1716,7 +1726,7 @@ namespace ts.server { for (const info of this.openFiles) { // collect all orphaned script infos from open files if (info.containingProjects.length === 0) { - this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ false); + this.assignScriptInfoToInferredProject(info); } // Or remove the root of inferred project if is referenced in more than one projects else { @@ -1752,7 +1762,7 @@ namespace ts.server { if (configFileName) { project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { - project = this.openConfigFile(configFileName, fileName); + project = this.createConfiguredProject(configFileName, fileName); // even if opening config file was successful, it could still // contain errors that were tolerated. @@ -1771,8 +1781,13 @@ namespace ts.server { project.markAsDirty(); } - // at this point if file is the part of some configured/external project then this project should be created - this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true); + // At this point if file is part of any any configured or external project, then it would be present in the containing projects + // So if it still doesnt have any containing projects, it needs to be part of inferred project + if (info.containingProjects.length === 0) { + this.assignScriptInfoToInferredProject(info); + } + this.addToListOfOpenFiles(info); + // Delete the orphan files here because there might be orphan script infos (which are not part of project) // when some file/s were closed which resulted in project removal. // It was then postponed to cleanup these script infos so that they can be reused if @@ -2084,7 +2099,7 @@ namespace ts.server { let project = this.findConfiguredProjectByProjectName(tsconfigFile); if (!project) { // errors are stored in the project - project = this.openConfigFile(tsconfigFile); + project = this.createConfiguredProject(tsconfigFile); } if (project && !contains(exisingConfigFiles, tsconfigFile)) { // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project @@ -2095,7 +2110,7 @@ namespace ts.server { else { // no config files - remove the item from the collection this.externalProjectToConfiguredProjectMap.delete(proj.projectFileName); - this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition); + this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition); } if (!suppressRefreshOfInferredProjects) { this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true); diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index 432df8c53d0..04b12f16563 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -35,7 +35,7 @@ namespace ts { export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] { return flatMapIter(refactors.values(), refactor => - context.cancellationToken && context.cancellationToken.isCancellationRequested() ? [] : refactor.getAvailableActions(context)); + context.cancellationToken && context.cancellationToken.isCancellationRequested() ? undefined : refactor.getAvailableActions(context)); } export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined {