Skip to content

Commit d6dfd9a

Browse files
committed
Do not remove inferred project immediately and try to reuse it on next file open
1 parent 6164582 commit d6dfd9a

File tree

6 files changed

+225
-107
lines changed

6 files changed

+225
-107
lines changed

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 129 additions & 42 deletions
Large diffs are not rendered by default.

src/server/editorServices.ts

Lines changed: 75 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -708,17 +708,11 @@ namespace ts.server {
708708
this.handleDeletedFile(info);
709709
}
710710
else if (!info.isScriptOpen()) {
711-
if (info.containingProjects.length === 0) {
712-
// Orphan script info, remove it as we can always reload it on next open file request
713-
this.stopWatchingScriptInfo(info);
714-
this.deleteScriptInfo(info);
715-
}
716-
else {
717-
// file has been changed which might affect the set of referenced files in projects that include
718-
// this file and set of inferred projects
719-
info.delayReloadNonMixedContentFile();
720-
this.delayUpdateProjectGraphs(info.containingProjects);
721-
}
711+
Debug.assert(info.containingProjects.length !== 0);
712+
// file has been changed which might affect the set of referenced files in projects that include
713+
// this file and set of inferred projects
714+
info.delayReloadNonMixedContentFile();
715+
this.delayUpdateProjectGraphs(info.containingProjects);
722716
}
723717
}
724718

@@ -851,15 +845,23 @@ namespace ts.server {
851845

852846
const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) ||
853847
this.getOrCreateSingleInferredProjectIfEnabled() ||
854-
this.createInferredProject(info.isDynamic ? this.currentDirectory : getDirectoryPath(info.path));
848+
this.getOrCreateSingleInferredWithoutProjectRoot(info.isDynamic ? this.currentDirectory : getDirectoryPath(info.path));
855849

856850
project.addRoot(info);
851+
if (info.containingProjects[0] !== project) {
852+
// Ensure this is first project, we could be in this scenario because info could be part of orphan project
853+
info.detachFromProject(project);
854+
info.containingProjects.unshift(project);
855+
}
857856
project.updateGraph();
858857

