зеркало из https://github.com/dotnet/razor.git
Refresh diagnostics on project changes (#10964)
This notifies a client to refresh diagnostics when we experience project snapshot changes (except for document changed). This should help for cases where the project engine is updating but doesn't have all taghelper information yet.
This commit is contained in:
Родитель
9f19fa87df
Коммит
bfda0df2b3
|
@ -105,6 +105,7 @@ internal static class IServiceCollectionExtensions
|
|||
services.AddHandlerWithCapabilities<DocumentPullDiagnosticsEndpoint>();
|
||||
services.AddSingleton<RazorTranslateDiagnosticsService>();
|
||||
services.AddSingleton(sp => new Lazy<RazorTranslateDiagnosticsService>(sp.GetRequiredService<RazorTranslateDiagnosticsService>));
|
||||
services.AddSingleton<IRazorStartupService, WorkspaceDiagnosticsRefresher>();
|
||||
}
|
||||
|
||||
public static void AddHoverServices(this IServiceCollection services)
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
|
||||
using Microsoft.AspNetCore.Razor.Utilities;
|
||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||
using Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer;
|
||||
|
||||
internal sealed class WorkspaceDiagnosticsRefresher : IRazorStartupService, IDisposable
|
||||
{
|
||||
private readonly AsyncBatchingWorkQueue _queue;
|
||||
private readonly IProjectSnapshotManager _projectSnapshotManager;
|
||||
private readonly IClientCapabilitiesService _clientCapabilitiesService;
|
||||
private readonly IClientConnection _clientConnection;
|
||||
private bool? _supported;
|
||||
private CancellationTokenSource _disposeTokenSource = new();
|
||||
|
||||
public WorkspaceDiagnosticsRefresher(
|
||||
IProjectSnapshotManager projectSnapshotManager,
|
||||
IClientCapabilitiesService clientCapabilitiesService,
|
||||
IClientConnection clientConnection,
|
||||
TimeSpan? delay = null)
|
||||
{
|
||||
_clientConnection = clientConnection;
|
||||
_projectSnapshotManager = projectSnapshotManager;
|
||||
_clientCapabilitiesService = clientCapabilitiesService;
|
||||
_queue = new(
|
||||
delay ?? TimeSpan.FromMilliseconds(200),
|
||||
ProcessBatchAsync,
|
||||
_disposeTokenSource.Token);
|
||||
_projectSnapshotManager.Changed += ProjectSnapshotManager_Changed;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposeTokenSource.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_projectSnapshotManager.Changed -= ProjectSnapshotManager_Changed;
|
||||
_disposeTokenSource.Cancel();
|
||||
_disposeTokenSource.Dispose();
|
||||
}
|
||||
|
||||
private ValueTask ProcessBatchAsync(CancellationToken token)
|
||||
{
|
||||
_clientConnection
|
||||
.SendNotificationAsync(Methods.WorkspaceDiagnosticRefreshName, token)
|
||||
.Forget();
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private void ProjectSnapshotManager_Changed(object? sender, ProjectChangeEventArgs e)
|
||||
{
|
||||
if (e.SolutionIsClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_supported ??= GetSupported();
|
||||
|
||||
if (_supported != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Kind is not ProjectChangeKind.DocumentChanged)
|
||||
{
|
||||
_queue.AddWork();
|
||||
}
|
||||
}
|
||||
|
||||
private bool? GetSupported()
|
||||
{
|
||||
if (!_clientCapabilitiesService.CanGetClientCapabilities)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _clientCapabilitiesService.ClientCapabilities.Workspace?.Diagnostics?.RefreshSupport;
|
||||
}
|
||||
|
||||
internal TestAccessor GetTestAccessor()
|
||||
=> new(this);
|
||||
|
||||
internal sealed class TestAccessor(WorkspaceDiagnosticsRefresher instance)
|
||||
{
|
||||
|
||||
public Task WaitForRefreshAsync()
|
||||
{
|
||||
if (instance._disposeTokenSource.IsCancellationRequested)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return instance._queue.WaitUntilCurrentBatchCompletesAsync();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,27 +5,24 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
|
||||
using Microsoft.AspNetCore.Razor.Utilities;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer;
|
||||
|
||||
internal class WorkspaceSemanticTokensRefreshNotifier : IWorkspaceSemanticTokensRefreshNotifier, IDisposable
|
||||
internal sealed class WorkspaceSemanticTokensRefreshNotifier : IWorkspaceSemanticTokensRefreshNotifier, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250);
|
||||
|
||||
private readonly IClientCapabilitiesService _clientCapabilitiesService;
|
||||
private readonly IClientConnection _clientConnection;
|
||||
private readonly CancellationTokenSource _disposeTokenSource;
|
||||
private readonly IDisposable _optionsChangeListener;
|
||||
|
||||
private readonly object _gate = new();
|
||||
private bool? _supportsRefresh;
|
||||
private bool _waitingToRefresh;
|
||||
private Task _refreshTask = Task.CompletedTask;
|
||||
private readonly AsyncBatchingWorkQueue _queue;
|
||||
|
||||
private bool _isColoringBackground;
|
||||
private bool? _supportsRefresh;
|
||||
|
||||
public WorkspaceSemanticTokensRefreshNotifier(
|
||||
IClientCapabilitiesService clientCapabilitiesService,
|
||||
|
@ -37,10 +34,24 @@ internal class WorkspaceSemanticTokensRefreshNotifier : IWorkspaceSemanticTokens
|
|||
|
||||
_disposeTokenSource = new();
|
||||
|
||||
_queue = new(
|
||||
TimeSpan.FromMilliseconds(250),
|
||||
ProcessBatchAsync,
|
||||
_disposeTokenSource.Token);
|
||||
|
||||
_isColoringBackground = optionsMonitor.CurrentValue.ColorBackground;
|
||||
_optionsChangeListener = optionsMonitor.OnChange(HandleOptionsChange);
|
||||
}
|
||||
|
||||
private ValueTask ProcessBatchAsync(CancellationToken token)
|
||||
{
|
||||
_clientConnection
|
||||
.SendNotificationAsync(Methods.WorkspaceSemanticTokensRefreshName, token)
|
||||
.Forget();
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposeTokenSource.IsCancellationRequested)
|
||||
|
@ -70,14 +81,6 @@ internal class WorkspaceSemanticTokensRefreshNotifier : IWorkspaceSemanticTokens
|
|||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_waitingToRefresh)
|
||||
{
|
||||
// We're going to refresh shortly.
|
||||
return;
|
||||
}
|
||||
|
||||
// We could have been called before the LSP server has even been initialized
|
||||
if (!_clientCapabilitiesService.CanGetClientCapabilities)
|
||||
{
|
||||
|
@ -91,37 +94,22 @@ internal class WorkspaceSemanticTokensRefreshNotifier : IWorkspaceSemanticTokens
|
|||
return;
|
||||
}
|
||||
|
||||
_refreshTask = RefreshAfterDelayAsync();
|
||||
_waitingToRefresh = true;
|
||||
}
|
||||
|
||||
async Task RefreshAfterDelayAsync()
|
||||
{
|
||||
await Task.Delay(s_delay, _disposeTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
_clientConnection
|
||||
.SendNotificationAsync(Methods.WorkspaceSemanticTokensRefreshName, _disposeTokenSource.Token)
|
||||
.Forget();
|
||||
|
||||
_waitingToRefresh = false;
|
||||
}
|
||||
_queue.AddWork();
|
||||
}
|
||||
|
||||
internal TestAccessor GetTestAccessor()
|
||||
=> new(this);
|
||||
|
||||
internal class TestAccessor(WorkspaceSemanticTokensRefreshNotifier instance)
|
||||
internal sealed class TestAccessor(WorkspaceSemanticTokensRefreshNotifier instance)
|
||||
{
|
||||
public async Task WaitForNotificationAsync()
|
||||
public Task WaitForNotificationAsync()
|
||||
{
|
||||
Task refreshTask;
|
||||
|
||||
lock (instance._gate)
|
||||
if (instance._disposeTokenSource.IsCancellationRequested)
|
||||
{
|
||||
refreshTask = instance._refreshTask;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
await refreshTask.ConfigureAwait(false);
|
||||
return instance._queue.WaitUntilCurrentBatchCompletesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Test;
|
||||
|
||||
public class WorkspaceDiagnosticRefreshTest(ITestOutputHelper testOutputHelper) : LanguageServerTestBase(testOutputHelper)
|
||||
{
|
||||
private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(10);
|
||||
|
||||
[Fact]
|
||||
public async Task WorkspaceRefreshSent()
|
||||
{
|
||||
var projectSnapshotManager = CreateProjectSnapshotManager();
|
||||
var clientConnection = new StrictMock<IClientConnection>();
|
||||
clientConnection
|
||||
.Setup(c => c.SendNotificationAsync(Methods.WorkspaceDiagnosticRefreshName, It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask)
|
||||
.Verifiable();
|
||||
|
||||
using var publisher = new WorkspaceDiagnosticsRefresher(
|
||||
projectSnapshotManager,
|
||||
new TestClientCapabilitiesService(new()
|
||||
{
|
||||
Workspace = new()
|
||||
{
|
||||
Diagnostics = new()
|
||||
{
|
||||
RefreshSupport = true
|
||||
}
|
||||
}
|
||||
}),
|
||||
clientConnection.Object,
|
||||
s_delay);
|
||||
|
||||
var testAccessor = publisher.GetTestAccessor();
|
||||
|
||||
await projectSnapshotManager.UpdateAsync(
|
||||
static updater =>
|
||||
{
|
||||
updater.CreateAndAddProject("C:/path/to/project.csproj");
|
||||
});
|
||||
|
||||
await testAccessor.WaitForRefreshAsync();
|
||||
|
||||
clientConnection.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WorkspaceRefreshSent_MultipleTimes()
|
||||
{
|
||||
var projectSnapshotManager = CreateProjectSnapshotManager();
|
||||
var clientConnection = new StrictMock<IClientConnection>();
|
||||
clientConnection
|
||||
.Setup(c => c.SendNotificationAsync(Methods.WorkspaceDiagnosticRefreshName, It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
using var publisher = new WorkspaceDiagnosticsRefresher(
|
||||
projectSnapshotManager,
|
||||
new TestClientCapabilitiesService(new()
|
||||
{
|
||||
Workspace = new()
|
||||
{
|
||||
Diagnostics = new()
|
||||
{
|
||||
RefreshSupport = true
|
||||
}
|
||||
}
|
||||
}),
|
||||
clientConnection.Object,
|
||||
s_delay);
|
||||
|
||||
var testAccessor = publisher.GetTestAccessor();
|
||||
|
||||
await projectSnapshotManager.UpdateAsync(
|
||||
static updater =>
|
||||
{
|
||||
updater.CreateAndAddProject("C:/path/to/project.csproj");
|
||||
});
|
||||
|
||||
await testAccessor.WaitForRefreshAsync();
|
||||
|
||||
await projectSnapshotManager.UpdateAsync(
|
||||
static updater =>
|
||||
{
|
||||
var project = (ProjectSnapshot)updater.GetProjects().First();
|
||||
var directory = Path.GetDirectoryName(project.FilePath);
|
||||
Assert.NotNull(directory);
|
||||
|
||||
var filePath = Path.Combine(directory, "document.razor");
|
||||
updater.CreateAndAddDocument(project, filePath);
|
||||
});
|
||||
|
||||
await testAccessor.WaitForRefreshAsync();
|
||||
|
||||
clientConnection.Verify(
|
||||
c => c.SendNotificationAsync(Methods.WorkspaceDiagnosticRefreshName, It.IsAny<CancellationToken>()),
|
||||
Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WorkspaceRefreshNotSent_ClientDoesNotSupport()
|
||||
{
|
||||
var projectSnapshotManager = CreateProjectSnapshotManager();
|
||||
var clientConnection = new StrictMock<IClientConnection>();
|
||||
|
||||
using var publisher = new WorkspaceDiagnosticsRefresher(
|
||||
projectSnapshotManager,
|
||||
new TestClientCapabilitiesService(new()
|
||||
{
|
||||
Workspace = new()
|
||||
{
|
||||
Diagnostics = new()
|
||||
{
|
||||
RefreshSupport = false
|
||||
}
|
||||
}
|
||||
}),
|
||||
clientConnection.Object,
|
||||
s_delay);
|
||||
|
||||
var testAccessor = publisher.GetTestAccessor();
|
||||
|
||||
await projectSnapshotManager.UpdateAsync(
|
||||
static updater =>
|
||||
{
|
||||
updater.CreateAndAddProject("C:/path/to/project.csproj");
|
||||
});
|
||||
|
||||
await testAccessor.WaitForRefreshAsync();
|
||||
|
||||
clientConnection
|
||||
.Verify(c => c.SendNotificationAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WorkspaceRefreshNotSent_RefresherDisposed()
|
||||
{
|
||||
var projectSnapshotManager = CreateProjectSnapshotManager();
|
||||
var clientConnection = new StrictMock<IClientConnection>();
|
||||
|
||||
var publisher = new WorkspaceDiagnosticsRefresher(
|
||||
projectSnapshotManager,
|
||||
new TestClientCapabilitiesService(new()
|
||||
{
|
||||
Workspace = new()
|
||||
{
|
||||
Diagnostics = new()
|
||||
{
|
||||
RefreshSupport = false
|
||||
}
|
||||
}
|
||||
}),
|
||||
clientConnection.Object,
|
||||
s_delay);
|
||||
|
||||
var testAccessor = publisher.GetTestAccessor();
|
||||
|
||||
publisher.Dispose();
|
||||
|
||||
await projectSnapshotManager.UpdateAsync(
|
||||
static updater =>
|
||||
{
|
||||
updater.CreateAndAddProject("C:/path/to/project.csproj");
|
||||
});
|
||||
|
||||
await testAccessor.WaitForRefreshAsync();
|
||||
|
||||
clientConnection
|
||||
.Verify(c => c.SendNotificationAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче