Create basic service and endpoints to gather diagnostics and pass them over to OOP

Wanted to validate my Roslyn code would work first, before moving our services around, in case the order of this work seems backwards to usual :)
This commit is contained in:
David Wengier 2024-09-11 16:50:50 +10:00
Родитель 062682d321
Коммит a782f9e8e3
6 изменённых файлов: 273 добавлений и 1 удалений

Просмотреть файл

@ -32,5 +32,6 @@
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Formatting" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFormattingService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToImplementation" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToImplementationService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SpellCheck" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSpellCheckService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Diagnostics" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteDiagnosticsService+Factory" />
</ItemGroup>
</Project>

Просмотреть файл

@ -131,7 +131,6 @@ internal partial class RazorLanguageServer : SystemTextJsonLanguageServer<RazorR
services.AddLifeCycleServices(this, _clientConnection, _lspServerActivationTracker);
services.AddDiagnosticServices();
services.AddSemanticTokensServices(featureOptions);
services.AddDocumentManagementServices(featureOptions);
services.AddCompletionServices();
@ -143,6 +142,9 @@ internal partial class RazorLanguageServer : SystemTextJsonLanguageServer<RazorR
if (!featureOptions.UseRazorCohostServer)
{
// Diagnostics
services.AddDiagnosticServices();
// Auto insert
services.AddSingleton<IOnAutoInsertProvider, CloseTextTagOnAutoInsertProvider>();
services.AddSingleton<IOnAutoInsertProvider, AutoClosingTagOnAutoInsertProvider>();

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using RoslynLspDiagnostic = Roslyn.LanguageServer.Protocol.Diagnostic;
namespace Microsoft.CodeAnalysis.Razor.Remote;
internal interface IRemoteDiagnosticsService : IRemoteJsonService
{
ValueTask<ImmutableArray<RoslynLspDiagnostic>> GetDiagnosticsAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
ImmutableArray<RoslynLspDiagnostic> csharpDiagnostics,
ImmutableArray<RoslynLspDiagnostic> htmlDiagnostics,
CancellationToken cancellationToken);
}

Просмотреть файл

@ -35,6 +35,7 @@ internal static class RazorServices
(typeof(IRemoteDocumentSymbolService), null),
(typeof(IRemoteRenameService), null),
(typeof(IRemoteGoToImplementationService), null),
(typeof(IRemoteDiagnosticsService), null),
];
private const string ComponentName = "Razor";

Просмотреть файл

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using LspDiagnostic = Roslyn.LanguageServer.Protocol.Diagnostic;
namespace Microsoft.CodeAnalysis.Remote.Razor;
internal sealed class RemoteDiagnosticsService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteDiagnosticsService
{
internal sealed class Factory : FactoryBase<IRemoteDiagnosticsService>
{
protected override IRemoteDiagnosticsService CreateService(in ServiceArgs args)
=> new RemoteDiagnosticsService(in args);
}
public ValueTask<ImmutableArray<LspDiagnostic>> GetDiagnosticsAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
ImmutableArray<LspDiagnostic> csharpDiagnostics,
ImmutableArray<LspDiagnostic> htmlDiagnostics,
CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetDiagnosticsAsync(context, csharpDiagnostics, htmlDiagnostics, cancellationToken),
cancellationToken);
private async ValueTask<ImmutableArray<LspDiagnostic>> GetDiagnosticsAsync(
RemoteDocumentContext context,
ImmutableArray<LspDiagnostic> csharpDiagnostics,
ImmutableArray<LspDiagnostic> htmlDiagnostics,
CancellationToken cancellationToken)
{
// TODO: More work!
return htmlDiagnostics;
}
}

Просмотреть файл

