remove multiple collections for open files

This commit is contained in:
Vladimir Matveev 2016-06-28 17:45:29 -07:00
parent 9bec244469
commit d7bf32270e
5 changed files with 256 additions and 213 deletions

View File

@ -54,6 +54,20 @@ namespace ts.server {
}
}
/**
* TODO: enforce invariants:
* - script info can be never migrate to state - root file in inferred project, this is only a starting point
* - if script info has more that one containing projects - it is not a root file in inferred project because:
* - references in inferred project supercede the root part
* - root/reference in non-inferred project beats root in inferred project
*/
function isRootFileInInferredProject(info: ScriptInfo): boolean {
if (info.containingProjects.length === 0) {
return false;
}
return info.containingProjects[0].projectKind === ProjectKind.Inferred && info.containingProjects[0].isRoot(info);
}
class DirectoryWatchers {
/**
* a path to directory watcher map that detects added tsconfig files
@ -121,19 +135,11 @@ namespace ts.server {
/**
* projects specified by a tsconfig.json file
**/
configuredProjects: ConfiguredProject[] = [];
/**
* open, non-configured root files
**/
openFileRoots: ScriptInfo[] = [];
readonly configuredProjects: ConfiguredProject[] = [];
/**
* open files referenced by some project
**/
openFilesReferenced: ScriptInfo[] = [];
/**
* open files that are roots of a configured project
**/
openFileRootsConfigured: ScriptInfo[] = [];
* list of open files
*/
openFiles: ScriptInfo[] = [];
private readonly directoryWatchers: DirectoryWatchers;
@ -213,11 +219,7 @@ namespace ts.server {
return;
}
for (const openFile of this.openFileRoots) {
this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName);
}
for (const openFile of this.openFilesReferenced) {
for (const openFile of this.openFiles) {
this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName);
}
@ -266,14 +268,14 @@ namespace ts.server {
// Call updateProjectStructure to clean up inferred projects we may have
// created for the new files
this.updateProjectStructure();
this.refreshInferredProjects();
}
}
private onConfigChangedForConfiguredProject(project: ConfiguredProject) {
this.log(`Config file changed: ${project.configFileName}`);
this.updateConfiguredProject(project);
this.updateProjectStructure();
this.refreshInferredProjects();
}
/**
@ -287,16 +289,8 @@ namespace ts.server {
}
this.log(`Detected newly added tsconfig file: ${fileName}`);
const { projectOptions } = this.convertConfigFileContentToProjectOptions(fileName);
const rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f));
// We should only care about the new tsconfig file if it contains any
// opened root files of existing inferred projects
for (const rootFile of this.openFileRoots) {
if (contains(rootFilesInTsconfig, this.getCanonicalFileName(rootFile.fileName))) {
this.reloadProjects();
break;
}
}
// TODO: add tests to check correct migration of currently open file if it is referenced from the root file of configured project
this.reloadProjects();
}
private getCanonicalFileName(fileName: string) {
@ -340,59 +334,50 @@ namespace ts.server {
return undefined;
}
private addOpenFile(info: ScriptInfo): void {
private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void {
const externalProject = this.findContainingExternalProject(info.fileName);
if (externalProject) {
// file is already included in some external project - do nothing
if (addToListOfOpenFiles) {
this.openFiles.push(info);
}
return;
}
const configuredProject = this.findContainingConfiguredProject(info);
if (configuredProject) {
// file is the part of configured project
configuredProject.addOpenRef();
if (configuredProject.isRoot(info)) {
this.openFileRootsConfigured.push(info);
}
else {
this.openFilesReferenced.push(info);
if (addToListOfOpenFiles) {
configuredProject.addOpenRef();
this.openFiles.push(info);
}
return;
}
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
const inferredProject = this.addFileToInferredProject(info);
const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info);
if (!this.useOneInferredProject) {
// if useOneInferredProject is not set then try to fixup ownership of open files
const openFileRoots: ScriptInfo[] = [];
// for each inferred project root r
for (const rootFile of this.openFileRoots) {
// if r referenced by the new project
if (inferredProject.containsScriptInfo(rootFile)) {
// remove inferred project that was initially created for rootFile
const defaultProject = rootFile.getDefaultProject();
if (defaultProject === inferredProject) {
continue;
}
Debug.assert(defaultProject.projectKind === ProjectKind.Inferred);
for (const f of this.openFiles) {
const defaultProject = f.getDefaultProject();
if (isRootFileInInferredProject(info) && defaultProject !== inferredProject && inferredProject.containsScriptInfo(f)) {
// open file used to be root in inferred project,
// this inferred project is different from the one we've just created for current file
// and new inferred project references this open file.
// We should delete old inferred project and attach open file to the new one
this.removeProject(defaultProject);
// put r in referenced open file list
this.openFilesReferenced.push(rootFile);
// set default project of r to the new project
rootFile.attachToProject(inferredProject);
}
else {
// otherwise, keep r as root of inferred project
openFileRoots.push(rootFile);
f.attachToProject(inferredProject);
}
}
this.openFileRoots = openFileRoots;
}
}
this.openFileRoots.push(info);
if (addToListOfOpenFiles) {
this.openFiles.push(info);
}
}
/**
@ -405,8 +390,8 @@ namespace ts.server {
// to the disk, and the server's version of the file can be out of sync.
info.reloadFromFile();
removeItemFromSet(this.openFileRoots, info);
removeItemFromSet(this.openFileRootsConfigured, info);
removeItemFromSet(this.openFiles, info);
info.isOpen = false;
// collect all projects that should be removed
let projectsToRemove: Project[];
@ -427,39 +412,22 @@ namespace ts.server {
this.removeProject(project);
}
const openFilesReferenced: ScriptInfo[] = [];
const orphanFiles: ScriptInfo[] = [];
// for all open, referenced files f
for (const f of this.openFilesReferenced) {
if (f === info) {
// skip closed file
continue;
}
let orphanFiles: ScriptInfo[];
// for all open files
for (const f of this.openFiles) {
// collect orphanted files and try to re-add them as newly opened
if (f.containingProjects.length === 0) {
orphanFiles.push(f);
}
else {
// otherwise add it back to the list of referenced files
openFilesReferenced.push(f);
(orphanFiles || (orphanFiles = [])).push(f);
}
}
this.openFilesReferenced = openFilesReferenced;
// treat orphaned files as newly opened
for (const f of orphanFiles) {
this.addOpenFile(f);
if (orphanFiles) {
for (const f of orphanFiles) {
this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false);
}
}
}
else {
// just close file
removeItemFromSet(this.openFilesReferenced, info);
}
// projectsToRemove should already cover it
// this.releaseNonReferencedConfiguredProjects();
info.isOpen = false;
}
/**
@ -539,19 +507,10 @@ namespace ts.server {
counter = printProjects(this.logger, this.configuredProjects, counter);
counter = printProjects(this.logger, this.inferredProjects, counter);
this.logger.info("Open file roots of inferred projects: ");
for (const rootFile of this.openFileRoots) {
this.logger.info("Open files: ");
for (const rootFile of this.openFiles) {
this.logger.info(rootFile.fileName);
}
this.logger.info("Open files referenced by inferred or configured projects: ");
for (const referencedFile of this.openFilesReferenced) {
const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.getDefaultProject().projectKind]}`;
this.logger.info(fileInfo);
}
this.logger.info("Open file roots of configured projects: ");
for (const configuredRoot of this.openFileRootsConfigured) {
this.logger.info(configuredRoot.fileName);
}
this.logger.endGroup();
@ -718,33 +677,44 @@ namespace ts.server {
// if the root file was opened by client, it would belong to either
// openFileRoots or openFileReferenced.
if (info.isOpen) {
if (contains(this.openFileRoots, info)) {
removeItemFromSet(this.openFileRoots, info);
// delete inferred project
let toRemove: Project[];
// TODO: unify logic
for (const p of info.containingProjects) {
if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) {
(toRemove || (toRemove = [])).push(p);
}
}
if (toRemove) {
for (const p of toRemove) {
p.removeFile(info);
if (!p.hasRoots()) {
this.removeProject(p);
}
}
// delete inferred project
let toRemove: Project;
if (isRootFileInInferredProject(info)) {
toRemove = info.containingProjects[0];
}
if (toRemove) {
toRemove.removeFile(info);
if (!toRemove.hasRoots()) {
this.removeProject(toRemove);
}
}
if (contains(this.openFilesReferenced, info)) {
removeItemFromSet(this.openFilesReferenced, info);
}
if (project.projectKind === ProjectKind.Configured) {
this.openFileRootsConfigured.push(info);
}
// if (contains(this.openFileRoots, info)) {
// removeItemFromSet(this.openFileRoots, info);
// // delete inferred project
// let toRemove: Project[];
// // TODO: unify logic
// for (const p of info.containingProjects) {
// if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) {
// (toRemove || (toRemove = [])).push(p);
// }
// }
// if (toRemove) {
// for (const p of toRemove) {
// p.removeFile(info);
// if (!p.hasRoots()) {
// this.removeProject(p);
// }
// }
// }
// }
// if (contains(this.openFilesReferenced, info)) {
// removeItemFromSet(this.openFilesReferenced, info);
// }
// if (project.projectKind === ProjectKind.Configured) {
// this.openFileRootsConfigured.push(info);
// }
}
}
project.addRoot(info);
@ -784,7 +754,7 @@ namespace ts.server {
}
}
addFileToInferredProject(root: ScriptInfo) {
createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) {
const useExistingProject = this.useOneInferredProject && this.inferredProjects.length;
const project = useExistingProject
? this.inferredProjects[0]
@ -887,11 +857,11 @@ namespace ts.server {
*/
reloadProjects() {
this.log("reload projects.");
// First check if there is new tsconfig file added for inferred project roots
for (const info of this.openFileRoots) {
// try to reload config file for all open files
for (const info of this.openFiles) {
this.openOrUpdateConfiguredProjectForFile(info.fileName);
}
this.updateProjectStructure();
this.refreshInferredProjects();
}
/**
@ -899,20 +869,60 @@ namespace ts.server {
* It is called on the premise that all the configured projects are
* up to date.
*/
updateProjectStructure() {
refreshInferredProjects() {
this.log("updating project structure from ...", "Info");
this.printProjects();
const unattachedOpenFiles: ScriptInfo[] = [];
const openFileRootsConfigured: ScriptInfo[] = [];
// collect all orphanted script infos that used to be roots of configured projects
for (const info of this.openFileRootsConfigured) {
// collect all orphanted script infos from open files
for (const info of this.openFiles) {
if (info.containingProjects.length === 0) {
unattachedOpenFiles.push(info);
}
else {
openFileRootsConfigured.push(info);
if (isRootFileInInferredProject(info) && info.containingProjects.length > 1) {
const inferredProject = info.containingProjects[0];
Debug.assert(inferredProject.projectKind === ProjectKind.Inferred);
inferredProject.removeFile(info);
if (!inferredProject.hasRoots()) {
this.removeProject(inferredProject);
}
}
// let inConfiguredProject = false;
// let inExternalProject = false;
// for (const p of info.containingProjects) {
// inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured;
// inExternalProject = inExternalProject || p.projectKind === ProjectKind.External;
// }
// if (inConfiguredProject || inExternalProject) {
// const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred);
// for (const p of inferredProjects) {
// p.removeFile(rootFile, /*detachFromProject*/ true);
// if (!p.hasRoots()) {
// this.removeProject(p);
// }
// }
// }
// else {
// if (rootFile.containingProjects.length > 1) {
// // TODO: fixme
// // file is contained in more than one inferred project - keep only ones where it is used as reference
// const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile));
// for (const root of roots) {
// this.removeProject(root);
// }
// Debug.assert(info.containingProjects.length > 0);
// }
// }
}
// if (info.containingProjects.length === 0) {
// unattachedOpenFiles.push(info);
// }
// else {
// openFileRootsConfigured.push(info);
// }
// const project = info.defaultProject;
// if (!project || !(project.containsScriptInfo(info))) {
// info.defaultProject = undefined;
@ -922,30 +932,34 @@ namespace ts.server {
// openFileRootsConfigured.push(info);
// }
}
this.openFileRootsConfigured = openFileRootsConfigured;
// First loop through all open files that are referenced by projects but are not
// project roots. For each referenced file, see if the default project still
// references that file. If so, then just keep the file in the referenced list.
// If not, add the file to an unattached list, to be rechecked later.
const openFilesReferenced: ScriptInfo[] = [];
for (const referencedFile of this.openFilesReferenced) {
// check if any of projects that used to reference this file are still referencing it
if (referencedFile.containingProjects.length === 0) {
unattachedOpenFiles.push(referencedFile);
}
else {
openFilesReferenced.push(referencedFile);
}
// referencedFile.defaultProject.updateGraph();
// if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) {
// openFilesReferenced.push(referencedFile);
// }
// else {
// unattachedOpenFiles.push(referencedFile);
// }
for (const unattached of unattachedOpenFiles) {
this.assignScriptInfoToInferredProjectIfNecessary(unattached, /*addToListOfOpenFiles*/ false);
}
this.openFilesReferenced = openFilesReferenced;
// this.openFileRootsConfigured = openFileRootsConfigured;
// // First loop through all open files that are referenced by projects but are not
// // project roots. For each referenced file, see if the default project still
// // references that file. If so, then just keep the file in the referenced list.
// // If not, add the file to an unattached list, to be rechecked later.
// const openFilesReferenced: ScriptInfo[] = [];
// for (const referencedFile of this.openFilesReferenced) {
// // check if any of projects that used to reference this file are still referencing it
// if (referencedFile.containingProjects.length === 0) {
// unattachedOpenFiles.push(referencedFile);
// }
// else {
// openFilesReferenced.push(referencedFile);
// }
// // referencedFile.defaultProject.updateGraph();
// // if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) {
// // openFilesReferenced.push(referencedFile);
// // }
// // else {
// // unattachedOpenFiles.push(referencedFile);
// // }
// }
// this.openFilesReferenced = openFilesReferenced;
// Then, loop through all of the open files that are project roots.
// For each root file, note the project that it roots. Then see if
@ -954,42 +968,42 @@ namespace ts.server {
// projects newly references the file, remove its project from the
// inferred projects list (since it is no longer a root) and add
// the file to the open, referenced file list.
const openFileRoots: ScriptInfo[] = [];
for (const rootFile of this.openFileRoots) {
let inConfiguredProject = false;
let inExternalProject = false;
for (const p of rootFile.containingProjects) {
inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured;
inExternalProject = inExternalProject || p.projectKind === ProjectKind.External;
}
if (inConfiguredProject || inExternalProject) {
const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred);
for (const p of inferredProjects) {
p.removeFile(rootFile, /*detachFromProject*/ true);
if (!p.hasRoots()) {
this.removeProject(p);
}
}
if (inConfiguredProject) {
this.openFileRootsConfigured.push(rootFile);
}
}
else {
if (rootFile.containingProjects.length === 1) {
// file contained only in one project
openFileRoots.push(rootFile);
}
else {
// TODO: fixme
// file is contained in more than one inferred project - keep only ones where it is used as reference
const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile));
for (const root of roots) {
this.removeProject(root);
}
Debug.assert(rootFile.containingProjects.length > 0);
this.openFilesReferenced.push(rootFile);
}
}
// const openFileRoots: ScriptInfo[] = [];
// for (const rootFile of this.openFileRoots) {
// let inConfiguredProject = false;
// let inExternalProject = false;
// for (const p of rootFile.containingProjects) {
// inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured;
// inExternalProject = inExternalProject || p.projectKind === ProjectKind.External;
// }
// if (inConfiguredProject || inExternalProject) {
// const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred);
// for (const p of inferredProjects) {
// p.removeFile(rootFile, /*detachFromProject*/ true);
// if (!p.hasRoots()) {
// this.removeProject(p);
// }
// }
// if (inConfiguredProject) {
// this.openFileRootsConfigured.push(rootFile);
// }
// }
// else {
// if (rootFile.containingProjects.length === 1) {
// // file contained only in one project
// openFileRoots.push(rootFile);
// }
// else {
// // TODO: fixme
// // file is contained in more than one inferred project - keep only ones where it is used as reference
// const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile));
// for (const root of roots) {
// this.removeProject(root);
// }
// Debug.assert(rootFile.containingProjects.length > 0);
// this.openFilesReferenced.push(rootFile);
// }
// }
// if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) {
// // file was included in non-inferred project - drop old inferred project
@ -1026,15 +1040,15 @@ namespace ts.server {
// this.openFilesReferenced.push(rootFile);
// }
// }
}
this.openFileRoots = openFileRoots;
// }
// this.openFileRoots = openFileRoots;
// Finally, if we found any open, referenced files that are no longer
// referenced by their default project, treat them as newly opened
// by the editor.
for (const f of unattachedOpenFiles) {
this.addOpenFile(f);
}
// for (const f of unattachedOpenFiles) {
// this.addOpenFile(f);
// }
for (const p of this.inferredProjects) {
p.updateGraph();
}
@ -1057,7 +1071,7 @@ namespace ts.server {
// at this point if file is the part of some configured/external project then this project should be created
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind);
this.addOpenFile(info);
this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true);
this.printProjects();
return { configFileName, configFileErrors };
}
@ -1123,7 +1137,7 @@ namespace ts.server {
}
if (openFiles || changedFiles || closedFiles) {
this.updateProjectStructure();
this.refreshInferredProjects();
}
}
@ -1137,14 +1151,14 @@ namespace ts.server {
this.removeProject(configuredProject);
}
}
this.updateProjectStructure();
this.refreshInferredProjects();
}
else {
// close external project
const externalProject = this.findExternalProjectByProjectName(uncheckedFileName);
if (externalProject) {
this.removeProject(externalProject);
this.updateProjectStructure();
this.refreshInferredProjects();
}
}
}

