Skip to content

Commit 784e0ef

Browse files
committed
Defer removing configured project on remove
1 parent 4c1fa52 commit 784e0ef

25 files changed

Lines changed: 7047 additions & 5895 deletions

File tree

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,
@@ -32,6 +33,7 @@ interface ProjectData {
3233
isClosed: ReturnType<Project["isClosed"]>;
3334
isOrphan: ReturnType<Project["isOrphan"]>;
3435
noOpenRef: boolean;
36+
deferredClose: ConfiguredProject["deferredClose"];
3537
documentPositionMappers: SourceMapper["documentPositionMappers"];
3638
autoImportProviderHost: Project["autoImportProviderHost"];
3739
noDtsResolutionProject: Project["noDtsResolutionProject"];
@@ -116,6 +118,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
116118
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isClosed", project.isClosed(), projectDiff, projectPropertyLogs);
117119
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isOrphan", !isBackgroundProject(project) && project.isOrphan(), projectDiff, projectPropertyLogs);
118120
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "noOpenRef", isConfiguredProject(project) && !project.hasOpenRef(), projectDiff, projectPropertyLogs);
121+
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "deferredClose", isConfiguredProject(project) && project.deferredClose, projectDiff, projectPropertyLogs);
119122
projectDiff = printMapPropertyValue(
120123
PrintPropertyWhen.Changed,
121124
data?.documentPositionMappers,
@@ -146,6 +149,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
146149
isClosed: project.isClosed(),
147150
isOrphan: !isBackgroundProject(project) && project.isOrphan(),
148151
noOpenRef: isConfiguredProject(project) && !project.hasOpenRef(),
152+
deferredClose: isConfiguredProject(project) && project.deferredClose,
149153
autoImportProviderHost: project.autoImportProviderHost,
150154
noDtsResolutionProject: project.noDtsResolutionProject,
151155
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,
@@ -1352,6 +1353,7 @@ export class ProjectService {
13521353
}
13531354

13541355
private delayUpdateProjectGraph(project: Project) {
1356+
if (isProjectDeferredClose(project)) return;
13551357
project.markAsDirty();
13561358
if (isBackgroundProject(project)) return;
13571359
const projectName = project.getProjectName();
@@ -1784,21 +1786,24 @@ export class ProjectService {
17841786
/** @internal */
17851787
private onConfigFileChanged(canonicalConfigFilePath: NormalizedPath, eventKind: FileWatcherEventKind) {
17861788
const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath)!;
1789+
const project = this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
1790+
const wasDefferedClose = project?.deferredClose;
17871791
if (eventKind === FileWatcherEventKind.Deleted) {
17881792
// Update the cached status
17891793
// We arent updating or removing the cached config file presence info as that will be taken care of by
17901794
// releaseParsedConfig when the project is closed or doesnt need this config any more (depending on tracking open files)
17911795
configFileExistenceInfo.exists = false;
17921796

1793-
// Remove the configured project for this config file
1794-
const project = configFileExistenceInfo.config?.projects.has(canonicalConfigFilePath) ?
1795-
this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath) :
1796-
undefined;
1797-
if (project) this.removeProject(project);
1797+
// Deferred remove the configured project for this config file
1798+
if (project) project.deferredClose = true;
17981799
}
17991800
else {
18001801
// Update the cached status
18011802
configFileExistenceInfo.exists = true;
1803+
if (wasDefferedClose) {
1804+
project.deferredClose = undefined;
1805+
project.markAsDirty();
1806+
}
18021807
}
18031808

18041809
// Update projects watching config
@@ -1812,7 +1817,7 @@ export class ProjectService {
18121817
// Get open files to reload projects for
18131818
this.delayReloadConfiguredProjectsForFile(
18141819
configFileExistenceInfo,
1815-
eventKind !== FileWatcherEventKind.Deleted ?
1820+
!wasDefferedClose && eventKind !== FileWatcherEventKind.Deleted ?
18161821
identity : // Reload open files if they are root of inferred project
18171822
returnTrue, // Reload all the open files impacted by config file
18181823
"Change in config file detected",
@@ -1827,13 +1832,13 @@ export class ProjectService {
18271832
* shouldReloadProjectFor provides a way to filter out files to reload configured project for
18281833
*/
18291834
private delayReloadConfiguredProjectsForFile(
1830-
configFileExistenceInfo: ConfigFileExistenceInfo,
1835+
configFileExistenceInfo: ConfigFileExistenceInfo | undefined,
18311836
shouldReloadProjectFor: (infoIsRootOfInferredProject: boolean) => boolean,
18321837
reason: string,
18331838
) {
18341839
const updatedProjects = new Set<ConfiguredProject>();
18351840
// try to reload config file for all open files
1836-
configFileExistenceInfo.openFilesImpactedByConfigFile?.forEach((infoIsRootOfInferredProject, path) => {
1841+
configFileExistenceInfo?.openFilesImpactedByConfigFile?.forEach((infoIsRootOfInferredProject, path) => {
18371842
// Invalidate default config file name for open file
18381843
this.configFileForOpenFiles.delete(path);
18391844
// Filter out the files that need to be ignored
@@ -2380,7 +2385,8 @@ export class ProjectService {
23802385
findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined {
23812386
// make sure that casing of config file name is consistent
23822387
const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName));
2383-
return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
2388+
const result = this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
2389+
return !result?.deferredClose ? result : undefined;
23842390
}
23852391

23862392
private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string): ConfiguredProject | undefined {
@@ -2530,6 +2536,7 @@ export class ProjectService {
25302536
this.documentRegistry,
25312537
configFileExistenceInfo.config.cachedDirectoryStructureHost,
25322538
);
2539+
Debug.assert(!this.configuredProjects.has(canonicalConfigFilePath));
25332540
this.configuredProjects.set(canonicalConfigFilePath, project);
25342541
this.createConfigFileWatcherForParsedConfig(configFileName, canonicalConfigFilePath, project);
25352542
return project;
@@ -3487,6 +3494,7 @@ export class ProjectService {
34873494
this.externalProjectToConfiguredProjectMap.forEach(projects =>
34883495
projects.forEach(project => {
34893496
if (
3497+
!project.deferredClose &&
34903498
!project.isClosed() &&
34913499
project.hasExternalProjectRef() &&
34923500
project.pendingUpdateLevel === ProgramUpdateLevel.Full &&
@@ -3775,10 +3783,7 @@ export class ProjectService {
37753783
return originalLocation;
37763784

37773785
function addOriginalConfiguredProject(originalProject: ConfiguredProject) {
3778-
if (!project.originalConfiguredProjects) {
3779-
project.originalConfiguredProjects = new Set();
3780-
}
3781-
project.originalConfiguredProjects.add(originalProject.canonicalConfigFilePath);
3786+
(project.originalConfiguredProjects ??= new Set()).add(originalProject.canonicalConfigFilePath);
37823787
}
37833788
}
37843789

@@ -4010,7 +4015,7 @@ export class ProjectService {
40104015
private removeOrphanConfiguredProjects(toRetainConfiguredProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) {
40114016
const toRemoveConfiguredProjects = new Map(this.configuredProjects);
40124017
const markOriginalProjectsAsUsed = (project: Project) => {
4013-
if (!project.isOrphan() && project.originalConfiguredProjects) {
4018+
if (project.originalConfiguredProjects && (isConfiguredProject(project) || !project.isOrphan())) {
40144019
project.originalConfiguredProjects.forEach(
40154020
(_value, configuredProjectPath) => {
40164021
const project = this.getConfiguredProjectByCanonicalConfigFilePath(configuredProjectPath);
@@ -4032,24 +4037,22 @@ export class ProjectService {
40324037
this.inferredProjects.forEach(markOriginalProjectsAsUsed);
40334038
this.externalProjects.forEach(markOriginalProjectsAsUsed);
40344039
this.configuredProjects.forEach(project => {
4040+
if (!toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) return;
40354041
// 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
40364042
if (project.hasOpenRef()) {
40374043
retainConfiguredProject(project);
40384044
}
4039-
else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) {
4040-
// If the configured project for project reference has more than zero references, keep it alive
4041-
forEachReferencedProject(
4042-
project,
4043-
ref => isRetained(ref) && retainConfiguredProject(project),
4044-
);
4045+
// If the configured project for project reference has more than zero references, keep it alive
4046+
else if (forEachReferencedProject(project, ref => isRetained(ref))) {
4047+
retainConfiguredProject(project);
40454048
}
40464049
});
40474050

40484051
// Remove all the non marked projects
40494052
toRemoveConfiguredProjects.forEach(project => this.removeProject(project));
40504053

40514054
function isRetained(project: ConfiguredProject) {
4052-
return project.hasOpenRef() || !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath);
4055+
return !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath) || project.hasOpenRef();
40534056
}
40544057

40554058
function retainConfiguredProject(project: ConfiguredProject) {
@@ -4162,7 +4165,7 @@ export class ProjectService {
41624165
synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[], includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics[] {
41634166
const files: ProjectFilesWithTSDiagnostics[] = [];
41644167
this.collectChanges(knownProjects, this.externalProjects, includeProjectReferenceRedirectInfo, files);
4165-
this.collectChanges(knownProjects, this.configuredProjects.values(), includeProjectReferenceRedirectInfo, files);
4168+
this.collectChanges(knownProjects, mapDefinedIterator(this.configuredProjects.values(), p => p.deferredClose ? undefined : p), includeProjectReferenceRedirectInfo, files);
41664169
this.collectChanges(knownProjects, this.inferredProjects, includeProjectReferenceRedirectInfo, files);
41674170
return files;
41684171
}
@@ -4630,28 +4633,28 @@ export class ProjectService {
46304633

46314634
// Process all pending plugins, partitioned by project. This way a project with few plugins doesn't need to wait
46324635
// on a project with many plugins.
4633-
await Promise.all(map(pendingPlugins, ([project, promises]) => this.enableRequestedPluginsForProjectAsync(project, promises)));
4636+
let sendProjectsUpdatedInBackgroundEvent = false;
4637+
await Promise.all(map(pendingPlugins, async ([project, promises]) => {
4638+
// Await all pending plugin imports. This ensures all requested plugin modules are fully loaded
4639+
// prior to patching the language service, and that any promise rejections are observed.
4640+
const results = await Promise.all(promises);
4641+
if (project.isClosed() || isProjectDeferredClose(project)) {
4642+
this.logger.info(`Cancelling plugin enabling for ${project.getProjectName()} as it is ${project.isClosed() ? "closed" : "deferred close"}`);
4643+
// project is not alive, so don't enable plugins.
4644+
return;
4645+
}
4646+
sendProjectsUpdatedInBackgroundEvent = true;
4647+
for (const result of results) {
4648+
this.endEnablePlugin(project, result);
4649+
}
4650+
4651+
// Plugins may have modified external files, so mark the project as dirty.
4652+
this.delayUpdateProjectGraph(project);
4653+
}));
46344654

46354655
// Clear the pending operation and notify the client that projects have been updated.
46364656
this.currentPluginEnablementPromise = undefined;
4637-
this.sendProjectsUpdatedInBackgroundEvent();
4638-
}
4639-
4640-
private async enableRequestedPluginsForProjectAsync(project: Project, promises: Promise<BeginEnablePluginResult>[]) {
4641-
// Await all pending plugin imports. This ensures all requested plugin modules are fully loaded
4642-
// prior to patching the language service, and that any promise rejections are observed.
4643-
const results = await Promise.all(promises);
4644-
if (project.isClosed()) {
4645-
// project is not alive, so don't enable plugins.
4646-
return;
4647-
}
4648-
4649-
for (const result of results) {
4650-
this.endEnablePlugin(project, result);
4651-
}
4652-
4653-
// Plugins may have modified external files, so mark the project as dirty.
4654-
this.delayUpdateProjectGraph(project);
4657+
if (sendProjectsUpdatedInBackgroundEvent) this.sendProjectsUpdatedInBackgroundEvent();
46554658
}
46564659

46574660
configurePlugin(args: protocol.ConfigurePluginRequestArguments) {

src/server/project.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2713,6 +2713,9 @@ export class ConfiguredProject extends Project {
27132713
/** @internal */
27142714
skipConfigDiagEvent?: true;
27152715

2716+
/** @internal */
2717+
deferredClose?: boolean;
2718+
27162719
/** @internal */
27172720
constructor(
27182721
configFileName: NormalizedPath,
@@ -2773,6 +2776,7 @@ export class ConfiguredProject extends Project {
27732776
* @returns: true if set of files in the project stays the same and false - otherwise.
27742777
*/
27752778
override updateGraph(): boolean {
2779+
if (this.deferredClose) return false;
27762780
const isInitialLoad = this.isInitialLoadPending();
27772781
this.isInitialLoadPending = returnFalse;
27782782
const updateLevel = this.pendingUpdateLevel;
@@ -2892,6 +2896,11 @@ export class ConfiguredProject extends Project {
28922896
super.close();
28932897
}
28942898

2899+
override markAsDirty() {
2900+
if (this.deferredClose) return;
2901+
super.markAsDirty();
2902+
}
2903+
28952904
/** @internal */
28962905
addExternalProjectReference() {
28972906
this.externalProjectRefCount++;
@@ -2941,6 +2950,7 @@ export class ConfiguredProject extends Project {
29412950
}
29422951

29432952
const configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(this.canonicalConfigFilePath)!;
2953+
if (this.deferredClose) return !!configFileExistenceInfo.openFilesImpactedByConfigFile?.size;
29442954
if (this.projectService.hasPendingProjectUpdate(this)) {
29452955
// If there is pending update for this project,
29462956
// we dont know if this project would be needed by any of the open files impacted by this config file
@@ -2966,6 +2976,10 @@ export class ConfiguredProject extends Project {
29662976
) || false;
29672977
}
29682978

2979+
override isOrphan(): boolean {
2980+
return !!this.deferredClose;
2981+
}
2982+
29692983
/** @internal */
29702984
hasExternalProjectRef() {
29712985
return !!this.externalProjectRefCount;
@@ -3023,3 +3037,8 @@ export function isExternalProject(project: Project): project is ExternalProject
30233037
export function isBackgroundProject(project: Project): project is AutoImportProviderProject | AuxiliaryProject {
30243038
return project.projectKind === ProjectKind.AutoImportProvider || project.projectKind === ProjectKind.Auxiliary;
30253039
}
3040+
3041+
/** @internal */
3042+
export function isProjectDeferredClose(project: Project): project is ConfiguredProject {
3043+
return isConfiguredProject(project) && !!project.deferredClose;
3044+
}

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
@@ -2895,6 +2895,8 @@ declare namespace ts {
28952895
getAllProjectErrors(): readonly Diagnostic[];
28962896
setProjectErrors(projectErrors: Diagnostic[]): void;
28972897
close(): void;
2898+
markAsDirty(): void;
2899+
isOrphan(): boolean;
28982900
getEffectiveTypeRoots(): string[];
28992901
}
29002902
/**
@@ -3325,7 +3327,6 @@ declare namespace ts {
33253327
hasDeferredExtension(): boolean;
33263328
private enableRequestedPluginsAsync;
33273329
private enableRequestedPluginsWorker;
3328-
private enableRequestedPluginsForProjectAsync;
33293330
configurePlugin(args: protocol.ConfigurePluginRequestArguments): void;
33303331
}
33313332
function formatMessage<T extends protocol.Message>(msg: T, logger: Logger, byteLength: (s: string, encoding: BufferEncoding) => number, newLine: string): string;

0 commit comments

Comments
 (0)