Skip to content

Commit 97a2e42

Browse files
authored
Merge pull request microsoft#19457 from Microsoft/openFilesRetainProjectRoot
Retain projectRootPath for the opened files so that it can be used when files move between configured/inferred project
2 parents fd89808 + 3d45f5b commit 97a2e42

File tree

4 files changed

+118
-42
lines changed

4 files changed

+118
-42
lines changed

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ namespace ts.projectSystem {
356356
}
357357

358358
function checkOpenFiles(projectService: server.ProjectService, expectedFiles: FileOrFolder[]) {
359-
checkFileNames("Open files", projectService.openFiles.map(info => info.fileName), expectedFiles.map(file => file.path));
359+
checkFileNames("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path));
360360
}
361361

362362
/**
@@ -4293,6 +4293,74 @@ namespace ts.projectSystem {
42934293
checkNumberOfConfiguredProjects(service, 1);
42944294
checkNumberOfInferredProjects(service, 0);
42954295
});
4296+
4297+
it("should use projectRootPath when searching for inferred project again", () => {
4298+
const projectDir = "/a/b/projects/project";
4299+
const configFileLocation = `${projectDir}/src`;
4300+
const f1 = {
4301+
path: `${configFileLocation}/file1.ts`,
4302+
content: ""
4303+
};
4304+
const configFile = {
4305+
path: `${configFileLocation}/tsconfig.json`,
4306+
content: "{}"
4307+
};
4308+
const configFile2 = {
4309+
path: "/a/b/projects/tsconfig.json",
4310+
content: "{}"
4311+
};
4312+
const host = createServerHost([f1, libFile, configFile, configFile2]);
4313+
const service = createProjectService(host);
4314+
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
4315+
checkNumberOfProjects(service, { configuredProjects: 1 });
4316+
assert.isDefined(service.configuredProjects.get(configFile.path));
4317+
checkWatchedFiles(host, [libFile.path, configFile.path]);
4318+
checkWatchedDirectories(host, [], /*recursive*/ false);
4319+
const typeRootLocations = getTypeRootsFromLocation(configFileLocation);
4320+
checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true);
4321+
4322+
// Delete config file - should create inferred project and not configured project
4323+
host.reloadFS([f1, libFile, configFile2]);
4324+
host.runQueuedTimeoutCallbacks();
4325+
checkNumberOfProjects(service, { inferredProjects: 1 });
4326+
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
4327+
checkWatchedDirectories(host, [], /*recursive*/ false);
4328+
checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true);
4329+
});
4330+
4331+
it("should use projectRootPath when searching for inferred project again 2", () => {
4332+
const projectDir = "/a/b/projects/project";
4333+
const configFileLocation = `${projectDir}/src`;
4334+
const f1 = {
4335+
path: `${configFileLocation}/file1.ts`,
4336+
content: ""
4337+
};
4338+
const configFile = {
4339+
path: `${configFileLocation}/tsconfig.json`,
4340+
content: "{}"
4341+
};
4342+
const configFile2 = {
4343+
path: "/a/b/projects/tsconfig.json",
4344+
content: "{}"
4345+
};
4346+
const host = createServerHost([f1, libFile, configFile, configFile2]);
4347+
const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true });
4348+
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
4349+
checkNumberOfProjects(service, { configuredProjects: 1 });
4350+
assert.isDefined(service.configuredProjects.get(configFile.path));
4351+
checkWatchedFiles(host, [libFile.path, configFile.path]);
4352+
checkWatchedDirectories(host, [], /*recursive*/ false);
4353+
checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true);
4354+
4355+
// Delete config file - should create inferred project with project root path set
4356+
host.reloadFS([f1, libFile, configFile2]);
4357+
host.runQueuedTimeoutCallbacks();
4358+
checkNumberOfProjects(service, { inferredProjects: 1 });
4359+
assert.equal(service.inferredProjects[0].projectRootPath, projectDir);
4360+
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
4361+
checkWatchedDirectories(host, [], /*recursive*/ false);
4362+
checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true);
4363+
});
42964364
});
42974365

