@@ -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 ) {
0 commit comments