859858
if (!this.useSingleInferredProject && !project.projectRootPath) {
860859
// Note that we need to create a copy of the array since the list of project can change
861-
for (const inferredProject of this.inferredProjects.slice(0, this.inferredProjects.length - 1)) {
862-
Debug.assert(inferredProject !== project);
860+
for (const inferredProject of this.inferredProjects) {
861+
if (inferredProject === project || inferredProject.isOrphan()) {
862+
continue;
863+
}
864+
863865
// Remove the inferred project if the root of it is now part of newly created inferred project
864866
// e.g through references
865867
// Which means if any root of inferred project is part of more than 1 project can be removed
@@ -870,8 +872,8 @@ namespace ts.server {
870872
// instead of scanning all open files
871873
const roots = inferredProject.getRootScriptInfos();
872874
Debug.assert(roots.length === 1 || !!inferredProject.projectRootPath);
873-
if (roots.length === 1 && roots[0].containingProjects.length > 1) {
874-
this.removeProject(inferredProject);
875+
if (roots.length === 1 && forEach(roots[0].containingProjects, p => p !== roots[0].containingProjects[0] && !p.isOrphan())) {
876+
inferredProject.removeFile(roots[0], /*fileExists*/ true, /*detachFromProject*/ true);
875877
}
876878
}
877879
}
@@ -896,9 +898,8 @@ namespace ts.server {
896898
this.openFilesWithNonRootedDiskPath.delete(canonicalFileName);
897899
}
898900

899-
900901
// collect all projects that should be removed
901-
let projectsToRemove: Project[];
902+
let ensureProjectsForOpenFiles = false;
902903
for (const p of info.containingProjects) {
903904
if (p.projectKind === ProjectKind.Configured) {
904905
if (info.hasMixedContent) {
@@ -908,15 +909,14 @@ namespace ts.server {
908909
// if it would need to be re-created with next file open
909910
}
910911
else if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) {
911-
// If this was the open root file of inferred project
912+
// If this was the last open root file of inferred project
912913
if ((p as InferredProject).isProjectWithSingleRoot()) {
913-
// - when useSingleInferredProject is not set, we can guarantee that this will be the only root
914-
// - other wise remove the project if it is the only root
915-
(projectsToRemove || (projectsToRemove = [])).push(p);
916-
}
917-
else {
918-
p.removeFile(info, fileExists, /*detachFromProject*/ true);
914+
ensureProjectsForOpenFiles = true;
919915
}
916+
917+
p.removeFile(info, fileExists, /*detachFromProject*/ true);
918+
// Do not remove the project even if this was last root of the inferred project
919+
// so that we can reuse this project, if it would need to be re-created with next file open
920920
}
921921

922922
if (!p.languageServiceEnabled) {
@@ -927,27 +927,22 @@ namespace ts.server {
927927
}
928928
}
929929

930-
if (projectsToRemove) {
931-
for (const project of projectsToRemove) {
932-
this.removeProject(project);
933-
}
930+
this.openFiles.delete(info.path);
934931

932+
if (ensureProjectsForOpenFiles) {
935933
// collect orphaned files and assign them to inferred project just like we treat open of a file
936934
this.openFiles.forEach((projectRootPath, path) => {
937-
if (info.path !== path) {
938-
const f = this.getScriptInfoForPath(path as Path);
939-
if (f.isOrphan()) {
940-
this.assignOrphanScriptInfoToInferredProject(f, projectRootPath);
941-
}
935+
const info = this.getScriptInfoForPath(path as Path);
936+
// collect all orphaned script infos from open files
937+
if (info.isOrphan()) {
938+
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
942939
}
943940
});
944-
945-
// Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project)
946-
// is postponed to next file open so that if file from same project is opened,
947-
// we wont end up creating same script infos
948941
}
949942

950-
this.openFiles.delete(info.path);
943+
// Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project)
944+
// is postponed to next file open so that if file from same project is opened,
945+
// we wont end up creating same script infos
951946

952947
// If the current info is being just closed - add the watcher file to track changes
953948
// But if file was deleted, handle that part
@@ -959,16 +954,6 @@ namespace ts.server {
959954
}
960955
}
961956

962-
private deleteOrphanScriptInfoNotInAnyProject() {
963-
this.filenameToScriptInfo.forEach(info => {
964-
if (!info.isScriptOpen() && info.isOrphan()) {
965-
// if there are not projects that include this script info - delete it
966-
this.stopWatchingScriptInfo(info);
967-
this.deleteScriptInfo(info);
968-
}
969-
});
970-
}
971-
972957
private deleteScriptInfo(info: ScriptInfo) {
973958
this.filenameToScriptInfo.delete(info.path);
974959
const realpath = info.getRealpathIfDifferent();
@@ -1673,6 +1658,21 @@ namespace ts.server {
16731658
return this.createInferredProject(/*currentDirectory*/ undefined, /*isSingleInferredProject*/ true);
16741659
}
16751660

1661+
private getOrCreateSingleInferredWithoutProjectRoot(currentDirectory: string | undefined): InferredProject {
1662+
Debug.assert(!this.useSingleInferredProject);
1663+
const expectedCurrentDirectory = this.toCanonicalFileName(this.getNormalizedAbsolutePath(currentDirectory || ""));
1664+
// Reuse the project with same current directory but no roots
1665+
for (const inferredProject of this.inferredProjects) {
1666+
if (!inferredProject.projectRootPath &&
1667+
inferredProject.isOrphan() &&
1668+
inferredProject.canonicalCurrentDirectory === expectedCurrentDirectory) {
1669+
return inferredProject;
1670+
}
1671+
}
1672+
1673+
return this.createInferredProject(currentDirectory);
1674+
}
1675+
16761676
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject {
16771677
const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects;
16781678
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory);
@@ -1719,6 +1719,7 @@ namespace ts.server {
17191719
for (const project of toAddInfo.containingProjects) {
17201720
// Add the projects only if they can use symLink targets and not already in the list
17211721
if (project.languageServiceEnabled &&
1722+
!project.isOrphan() &&
17221723
!project.getCompilerOptions().preserveSymlinks &&
17231724
!contains(info.containingProjects, project)) {
17241725
if (!projects) {
@@ -1947,16 +1948,14 @@ namespace ts.server {
19471948
// so it will be added to inferred project as a root. (for sake of this example assume single inferred project is false)
19481949
// So at this poing a.ts is part of first inferred project and second inferred project (of which c.ts is root)
19491950
// And hence it needs to be removed from the first inferred project.
1950-
if (info.containingProjects.length > 1 &&
1951-
info.containingProjects[0].projectKind === ProjectKind.Inferred &&
1952-
info.containingProjects[0].isRoot(info)) {
1953-
const inferredProject = info.containingProjects[0] as InferredProject;
1954-
if (inferredProject.isProjectWithSingleRoot()) {
1955-
this.removeProject(inferredProject);
1956-
}
1957-
else {
1958-
inferredProject.removeFile(info, /*fileExists*/ true, /*detachFromProject*/ true);
1959-
}
1951+
Debug.assert(info.containingProjects.length > 0);
1952+
const firstProject = info.containingProjects[0];
1953+
1954+
if (!firstProject.isOrphan() &&
1955+
firstProject.projectKind === ProjectKind.Inferred &&
1956+
firstProject.isRoot(info) &&
1957+
forEach(info.containingProjects, p => p !== firstProject && !p.isOrphan())) {
1958+
firstProject.removeFile(info, /*fileExists*/ true, /*detachFromProject*/ true);
19601959
}
19611960
}
19621961

@@ -2050,9 +2049,9 @@ namespace ts.server {
20502049
if (info.isOrphan()) {
20512050
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
20522051
}
2053-
20542052
Debug.assert(!info.isOrphan());
20552053

2054+
20562055
// Remove the configured projects that have zero references from open files.
20572056
// This was postponed from closeOpenFile to after opening next file,
20582057
// so that we can reuse the project if we need to right away
@@ -2062,11 +2061,26 @@ namespace ts.server {
20622061
}
20632062
});
20642063

2064+
// Remove orphan inferred projects now that we have reused projects
2065+
// We need to create a duplicate because we cant guarantee order after removal
2066+
for (const inferredProject of this.inferredProjects.slice()) {
2067+
if (inferredProject.isOrphan()) {
2068+
this.removeProject(inferredProject);
2069+
}
2070+
}
2071+
20652072
// Delete the orphan files here because there might be orphan script infos (which are not part of project)
20662073
// when some file/s were closed which resulted in project removal.
20672074
// It was then postponed to cleanup these script infos so that they can be reused if
20682075
// the file from that old project is reopened because of opening file from here.
2069-
this.deleteOrphanScriptInfoNotInAnyProject();
2076+
this.filenameToScriptInfo.forEach(info => {
2077+
if (!info.isScriptOpen() && info.isOrphan()) {
2078+
// if there are not projects that include this script info - delete it
2079+
this.stopWatchingScriptInfo(info);
2080+
this.deleteScriptInfo(info);
2081+
}
2082+
});
2083+
20702084
this.printProjects();
20712085

20722086
return { configFileName, configFileErrors };

src/server/project.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,11 @@ namespace ts.server {
591591
return this.rootFiles && this.rootFiles.length > 0;
592592
}
593593

594+
/*@internal*/
595+
isOrphan() {
596+
return false;
597+
}
598+
594599
getRootFiles() {
595600
return this.rootFiles && this.rootFiles.map(info => info.fileName);
596601
}
@@ -1173,6 +1178,10 @@ namespace ts.server {
11731178
/** this is canonical project root path */
11741179
readonly projectRootPath: string | undefined;
11751180

1181+
/*@internal*/
1182+
/** stored only if their is no projectRootPath and this isnt single inferred project */
1183+
readonly canonicalCurrentDirectory: string | undefined;
1184+
11761185
/*@internal*/
11771186
constructor(
11781187
projectService: ProjectService,
@@ -1191,6 +1200,9 @@ namespace ts.server {
11911200
projectService.host,
11921201
currentDirectory);
11931202
this.projectRootPath = projectRootPath && projectService.toCanonicalFileName(projectRootPath);
1203+
if (!projectRootPath && !projectService.useSingleInferredProject) {
1204+
this.canonicalCurrentDirectory = projectService.toCanonicalFileName(this.currentDirectory);
1205+
}
11941206
this.enableGlobalPlugins();
11951207
}
11961208

@@ -1213,6 +1225,11 @@ namespace ts.server {
12131225
}
12141226
}
12151227

1228+
/*@internal*/
1229+
isOrphan() {
1230+
return !this.hasRoots();
1231+
}
1232+
12161233
isProjectWithSingleRoot() {
12171234
// - when useSingleInferredProject is not set and projectRootPath is not set,
12181235
// we can guarantee that this will be the only root

src/server/scriptInfo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ namespace ts.server {
460460
}
461461

462462
isOrphan() {
463-
return this.containingProjects.length === 0;
463+
return !forEach(this.containingProjects, p => !p.isOrphan());
464464
}
465465

466466
/**

src/server/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,7 @@ namespace ts.server {
865865
symLinkedProjects = this.projectService.getSymlinkedProjects(scriptInfo);
866866
}
867867
// filter handles case when 'projects' is undefined
868-
projects = filter(projects, p => p.languageServiceEnabled);
868+
projects = filter(projects, p => p.languageServiceEnabled && !p.isOrphan());
869869
if ((!projects || !projects.length) && !symLinkedProjects) {
870870
return Errors.ThrowNoProject();
871871
}
@@ -1336,7 +1336,7 @@ namespace ts.server {
13361336
symLinkedProjects ? { projects, symLinkedProjects } : projects,
13371337
(project, info) => {
13381338
let result: protocol.CompileOnSaveAffectedFileListSingleProject;
1339-
if (project.compileOnSaveEnabled && project.languageServiceEnabled && !project.getCompilationSettings().noEmit) {
1339+
if (project.compileOnSaveEnabled && project.languageServiceEnabled && !project.isOrphan() && !project.getCompilationSettings().noEmit) {
13401340
result = {
13411341
projectFileName: project.getProjectName(),
13421342
fileNames: project.getCompileOnSaveAffectedFileList(info),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8067,7 +8067,6 @@ declare namespace ts.server {
80678067
* @param info The file that has been closed or newly configured
80688068
*/
80698069
private closeOpenFile;
8070-
private deleteOrphanScriptInfoNotInAnyProject;
80718070
private deleteScriptInfo;
80728071
private configFileExists;
80738072
private setConfigFileExistenceByNewConfiguredProject;
@@ -8124,6 +8123,7 @@ declare namespace ts.server {
81248123
private sendConfigFileDiagEvent;
81258124
private getOrCreateInferredProjectForProjectRootPathIfEnabled;
81268125
private getOrCreateSingleInferredProjectIfEnabled;
8126+
private getOrCreateSingleInferredWithoutProjectRoot;
81278127
private createInferredProject;
81288128
getScriptInfo(uncheckedFileName: string): ScriptInfo;
81298129
private watchClosedScriptInfo;

0 commit comments

Comments
 (0)