42984366
describe("cancellationToken", () => {

src/server/editorServices.ts

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -352,9 +352,9 @@ namespace ts.server {
352352
*/
353353
readonly configuredProjects = createMap<ConfiguredProject>();
354354
/**
355-
* list of open files
355+
* Open files: with value being project root path, and key being Path of the file that is open
356356
*/
357-
readonly openFiles: ScriptInfo[] = [];
357+
readonly openFiles = createMap<NormalizedPath>();
358358

359359
private compilerOptionsForInferredProjects: CompilerOptions;
360360
private compilerOptionsForInferredProjectsPerProjectRoot = createMap<CompilerOptions>();
@@ -582,7 +582,7 @@ namespace ts.server {
582582
const event: ProjectsUpdatedInBackgroundEvent = {
583583
eventName: ProjectsUpdatedInBackgroundEvent,
584584
data: {
585-
openFiles: this.openFiles.map(f => f.fileName)
585+
openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path as Path).fileName)
586586
}
587587
};
588588
this.eventHandler(event);
@@ -891,7 +891,7 @@ namespace ts.server {
891891
}
892892

893893
/*@internal*/
894-
assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath?: string) {
894+
assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
895895
Debug.assert(info.isOrphan());
896896

897897
const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) ||
@@ -935,7 +935,7 @@ namespace ts.server {
935935
info.close();
936936
this.stopWatchingConfigFilesForClosedScriptInfo(info);
937937

938-
unorderedRemoveItem(this.openFiles, info);
938+
this.openFiles.delete(info.path);
939939

940940
const fileExists = this.host.fileExists(info.fileName);
941941

@@ -974,11 +974,12 @@ namespace ts.server {
974974
}
975975

976976
// collect orphaned files and assign them to inferred project just like we treat open of a file
977-
for (const f of this.openFiles) {
977+
this.openFiles.forEach((projectRootPath, path) => {
978+
const f = this.getScriptInfoForPath(path as Path);
978979
if (f.isOrphan()) {
979-
this.assignOrphanScriptInfoToInferredProject(f);
980+
this.assignOrphanScriptInfoToInferredProject(f, projectRootPath);
980981
}
981-
}
982+
});
982983

983984
// Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project)
984985
// is postponed to next file open so that if file from same project is opened,
@@ -1172,7 +1173,7 @@ namespace ts.server {
11721173
* This is called by inferred project whenever script info is added as a root
11731174
*/
11741175
/* @internal */
1175-
startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
1176+
startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
11761177
Debug.assert(info.isScriptOpen());
11771178
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
11781179
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
@@ -1194,7 +1195,7 @@ namespace ts.server {
11941195
!this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) {
11951196
this.createConfigFileWatcherOfConfigFileExistence(configFileName, canonicalConfigFilePath, configFileExistenceInfo);
11961197
}
1197-
});
1198+
}, projectRootPath);
11981199
}
11991200

