Merge pull request #19457 from Microsoft/openFilesRetainProjectRoot

Retain projectRootPath for the opened files so that it can be used when files move between configured/inferred project
This commit is contained in:
Sheetal Nandi 2017-10-25 15:45:45 -07:00 committed by GitHub
commit 97a2e42c02
4 changed files with 118 additions and 42 deletions

View file

@ -356,7 +356,7 @@ namespace ts.projectSystem {
}
function checkOpenFiles(projectService: server.ProjectService, expectedFiles: FileOrFolder[]) {
checkFileNames("Open files", projectService.openFiles.map(info => info.fileName), expectedFiles.map(file => file.path));
checkFileNames("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path));
}
/**
@ -4293,6 +4293,74 @@ namespace ts.projectSystem {
checkNumberOfConfiguredProjects(service, 1);
checkNumberOfInferredProjects(service, 0);
});
it("should use projectRootPath when searching for inferred project again", () => {
const projectDir = "/a/b/projects/project";
const configFileLocation = `${projectDir}/src`;
const f1 = {
path: `${configFileLocation}/file1.ts`,
content: ""
};
const configFile = {
path: `${configFileLocation}/tsconfig.json`,
content: "{}"
};
const configFile2 = {
path: "/a/b/projects/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, libFile, configFile, configFile2]);
const service = createProjectService(host);
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
checkNumberOfProjects(service, { configuredProjects: 1 });
assert.isDefined(service.configuredProjects.get(configFile.path));
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
const typeRootLocations = getTypeRootsFromLocation(configFileLocation);
checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true);
// Delete config file - should create inferred project and not configured project
host.reloadFS([f1, libFile, configFile2]);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(service, { inferredProjects: 1 });
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true);
});
it("should use projectRootPath when searching for inferred project again 2", () => {
const projectDir = "/a/b/projects/project";
const configFileLocation = `${projectDir}/src`;
const f1 = {
path: `${configFileLocation}/file1.ts`,
content: ""
};
const configFile = {
path: `${configFileLocation}/tsconfig.json`,
content: "{}"
};
const configFile2 = {
path: "/a/b/projects/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, libFile, configFile, configFile2]);
const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true });
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
checkNumberOfProjects(service, { configuredProjects: 1 });
assert.isDefined(service.configuredProjects.get(configFile.path));
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true);
// Delete config file - should create inferred project with project root path set
host.reloadFS([f1, libFile, configFile2]);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(service, { inferredProjects: 1 });
assert.equal(service.inferredProjects[0].projectRootPath, projectDir);
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true);
});
});
describe("cancellationToken", () => {

View file

@ -352,9 +352,9 @@ namespace ts.server {
*/
readonly configuredProjects = createMap<ConfiguredProject>();
/**
* list of open files
* Open files: with value being project root path, and key being Path of the file that is open
*/
readonly openFiles: ScriptInfo[] = [];
readonly openFiles = createMap<NormalizedPath>();
private compilerOptionsForInferredProjects: CompilerOptions;
private compilerOptionsForInferredProjectsPerProjectRoot = createMap<CompilerOptions>();
@ -582,7 +582,7 @@ namespace ts.server {
const event: ProjectsUpdatedInBackgroundEvent = {
eventName: ProjectsUpdatedInBackgroundEvent,
data: {
openFiles: this.openFiles.map(f => f.fileName)
openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path as Path).fileName)
}
};
this.eventHandler(event);
@ -891,7 +891,7 @@ namespace ts.server {
}
/*@internal*/
assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath?: string) {
assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
Debug.assert(info.isOrphan());
const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) ||
@ -935,7 +935,7 @@ namespace ts.server {
info.close();
this.stopWatchingConfigFilesForClosedScriptInfo(info);
unorderedRemoveItem(this.openFiles, info);
this.openFiles.delete(info.path);
const fileExists = this.host.fileExists(info.fileName);
@ -974,11 +974,12 @@ namespace ts.server {
}
// collect orphaned files and assign them to inferred project just like we treat open of a file
for (const f of this.openFiles) {
this.openFiles.forEach((projectRootPath, path) => {
const f = this.getScriptInfoForPath(path as Path);
if (f.isOrphan()) {
this.assignOrphanScriptInfoToInferredProject(f);
this.assignOrphanScriptInfoToInferredProject(f, projectRootPath);
}
}
});
// 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,
@ -1172,7 +1173,7 @@ namespace ts.server {
* This is called by inferred project whenever script info is added as a root
*/
/* @internal */
startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
Debug.assert(info.isScriptOpen());
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
@ -1194,7 +1195,7 @@ namespace ts.server {
!this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) {
this.createConfigFileWatcherOfConfigFileExistence(configFileName, canonicalConfigFilePath, configFileExistenceInfo);
}
});
}, projectRootPath);
}
/**
@ -1262,7 +1263,7 @@ namespace ts.server {
* The server must start searching from the directory containing
* the newly opened file.
*/
private getConfigFileNameForFile(info: ScriptInfo, projectRootPath?: NormalizedPath) {
private getConfigFileNameForFile(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
Debug.assert(info.isScriptOpen());
this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`);
const configFileName = this.forEachConfigFileLocation(info,
@ -1301,9 +1302,9 @@ namespace ts.server {
printProjects(this.inferredProjects, counter);
this.logger.info("Open files: ");
for (const rootFile of this.openFiles) {
this.logger.info(`\t${rootFile.fileName}`);
}
this.openFiles.forEach((projectRootPath, path) => {
this.logger.info(`\tFileName: ${this.getScriptInfoForPath(path as Path).fileName} ProjectRootPath: ${projectRootPath}`);
});
this.logger.endGroup();
}
@ -1605,7 +1606,7 @@ namespace ts.server {
});
}
private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: string | undefined): InferredProject | undefined {
private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: NormalizedPath | undefined): InferredProject | undefined {
if (!this.useInferredProjectPerProjectRoot) {
return undefined;
}
@ -1659,7 +1660,7 @@ namespace ts.server {
return this.createInferredProject(/*currentDirectory*/ undefined, /*isSingleInferredProject*/ true);
}
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: string): InferredProject {
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject {
const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects;
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory);
if (isSingleInferredProject) {
@ -1796,23 +1797,19 @@ namespace ts.server {
// as there is no need to load contents of the files from the disk
// Reload Projects
this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false);
this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false, returnTrue);
this.refreshInferredProjects();
}
private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) {
// Get open files to reload projects for
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(
configFileExistenceInfo.openFilesImpactedByConfigFile,
/*delayReload*/ true,
ignoreIfNotRootOfInferredProject ?
isRootOfInferredProject => isRootOfInferredProject : // Reload open files if they are root of inferred project
returnTrue // Reload all the open files impacted by config file
);
this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true);
this.delayInferredProjectsRefresh();
}
@ -1821,16 +1818,24 @@ namespace ts.server {
* If the config file is found and it refers to existing project, it reloads it either immediately
* 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
* reloadForInfo provides a way to filter out files to reload configured project for
*/
private reloadConfiguredProjectForFiles(openFiles: ReadonlyArray<ScriptInfo>, delayReload: boolean) {
private reloadConfiguredProjectForFiles<T>(openFiles: Map<T>, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean) {
const updatedProjects = createMap<true>();
// try to reload config file for all open files
for (const info of openFiles) {
openFiles.forEach((openFileValue, path) => {
// Filter out the files that need to be ignored
if (!shouldReloadProjectFor(openFileValue)) {
return;
}
const info = this.getScriptInfoForPath(path as Path);
Debug.assert(info.isScriptOpen());
// This tries to search for a tsconfig.json for the given file. If we found it,
// we first detect if there is already a configured project created for it: if so,
// we re- read the tsconfig file content and update the project only if we havent already done so
// otherwise we create a new one.
const configFileName = this.getConfigFileNameForFile(info);
const configFileName = this.getConfigFileNameForFile(info, this.openFiles.get(path));
if (configFileName) {
const project = this.findConfiguredProjectByProjectName(configFileName);
if (!project) {
@ -1848,7 +1853,7 @@ namespace ts.server {
updatedProjects.set(configFileName, true);
}
}
}
});
}
/**
@ -1893,16 +1898,17 @@ namespace ts.server {
this.logger.info("refreshInferredProjects: updating project structure from ...");
this.printProjects();
for (const info of this.openFiles) {
this.openFiles.forEach((projectRootPath, path) => {
const info = this.getScriptInfoForPath(path as Path);
// collect all orphaned script infos from open files
if (info.isOrphan()) {
this.assignOrphanScriptInfoToInferredProject(info);
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
}
else {
// Or remove the root of inferred project if is referenced in more than one projects
this.removeRootOfInferredProjectIfNowPartOfOtherProject(info);
}
}
});
for (const p of this.inferredProjects) {
p.updateGraph();
@ -1956,7 +1962,7 @@ namespace ts.server {
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
}
Debug.assert(!info.isOrphan());
this.openFiles.push(info);
this.openFiles.set(info.path, projectRootPath);
if (sendConfigFileDiagEvent) {
configFileErrors = project.getAllProjectErrors();

View file

@ -1064,7 +1064,7 @@ namespace ts.server {
projectService: ProjectService,
documentRegistry: DocumentRegistry,
compilerOptions: CompilerOptions,
projectRootPath: string | undefined,
projectRootPath: NormalizedPath | undefined,
currentDirectory: string | undefined) {
super(InferredProject.newName(),
ProjectKind.Inferred,
@ -1080,7 +1080,8 @@ namespace ts.server {
}
addRoot(info: ScriptInfo) {
this.projectService.startWatchingConfigFilesForInferredProjectRoot(info);
Debug.assert(info.isScriptOpen());
this.projectService.startWatchingConfigFilesForInferredProjectRoot(info, this.projectService.openFiles.get(info.path));
if (!this._isJsInferredProject && info.isJavaScript()) {
this.toggleJsInferredProject(/*isJsInferredProject*/ true);
}

View file

@ -7435,9 +7435,9 @@ declare namespace ts.server {
*/
readonly configuredProjects: Map<ConfiguredProject>;
/**
* list of open files
* Open files: with value being project root path, and key being Path of the file that is open
*/
readonly openFiles: ScriptInfo[];
readonly openFiles: Map<NormalizedPath>;
private compilerOptionsForInferredProjects;
private compilerOptionsForInferredProjectsPerProjectRoot;
/**
@ -7556,7 +7556,7 @@ declare namespace ts.server {
* The server must start searching from the directory containing
* the newly opened file.
*/
private getConfigFileNameForFile(info, projectRootPath?);
private getConfigFileNameForFile(info, projectRootPath);
private printProjects();
private findConfiguredProjectByProjectName(configFileName);
private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
@ -7592,8 +7592,9 @@ declare namespace ts.server {
* If the config file is found and it refers to existing project, it reloads it either immediately
* 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
* reloadForInfo provides a way to filter out files to reload configured project for
*/
private reloadConfiguredProjectForFiles(openFiles, delayReload);
private reloadConfiguredProjectForFiles<T>(openFiles, delayReload, shouldReloadProjectFor);
/**
* Remove the root of inferred project if script info is part of another project
*/