View File

@ -122,11 +122,11 @@ namespace ts.server {
}
hasRoots() {
return this.rootFiles.length > 0;
return this.rootFiles && this.rootFiles.length > 0;
}
getRootFiles() {
return this.rootFiles.map(info => info.fileName);
return this.rootFiles && this.rootFiles.map(info => info.fileName);
}
getFileNames() {
@ -161,7 +161,7 @@ namespace ts.server {
}
isRoot(info: ScriptInfo) {
return this.rootFilesMap.contains(info.path);
return this.rootFilesMap && this.rootFilesMap.contains(info.path);
}
// add a root file to project
@ -190,14 +190,15 @@ namespace ts.server {
this.projectStateVersion++;
}
updateGraph() {
updateGraph(): boolean {
if (!this.languageServiceEnabled) {
return;
return true;
}
const oldProgram = this.program;
this.program = this.languageService.getProgram();
const oldProjectStructureVersion = this.projectStructureVersion;
// bump up the version if
// - oldProgram is not set - this is a first time updateGraph is called
// - newProgram is different from the old program and structure of the old program was not reused.
@ -205,16 +206,18 @@ namespace ts.server {
this.projectStructureVersion++;
if (oldProgram) {
for (const f of oldProgram.getSourceFiles()) {
if (!this.program.getSourceFileByPath(f.path)) {
// new program does not contain this file - detach it from the project
const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName);
if (scriptInfoToDetach) {
scriptInfoToDetach.detachFromProject(this);
}
if (this.program.getSourceFileByPath(f.path)) {
continue;
}
// new program does not contain this file - detach it from the project
const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName);
if (scriptInfoToDetach) {
scriptInfoToDetach.detachFromProject(this);
}
}
}
}
return oldProjectStructureVersion === this.projectStructureVersion;
}
getScriptInfoLSHost(fileName: string) {

View File

@ -37,11 +37,37 @@ namespace ts.server {
}
isAttached(project: Project) {
return contains(this.containingProjects, project);
// unrolled for common cases
switch (this.containingProjects.length) {
case 0: return false;
case 1: return this.containingProjects[0] === project;
case 2: return this.containingProjects[0] === project || this.containingProjects[1] === project;
default: return contains(this.containingProjects, project);
}
}
detachFromProject(project: Project) {
removeItemFromSet(this.containingProjects, project);
// unrolled for common cases
switch (this.containingProjects.length) {
case 0:
return;
case 1:
if (this.containingProjects[0] === project) {
this.containingProjects.pop();
}
break;
case 2:
if (this.containingProjects[0] === project) {
this.containingProjects[0] = this.containingProjects.pop();
}
if (this.containingProjects[1] === project) {
this.containingProjects.pop();
}
break;
default:
removeItemFromSet(this.containingProjects, project);
break;
}
}
detachAllProjects() {

View File

@ -307,7 +307,7 @@ namespace ts.server {
private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) {
this.host.setTimeout(() => {
if (matchSeq(seq)) {
this.projectService.updateProjectStructure();
this.projectService.refreshInferredProjects();
}
}, ms);
}

View File

@ -81,7 +81,7 @@ namespace ts {
const projectService = new server.ProjectService(serverHost, logger, { isCancellationRequested: () => false }, /*useOneInferredProject*/ false);
const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */true, /*containingProject*/ undefined);
const project = projectService.addFileToInferredProject(rootScriptInfo);
const project = projectService.createInferredProjectWithRootFileIfNecessary(rootScriptInfo);
project.setCompilerOptions({ module: ts.ModuleKind.AMD } );
return {
project,