12001201
/**
@@ -1262,7 +1263,7 @@ namespace ts.server {
12621263
* The server must start searching from the directory containing
12631264
* the newly opened file.
12641265
*/
1265-
private getConfigFileNameForFile(info: ScriptInfo, projectRootPath?: NormalizedPath) {
1266+
private getConfigFileNameForFile(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
12661267
Debug.assert(info.isScriptOpen());
12671268
this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`);
12681269
const configFileName = this.forEachConfigFileLocation(info,
@@ -1301,9 +1302,9 @@ namespace ts.server {
13011302
printProjects(this.inferredProjects, counter);
13021303

13031304
this.logger.info("Open files: ");
1304-
for (const rootFile of this.openFiles) {
1305-
this.logger.info(`\t${rootFile.fileName}`);
1306-
}
1305+
this.openFiles.forEach((projectRootPath, path) => {
1306+
this.logger.info(`\tFileName: ${this.getScriptInfoForPath(path as Path).fileName} ProjectRootPath: ${projectRootPath}`);
1307+
});
13071308

13081309
this.logger.endGroup();
13091310
}
@@ -1605,7 +1606,7 @@ namespace ts.server {
16051606
});
16061607
}
16071608

1608-
private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: string | undefined): InferredProject | undefined {
1609+
private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: NormalizedPath | undefined): InferredProject | undefined {
16091610
if (!this.useInferredProjectPerProjectRoot) {
16101611
return undefined;
16111612
}
@@ -1659,7 +1660,7 @@ namespace ts.server {
16591660
return this.createInferredProject(/*currentDirectory*/ undefined, /*isSingleInferredProject*/ true);
16601661
}
16611662

1662-
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: string): InferredProject {
1663+
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject {
16631664
const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects;
16641665
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory);
16651666
if (isSingleInferredProject) {
@@ -1796,23 +1797,19 @@ namespace ts.server {
17961797
// as there is no need to load contents of the files from the disk
17971798

17981799
// Reload Projects
1799-
this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false);
1800+
this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false, returnTrue);
18001801
this.refreshInferredProjects();
18011802
}
18021803

18031804
private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) {
18041805
// Get open files to reload projects for
1805-
const openFiles = mapDefinedIter(
1806-
configFileExistenceInfo.openFilesImpactedByConfigFile.entries(),
1807-
([path, isRootOfInferredProject]) => {
1808-
if (!ignoreIfNotRootOfInferredProject || isRootOfInferredProject) {
1809-
const info = this.getScriptInfoForPath(path as Path);
1810-
Debug.assert(!!info);
1811-
return info;
1812-
}
1813-
}
1806+
this.reloadConfiguredProjectForFiles(
1807+
configFileExistenceInfo.openFilesImpactedByConfigFile,
1808+
/*delayReload*/ true,
1809+
ignoreIfNotRootOfInferredProject ?
1810+
isRootOfInferredProject => isRootOfInferredProject : // Reload open files if they are root of inferred project
1811+
returnTrue // Reload all the open files impacted by config file
18141812
);
1815-
this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true);
18161813
this.delayInferredProjectsRefresh();
18171814
}
18181815

@@ -1821,16 +1818,24 @@ namespace ts.server {
18211818
* If the config file is found and it refers to existing project, it reloads it either immediately
18221819
* or schedules it for reload depending on delayReload option
18231820
* If the there is no existing project it just opens the configured project for the config file
1821+
* reloadForInfo provides a way to filter out files to reload configured project for
18241822
*/
1825-
private reloadConfiguredProjectForFiles(openFiles: ReadonlyArray<ScriptInfo>, delayReload: boolean) {
1823+
private reloadConfiguredProjectForFiles<T>(openFiles: Map<T>, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean) {
18261824
const updatedProjects = createMap<true>();
18271825
// try to reload config file for all open files
1828-
for (const info of openFiles) {
1826+
openFiles.forEach((openFileValue, path) => {
1827+
// Filter out the files that need to be ignored
1828+
if (!shouldReloadProjectFor(openFileValue)) {
1829+
return;
1830+
}
1831+
1832+
const info = this.getScriptInfoForPath(path as Path);
1833+
Debug.assert(info.isScriptOpen());
18291834
// This tries to search for a tsconfig.json for the given file. If we found it,
18301835
// we first detect if there is already a configured project created for it: if so,
18311836
// we re- read the tsconfig file content and update the project only if we havent already done so
18321837
// otherwise we create a new one.
1833-
const configFileName = this.getConfigFileNameForFile(info);
1838+
const configFileName = this.getConfigFileNameForFile(info, this.openFiles.get(path));
18341839
if (configFileName) {
18351840
const project = this.findConfiguredProjectByProjectName(configFileName);
18361841
if (!project) {
@@ -1848,7 +1853,7 @@ namespace ts.server {
18481853
updatedProjects.set(configFileName, true);
18491854
}
18501855
}
1851-
}
1856+
});
18521857
}
18531858

18541859
/**
@@ -1893,16 +1898,17 @@ namespace ts.server {
18931898
this.logger.info("refreshInferredProjects: updating project structure from ...");
18941899
this.printProjects();
18951900

1896-
for (const info of this.openFiles) {
1901+
this.openFiles.forEach((projectRootPath, path) => {
1902+
const info = this.getScriptInfoForPath(path as Path);
18971903
// collect all orphaned script infos from open files
18981904
if (info.isOrphan()) {
1899-
this.assignOrphanScriptInfoToInferredProject(info);
1905+
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
19001906
}
19011907
else {
19021908
// Or remove the root of inferred project if is referenced in more than one projects
19031909
this.removeRootOfInferredProjectIfNowPartOfOtherProject(info);
19041910
}
1905-
}
1911+
});
19061912

19071913
for (const p of this.inferredProjects) {
19081914
p.updateGraph();
@@ -1956,7 +1962,7 @@ namespace ts.server {
19561962
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
19571963
}
19581964
Debug.assert(!info.isOrphan());
1959-
this.openFiles.push(info);
1965+
this.openFiles.set(info.path, projectRootPath);
19601966

19611967
if (sendConfigFileDiagEvent) {
19621968
configFileErrors = project.getAllProjectErrors();

src/server/project.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,7 @@ namespace ts.server {
10641064
projectService: ProjectService,
10651065
documentRegistry: DocumentRegistry,
10661066
compilerOptions: CompilerOptions,
1067-
projectRootPath: string | undefined,
1067+
projectRootPath: NormalizedPath | undefined,
10681068
currentDirectory: string | undefined) {
10691069
super(InferredProject.newName(),
10701070
ProjectKind.Inferred,
@@ -1080,7 +1080,8 @@ namespace ts.server {
10801080
}
10811081

10821082
addRoot(info: ScriptInfo) {
1083-
this.projectService.startWatchingConfigFilesForInferredProjectRoot(info);
1083+
Debug.assert(info.isScriptOpen());
1084+
this.projectService.startWatchingConfigFilesForInferredProjectRoot(info, this.projectService.openFiles.get(info.path));
10841085
if (!this._isJsInferredProject && info.isJavaScript()) {
10851086
this.toggleJsInferredProject(/*isJsInferredProject*/ true);
10861087
}

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7435,9 +7435,9 @@ declare namespace ts.server {
74357435
*/
74367436
readonly configuredProjects: Map<ConfiguredProject>;
74377437
/**
7438-
* list of open files
7438+
* Open files: with value being project root path, and key being Path of the file that is open
74397439
*/
7440-
readonly openFiles: ScriptInfo[];
7440+
readonly openFiles: Map<NormalizedPath>;
74417441
private compilerOptionsForInferredProjects;
74427442
private compilerOptionsForInferredProjectsPerProjectRoot;
74437443
/**
@@ -7556,7 +7556,7 @@ declare namespace ts.server {
75567556
* The server must start searching from the directory containing
75577557
* the newly opened file.
75587558
*/
7559-
private getConfigFileNameForFile(info, projectRootPath?);
7559+
private getConfigFileNameForFile(info, projectRootPath);
75607560
private printProjects();
75617561
private findConfiguredProjectByProjectName(configFileName);
75627562
private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
@@ -7592,8 +7592,9 @@ declare namespace ts.server {
75927592
* If the config file is found and it refers to existing project, it reloads it either immediately
75937593
* or schedules it for reload depending on delayReload option
75947594
* If the there is no existing project it just opens the configured project for the config file
7595+
* reloadForInfo provides a way to filter out files to reload configured project for
75957596
*/
7596-
private reloadConfiguredProjectForFiles(openFiles, delayReload);
7597+
private reloadConfiguredProjectForFiles<T>(openFiles, delayReload, shouldReloadProjectFor);
75977598
/**
75987599
* Remove the root of inferred project if script info is part of another project
75997600
*/

0 commit comments

Comments
 (0)