Skip to content

Commit 46968ad

Browse files
Make IRazorFileChangeListener async
1 parent 59b2d77 commit 46968ad

File tree

5 files changed

+58
-59
lines changed

5 files changed

+58
-59
lines changed
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

4+
using System.Threading;
5+
using System.Threading.Tasks;
46
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
57

68
namespace Microsoft.AspNetCore.Razor.LanguageServer;
79

810
internal interface IRazorFileChangeListener
911
{
10-
void RazorFileChanged(string filePath, RazorFileChangeKind kind);
12+
Task RazorFileChangedAsync(string filePath, RazorFileChangeKind kind, CancellationToken cancellationToken);
1113
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetector.cs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
1313
using Microsoft.AspNetCore.Razor.Utilities;
1414
using Microsoft.CodeAnalysis.Razor;
15+
using Microsoft.VisualStudio.Threading;
1516

1617
namespace Microsoft.AspNetCore.Razor.LanguageServer;
1718

18-
internal class RazorFileChangeDetector : IFileChangeDetector
19+
internal class RazorFileChangeDetector : IFileChangeDetector, IDisposable
1920
{
2021
private static readonly TimeSpan s_delay = TimeSpan.FromSeconds(1);
2122
private static readonly ImmutableArray<string> s_razorFileExtensions = [".razor", ".cshtml"];
@@ -29,6 +30,7 @@ internal class RazorFileChangeDetector : IFileChangeDetector
2930
private readonly List<FileSystemWatcher> _watchers;
3031
private readonly object _pendingNotificationsLock = new();
3132

33+
private readonly CancellationTokenSource _disposeTokenSource;
3234
private readonly TimeSpan _delay;
3335

3436
public RazorFileChangeDetector(
@@ -48,6 +50,13 @@ protected RazorFileChangeDetector(
4850
_watchers = new List<FileSystemWatcher>(s_razorFileExtensions.Length);
4951
PendingNotifications = new Dictionary<string, DelayedFileChangeNotification>(FilePathComparer.Instance);
5052
_delay = delay;
53+
_disposeTokenSource = new();
54+
}
55+
56+
public void Dispose()
57+
{
58+
_disposeTokenSource.Cancel();
59+
_disposeTokenSource.Dispose();
5160
}
5261

5362
// Used in tests to ensure we can control when delayed notification work starts.
@@ -58,24 +67,19 @@ protected RazorFileChangeDetector(
5867

5968
public async Task StartAsync(string workspaceDirectory, CancellationToken cancellationToken)
6069
{
61-
if (workspaceDirectory is null)
62-
{
63-
throw new ArgumentNullException(nameof(workspaceDirectory));
64-
}
65-
6670
// Dive through existing Razor files and fabricate "added" events so listeners can accurately listen to state changes for them.
6771

6872
workspaceDirectory = FilePathNormalizer.Normalize(workspaceDirectory);
6973

7074
var existingRazorFiles = GetExistingRazorFiles(workspaceDirectory);
7175

72-
await _dispatcher.RunAsync(() =>
76+
foreach (var razorFilePath in existingRazorFiles)
7377
{
74-
foreach (var razorFilePath in existingRazorFiles)
78+
foreach (var listener in _listeners)
7579
{
76-
FileSystemWatcher_RazorFileEvent(razorFilePath, RazorFileChangeKind.Added);
80+
await listener.RazorFileChangedAsync(razorFilePath, RazorFileChangeKind.Added, cancellationToken).ConfigureAwait(false);
7781
}
78-
}, cancellationToken).ConfigureAwait(false);
82+
}
7983

8084
// This is an entry point for testing
8185
OnInitializationFinished();
@@ -200,7 +204,7 @@ private async Task NotifyAfterDelayAsync(string physicalFilePath)
200204

201205
await _dispatcher.RunAsync(
202206
() => NotifyAfterDelay_ProjectSnapshotManagerDispatcher(physicalFilePath),
203-
CancellationToken.None).ConfigureAwait(false);
207+
_disposeTokenSource.Token).ConfigureAwait(false);
204208
}
205209

206210
private void NotifyAfterDelay_ProjectSnapshotManagerDispatcher(string physicalFilePath)
@@ -224,15 +228,12 @@ private void NotifyAfterDelay_ProjectSnapshotManagerDispatcher(string physicalFi
224228
return;
225229
}
226230

227-
FileSystemWatcher_RazorFileEvent(physicalFilePath, notification.ChangeKind.Value);
228-
}
229-
}
231+
var kind = notification.ChangeKind.Value;
230232

