More updates per PR feedback

This commit is contained in:
Sheetal Nandi 2017-08-07 14:47:32 -07:00
parent 02b8a7de65
commit f723beb244
5 changed files with 284 additions and 254 deletions

View File

@ -534,6 +534,19 @@ namespace ts {
return result;
}
export function mapDefinedIter<T, U>(iter: Iterator<T>, 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.

View File

@ -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,

View File

@ -263,6 +263,8 @@ namespace ts.projectSystem {
function invokeWatcherCallbacks<T>(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);

View File

@ -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<boolean>;
openFilesImpactedByConfigFile: Map<boolean>;
/**
* 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<number> = createMap<number>();
/**
* 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<ConfigFileExistence>();
private readonly mapOfConfigFileExistenceInfo = createMap<ConfigFileExistenceInfo>();
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((<ConfiguredProject>project).canonicalConfigFilePath);
this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath);
this.setConfigFilePresenceByClosedConfigFile(<ConfiguredProject>project);
this.setConfigFileExistenceInfoByClosedConfiguredProject(<ConfiguredProject>project);
break;
case ProjectKind.Inferred:
unorderedRemoveItem(this.inferredProjects, <InferredProject>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) {
((<ConfiguredProject>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) {
((<ConfiguredProject>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<boolean>();
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<boolean>();
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<boolean>()
openFilesImpactedByConfigFile: createMap<boolean>()
});
}
}
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<boolean>()
};
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<boolean>()
};
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<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, 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<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, 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<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, clientFileName?: string) {
const projectRootFilesMap = project.getRootFilesMap();
const newRootScriptInfoMap: Map<ProjectRoot> = createMap<ProjectRoot>();
@ -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<boolean>, 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<true>();
// 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);

View File

@ -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 {