Skip to content

Commit 6fcbc07

Browse files
committed
Defer removing configured project on remove
1 parent 40bc18e commit 6fcbc07

File tree

25 files changed

+7047
-5895
lines changed

25 files changed

+7047
-5895
lines changed

src/harness/projectServiceStateLogger.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import {
1414
AutoImportProviderProject,
1515
AuxiliaryProject,
16+
ConfiguredProject,
1617
isBackgroundProject,
1718
isConfiguredProject,
1819
LogLevel,
@@ -34,6 +35,7 @@ interface ProjectData {
3435
isClosed: ReturnType<Project["isClosed"]>;
3536
isOrphan: ReturnType<Project["isOrphan"]>;
3637
noOpenRef: boolean;
38+
deferredClose: ConfiguredProject["deferredClose"];
3739
documentPositionMappers: SourceMapper["documentPositionMappers"];
3840
autoImportProviderHost: Project["autoImportProviderHost"];
3941
noDtsResolutionProject: Project["noDtsResolutionProject"];
@@ -118,6 +120,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
118120
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isClosed", project.isClosed(), projectDiff, projectPropertyLogs);
119121
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isOrphan", !isBackgroundProject(project) && project.isOrphan(), projectDiff, projectPropertyLogs);
120122
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "noOpenRef", isConfiguredProject(project) && !project.hasOpenRef(), projectDiff, projectPropertyLogs);
123+
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "deferredClose", isConfiguredProject(project) && project.deferredClose, projectDiff, projectPropertyLogs);
121124
projectDiff = printMapPropertyValue(
122125
PrintPropertyWhen.Changed,
123126
data?.documentPositionMappers,
@@ -148,6 +151,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
148151
isClosed: project.isClosed(),
149152
isOrphan: !isBackgroundProject(project) && project.isOrphan(),
150153
noOpenRef: isConfiguredProject(project) && !project.hasOpenRef(),
154+
deferredClose: isConfiguredProject(project) && project.deferredClose,
151155
autoImportProviderHost: project.autoImportProviderHost,
152156
noDtsResolutionProject: project.noDtsResolutionProject,
153157
originalConfiguredProjects: project.originalConfiguredProjects && new Set(project.originalConfiguredProjects),

src/server/editorServices.ts

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ import {
160160
isDynamicFileName,
161161
isInferredProject,
162162
isInferredProjectName,
163+
isProjectDeferredClose,
163164
ITypingsInstaller,
164165
Logger,
165166
LogLevel,
@@ -1339,6 +1340,7 @@ export class ProjectService {
13391340
}
13401341

13411342
private delayUpdateProjectGraph(project: Project) {
1343+
if (isProjectDeferredClose(project)) return;
13421344
project.markAsDirty();
13431345
if (isBackgroundProject(project)) return;
13441346
const projectName = project.getProjectName();
@@ -1771,21 +1773,24 @@ export class ProjectService {
17711773
/** @internal */
17721774
private onConfigFileChanged(canonicalConfigFilePath: NormalizedPath, eventKind: FileWatcherEventKind) {
17731775
const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath)!;
1776+
const project = this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
1777+
const wasDefferedClose = project?.deferredClose;
17741778
if (eventKind === FileWatcherEventKind.Deleted) {
17751779
// Update the cached status
17761780
// We arent updating or removing the cached config file presence info as that will be taken care of by
17771781
// releaseParsedConfig when the project is closed or doesnt need this config any more (depending on tracking open files)
17781782
configFileExistenceInfo.exists = false;
17791783

1780-
// Remove the configured project for this config file
1781-
const project = configFileExistenceInfo.config?.projects.has(canonicalConfigFilePath) ?
1782-
this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath) :
1783-
undefined;
1784-
if (project) this.removeProject(project);
1784+
// Deferred remove the configured project for this config file
1785+
if (project) project.deferredClose = true;
17851786
}
17861787
else {
17871788
// Update the cached status
17881789
configFileExistenceInfo.exists = true;
1790+
if (wasDefferedClose) {
1791+
project.deferredClose = undefined;
1792+
project.markAsDirty();
1793+
}
17891794
}
17901795

17911796
// Update projects watching config
@@ -1799,7 +1804,7 @@ export class ProjectService {
17991804
// Get open files to reload projects for
18001805
this.delayReloadConfiguredProjectsForFile(
18011806
configFileExistenceInfo,
1802-
eventKind !== FileWatcherEventKind.Deleted ?
1807+
!wasDefferedClose && eventKind !== FileWatcherEventKind.Deleted ?
18031808
identity : // Reload open files if they are root of inferred project
18041809
returnTrue, // Reload all the open files impacted by config file
18051810
"Change in config file detected",
@@ -1814,13 +1819,13 @@ export class ProjectService {
18141819
* shouldReloadProjectFor provides a way to filter out files to reload configured project for
18151820
*/
18161821
private delayReloadConfiguredProjectsForFile(
1817-
configFileExistenceInfo: ConfigFileExistenceInfo,
1822+
configFileExistenceInfo: ConfigFileExistenceInfo | undefined,
18181823
shouldReloadProjectFor: (infoIsRootOfInferredProject: boolean) => boolean,
18191824
reason: string,
18201825
) {
18211826
const updatedProjects = new Set<ConfiguredProject>();
18221827
// try to reload config file for all open files
1823-
configFileExistenceInfo.openFilesImpactedByConfigFile?.forEach((infoIsRootOfInferredProject, path) => {
1828+
configFileExistenceInfo?.openFilesImpactedByConfigFile?.forEach((infoIsRootOfInferredProject, path) => {
18241829
// Invalidate default config file name for open file
18251830
this.configFileForOpenFiles.delete(path);
18261831
// Filter out the files that need to be ignored
@@ -2367,7 +2372,8 @@ export class ProjectService {
23672372
findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined {
23682373
// make sure that casing of config file name is consistent
23692374
const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName));
2370-
return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
2375+
const result = this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
2376+
return !result?.deferredClose ? result : undefined;
23712377
}
23722378

23732379
private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string): ConfiguredProject | undefined {
@@ -2517,6 +2523,7 @@ export class ProjectService {
25172523
this.documentRegistry,
25182524
configFileExistenceInfo.config.cachedDirectoryStructureHost,
25192525
);
2526+
Debug.assert(!this.configuredProjects.has(canonicalConfigFilePath));
25202527
this.configuredProjects.set(canonicalConfigFilePath, project);
25212528
this.createConfigFileWatcherForParsedConfig(configFileName, canonicalConfigFilePath, project);
25222529
return project;
@@ -3467,6 +3474,7 @@ export class ProjectService {
34673474
this.externalProjectToConfiguredProjectMap.forEach(projects =>
34683475
projects.forEach(project => {
34693476
if (
3477+
!project.deferredClose &&
34703478
!project.isClosed() &&
34713479
project.hasExternalProjectRef() &&
34723480
project.pendingUpdateLevel === ProgramUpdateLevel.Full &&
@@ -3755,10 +3763,7 @@ export class ProjectService {
37553763
return originalLocation;
37563764

37573765
function addOriginalConfiguredProject(originalProject: ConfiguredProject) {
3758-
if (!project.originalConfiguredProjects) {
3759-
project.originalConfiguredProjects = new Set();
3760-
}
3761-
project.originalConfiguredProjects.add(originalProject.canonicalConfigFilePath);
3766+
(project.originalConfiguredProjects ??= new Set()).add(originalProject.canonicalConfigFilePath);
37623767
}
37633768
}
37643769

@@ -3990,7 +3995,7 @@ export class ProjectService {
39903995
private removeOrphanConfiguredProjects(toRetainConfiguredProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) {
39913996
const toRemoveConfiguredProjects = new Map(this.configuredProjects);
39923997
const markOriginalProjectsAsUsed = (project: Project) => {
3993-
if (!project.isOrphan() && project.originalConfiguredProjects) {
3998+
if (project.originalConfiguredProjects && (isConfiguredProject(project) || !project.isOrphan())) {
39943999
project.originalConfiguredProjects.forEach(
39954000
(_value, configuredProjectPath) => {
39964001
const project = this.getConfiguredProjectByCanonicalConfigFilePath(configuredProjectPath);
@@ -4012,24 +4017,22 @@ export class ProjectService {
40124017
this.inferredProjects.forEach(markOriginalProjectsAsUsed);
40134018
this.externalProjects.forEach(markOriginalProjectsAsUsed);
40144019
this.configuredProjects.forEach(project => {
4020+
if (!toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) return;
40154021
// If project has open ref (there are more than zero references from external project/open file), keep it alive as well as any project it references
40164022
if (project.hasOpenRef()) {
40174023
retainConfiguredProject(project);
40184024
}
4019-
else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) {
4020-
// If the configured project for project reference has more than zero references, keep it alive
4021-
forEachReferencedProject(
4022-
project,
4023-
ref => isRetained(ref) && retainConfiguredProject(project),
4024-
);
4025+
// If the configured project for project reference has more than zero references, keep it alive
4026+
else if (forEachReferencedProject(project, ref => isRetained(ref))) {
4027+
retainConfiguredProject(project);
40254028
}
40264029
});
40274030

40284031
// Remove all the non marked projects
40294032
toRemoveConfiguredProjects.forEach(project => this.removeProject(project));
40304033

40314034
function isRetained(project: ConfiguredProject) {
4032-
return project.hasOpenRef() || !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath);
4035+
return !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath) || project.hasOpenRef();
40334036
}
40344037

40354038
function retainConfiguredProject(project: ConfiguredProject) {
@@ -4142,7 +4145,7 @@ export class ProjectService {
41424145
synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[], includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics[] {
41434146
const files: ProjectFilesWithTSDiagnostics[] = [];
41444147
this.collectChanges(knownProjects, this.externalProjects, includeProjectReferenceRedirectInfo, files);
4145-
this.collectChanges(knownProjects, this.configuredProjects.values(), includeProjectReferenceRedirectInfo, files);
4148+
this.collectChanges(knownProjects, mapDefinedIterator(this.configuredProjects.values(), p => p.deferredClose ? undefined : p), includeProjectReferenceRedirectInfo, files);
41464149
this.collectChanges(knownProjects, this.inferredProjects, includeProjectReferenceRedirectInfo, files);
41474150
return files;
41484151
}
@@ -4610,28 +4613,28 @@ export class ProjectService {
46104613

46114614
// Process all pending plugins, partitioned by project. This way a project with few plugins doesn't need to wait
46124615
// on a project with many plugins.
4613-
await Promise.all(map(pendingPlugins, ([project, promises]) => this.enableRequestedPluginsForProjectAsync(project, promises)));
4616+
let sendProjectsUpdatedInBackgroundEvent = false;
4617+
await Promise.all(map(pendingPlugins, async ([project, promises]) => {
4618+
// Await all pending plugin imports. This ensures all requested plugin modules are fully loaded
4619+
// prior to patching the language service, and that any promise rejections are observed.
4620+
const results = await Promise.all(promises);
4621+
if (project.isClosed() || isProjectDeferredClose(project)) {
4622+
this.logger.info(`Cancelling plugin enabling for ${project.getProjectName()} as it is ${project.isClosed() ? "closed" : "deferred close"}`);
4623+
// project is not alive, so don't enable plugins.
4624+
return;
4625+
}
4626+
sendProjectsUpdatedInBackgroundEvent = true;
4627+
for (const result of results) {
4628+
this.endEnablePlugin(project, result);
4629+
}
4630+
4631+
// Plugins may have modified external files, so mark the project as dirty.
4632+
this.delayUpdateProjectGraph(project);
4633+
}));
46144634

46154635
// Clear the pending operation and notify the client that projects have been updated.
46164636
this.currentPluginEnablementPromise = undefined;
4617-
this.sendProjectsUpdatedInBackgroundEvent();
4618-
}
4619-
4620-
private async enableRequestedPluginsForProjectAsync(project: Project, promises: Promise<BeginEnablePluginResult>[]) {
4621-
// Await all pending plugin imports. This ensures all requested plugin modules are fully loaded
4622-
// prior to patching the language service, and that any promise rejections are observed.
4623-
const results = await Promise.all(promises);
4624-
if (project.isClosed()) {
4625-
// project is not alive, so don't enable plugins.
4626-
return;
4627-
}
4628-
4629-
for (const result of results) {
4630-
this.endEnablePlugin(project, result);
4631-
}
4632-
4633-
// Plugins may have modified external files, so mark the project as dirty.
4634-
this.delayUpdateProjectGraph(project);
4637+
if (sendProjectsUpdatedInBackgroundEvent) this.sendProjectsUpdatedInBackgroundEvent();
46354638
}
46364639

46374640
configurePlugin(args: protocol.ConfigurePluginRequestArguments) {

src/server/project.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2708,6 +2708,9 @@ export class ConfiguredProject extends Project {
27082708
/** @internal */
27092709
private compilerHost?: CompilerHost;
27102710

2711+
/** @internal */
2712+
deferredClose?: boolean;
2713+
27112714
/** @internal */
27122715
constructor(
27132716
configFileName: NormalizedPath,
@@ -2768,6 +2771,7 @@ export class ConfiguredProject extends Project {
27682771
* @returns: true if set of files in the project stays the same and false - otherwise.
27692772
*/
27702773
override updateGraph(): boolean {
2774+
if (this.deferredClose) return false;
27712775
const isInitialLoad = this.isInitialLoadPending();
27722776
this.isInitialLoadPending = returnFalse;
27732777
const updateLevel = this.pendingUpdateLevel;
@@ -2884,6 +2888,11 @@ export class ConfiguredProject extends Project {
28842888
super.close();
28852889
}
28862890

2891+
override markAsDirty() {
2892+
if (this.deferredClose) return;
2893+
super.markAsDirty();
2894+
}
2895+
28872896
/** @internal */
28882897
addExternalProjectReference() {
28892898
this.externalProjectRefCount++;
@@ -2933,6 +2942,7 @@ export class ConfiguredProject extends Project {
29332942
}
29342943

29352944
const configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(this.canonicalConfigFilePath)!;
2945+
if (this.deferredClose) return !!configFileExistenceInfo.openFilesImpactedByConfigFile?.size;
29362946
if (this.projectService.hasPendingProjectUpdate(this)) {
29372947
// If there is pending update for this project,
29382948
// we dont know if this project would be needed by any of the open files impacted by this config file
@@ -2958,6 +2968,10 @@ export class ConfiguredProject extends Project {
29582968
) || false;
29592969
}
29602970

2971+
override isOrphan(): boolean {
2972+
return !!this.deferredClose;
2973+
}
2974+
29612975
/** @internal */
29622976
hasExternalProjectRef() {
29632977
return !!this.externalProjectRefCount;
@@ -3015,3 +3029,8 @@ export function isExternalProject(project: Project): project is ExternalProject
30153029
export function isBackgroundProject(project: Project): project is AutoImportProviderProject | AuxiliaryProject {
30163030
return project.projectKind === ProjectKind.AutoImportProvider || project.projectKind === ProjectKind.Auxiliary;
30173031
}
3032+
3033+
/** @internal */
3034+
export function isProjectDeferredClose(project: Project): project is ConfiguredProject {
3035+
return isConfiguredProject(project) && !!project.deferredClose;
3036+
}

src/server/scriptInfo.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
isConfiguredProject,
4545
isExternalProject,
4646
isInferredProject,
47+
isProjectDeferredClose,
4748
maxFileSize,
4849
NormalizedPath,
4950
Project,
@@ -567,7 +568,10 @@ export class ScriptInfo {
567568
case 0:
568569
return Errors.ThrowNoProject();
569570
case 1:
570-
return ensurePrimaryProjectKind(this.containingProjects[0]);
571+
return ensurePrimaryProjectKind(
572+
!isProjectDeferredClose(this.containingProjects[0]) ?
573+
this.containingProjects[0] : undefined,
574+
);
571575
default:
572576
// If this file belongs to multiple projects, below is the order in which default project is used
573577
// - for open script info, its default configured project during opening is default if info is part of it
@@ -583,6 +587,7 @@ export class ScriptInfo {
583587
for (let index = 0; index < this.containingProjects.length; index++) {
584588
const project = this.containingProjects[index];
585589
if (isConfiguredProject(project)) {
590+
if (project.deferredClose) continue;
586591
if (!project.isSourceOfProjectReferenceRedirect(this.fileName)) {
587592
// If we havent found default configuredProject and
588593
// its not the last one, find it and use that one if there

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3485,6 +3485,8 @@ declare namespace ts {
34853485
getAllProjectErrors(): readonly Diagnostic[];
34863486
setProjectErrors(projectErrors: Diagnostic[]): void;
34873487
close(): void;
3488+
markAsDirty(): void;
3489+
isOrphan(): boolean;
34883490
getEffectiveTypeRoots(): string[];
34893491
}
34903492
/**
@@ -3916,7 +3918,6 @@ declare namespace ts {
39163918
hasDeferredExtension(): boolean;
39173919
private enableRequestedPluginsAsync;
39183920
private enableRequestedPluginsWorker;
3919-
private enableRequestedPluginsForProjectAsync;
39203921
configurePlugin(args: protocol.ConfigurePluginRequestArguments): void;
39213922
}
39223923
function formatMessage<T extends protocol.Message>(msg: T, logger: Logger, byteLength: (s: string, encoding: BufferEncoding) => number, newLine: string): string;

0 commit comments

Comments
 (0)