231-
private void FileSystemWatcher_RazorFileEvent(string physicalFilePath, RazorFileChangeKind kind)
232-
{
233-
foreach (var listener in _listeners)
234-
{
235-
listener.RazorFileChanged(physicalFilePath, kind);
233+
foreach (var listener in _listeners)
234+
{
235+
listener.RazorFileChangedAsync(physicalFilePath, kind, _disposeTokenSource.Token).Forget();
236+
}
236237
}
237238
}
238239

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileSynchronizer.cs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Threading;
6+
using System.Threading.Tasks;
67
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
78
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
89
using Microsoft.VisualStudio.Threading;
@@ -13,21 +14,11 @@ internal class RazorFileSynchronizer(IRazorProjectService projectService) : IRaz
1314
{
1415
private readonly IRazorProjectService _projectService = projectService;
1516

16-
public void RazorFileChanged(string filePath, RazorFileChangeKind kind)
17-
{
18-
if (filePath is null)
17+
public Task RazorFileChangedAsync(string filePath, RazorFileChangeKind kind, CancellationToken cancellationToken)
18+
=> kind switch
1919
{
20-
throw new ArgumentNullException(nameof(filePath));
21-
}
22-
23-
switch (kind)
24-
{
25-
case RazorFileChangeKind.Added:
26-
_projectService.AddDocumentAsync(filePath, CancellationToken.None).Forget();
27-
break;
28-
case RazorFileChangeKind.Removed:
29-
_projectService.RemoveDocumentAsync(filePath, CancellationToken.None).Forget();
30-
break;
31-
}
32-
}
20+
RazorFileChangeKind.Added => _projectService.AddDocumentAsync(filePath, cancellationToken),
21+
RazorFileChangeKind.Removed => _projectService.RemoveDocumentAsync(filePath, cancellationToken),
22+
_ => Task.CompletedTask
23+
};
3324
}

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileChangeDetectorTest.cs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,25 @@ public async Task StartAsync_NotifiesListenersOfExistingRazorFiles()
2424
{
2525
// Arrange
2626
var args1 = new List<(string FilePath, RazorFileChangeKind Kind)>();
27-
var listener1 = new Mock<IRazorFileChangeListener>(MockBehavior.Strict);
28-
listener1.Setup(l => l.RazorFileChanged(It.IsAny<string>(), It.IsAny<RazorFileChangeKind>()))
29-
.Callback<string, RazorFileChangeKind>((filePath, kind) => args1.Add((filePath, kind)));
27+
var listenerMock1 = new StrictMock<IRazorFileChangeListener>();
28+
listenerMock1
29+
.Setup(l => l.RazorFileChangedAsync(It.IsAny<string>(), It.IsAny<RazorFileChangeKind>(), It.IsAny<CancellationToken>()))
30+
.Returns(Task.CompletedTask)
31+
.Callback((string filePath, RazorFileChangeKind kind, CancellationToken _) => args1.Add((filePath, kind)));
32+
3033
var args2 = new List<(string FilePath, RazorFileChangeKind Kind)>();
31-
var listener2 = new Mock<IRazorFileChangeListener>(MockBehavior.Strict);
32-
listener2.Setup(l => l.RazorFileChanged(It.IsAny<string>(), It.IsAny<RazorFileChangeKind>()))
33-
.Callback<string, RazorFileChangeKind>((filePath, kind) => args2.Add((filePath, kind)));
34-
var existingRazorFiles = new[] { "c:/path/to/index.razor", "c:/other/path/_Host.cshtml" };
34+
var listenerMock2 = new StrictMock<IRazorFileChangeListener>();
35+
listenerMock2
36+
.Setup(l => l.RazorFileChangedAsync(It.IsAny<string>(), It.IsAny<RazorFileChangeKind>(), It.IsAny<CancellationToken>()))
37+
.Returns(Task.CompletedTask)
38+
.Callback((string filePath, RazorFileChangeKind kind, CancellationToken _) => args2.Add((filePath, kind)));
39+
40+
string[] existingRazorFiles = ["c:/path/to/index.razor", "c:/other/path/_Host.cshtml"];
3541
var cts = new CancellationTokenSource();
36-
var detector = new TestRazorFileChangeDetector(
42+
using var detector = new TestRazorFileChangeDetector(
3743
cts,
3844
Dispatcher,
39-
[listener1.Object, listener2.Object],
45+
[listenerMock1.Object, listenerMock2.Object],
4046
existingRazorFiles);
4147

4248
// Act
@@ -75,10 +81,11 @@ public async Task FileSystemWatcher_RazorFileEvent_Background_NotifiesChange()
7581
var changeKind = RazorFileChangeKind.Added;
7682
var listenerMock = new StrictMock<IRazorFileChangeListener>();
7783
listenerMock
78-
.Setup(l => l.RazorFileChanged(filePath, changeKind))
84+
.Setup(l => l.RazorFileChangedAsync(filePath, changeKind, It.IsAny<CancellationToken>()))
85+
.Returns(Task.CompletedTask)
7986
.Verifiable();
8087

81-
var fileChangeDetector = new SimpleTestRazorFileChangeDetector(Dispatcher, [listenerMock.Object], TimeSpan.FromMilliseconds(50))
88+
using var fileChangeDetector = new SimpleTestRazorFileChangeDetector(Dispatcher, [listenerMock.Object], TimeSpan.FromMilliseconds(50))
8289
{
8390
BlockNotificationWorkStart = new ManualResetEventSlim(initialState: false),
8491
};
@@ -106,10 +113,11 @@ public void FileSystemWatcher_RazorFileEvent_Background_AddRemoveDoesNotNotify()
106113
var listenerCalled = false;
107114
var listenerMock = new StrictMock<IRazorFileChangeListener>();
108115
listenerMock
109-
.Setup(l => l.RazorFileChanged(filePath, It.IsAny<RazorFileChangeKind>()))
116+
.Setup(l => l.RazorFileChangedAsync(filePath, It.IsAny<RazorFileChangeKind>(), It.IsAny<CancellationToken>()))
117+
.Returns(Task.CompletedTask)
110118
.Callback(() => listenerCalled = true);
111119

112-
var fileChangeDetector = new SimpleTestRazorFileChangeDetector(Dispatcher, [listenerMock.Object], TimeSpan.FromMilliseconds(10))
120+
using var fileChangeDetector = new SimpleTestRazorFileChangeDetector(Dispatcher, [listenerMock.Object], TimeSpan.FromMilliseconds(10))
113121
{
114122
NotifyNotificationNoop = new ManualResetEventSlim(initialState: false),
115123
BlockNotificationWorkStart = new ManualResetEventSlim(initialState: false)
@@ -133,10 +141,11 @@ public async Task FileSystemWatcher_RazorFileEvent_Background_NotificationNoopTo
133141
var listenerMock = new StrictMock<IRazorFileChangeListener>();
134142
var callCount = 0;
135143
listenerMock
136-
.Setup(l => l.RazorFileChanged(filePath, RazorFileChangeKind.Added))
144+
.Setup(l => l.RazorFileChangedAsync(filePath, RazorFileChangeKind.Added, It.IsAny<CancellationToken>()))
145+
.Returns(Task.CompletedTask)
137146
.Callback(() => callCount++);
138147

139-
var fileChangeDetector = new SimpleTestRazorFileChangeDetector(Dispatcher, [listenerMock.Object], TimeSpan.FromMilliseconds(50))
148+
using var fileChangeDetector = new SimpleTestRazorFileChangeDetector(Dispatcher, [listenerMock.Object], TimeSpan.FromMilliseconds(50))
140149
{
141150
BlockNotificationWorkStart = new ManualResetEventSlim(initialState: false),
142151
};

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileSynchronizerTest.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ public async Task RazorFileChanged_Added_AddsRazorDocument()
2828
var synchronizer = new RazorFileSynchronizer(projectService.Object);
2929

3030
// Act
31-
await RunOnDispatcherAsync(() =>
32-
synchronizer.RazorFileChanged(filePath, RazorFileChangeKind.Added));
31+
await synchronizer.RazorFileChangedAsync(filePath, RazorFileChangeKind.Added, DisposalToken);
3332

3433
// Assert
3534
projectService.VerifyAll();
@@ -48,8 +47,7 @@ public async Task RazorFileChanged_Added_AddsCSHTMLDocument()
4847
var synchronizer = new RazorFileSynchronizer(projectService.Object);
4948

5049
// Act
51-
await RunOnDispatcherAsync(() =>
52-
synchronizer.RazorFileChanged(filePath, RazorFileChangeKind.Added));
50+
await synchronizer.RazorFileChangedAsync(filePath, RazorFileChangeKind.Added, DisposalToken);
5351

5452
// Assert
5553
projectService.VerifyAll();
@@ -68,8 +66,7 @@ public async Task RazorFileChanged_Removed_RemovesRazorDocument()
6866
var synchronizer = new RazorFileSynchronizer(projectService.Object);
6967

7068
// Act
71-
await RunOnDispatcherAsync(() =>
72-
synchronizer.RazorFileChanged(filePath, RazorFileChangeKind.Removed));
69+
await synchronizer.RazorFileChangedAsync(filePath, RazorFileChangeKind.Removed, DisposalToken);
7370

7471
// Assert
7572
projectService.VerifyAll();
@@ -88,8 +85,7 @@ public async Task RazorFileChanged_Removed_RemovesCSHTMLDocument()
8885
var synchronizer = new RazorFileSynchronizer(projectService.Object);
8986

9087
// Act
91-
await RunOnDispatcherAsync(() =>
92-
synchronizer.RazorFileChanged(filePath, RazorFileChangeKind.Removed));
88+
await synchronizer.RazorFileChangedAsync(filePath, RazorFileChangeKind.Removed, DisposalToken);
9389

9490
// Assert
9591
projectService.VerifyAll();

0 commit comments

Comments
 (0)