@ -0,0 +1,205 @@
// 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.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
using RoslynLspDiagnostic = Roslyn.LanguageServer.Protocol.Diagnostic;
using RoslynVSInternalDiagnosticReport = Roslyn.LanguageServer.Protocol.VSInternalDiagnosticReport;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
#pragma warning disable RS0030 // Do not use banned APIs
[Shared]
[CohostEndpoint(VSInternalMethods.DocumentPullDiagnosticName)]
[Export(typeof(IDynamicRegistrationProvider))]
[ExportCohostStatelessLspService(typeof(CohostDocumentPullDiagnosticsEndpoint))]
[method: ImportingConstructor]
#pragma warning restore RS0030 // Do not use banned APIs
internal class CohostDocumentPullDiagnosticsEndpoint(
IRemoteServiceInvoker remoteServiceInvoker,
IHtmlDocumentSynchronizer htmlDocumentSynchronizer,
LSPRequestInvoker requestInvoker,
IFilePathService filePathService,
ILoggerFactory loggerFactory)
: AbstractRazorCohostDocumentRequestHandler<VSInternalDocumentDiagnosticsParams, RoslynVSInternalDiagnosticReport[]?>, IDynamicRegistrationProvider
{
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer;
private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
private readonly IFilePathService _filePathService = filePathService;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostDocumentPullDiagnosticsEndpoint>();
protected override bool MutatesSolutionState => false;
protected override bool RequiresLSPSolution => true;
public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext)
{
// TODO: if (clientCapabilities.TextDocument?.Diagnostic?.DynamicRegistration is true)
{
return new Registration()
{
Method = VSInternalMethods.DocumentPullDiagnosticName,
RegisterOptions = new VSInternalDiagnosticRegistrationOptions()
{
DocumentSelector = filter,
DiagnosticKinds = [VSInternalDiagnosticKind.Syntax]
}
};
}
// return null;
}
protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSInternalDocumentDiagnosticsParams request)
=> request.TextDocument?.ToRazorTextDocumentIdentifier();
protected override Task<RoslynVSInternalDiagnosticReport[]?> HandleRequestAsync(VSInternalDocumentDiagnosticsParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
=> HandleRequestAsync(context.TextDocument.AssumeNotNull(), cancellationToken);
private async Task<RoslynVSInternalDiagnosticReport[]?> HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken)
{
// Diagnostics is a little different, because Roslyn is not designed to run diagnostics in OOP. Their system will transition to OOP
// as it needs, but we have to start here in devenv. This is not as big a problem as it sounds, specifically for diagnostics, because
// we only need to tell Roslyn the document we need diagnostics for. If we had to map positions or ranges etc. it would be worse
// because we'd have to transition to our OOP to find out that info, then back here to get the diagnostics, then back to OOP to process.
_logger.LogDebug($"Getting diagnostics for {razorDocument.FilePath}");
var csharpTask = GetCSharpDiagnosticsAsync(razorDocument, cancellationToken);
var htmlTask = GetHtmlDiagnosticsAsync(razorDocument, cancellationToken);
try
{
await Task.WhenAll(htmlTask, csharpTask).ConfigureAwait(false);
}
catch (Exception e)
{
if (e is not OperationCanceledException)
{
_logger.LogError(e, $"Exception thrown in PullDiagnostic delegation");
}
// Return null if any of the tasks getting diagnostics results in an error
return null;
}
var csharpDiagnostics = await csharpTask.ConfigureAwait(false);
var htmlDiagnostics = await htmlTask.ConfigureAwait(false);
_logger.LogDebug($"Calling OOP with the {csharpDiagnostics.Length} C# and {htmlDiagnostics.Length} Html diagnostics");
var diagnostics = await _remoteServiceInvoker.TryInvokeAsync<IRemoteDiagnosticsService, ImmutableArray<RoslynLspDiagnostic>>(
razorDocument.Project.Solution,
(service, solutionInfo, cancellationToken) => service.GetDiagnosticsAsync(solutionInfo, razorDocument.Id, csharpDiagnostics, htmlDiagnostics, cancellationToken),
cancellationToken).ConfigureAwait(false);
if (diagnostics.IsDefaultOrEmpty)
{
return null;
}
return
[
new()
{
Diagnostics = diagnostics.ToArray(),
ResultId = Guid.NewGuid().ToString()
}
];
}
private Task<ImmutableArray<RoslynLspDiagnostic>> GetCSharpDiagnosticsAsync(TextDocument razorDocument, CancellationToken cancellationToken)
{
// TODO: This code will not work when the source generator is hooked up.
// How do we get the source generated C# document without OOP? Can we reverse engineer a file path?
var projectKey = razorDocument.Project.ToProjectKey();
var csharpFilePath = _filePathService.GetRazorCSharpFilePath(projectKey, razorDocument.FilePath.AssumeNotNull());
// We put the project Id in the generated document path, so there can only be one document
if (razorDocument.Project.Solution.GetDocumentIdsWithFilePath(csharpFilePath) is not [{ } generatedDocumentId] ||
razorDocument.Project.GetDocument(generatedDocumentId) is not { } generatedDocument)
{
return SpecializedTasks.EmptyImmutableArray<RoslynLspDiagnostic>();
}
_logger.LogDebug($"Getting C# diagnostics for {generatedDocument.FilePath}");
return ExternalHandlers.Diagnostics.GetDocumentDiagnosticsAsync(generatedDocument, supportsVisualStudioExtensions: true, cancellationToken);
}
private async Task<ImmutableArray<RoslynLspDiagnostic>> GetHtmlDiagnosticsAsync(TextDocument razorDocument, CancellationToken cancellationToken)
{
var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
if (htmlDocument is null)
{
return [];
}
var diagnosticsParams = new VSInternalDocumentDiagnosticsParams
{
TextDocument = new TextDocumentIdentifier { Uri = htmlDocument.Uri }
};
_logger.LogDebug($"Getting Html diagnostics for {htmlDocument.Uri}");
var result = await _requestInvoker.ReinvokeRequestOnServerAsync<VSInternalDocumentDiagnosticsParams, VSInternalDiagnosticReport[]?>(
htmlDocument.Buffer,
VSInternalMethods.DocumentPullDiagnosticName,
RazorLSPConstants.HtmlLanguageServerName,
diagnosticsParams,
cancellationToken).ConfigureAwait(false);
if (result?.Response is null)
{
return [];
}
// This is, to say the least, not ideal. In future we're going to normalize on to Roslyn LSP types, and this can go.
var options = new JsonSerializerOptions();
foreach (var converter in RazorServiceDescriptorsWrapper.GetLspConverters())
{
options.Converters.Add(converter);
}
var hmlDiagnostics = JsonSerializer.Deserialize<RoslynVSInternalDiagnosticReport[]>(JsonSerializer.SerializeToDocument(result.Response), options);
if (hmlDiagnostics is not { } convertedHtmlDiagnostics)
{
return [];
}
using var allDiagnostics = new PooledArrayBuilder<RoslynLspDiagnostic>();
foreach (var report in convertedHtmlDiagnostics)
{
if (report.Diagnostics is not null)
{
allDiagnostics.AddRange(report.Diagnostics);
}
}
return allDiagnostics.ToImmutable();
}
internal TestAccessor GetTestAccessor() => new(this);
internal readonly struct TestAccessor(CohostDocumentPullDiagnosticsEndpoint instance)
{
public Task<RoslynVSInternalDiagnosticReport[]?> HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken)
=> instance.HandleRequestAsync(razorDocument, cancellationToken);
}
}