66using System . Collections . Immutable ;
77using System . Threading ;
88using System . Threading . Tasks ;
9+ using Microsoft . AspNetCore . Razor . ProjectSystem ;
910using Microsoft . AspNetCore . Razor . Utilities ;
1011using Microsoft . CodeAnalysis . Razor . Logging ;
1112using Microsoft . CodeAnalysis . Razor . ProjectSystem ;
@@ -32,21 +33,29 @@ internal partial class OpenDocumentGenerator : IRazorStartupService, IDisposable
3233 private readonly LanguageServerFeatureOptions _options ;
3334 private readonly ILogger _logger ;
3435
35- private readonly AsyncBatchingWorkQueue < DocumentSnapshot > _workQueue ;
36+ private readonly AsyncBatchingWorkQueue < DocumentKey > _workQueue ;
3637 private readonly CancellationTokenSource _disposeTokenSource ;
38+ private readonly HashSet < DocumentKey > _workerSet ;
39+
40+ // Note: This is likely to always be false. Only the Visual Studio ProjectSnapshotManager
41+ // is notified of the solution opening and closing, so the language server shouldn't
42+ // update this value. However, this may change at some point and keeping the check here means
43+ // that the logic between this class and the Visual Studio BackgroundDocumentGenerator are in sync.
44+ private bool _solutionIsClosing ;
3745
3846 public OpenDocumentGenerator (
3947 IEnumerable < IDocumentProcessedListener > listeners ,
4048 ProjectSnapshotManager projectManager ,
4149 LanguageServerFeatureOptions options ,
4250 ILoggerFactory loggerFactory )
4351 {
44- _listeners = listeners . ToImmutableArray ( ) ;
52+ _listeners = [ .. listeners ] ;
4553 _projectManager = projectManager ;
4654 _options = options ;
4755
56+ _workerSet = [ ] ;
4857 _disposeTokenSource = new ( ) ;
49- _workQueue = new AsyncBatchingWorkQueue < DocumentSnapshot > (
58+ _workQueue = new AsyncBatchingWorkQueue < DocumentKey > (
5059 s_delay ,
5160 ProcessBatchAsync ,
5261 _disposeTokenSource . Token ) ;
@@ -66,15 +75,30 @@ public void Dispose()
6675 _disposeTokenSource . Dispose ( ) ;
6776 }
6877
69- private async ValueTask ProcessBatchAsync ( ImmutableArray < DocumentSnapshot > items , CancellationToken token )
78+ private async ValueTask ProcessBatchAsync ( ImmutableArray < DocumentKey > items , CancellationToken token )
7079 {
71- foreach ( var document in items . GetMostRecentUniqueItems ( Comparer . Instance ) )
80+ _workerSet . Clear ( ) ;
81+
82+ foreach ( var key in items . GetMostRecentUniqueItems ( _workerSet ) )
7283 {
7384 if ( token . IsCancellationRequested )
7485 {
7586 return ;
7687 }
7788
89+ // If the solution is closing, avoid any in-progress work.
90+ if ( _solutionIsClosing )
91+ {
92+ return ;
93+ }
94+
95+ if ( ! _projectManager . TryGetDocument ( key , out var document ) )
96+ {
97+ continue ;
98+ }
99+
100+ _logger . LogDebug ( $ "Generating { key } at version { document . Version } ") ;
101+
78102 var codeDocument = await document . GetGeneratedOutputAsync ( token ) . ConfigureAwait ( false ) ;
79103
80104 foreach ( var listener in _listeners )
@@ -91,26 +115,27 @@ private async ValueTask ProcessBatchAsync(ImmutableArray<DocumentSnapshot> items
91115
92116 private void ProjectManager_Changed ( object ? sender , ProjectChangeEventArgs args )
93117 {
94- // Don 't do any work if the solution is closing
118+ // We don 't want to do any work on solution close.
95119 if ( args . IsSolutionClosing )
96120 {
121+ _solutionIsClosing = true ;
97122 return ;
98123 }
99124
125+ _solutionIsClosing = false ;
126+
100127 _logger . LogDebug ( $ "Got a project change of type { args . Kind } for { args . ProjectKey . Id } ") ;
101128
102129 switch ( args . Kind )
103130 {
131+ case ProjectChangeKind . ProjectAdded :
104132 case ProjectChangeKind . ProjectChanged :
105133 {
106134 var newProject = args . Newer . AssumeNotNull ( ) ;
107135
108136 foreach ( var documentFilePath in newProject . DocumentFilePaths )
109137 {
110- if ( newProject . TryGetDocument ( documentFilePath , out var document ) )
111- {
112- EnqueueIfNecessary ( document ) ;
113- }
138+ EnqueueIfNecessary ( new ( newProject . Key , documentFilePath ) ) ;
114139 }
115140
116141 break ;
@@ -125,14 +150,11 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
125150 var newProject = args . Newer . AssumeNotNull ( ) ;
126151 var documentFilePath = args . DocumentFilePath . AssumeNotNull ( ) ;
127152
128- if ( newProject . TryGetDocument ( documentFilePath , out var document ) )
129- {
130- EnqueueIfNecessary ( document ) ;
153+ EnqueueIfNecessary ( new ( newProject . Key , documentFilePath ) ) ;
131154
132- foreach ( var relatedDocument in newProject . GetRelatedDocuments ( document ) )
133- {
134- EnqueueIfNecessary ( relatedDocument ) ;
135- }
155+ foreach ( var relatedDocumentFilePath in newProject . GetRelatedDocumentFilePaths ( documentFilePath ) )
156+ {
157+ EnqueueIfNecessary ( new ( newProject . Key , relatedDocumentFilePath ) ) ;
136158 }
137159
138160 break ;
@@ -144,16 +166,14 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
144166 var oldProject = args . Older . AssumeNotNull ( ) ;
145167 var documentFilePath = args . DocumentFilePath . AssumeNotNull ( ) ;
146168
147- if ( oldProject . TryGetDocument ( documentFilePath , out var document ) )
169+ // For removals use the old snapshot to find related documents to update if they exist
170+ // in the new snapshot.
171+
172+ foreach ( var relatedDocumentFilePath in oldProject . GetRelatedDocumentFilePaths ( documentFilePath ) )
148173 {
149- foreach ( var relatedDocument in oldProject . GetRelatedDocuments ( document ) )
174+ if ( newProject . ContainsDocument ( relatedDocumentFilePath ) )
150175 {
151- var relatedDocumentFilePath = relatedDocument . FilePath ;
152-
153- if ( newProject . TryGetDocument ( relatedDocumentFilePath , out var newRelatedDocument ) )
154- {
155- EnqueueIfNecessary ( newRelatedDocument ) ;
156- }
176+ EnqueueIfNecessary ( new ( newProject . Key , relatedDocumentFilePath ) ) ;
157177 }
158178 }
159179
@@ -162,22 +182,24 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
162182
163183 case ProjectChangeKind . ProjectRemoved :
164184 {
165- // No-op. We don't need to enqueue recompilations if the project is being removed
185+ // No-op. We don't need to compile anything if the project is being removed
166186 break ;
167187 }
188+
189+ default :
190+ Assumed . Unreachable ( $ "Unknown { nameof ( ProjectChangeKind ) } : { args . Kind } ") ;
191+ break ;
168192 }
169193
170- void EnqueueIfNecessary ( DocumentSnapshot document )
194+ void EnqueueIfNecessary ( DocumentKey documentKey )
171195 {
172- if ( ! _projectManager . IsDocumentOpen ( document . FilePath ) &&
173- ! _options . UpdateBuffersForClosedDocuments )
196+ if ( ! _options . UpdateBuffersForClosedDocuments &&
197+ ! _projectManager . IsDocumentOpen ( documentKey . FilePath ) )
174198 {
175199 return ;
176200 }
177201
178- _logger . LogDebug ( $ "Enqueuing generation of { document . FilePath } in { document . Project . Key . Id } at version { document . Version } ") ;
179-
180- _workQueue . AddWork ( document ) ;
202+ _workQueue . AddWork ( documentKey ) ;
181203 }
182204 }
183205}
0 commit comments