Skip to content

Commit 8e085c4

Browse files
Fix initialization contract for RazorProjectInfo drivers
`AbstractRazorProjectInfoDriver`can't call `InitializeAsync(...)` in its constructor because the driver will only be partially constructed. To address that, add a `StartInitialization` method that drivers call from their constructor. This will kick off initialization and set the result of a `TaskCompletionSource` when it finishes.
1 parent af6e0d8 commit 8e085c4

File tree

3 files changed

+39
-11
lines changed

3 files changed

+39
-11
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public FileWatcherBasedRazorProjectInfoDriver(
5757
_fileSystemWatcher?.Dispose();
5858
_fileSystemWatcher = null;
5959
});
60+
61+
StartInitialization();
6062
}
6163

6264
protected override async Task InitializeAsync(CancellationToken cancellationToken)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs

+25-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.AspNetCore.Razor.ProjectSystem;
1212
using Microsoft.CodeAnalysis.Razor.Logging;
1313
using Microsoft.CodeAnalysis.Razor.Utilities;
14+
using Microsoft.VisualStudio.Threading;
1415

1516
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem;
1617

@@ -28,7 +29,7 @@ private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey);
2829
private readonly AsyncBatchingWorkQueue<Work> _workQueue;
2930
private readonly Dictionary<ProjectKey, RazorProjectInfo> _latestProjectInfoMap;
3031
private ImmutableArray<IRazorProjectInfoListener> _listeners;
31-
private readonly Task _initializeTask;
32+
private readonly TaskCompletionSource<bool> _initializationTaskSource;
3233

3334
protected CancellationToken DisposalToken => _disposeTokenSource.Token;
3435

@@ -40,7 +41,7 @@ protected AbstractRazorProjectInfoDriver(ILoggerFactory loggerFactory, TimeSpan?
4041
_workQueue = new AsyncBatchingWorkQueue<Work>(delay ?? DefaultDelay, ProcessBatchAsync, _disposeTokenSource.Token);
4142
_latestProjectInfoMap = [];
4243
_listeners = [];
43-
_initializeTask = InitializeAsync(_disposeTokenSource.Token);
44+
_initializationTaskSource = new();
4445
}
4546

4647
public void Dispose()
@@ -57,10 +58,29 @@ public void Dispose()
5758
public Task WaitForInitializationAsync()
5859
{
5960
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
60-
return _initializeTask;
61+
return _initializationTaskSource.Task;
6162
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
6263
}
6364

65+
/// <summary>
66+
/// MUST be called in the constructor of any <see cref="AbstractRazorProjectInfoDriver"/> descendent
67+
/// to kick off initialization.
68+
/// </summary>
69+
protected void StartInitialization()
70+
{
71+
// Kick off initialization asynchronously and call TrySetResult(true) in the continuation.
72+
InitializeAsync(_disposeTokenSource.Token)
73+
.ContinueWith(
74+
_ =>
75+
{
76+
_initializationTaskSource.TrySetResult(true);
77+
},
78+
_disposeTokenSource.Token,
79+
TaskContinuationOptions.OnlyOnRanToCompletion,
80+
TaskScheduler.Default)
81+
.Forget();
82+
}
83+
6484
protected abstract Task InitializeAsync(CancellationToken cancellationToken);
6585

6686
private async ValueTask ProcessBatchAsync(ImmutableArray<Work> items, CancellationToken token)
@@ -125,7 +145,7 @@ protected void EnqueueRemove(ProjectKey projectKey)
125145

126146
public ImmutableArray<RazorProjectInfo> GetLatestProjectInfo()
127147
{
128-
if (!_initializeTask.IsCompleted)
148+
if (!_initializationTaskSource.Task.IsCompleted)
129149
{
130150
throw new InvalidOperationException($"{nameof(GetLatestProjectInfo)} cannot be called before initialization is complete.");
131151
}
@@ -145,7 +165,7 @@ public ImmutableArray<RazorProjectInfo> GetLatestProjectInfo()
145165

146166
public void AddListener(IRazorProjectInfoListener listener)
147167
{
148-
if (!_initializeTask.IsCompleted)
168+
if (!_initializationTaskSource.Task.IsCompleted)
149169
{
150170
throw new InvalidOperationException($"An {nameof(IRazorProjectInfoListener)} cannot be added before initialization is complete.");
151171
}

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs

+12-6
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@
1010

1111
namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem;
1212

13-
internal sealed partial class RazorProjectInfoDriver(
14-
IProjectSnapshotManager projectManager,
15-
ILoggerFactory loggerFactory,
16-
TimeSpan? delay = null)
17-
: AbstractRazorProjectInfoDriver(loggerFactory, delay)
13+
internal sealed partial class RazorProjectInfoDriver : AbstractRazorProjectInfoDriver
1814
{
19-
private readonly IProjectSnapshotManager _projectManager = projectManager;
15+
private readonly IProjectSnapshotManager _projectManager;
16+
17+
public RazorProjectInfoDriver(
18+
IProjectSnapshotManager projectManager,
19+
ILoggerFactory loggerFactory,
20+
TimeSpan? delay = null) : base(loggerFactory, delay)
21+
{
22+
_projectManager = projectManager;
23+
24+
StartInitialization();
25+
}
2026

2127
protected override Task InitializeAsync(CancellationToken cancellationToken)
2228
{

0 commit comments

Comments
 (0)