@@ -160,6 +160,7 @@ import {
160
160
isDynamicFileName ,
161
161
isInferredProject ,
162
162
isInferredProjectName ,
163
+ isProjectDeferredClose ,
163
164
ITypingsInstaller ,
164
165
Logger ,
165
166
LogLevel ,
@@ -1339,6 +1340,7 @@ export class ProjectService {
1339
1340
}
1340
1341
1341
1342
private delayUpdateProjectGraph ( project : Project ) {
1343
+ if ( isProjectDeferredClose ( project ) ) return ;
1342
1344
project . markAsDirty ( ) ;
1343
1345
if ( isBackgroundProject ( project ) ) return ;
1344
1346
const projectName = project . getProjectName ( ) ;
@@ -1771,21 +1773,24 @@ export class ProjectService {
1771
1773
/** @internal */
1772
1774
private onConfigFileChanged ( canonicalConfigFilePath : NormalizedPath , eventKind : FileWatcherEventKind ) {
1773
1775
const configFileExistenceInfo = this . configFileExistenceInfoCache . get ( canonicalConfigFilePath ) ! ;
1776
+ const project = this . getConfiguredProjectByCanonicalConfigFilePath ( canonicalConfigFilePath ) ;
1777
+ const wasDefferedClose = project ?. deferredClose ;
1774
1778
if ( eventKind === FileWatcherEventKind . Deleted ) {
1775
1779
// Update the cached status
1776
1780
// We arent updating or removing the cached config file presence info as that will be taken care of by
1777
1781
// releaseParsedConfig when the project is closed or doesnt need this config any more (depending on tracking open files)
1778
1782
configFileExistenceInfo . exists = false ;
1779
1783
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 ;
1785
1786
}
1786
1787
else {
1787
1788
// Update the cached status
1788
1789
configFileExistenceInfo . exists = true ;
1790
+ if ( wasDefferedClose ) {
1791
+ project . deferredClose = undefined ;
1792
+ project . markAsDirty ( ) ;
1793
+ }
1789
1794
}
1790
1795
1791
1796
// Update projects watching config
@@ -1799,7 +1804,7 @@ export class ProjectService {
1799
1804
// Get open files to reload projects for
1800
1805
this . delayReloadConfiguredProjectsForFile (
1801
1806
configFileExistenceInfo ,
1802
- eventKind !== FileWatcherEventKind . Deleted ?
1807
+ ! wasDefferedClose && eventKind !== FileWatcherEventKind . Deleted ?
1803
1808
identity : // Reload open files if they are root of inferred project
1804
1809
returnTrue , // Reload all the open files impacted by config file
1805
1810
"Change in config file detected" ,
@@ -1814,13 +1819,13 @@ export class ProjectService {
1814
1819
* shouldReloadProjectFor provides a way to filter out files to reload configured project for
1815
1820
*/
1816
1821
private delayReloadConfiguredProjectsForFile (
1817
- configFileExistenceInfo : ConfigFileExistenceInfo ,
1822
+ configFileExistenceInfo : ConfigFileExistenceInfo | undefined ,
1818
1823
shouldReloadProjectFor : ( infoIsRootOfInferredProject : boolean ) => boolean ,
1819
1824
reason : string ,
1820
1825
) {
1821
1826
const updatedProjects = new Set < ConfiguredProject > ( ) ;
1822
1827
// try to reload config file for all open files
1823
- configFileExistenceInfo . openFilesImpactedByConfigFile ?. forEach ( ( infoIsRootOfInferredProject , path ) => {
1828
+ configFileExistenceInfo ? .openFilesImpactedByConfigFile ?. forEach ( ( infoIsRootOfInferredProject , path ) => {
1824
1829
// Invalidate default config file name for open file
1825
1830
this . configFileForOpenFiles . delete ( path ) ;
1826
1831
// Filter out the files that need to be ignored
@@ -2367,7 +2372,8 @@ export class ProjectService {
2367
2372
findConfiguredProjectByProjectName ( configFileName : NormalizedPath ) : ConfiguredProject | undefined {
2368
2373
// make sure that casing of config file name is consistent
2369
2374
const canonicalConfigFilePath = asNormalizedPath ( this . toCanonicalFileName ( configFileName ) ) ;
2370
- return this . getConfiguredProjectByCanonicalConfigFilePath ( canonicalConfigFilePath ) ;
2375
+ const result = this . getConfiguredProjectByCanonicalConfigFilePath ( canonicalConfigFilePath ) ;
2376
+ return ! result ?. deferredClose ? result : undefined ;
2371
2377
}
2372
2378
2373
2379
private getConfiguredProjectByCanonicalConfigFilePath ( canonicalConfigFilePath : string ) : ConfiguredProject | undefined {
@@ -2517,6 +2523,7 @@ export class ProjectService {
2517
2523
this . documentRegistry ,
2518
2524
configFileExistenceInfo . config . cachedDirectoryStructureHost ,
2519
2525
) ;
2526
+ Debug . assert ( ! this . configuredProjects . has ( canonicalConfigFilePath ) ) ;
2520
2527
this . configuredProjects . set ( canonicalConfigFilePath , project ) ;
2521
2528
this . createConfigFileWatcherForParsedConfig ( configFileName , canonicalConfigFilePath , project ) ;
2522
2529
return project ;
@@ -3467,6 +3474,7 @@ export class ProjectService {
3467
3474
this . externalProjectToConfiguredProjectMap . forEach ( projects =>
3468
3475
projects . forEach ( project => {
3469
3476
if (
3477
+ ! project . deferredClose &&
3470
3478
! project . isClosed ( ) &&
3471
3479
project . hasExternalProjectRef ( ) &&
3472
3480
project . pendingUpdateLevel === ProgramUpdateLevel . Full &&
@@ -3755,10 +3763,7 @@ export class ProjectService {
3755
3763
return originalLocation ;
3756
3764
3757
3765
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 ) ;
3762
3767
}
3763
3768
}
3764
3769
@@ -3990,7 +3995,7 @@ export class ProjectService {
3990
3995
private removeOrphanConfiguredProjects ( toRetainConfiguredProjects : readonly ConfiguredProject [ ] | ConfiguredProject | undefined ) {
3991
3996
const toRemoveConfiguredProjects = new Map ( this . configuredProjects ) ;
3992
3997
const markOriginalProjectsAsUsed = ( project : Project ) => {
3993
- if ( ! project . isOrphan ( ) && project . originalConfiguredProjects ) {
3998
+ if ( project . originalConfiguredProjects && ( isConfiguredProject ( project ) || ! project . isOrphan ( ) ) ) {
3994
3999
project . originalConfiguredProjects . forEach (
3995
4000
( _value , configuredProjectPath ) => {
3996
4001
const project = this . getConfiguredProjectByCanonicalConfigFilePath ( configuredProjectPath ) ;
@@ -4012,24 +4017,22 @@ export class ProjectService {
4012
4017
this . inferredProjects . forEach ( markOriginalProjectsAsUsed ) ;
4013
4018
this . externalProjects . forEach ( markOriginalProjectsAsUsed ) ;
4014
4019
this . configuredProjects . forEach ( project => {
4020
+ if ( ! toRemoveConfiguredProjects . has ( project . canonicalConfigFilePath ) ) return ;
4015
4021
// 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
4016
4022
if ( project . hasOpenRef ( ) ) {
4017
4023
retainConfiguredProject ( project ) ;
4018
4024
}
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 ) ;
4025
4028
}
4026
4029
} ) ;
4027
4030
4028
4031
// Remove all the non marked projects
4029
4032
toRemoveConfiguredProjects . forEach ( project => this . removeProject ( project ) ) ;
4030
4033
4031
4034
function isRetained ( project : ConfiguredProject ) {
4032
- return project . hasOpenRef ( ) || ! toRemoveConfiguredProjects . has ( project . canonicalConfigFilePath ) ;
4035
+ return ! toRemoveConfiguredProjects . has ( project . canonicalConfigFilePath ) || project . hasOpenRef ( ) ;
4033
4036
}
4034
4037
4035
4038
function retainConfiguredProject ( project : ConfiguredProject ) {
@@ -4142,7 +4145,7 @@ export class ProjectService {
4142
4145
synchronizeProjectList ( knownProjects : protocol . ProjectVersionInfo [ ] , includeProjectReferenceRedirectInfo ?: boolean ) : ProjectFilesWithTSDiagnostics [ ] {
4143
4146
const files : ProjectFilesWithTSDiagnostics [ ] = [ ] ;
4144
4147
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 ) ;
4146
4149
this . collectChanges ( knownProjects , this . inferredProjects , includeProjectReferenceRedirectInfo , files ) ;
4147
4150
return files ;
4148
4151
}
@@ -4610,28 +4613,28 @@ export class ProjectService {
4610
4613
4611
4614
// Process all pending plugins, partitioned by project. This way a project with few plugins doesn't need to wait
4612
4615
// 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
+ } ) ) ;
4614
4634
4615
4635
// Clear the pending operation and notify the client that projects have been updated.
4616
4636
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 ( ) ;
4635
4638
}
4636
4639
4637
4640
configurePlugin ( args : protocol . ConfigurePluginRequestArguments ) {
0 commit comments