Skip to content

Commit b05db17

Browse files
authored
Improve document handling in VS Code and cohosting (#11825)
Part of #11759 Goes with dotnet/vscode-csharp#8259 This adds a notification for VS Code to tell us that a Razor file has closed, so we can clear our cache, and also adds checksum matching to Html requests in VS and VS Code so we can ensure we're not out-of-sync.
2 parents 509b358 + a026c92 commit b05db17

File tree

14 files changed

+173
-77
lines changed

14 files changed

+173
-77
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/HtmlDocumentServices/HtmlDocumentSynchronizer.SynchronizationRequest.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,30 @@ internal sealed partial class HtmlDocumentSynchronizer
1414
private class SynchronizationRequest(RazorDocumentVersion requestedVersion) : IDisposable
1515
{
1616
private readonly RazorDocumentVersion _requestedVersion = requestedVersion;
17-
private readonly TaskCompletionSource<bool> _tcs = new();
17+
private readonly TaskCompletionSource<SynchronizationResult> _tcs = new();
1818
private CancellationTokenSource? _cts;
1919

20-
public Task<bool> Task => _tcs.Task;
20+
public Task<SynchronizationResult> Task => _tcs.Task;
2121

2222
public RazorDocumentVersion RequestedVersion => _requestedVersion;
2323

24-
internal static SynchronizationRequest CreateAndStart(TextDocument document, RazorDocumentVersion requestedVersion, Func<TextDocument, CancellationToken, Task<bool>> syncFunction)
24+
internal static SynchronizationRequest CreateAndStart(TextDocument document, RazorDocumentVersion requestedVersion, Func<TextDocument, RazorDocumentVersion, CancellationToken, Task<SynchronizationResult>> syncFunction)
2525
{
2626
var request = new SynchronizationRequest(requestedVersion);
2727
request.Start(document, syncFunction);
2828
return request;
2929
}
3030

31-
private void Start(TextDocument document, Func<TextDocument, CancellationToken, Task<bool>> syncFunction)
31+
private void Start(TextDocument document, Func<TextDocument, RazorDocumentVersion, CancellationToken, Task<SynchronizationResult>> syncFunction)
3232
{
3333
_cts = new(TimeSpan.FromMinutes(1));
3434
_cts.Token.Register(Dispose);
35-
_ = syncFunction.Invoke(document, _cts.Token).ContinueWith((t, state) =>
35+
_ = syncFunction.Invoke(document, _requestedVersion, _cts.Token).ContinueWith((t, state) =>
3636
{
37-
var tcs = (TaskCompletionSource<bool>)state.AssumeNotNull();
37+
var tcs = (TaskCompletionSource<SynchronizationResult>)state.AssumeNotNull();
3838
if (t.IsCanceled)
3939
{
40-
tcs.SetResult(false);
40+
tcs.SetResult(default);
4141
}
4242
else if (t.Exception is { } ex)
4343
{
@@ -58,7 +58,7 @@ public void Dispose()
5858
_cts?.Cancel();
5959
_cts?.Dispose();
6060
_cts = null;
61-
_tcs.TrySetResult(false);
61+
_tcs.TrySetResult(default);
6262
}
6363
}
6464
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/HtmlDocumentServices/HtmlDocumentSynchronizer.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void DocumentRemoved(Uri razorFileUri)
4242
}
4343
}
4444

45-
public async Task<bool> TrySynchronizeAsync(TextDocument document, CancellationToken cancellationToken)
45+
public async Task<SynchronizationResult> TrySynchronizeAsync(TextDocument document, CancellationToken cancellationToken)
4646
{
4747
var requestedVersion = await RazorDocumentVersion.CreateAsync(document, cancellationToken).ConfigureAwait(false);
4848

@@ -56,7 +56,7 @@ public async Task<bool> TrySynchronizeAsync(TextDocument document, CancellationT
5656
return await GetSynchronizationRequestTaskAsync(document, requestedVersion).ConfigureAwait(false);
5757
}
5858

59-
private Task<bool> GetSynchronizationRequestTaskAsync(TextDocument document, RazorDocumentVersion requestedVersion)
59+
private Task<SynchronizationResult> GetSynchronizationRequestTaskAsync(TextDocument document, RazorDocumentVersion requestedVersion)
6060
{
6161
lock (_gate)
6262
{
@@ -71,7 +71,7 @@ private Task<bool> GetSynchronizationRequestTaskAsync(TextDocument document, Raz
7171

7272
#pragma warning disable VSTHRD103 // Use await instead of .Result
7373
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
74-
if (request.Task.IsCompleted && request.Task.Result == false)
74+
if (request.Task.IsCompleted && request.Task.Result.Equals(default))
7575
{
7676
_logger.LogDebug($"Already finished that version for {document.FilePath}, but was unsuccessful, so will recompute");
7777
request.Dispose();
@@ -92,7 +92,7 @@ private Task<bool> GetSynchronizationRequestTaskAsync(TextDocument document, Raz
9292
// for a different checksum, but the same workspace version, we assume the new request is the newer document.
9393

9494
_logger.LogDebug($"We've already seen {request.RequestedVersion} for {document.FilePath} so that's a no from me");
95-
return SpecializedTasks.False;
95+
return SpecializedTasks.Default<SynchronizationResult>();
9696
}
9797
else if (!request.Task.IsCompleted)
9898
{
@@ -110,7 +110,7 @@ private Task<bool> GetSynchronizationRequestTaskAsync(TextDocument document, Raz
110110
}
111111
}
112112

113-
private async Task<bool> PublishHtmlDocumentAsync(TextDocument document, CancellationToken cancellationToken)
113+
private async Task<SynchronizationResult> PublishHtmlDocumentAsync(TextDocument document, RazorDocumentVersion requestedVersion, CancellationToken cancellationToken)
114114
{
115115
string? htmlText;
116116
try
@@ -122,30 +122,31 @@ private async Task<bool> PublishHtmlDocumentAsync(TextDocument document, Cancell
122122
catch (Exception ex)
123123
{
124124
_logger.LogError(ex, $"Error getting Html text for {document.FilePath}. Html document contents will be stale");
125-
return false;
125+
return default;
126126
}
127127

128128
if (cancellationToken.IsCancellationRequested)
129129
{
130130
// Checking cancellation before logging, as a new request coming in doesn't count as "Couldn't get Html"
131-
return false;
131+
return default;
132132
}
133133

134134
if (htmlText is null)
135135
{
136136
_logger.LogError($"Couldn't get Html text for {document.FilePath}. Html document contents will be stale");
137-
return false;
137+
return default;
138138
}
139139

140140
try
141141
{
142-
await _htmlDocumentPublisher.PublishAsync(document, htmlText, cancellationToken).ConfigureAwait(false);
143-
return true;
142+
var result = new SynchronizationResult(true, requestedVersion.Checksum);
143+
await _htmlDocumentPublisher.PublishAsync(document, result, htmlText, cancellationToken).ConfigureAwait(false);
144+
return result;
144145
}
145146
catch (Exception ex)
146147
{
147148
_logger.LogError(ex, $"Error publishing Html text for {document.FilePath}. Html document contents will be stale");
148-
return false;
149+
return default;
149150
}
150151
}
151152

@@ -163,7 +164,7 @@ internal TestAccessor(HtmlDocumentSynchronizer instance)
163164
_instance = instance;
164165
}
165166

166-
public Task<bool> GetSynchronizationRequestTaskAsync(TextDocument document, RazorDocumentVersion requestedVersion)
167+
public Task<SynchronizationResult> GetSynchronizationRequestTaskAsync(TextDocument document, RazorDocumentVersion requestedVersion)
167168
=> _instance.GetSynchronizationRequestTaskAsync(document, requestedVersion);
168169
}
169170
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/HtmlDocumentServices/IHtmlDocumentPublisher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
99

1010
internal interface IHtmlDocumentPublisher
1111
{
12-
Task PublishAsync(TextDocument document, string htmlText, CancellationToken cancellationToken);
12+
Task PublishAsync(TextDocument document, SynchronizationResult synchronizationResult, string htmlText, CancellationToken cancellationToken);
1313
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/HtmlDocumentServices/IHtmlDocumentSynchronizer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
1111
internal interface IHtmlDocumentSynchronizer
1212
{
1313
void DocumentRemoved(Uri uri);
14-
Task<bool> TrySynchronizeAsync(TextDocument document, CancellationToken cancellationToken);
14+
Task<SynchronizationResult> TrySynchronizeAsync(TextDocument document, CancellationToken cancellationToken);
1515
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
5+
6+
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
7+
8+
/// <summary>
9+
/// A result of a synchronization operation for a Html document
10+
/// </summary>
11+
/// <remarks>
12+
/// If <see cref="Synchronized" /> is <see langword="false" />, <see cref="Checksum" /> will be <see langword="default" />.
13+
/// </remarks>
14+
internal readonly record struct SynchronizationResult(bool Synchronized, ChecksumWrapper Checksum);

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Microsoft.CodeAnalysis.Razor.CohostingShared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<Compile Include="$(MSBuildThisFileDirectory)DocumentSymbol\CohostDocumentSymbolEndpoint.cs" />
2525
<Compile Include="$(MSBuildThisFileDirectory)Formatting\CohostDocumentFormattingEndpoint.cs" />
2626
<Compile Include="$(MSBuildThisFileDirectory)FindAllReferences\CohostFindAllReferencesEndpoint.cs" />
27+
<Compile Include="$(MSBuildThisFileDirectory)HtmlDocumentServices\SynchronizationResult.cs" />
2728
<Compile Include="$(MSBuildThisFileDirectory)Navigation\CohostGoToDefinitionEndpoint.cs" />
2829
<Compile Include="$(MSBuildThisFileDirectory)Navigation\CohostGoToImplementationEndpoint.cs" />
2930
<Compile Include="$(MSBuildThisFileDirectory)LinkedEditingRange\CohostLinkedEditingRangeEndpoint.cs" />

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/HtmlDocumentPublisher.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Razor;
910
using Microsoft.CodeAnalysis;
1011
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
1112
using Microsoft.CodeAnalysis.Razor.Logging;
@@ -25,11 +26,9 @@ internal sealed class HtmlDocumentPublisher(
2526
private readonly TrackingLSPDocumentManager _documentManager = documentManager as TrackingLSPDocumentManager ?? throw new InvalidOperationException("Expected TrackingLSPDocumentManager");
2627
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<HtmlDocumentPublisher>();
2728

28-
public async Task PublishAsync(TextDocument document, string htmlText, CancellationToken cancellationToken)
29+
public async Task PublishAsync(TextDocument document, SynchronizationResult synchronizationResult, string htmlText, CancellationToken cancellationToken)
2930
{
30-
// TODO: Eventually, for VS Code, the following piece of logic needs to make an LSP call rather than directly update the
31-
// buffer, but the assembly this code currently lives in doesn't ship in VS Code, so we need to solve a few other things
32-
// before we get there.
31+
Assumed.True(synchronizationResult.Synchronized);
3332

3433
var uri = document.CreateUri();
3534
if (!_documentManager.TryGetDocument(uri, out var documentSnapshot) ||
@@ -55,7 +54,7 @@ public async Task PublishAsync(TextDocument document, string htmlText, Cancellat
5554
}
5655

5756
VisualStudioTextChange[] changes = [new(0, htmlDocument.Snapshot.Length, htmlText)];
58-
_documentManager.UpdateVirtualDocument<HtmlVirtualDocument>(uri, changes, documentSnapshot.Version, state: null);
57+
_documentManager.UpdateVirtualDocument<HtmlVirtualDocument>(uri, changes, documentSnapshot.Version, state: synchronizationResult.Checksum);
5958

6059
_logger.LogDebug($"Finished Html document generation for {document.FilePath} (into {uri})");
6160
}

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/HtmlRequestInvoker.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Razor;
910
using Microsoft.CodeAnalysis;
1011
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
1112
using Microsoft.CodeAnalysis.Razor.Logging;
@@ -32,7 +33,7 @@ internal sealed class HtmlRequestInvoker(
3233
public async Task<TResponse?> MakeHtmlLspRequestAsync<TRequest, TResponse>(TextDocument razorDocument, string method, TRequest request, TimeSpan threshold, Guid correlationId, CancellationToken cancellationToken) where TRequest : notnull
3334
{
3435
var syncResult = await _htmlDocumentSynchronizer.TrySynchronizeAsync(razorDocument, cancellationToken).ConfigureAwait(false);
35-
if (!syncResult)
36+
if (!syncResult.Synchronized)
3637
{
3738
_logger.LogDebug($"Couldn't synchronize for {razorDocument.FilePath}");
3839
return default;
@@ -50,6 +51,13 @@ internal sealed class HtmlRequestInvoker(
5051
return default;
5152
}
5253

54+
var existingChecksum = (ChecksumWrapper)htmlDocument.State.AssumeNotNull();
55+
if (!existingChecksum.Equals(syncResult.Checksum))
56+
{
57+
_logger.LogError($"Checksum for {snapshot.Uri}, {htmlDocument.State} doesn't match {syncResult.Checksum}.");
58+
return default;
59+
}
60+
5361
// If the request is for a text document, we need to update the Uri to point to the Html document,
5462
// and most importantly set it back again before leaving the method in case a caller uses it.
5563
Uri? originalUri = null;
@@ -62,8 +70,7 @@ internal sealed class HtmlRequestInvoker(
6270

6371
try
6472
{
65-
66-
_logger.LogDebug($"Making LSP request for {method} from {htmlDocument.Uri}{(request is ITextDocumentPositionParams positionParams ? $" at {positionParams.Position}" : "")}.");
73+
_logger.LogDebug($"Making LSP request for {method} from {htmlDocument.Uri}{(request is ITextDocumentPositionParams positionParams ? $" at {positionParams.Position}" : "")}, checksum {syncResult.Checksum}.");
6774

6875
// Passing Guid.Empty to this method will mean no tracking
6976
using var _ = _telemetryReporter.TrackLspRequest(Methods.TextDocumentCodeActionName, RazorLSPConstants.HtmlLanguageServerName, threshold, correlationId);

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/HtmlVirtualDocument.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient;
1010
internal class HtmlVirtualDocument(Uri uri, ITextBuffer textBuffer, ITelemetryReporter telemetryReporter)
1111
: GeneratedVirtualDocument<HtmlVirtualDocumentSnapshot>(uri, textBuffer, telemetryReporter)
1212
{
13-
protected override HtmlVirtualDocumentSnapshot GetUpdatedSnapshot(object? state) => new(Uri, TextBuffer.CurrentSnapshot, HostDocumentVersion);
13+
protected override HtmlVirtualDocumentSnapshot GetUpdatedSnapshot(object? state) => new(Uri, TextBuffer.CurrentSnapshot, HostDocumentVersion, state);
1414
}

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/HtmlVirtualDocumentSnapshot.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ internal class HtmlVirtualDocumentSnapshot : VirtualDocumentSnapshot
1212
public HtmlVirtualDocumentSnapshot(
1313
Uri uri,
1414
ITextSnapshot snapshot,
15-
long? hostDocumentSyncVersion)
15+
long? hostDocumentSyncVersion,
16+
object? state)
1617
{
1718
if (uri is null)
1819
{
@@ -27,11 +28,14 @@ public HtmlVirtualDocumentSnapshot(
2728
Uri = uri;
2829
Snapshot = snapshot;
2930
HostDocumentSyncVersion = hostDocumentSyncVersion;
31+
State = state;
3032
}
3133

3234
public override Uri Uri { get; }
3335

3436
public override ITextSnapshot Snapshot { get; }
3537

3638
public override long? HostDocumentSyncVersion { get; }
39+
40+
public object? State { get; }
3741
}

0 commit comments

Comments
 (0)