From e47b5c63cb720446aa60943d21663b13a565b5f5 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 19 Jul 2024 14:54:13 +1000 Subject: [PATCH 01/10] Create endpoint and remote service interface --- .../RemoteDocumentHighlight.cs | 27 ++++++ .../Remote/IRemoteDocumentHighlightService.cs | 15 +++ ...stDocumentHighlightPresentationEndpoint.cs | 94 +++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/DocumentHighlight/RemoteDocumentHighlight.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDocumentHighlightService.cs create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/DocumentHighlight/RemoteDocumentHighlight.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/DocumentHighlight/RemoteDocumentHighlight.cs new file mode 100644 index 00000000000..145bcbd424c --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/DocumentHighlight/RemoteDocumentHighlight.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Runtime.Serialization; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Protocol.DocumentHighlight; + +using DocumentHighlight = VisualStudio.LanguageServer.Protocol.DocumentHighlight; + +[DataContract] +internal readonly record struct RemoteDocumentHighlight( + [property: DataMember(Order = 0)] LinePositionSpan Position, + [property: DataMember(Order = 1)] DocumentHighlightKind Kind) +{ + public static RemoteDocumentHighlight FromLspDocumentHighlight(DocumentHighlight h) + => new RemoteDocumentHighlight(h.Range.ToLinePositionSpan(), h.Kind); + + public static DocumentHighlight ToLspDocumentHighlight(RemoteDocumentHighlight r) + => new DocumentHighlight + { + Range = r.Position.ToRange(), + Kind = r.Kind + }; +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDocumentHighlightService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDocumentHighlightService.cs new file mode 100644 index 00000000000..0f091812a2a --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDocumentHighlightService.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Protocol.DocumentHighlight; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteDocumentHighlightService +{ + ValueTask GetHighlightsAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId documentId, LinePosition position, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs new file mode 100644 index 00000000000..0fdc1408578 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Protocol.DocumentHighlight; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.LanguageClient.Extensions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(Methods.TextDocumentDocumentHighlightName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostDocumentHighlightEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal class CohostDocumentHighlightEndpoint( + IRemoteServiceInvoker remoteServiceInvoker, + IHtmlDocumentSynchronizer htmlDocumentSynchronizer, + LSPRequestInvoker requestInvoker) + : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + { + if (clientCapabilities.SupportsVisualStudioExtensions) + { + return new Registration + { + Method = Methods.TextDocumentDocumentHighlightName, + RegisterOptions = new DocumentHighlightRegistrationOptions() + { + DocumentSelector = filter + } + }; + } + + return null; + } + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(DocumentHighlightParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + protected override async Task HandleRequestAsync(DocumentHighlightParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + { + var razorDocument = context.TextDocument.AssumeNotNull(); + + var csharpResult = await _remoteServiceInvoker.TryInvokeAsync( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetHighlightsAsync(solutionInfo, razorDocument.Id, request.Position.ToLinePosition(), cancellationToken), + cancellationToken).ConfigureAwait(false); + + // If we got a response back, then either Razor or C# wants to do something with this, so we're good to go + if (csharpResult is not null) + { + return csharpResult.Select(RemoteDocumentHighlight.ToLspDocumentHighlight).ToArray(); + } + + // If we didn't get anything from Razor or Roslyn, lets ask Html what they want to do + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); + if (htmlDocument is null) + { + return null; + } + + request.TextDocument = request.TextDocument.WithUri(htmlDocument.Uri); + + var result = await _requestInvoker.ReinvokeRequestOnServerAsync( + htmlDocument.Buffer, + Methods.TextDocumentDocumentHighlightName, + RazorLSPConstants.HtmlLanguageServerName, + request, + cancellationToken).ConfigureAwait(false); + + // Since we don't need to map positions in Html, and document highlight results don't have Uri's, we can return these results directly + return result?.Response; + } +} From e6a914de23a11369c32a6b5c6cb04359aa71e6f9 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 19 Jul 2024 21:32:40 +1000 Subject: [PATCH 02/10] Add tests to ensure RazorServices and Services.props are correct It's a complete coincidence I'm adding this now, and definitely not because I made a mistake in one of the aforementioned places --- .../Remote/RazorServices.cs | 41 ++++--- .../RazorServicesTest.cs | 111 ++++++++++++++++++ .../Language/TestProject.cs | 10 +- 3 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index 5b79b2b5005..902bb5d3aed 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -1,6 +1,8 @@ // 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 Microsoft.AspNetCore.Razor.Serialization.MessagePack.Resolvers; using Microsoft.CodeAnalysis.ExternalAccess.Razor; @@ -8,14 +10,8 @@ namespace Microsoft.CodeAnalysis.Razor.Remote; internal static class RazorServices { - private const string ComponentName = "Razor"; - - public static readonly RazorServiceDescriptorsWrapper Descriptors = new( - ComponentName, - featureDisplayNameProvider: feature => $"Razor {feature} Feature", - additionalFormatters: [], - additionalResolvers: TopLevelResolvers.All, - interfaces: + // Internal for testing + internal static readonly IEnumerable<(Type, Type?)> MessagePackServices = [ (typeof(IRemoteLinkedEditingRangeService), null), (typeof(IRemoteTagHelperProviderService), null), @@ -24,14 +20,31 @@ internal static class RazorServices (typeof(IRemoteHtmlDocumentService), null), (typeof(IRemoteUriPresentationService), null), (typeof(IRemoteFoldingRangeService), null) - ]); + ]; + + // Internal for testing + internal static readonly IEnumerable<(Type, Type?)> JsonServices = + [ + (typeof(IRemoteSignatureHelpService), null), + ]; + + private const string ComponentName = "Razor"; + + public static readonly RazorServiceDescriptorsWrapper Descriptors = new( + ComponentName, + featureDisplayNameProvider: GetFeatureDisplayName, + additionalFormatters: [], + additionalResolvers: TopLevelResolvers.All, + interfaces: MessagePackServices); public static readonly RazorServiceDescriptorsWrapper JsonDescriptors = new( ComponentName, // Needs to match the above because so much of our ServiceHub infrastructure is convention based - featureDisplayNameProvider: feature => $"Razor {feature} Feature", + featureDisplayNameProvider: GetFeatureDisplayName, jsonConverters: RazorServiceDescriptorsWrapper.GetLspConverters(), - interfaces: - [ - (typeof(IRemoteSignatureHelpService), null), - ]); + interfaces: JsonServices); + + private static string GetFeatureDisplayName(string feature) + { + return $"Razor {feature} Feature"; + } } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs new file mode 100644 index 00000000000..8bfbdd8ba1c --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs @@ -0,0 +1,111 @@ +// 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.Linq; +using System.Xml; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Remote.Razor; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +public class RazorServicesTest(ITestOutputHelper testOutputHelper) : ToolingTestBase(testOutputHelper) +{ + private readonly static XmlDocument s_servicesFile = LoadServicesFile(); + + [Theory] + [MemberData(nameof(MessagePackServices))] + public void MessagePackServicesAreListedProperly(Type serviceType, Type? callbackType) + { + VerifyService(serviceType, callbackType); + } + + [Theory] + [MemberData(nameof(JsonServices))] + public void JsonServicesAreListedProperly(Type serviceType, Type? callbackType) + { + Assert.True(typeof(IRemoteJsonService).IsAssignableFrom(serviceType)); + VerifyService(serviceType, callbackType); + } + + [Fact] + public void RazorServicesContainsAllServices() + { + var services = new HashSet(RazorServices.MessagePackServices.Select(s => s.Item1.Name)); + services.UnionWith(RazorServices.JsonServices.Select(s => s.Item1.Name)); + var serviceNodes = s_servicesFile.SelectNodes("/Project/ItemGroup/ServiceHubService"); + foreach (XmlNode serviceNode in serviceNodes) + { + var serviceEntry = serviceNode.Attributes["Include"].Value; + var factoryName = serviceNode.Attributes["ClassName"].Value; + + var factoryType = typeof(ServiceArgs).Assembly.GetType(factoryName); + AssertEx.NotNull(factoryType, $"Could not load type for factory '{factoryName}'"); + + var interfaceType = factoryType.BaseType.GetGenericArguments()[0]; + Assert.True(services.Contains(interfaceType.Name), $"Service '{interfaceType.Name}' is not listed in RazorServices"); + } + } + + public static IEnumerable MessagePackServices() + { + foreach (var service in RazorServices.MessagePackServices) + { + yield return [service.Item1, service.Item2]; + } + } + + public static IEnumerable JsonServices() + { + foreach (var service in RazorServices.JsonServices) + { + yield return [service.Item1, service.Item2]; + } + } + + private static XmlDocument LoadServicesFile() + { + var document = new XmlDocument(); + document.Load(Path.Combine(TestProject.GetRepoRoot(), "eng", "targets", "Services.props")); + return document; + } + + private static void VerifyService(Type serviceType, Type? callbackType) + { + const string prefix = "IRemote"; + const string suffix = "Service"; + + Assert.Null(callbackType); + + var serviceName = serviceType.Name; + Assert.StartsWith(prefix, serviceName); + Assert.EndsWith(suffix, serviceName); + + var shortName = serviceName.Substring(prefix.Length, serviceName.Length - prefix.Length - suffix.Length); + var servicePropsEntry = $"Microsoft.VisualStudio.Razor.{shortName}"; + + var serviceNode = s_servicesFile.SelectSingleNode($"/Project/ItemGroup/ServiceHubService[@Include='{servicePropsEntry}']"); + AssertEx.NotNull(serviceNode, $"Expected entry in Services.props for {servicePropsEntry}"); + + var serviceImplName = $"Microsoft.CodeAnalysis.Remote.Razor.Remote{shortName}Service"; + var factoryName = serviceNode.Attributes["ClassName"].Value; + Assert.Equal($"{serviceImplName}+Factory", factoryName); + + var serviceImplType = typeof(ServiceArgs).Assembly.GetType(serviceImplName); + Assert.NotNull(serviceImplType); + + var factoryType = typeof(ServiceArgs).Assembly.GetType(factoryName); + Assert.NotNull(factoryType); + + Assert.True(serviceType.IsAssignableFrom(serviceImplType)); + + var interfaceType = factoryType.BaseType.GetGenericArguments()[0]; + Assert.Equal(serviceType, interfaceType); + } +} diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs index 2de9f92f000..fe15f6c91e2 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs @@ -50,6 +50,12 @@ public static string GetProjectDirectory(string directoryHint, Layer layer, bool return projectDirectory; } + public static string GetRepoRoot(bool useCurrentDirectory = false) + { + var baseDir = useCurrentDirectory ? Directory.GetCurrentDirectory() : AppContext.BaseDirectory; + return SearchUp(baseDir, "global.json"); + } + public static string GetProjectDirectory(Type type, Layer layer, bool useCurrentDirectory = false) { var baseDir = useCurrentDirectory ? Directory.GetCurrentDirectory() : AppContext.BaseDirectory; @@ -57,8 +63,8 @@ public static string GetProjectDirectory(Type type, Layer layer, bool useCurrent var repoRoot = SearchUp(baseDir, "global.json"); var assemblyName = type.Assembly.GetName().Name; var projectDirectory = layer == Layer.Compiler - ? Path.Combine(repoRoot, "src", layerFolderName, assemblyName, "test") - : Path.Combine(repoRoot, "src", layerFolderName, "test", assemblyName); + ? Path.Combine(repoRoot, "src", layerFolderName, assemblyName, "test") + : Path.Combine(repoRoot, "src", layerFolderName, "test", assemblyName); if (string.Equals(assemblyName, "Microsoft.AspNetCore.Razor.Language.Test", StringComparison.Ordinal)) { From 1ebf1de72a17f4ff4091fd42c74127d4df98f7bd Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 19 Jul 2024 22:19:54 +1000 Subject: [PATCH 03/10] Create remote service and register etc. --- eng/targets/Services.props | 1 + .../Extensions/LinePositionExtensions.cs | 4 + .../Extensions/LinePositionSpanExtensions.cs | 8 ++ .../Extensions/PositionExtensions.cs | 4 + .../Extensions/RangeExtensions.cs | 4 + .../RemoteDocumentHighlight.cs | 5 +- .../Remote/RazorServices.cs | 3 +- .../RemoteDocumentHighlightService.cs | 88 +++++++++++++++++++ 8 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 068c5f2e0ab..ede89dc2165 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -23,5 +23,6 @@ + diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionExtensions.cs index 0e547fe85a1..e8d877c4ff5 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using RLSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Workspaces; @@ -12,6 +13,9 @@ internal static class LinePositionExtensions public static Position ToPosition(this LinePosition linePosition) => new Position(linePosition.Line, linePosition.Character); + public static RLSP.Position ToRLSPPosition(this LinePosition linePosition) + => new RLSP.Position(linePosition.Line, linePosition.Character); + public static bool TryGetAbsoluteIndex(this LinePosition position, SourceText sourceText, ILogger logger, out int absoluteIndex) => sourceText.TryGetAbsoluteIndex(position.Line, position.Character, logger, out absoluteIndex); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs index ba6e6f19b29..c9027db4f32 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using RLSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Workspaces; @@ -15,6 +16,13 @@ public static Range ToRange(this LinePositionSpan linePositionSpan) End = linePositionSpan.End.ToPosition() }; + public static RLSP.Range ToRLSPRange(this LinePositionSpan linePositionSpan) + => new RLSP.Range + { + Start = linePositionSpan.Start.ToRLSPPosition(), + End = linePositionSpan.End.ToRLSPPosition() + }; + public static TextSpan ToTextSpan(this LinePositionSpan linePositionSpan, SourceText sourceText) => sourceText.GetTextSpan(linePositionSpan.Start.Line, linePositionSpan.Start.Character, linePositionSpan.End.Line, linePositionSpan.End.Character); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/PositionExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/PositionExtensions.cs index 767b310176a..dc71fc530b5 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/PositionExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/PositionExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using RLSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Workspaces; @@ -15,6 +16,9 @@ internal static class PositionExtensions public static LinePosition ToLinePosition(this Position position) => new LinePosition(position.Line, position.Character); + public static LinePosition ToLinePosition(this RLSP.Position position) + => new LinePosition(position.Line, position.Character); + public static bool TryGetAbsoluteIndex(this Position position, SourceText sourceText, ILogger? logger, out int absoluteIndex) { if (position is null) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RangeExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RangeExtensions.cs index 66ac2168234..4f3504b6f8f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RangeExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RangeExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; +using RLSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Workspaces; @@ -110,6 +111,9 @@ public static TextSpan ToTextSpan(this Range range, SourceText sourceText) public static LinePositionSpan ToLinePositionSpan(this Range range) => new LinePositionSpan(range.Start.ToLinePosition(), range.End.ToLinePosition()); + public static LinePositionSpan ToLinePositionSpan(this RLSP.Range range) + => new LinePositionSpan(range.Start.ToLinePosition(), range.End.ToLinePosition()); + public static bool IsUndefined(this Range range) { if (range is null) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/DocumentHighlight/RemoteDocumentHighlight.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/DocumentHighlight/RemoteDocumentHighlight.cs index 145bcbd424c..a0494059d38 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/DocumentHighlight/RemoteDocumentHighlight.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/DocumentHighlight/RemoteDocumentHighlight.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using RLSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Protocol.DocumentHighlight; @@ -15,8 +16,8 @@ internal readonly record struct RemoteDocumentHighlight( [property: DataMember(Order = 0)] LinePositionSpan Position, [property: DataMember(Order = 1)] DocumentHighlightKind Kind) { - public static RemoteDocumentHighlight FromLspDocumentHighlight(DocumentHighlight h) - => new RemoteDocumentHighlight(h.Range.ToLinePositionSpan(), h.Kind); + public static RemoteDocumentHighlight FromRLSPDocumentHighlight(RLSP.DocumentHighlight h) + => new RemoteDocumentHighlight(h.Range.ToLinePositionSpan(), (DocumentHighlightKind)h.Kind); public static DocumentHighlight ToLspDocumentHighlight(RemoteDocumentHighlight r) => new DocumentHighlight diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index 902bb5d3aed..644881de001 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -19,7 +19,8 @@ internal static class RazorServices (typeof(IRemoteSemanticTokensService), null), (typeof(IRemoteHtmlDocumentService), null), (typeof(IRemoteUriPresentationService), null), - (typeof(IRemoteFoldingRangeService), null) + (typeof(IRemoteFoldingRangeService), null), + (typeof(IRemoteDocumentHighlightService), null), ]; // Internal for testing diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs new file mode 100644 index 00000000000..e9e2578fe27 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.DocumentHighlight; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed partial class RemoteDocumentHighlightService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteDocumentHighlightService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs args) + => new RemoteDocumentHighlightService(in args); + } + + private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); + private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue(); + + public ValueTask GetHighlightsAsync( + RazorPinnedSolutionInfoWrapper solutionInfo, + DocumentId razorDocumentId, + LinePosition position, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + razorDocumentId, + context => GetHighlightsAsync(context, position, cancellationToken), + cancellationToken); + + private async ValueTask GetHighlightsAsync( + RemoteDocumentContext context, + LinePosition position, + CancellationToken cancellationToken) + { + var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); + if (!sourceText.TryGetAbsoluteIndex(position.Line, position.Character, out var index)) + { + return null; + } + + var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + var languageKind = _documentMappingService.GetLanguageKind(codeDocument, index, rightAssociative: true); + if (languageKind is RazorLanguageKind.Html) + { + // Returning null indicates we have nothing to say, so go to Html + return null; + } + else if (languageKind is RazorLanguageKind.CSharp) + { + var csharpDocument = codeDocument.GetCSharpDocument(); + if (_documentMappingService.TryMapToGeneratedDocumentPosition(csharpDocument, index, out var mappedPosition, out _)) + { + var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false); + + var highlights = await DocumentHighlights.GetHighlightsAsync(generatedDocument, mappedPosition, cancellationToken).ConfigureAwait(false); + + if (highlights is not null) + { + foreach (var highlight in highlights) + { + if (_documentMappingService.TryMapToHostDocumentRange(csharpDocument, highlight.Range.ToLinePositionSpan(), out var mappedRange)) + { + highlight.Range = mappedRange.ToRLSPRange(); + } + } + + return highlights.Select(RemoteDocumentHighlight.FromRLSPDocumentHighlight).ToArray(); + } + } + } + + // We don't do anything for Razor, but we return an empty array so the caller knows not to bother asking Html + return []; + } +} From 6831461b50e42ed6b468ad3b13cf5095db1e8fa9 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 19 Jul 2024 22:20:08 +1000 Subject: [PATCH 04/10] Turn off old endpoint in cohosting --- .../RazorLanguageServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index d28cb50e47c..f6c83b1a43a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -178,7 +178,6 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddTransient(sp => sp.GetRequiredService()); services.AddHandlerWithCapabilities(); - services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); @@ -186,6 +185,7 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption if (!featureOptions.UseRazorCohostServer) { + services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); From 626a8ab751a8b735137ce3dad738949b044cb539 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 20 Jul 2024 13:30:46 +1000 Subject: [PATCH 05/10] Tests! --- ...stDocumentHighlightPresentationEndpoint.cs | 16 +- .../CohostDocumentHighlightEndpointTest.cs | 171 ++++++++++++++++++ 2 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs index 0fdc1408578..686895b309b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; using Microsoft.CodeAnalysis.Razor.Protocol.DocumentHighlight; using Microsoft.CodeAnalysis.Razor.Remote; @@ -57,10 +58,11 @@ internal class CohostDocumentHighlightEndpoint( protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(DocumentHighlightParams request) => request.TextDocument.ToRazorTextDocumentIdentifier(); - protected override async Task HandleRequestAsync(DocumentHighlightParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) - { - var razorDocument = context.TextDocument.AssumeNotNull(); + protected override Task HandleRequestAsync(DocumentHighlightParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken); + private async Task HandleRequestAsync(DocumentHighlightParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { var csharpResult = await _remoteServiceInvoker.TryInvokeAsync( razorDocument.Project.Solution, (service, solutionInfo, cancellationToken) => service.GetHighlightsAsync(solutionInfo, razorDocument.Id, request.Position.ToLinePosition(), cancellationToken), @@ -91,4 +93,12 @@ internal class CohostDocumentHighlightEndpoint( // Since we don't need to map positions in Html, and document highlight results don't have Uri's, we can return these results directly return result?.Response; } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostDocumentHighlightEndpoint instance) + { + public Task HandleRequestAsync(DocumentHighlightParams request, TextDocument razorDocument, CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, razorDocument, cancellationToken); + } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs new file mode 100644 index 00000000000..778d19c87cd --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs @@ -0,0 +1,171 @@ +// 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.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostDocumentHighlightEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task Local() + { + var input = """ +
+ + @{ + var $$[|myVariable|] = "Hello"; + + var length = [|myVariable|].Length; + } + """; + + await VerifyDocumentHighlightsAsync(input); + } + + [Fact] + public async Task Method() + { + var input = """ +
+ + @code + { + void [|Method|]() + { + $$[|Method|](); + } + } + """; + + await VerifyDocumentHighlightsAsync(input); + } + + [Fact] + public async Task AttributeToField() + { + var input = """ +
+
+
+
+ + @code + { + private string [|_className|] = "hello"; + } + """; + + await VerifyDocumentHighlightsAsync(input); + } + + [Fact] + public async Task FieldToAttribute() + { + var input = """ +
+
+
+
+ + @code + { + private string $$[|_className|] = "hello"; + } + """; + + await VerifyDocumentHighlightsAsync(input); + } + + [Fact] + public async Task Html() + { + var input = """ +
+ +
+ + + @code + { + private string _className = "hello"; + } + """; + + await VerifyDocumentHighlightsAsync(input); + } + + [Fact] + public async Task Razor() + { + var input = """ + @in$$ject IDisposable Disposable + +
+
+
+
+ + @code + { + private string _className = "hello"; + } + """; + + await VerifyDocumentHighlightsAsync(input, expectEmptyArray: true); + } + + private async Task VerifyDocumentHighlightsAsync(string input, bool expectEmptyArray = false) + { + TestFileMarkupParser.GetPositionAndSpans(input, out var source, out int cursorPosition, out ImmutableArray spans); + var document = CreateProjectAndRazorDocument(source); + var inputText = await document.GetTextAsync(DisposalToken); + inputText.GetLineAndOffset(cursorPosition, out var lineIndex, out var characterIndex); + + var htmlResponse = new[] { new DocumentHighlight() }; + var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentDocumentHighlightName, htmlResponse)]); + + var endpoint = new CohostDocumentHighlightEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker); + + var request = new DocumentHighlightParams() + { + TextDocument = new TextDocumentIdentifier() { Uri = document.CreateUri() }, + Position = new Position(lineIndex, characterIndex) + }; + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken); + + Assert.NotNull(result); + + if (spans.Length == 0) + { + if (expectEmptyArray) + { + // No spans and expecting an empty array means this result is in a Razor context + Assert.Empty(result); + } + else + { + // No spans but not expecting an empty array means we should have gotten a response from the Html server + // so we just verify we got our fake one + Assert.Same(htmlResponse, result); + } + + return; + } + + var actual = TestFileMarkupParser.CreateTestFile(source, cursorPosition, result.SelectAsArray(h => h.Range.ToTextSpan(inputText))); + + AssertEx.EqualOrDiff(input, actual); + } +} From 31936ef308c7c6537db2f7dd6c1c16cfd8fd93fc Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 20 Jul 2024 13:31:01 +1000 Subject: [PATCH 06/10] Another test, which found a product bug, and a fix for the product bug --- .../RemoteDocumentHighlightService.cs | 39 ++++++++++++------- .../CohostDocumentHighlightEndpointTest.cs | 20 ++++++++++ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs index e9e2578fe27..f3f6973e7d7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; using Microsoft.CodeAnalysis.Razor.DocumentMapping; @@ -58,31 +59,39 @@ protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs // Returning null indicates we have nothing to say, so go to Html return null; } - else if (languageKind is RazorLanguageKind.CSharp) + else if (languageKind is RazorLanguageKind.Razor) { - var csharpDocument = codeDocument.GetCSharpDocument(); - if (_documentMappingService.TryMapToGeneratedDocumentPosition(csharpDocument, index, out var mappedPosition, out _)) - { - var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false); + // We don't do anything for Razor, but we return an empty array so the caller knows not to bother asking Html + return []; + } + + var csharpDocument = codeDocument.GetCSharpDocument(); + if (_documentMappingService.TryMapToGeneratedDocumentPosition(csharpDocument, index, out var mappedPosition, out _)) + { + var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false); - var highlights = await DocumentHighlights.GetHighlightsAsync(generatedDocument, mappedPosition, cancellationToken).ConfigureAwait(false); + var highlights = await DocumentHighlights.GetHighlightsAsync(generatedDocument, mappedPosition, cancellationToken).ConfigureAwait(false); - if (highlights is not null) + if (highlights is not null) + { + using var results = new PooledArrayBuilder(); + + foreach (var highlight in highlights) { - foreach (var highlight in highlights) + if (_documentMappingService.TryMapToHostDocumentRange(csharpDocument, highlight.Range.ToLinePositionSpan(), out var mappedRange)) { - if (_documentMappingService.TryMapToHostDocumentRange(csharpDocument, highlight.Range.ToLinePositionSpan(), out var mappedRange)) - { - highlight.Range = mappedRange.ToRLSPRange(); - } + highlight.Range = mappedRange.ToRLSPRange(); + results.Add(RemoteDocumentHighlight.FromRLSPDocumentHighlight(highlight)); } - - return highlights.Select(RemoteDocumentHighlight.FromRLSPDocumentHighlight).ToArray(); } + + return results.ToArray(); } } - // We don't do anything for Razor, but we return an empty array so the caller knows not to bother asking Html + // We couldn't produce any results from C#, but since we know the context is definitely C# we don't want to return null, otherwise + // we'd then go and ask Web Tools which could return who knows what results. Instead we return an empty array to indicate we've + // done some work. return []; } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs index 778d19c87cd..6c8d68e415f 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs @@ -125,6 +125,26 @@ public async Task Razor() await VerifyDocumentHighlightsAsync(input, expectEmptyArray: true); } + [Fact] + public async Task Inject() + { + var input = """ + @inject [|IDis$$posable|] Disposable + +
+
+ + @code + { + [|IDisposable|].Dispose() + { + } + } + """; + + await VerifyDocumentHighlightsAsync(input, expectEmptyArray: true); + } + private async Task VerifyDocumentHighlightsAsync(string input, bool expectEmptyArray = false) { TestFileMarkupParser.GetPositionAndSpans(input, out var source, out int cursorPosition, out ImmutableArray spans); From fb77765684c073912fa3a13328eb9bbdbe9cc976 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 20 Jul 2024 14:18:27 +1000 Subject: [PATCH 07/10] Make RemoteResponse re-usable and implement --- .../Remote/IRemoteDocumentHighlightService.cs | 2 +- .../Remote/IRemoteUriPresentationService.cs | 7 +---- .../Remote/RemoteResponse.cs | 16 ++++++++++++ .../RemoteDocumentHighlightService.cs | 21 ++++++--------- .../RemoteUriPresentationService.cs | 25 ++++++++---------- ...stDocumentHighlightPresentationEndpoint.cs | 11 +++++--- .../Cohost/CohostUriPresentationEndpoint.cs | 7 ++--- .../CohostDocumentHighlightEndpointTest.cs | 26 +++++-------------- 8 files changed, 56 insertions(+), 59 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteResponse.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDocumentHighlightService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDocumentHighlightService.cs index 0f091812a2a..b49b4b86889 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDocumentHighlightService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDocumentHighlightService.cs @@ -11,5 +11,5 @@ namespace Microsoft.CodeAnalysis.Razor.Remote; internal interface IRemoteDocumentHighlightService { - ValueTask GetHighlightsAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId documentId, LinePosition position, CancellationToken cancellationToken); + ValueTask> GetHighlightsAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId documentId, LinePosition position, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs index b5273b77ccb..0c0716604a9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ExternalAccess.Razor; @@ -12,10 +11,6 @@ namespace Microsoft.CodeAnalysis.Razor.Remote; internal interface IRemoteUriPresentationService { - ValueTask GetPresentationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken); + ValueTask> GetPresentationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken); - [DataContract] - public record struct Response( - [property: DataMember(Order = 0)] bool CallHtml, - [property: DataMember(Order = 1)] TextChange? TextChange); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteResponse.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteResponse.cs new file mode 100644 index 00000000000..2f7dcbe50cb --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteResponse.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Runtime.Serialization; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +[DataContract] +internal record struct RemoteResponse( + [property: DataMember(Order = 0)] bool ShouldCallHtml, + [property: DataMember(Order = 1)] T Result) +{ + public static RemoteResponse CallHtml => new(ShouldCallHtml: true, Result: default!); + public static RemoteResponse NoFurtherHandling => new(ShouldCallHtml: false, Result: default!); + public static RemoteResponse Results(T result) => new(ShouldCallHtml: false, Result: result); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs index f3f6973e7d7..5c940f5dbb8 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; @@ -15,6 +14,7 @@ using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; +using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; namespace Microsoft.CodeAnalysis.Remote.Razor; @@ -29,7 +29,7 @@ protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue(); - public ValueTask GetHighlightsAsync( + public ValueTask GetHighlightsAsync( RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePosition position, @@ -40,7 +40,7 @@ protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs context => GetHighlightsAsync(context, position, cancellationToken), cancellationToken); - private async ValueTask GetHighlightsAsync( + private async ValueTask GetHighlightsAsync( RemoteDocumentContext context, LinePosition position, CancellationToken cancellationToken) @@ -48,7 +48,7 @@ protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); if (!sourceText.TryGetAbsoluteIndex(position.Line, position.Character, out var index)) { - return null; + return Response.NoFurtherHandling; } var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); @@ -56,13 +56,11 @@ protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs var languageKind = _documentMappingService.GetLanguageKind(codeDocument, index, rightAssociative: true); if (languageKind is RazorLanguageKind.Html) { - // Returning null indicates we have nothing to say, so go to Html - return null; + return Response.CallHtml; } else if (languageKind is RazorLanguageKind.Razor) { - // We don't do anything for Razor, but we return an empty array so the caller knows not to bother asking Html - return []; + return Response.NoFurtherHandling; } var csharpDocument = codeDocument.GetCSharpDocument(); @@ -85,13 +83,10 @@ protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs } } - return results.ToArray(); + return Response.Results(results.ToArray()); } } - // We couldn't produce any results from C#, but since we know the context is definitely C# we don't want to return null, otherwise - // we'd then go and ask Web Tools which could return who knows what results. Instead we return an empty array to indicate we've - // done some work. - return []; + return Response.NoFurtherHandling; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs index 290c61a0a5b..632f4b59e83 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; +using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; namespace Microsoft.CodeAnalysis.Remote.Razor; @@ -25,7 +26,7 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); - public ValueTask GetPresentationAsync( + public ValueTask GetPresentationAsync( RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, @@ -37,11 +38,7 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar context => GetPresentationAsync(context, span, uris, cancellationToken), cancellationToken); - private static IRemoteUriPresentationService.Response CallHtml => new(CallHtml: true, TextChange: null); - private static IRemoteUriPresentationService.Response NoFurtherHandling => new(CallHtml: false, TextChange: null); - private static IRemoteUriPresentationService.Response TextChange(TextChange textChange) => new(CallHtml: false, TextChange: textChange); - - private async ValueTask GetPresentationAsync( + private async ValueTask GetPresentationAsync( RemoteDocumentContext context, LinePositionSpan span, Uri[]? uris, @@ -51,7 +48,7 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar if (!sourceText.TryGetAbsoluteIndex(span.Start.Line, span.Start.Character, out var index)) { // If the position is invalid then we shouldn't expect to be able to handle a Html response - return NoFurtherHandling; + return Response.NoFurtherHandling; } var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); @@ -63,13 +60,13 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar // our support for Uri presentation is to insert a Html tag, so we only support Html // If Roslyn add support in future then this is where it would go. - return NoFurtherHandling; + return Response.NoFurtherHandling; } var razorFileUri = UriPresentationHelper.GetComponentFileNameFromUriPresentationRequest(uris, Logger); if (razorFileUri is null) { - return CallHtml; + return Response.CallHtml; } var solution = context.TextDocument.Project.Solution; @@ -79,14 +76,14 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar var ids = solution.GetDocumentIdsWithFilePath(uriToFind); if (ids.Length == 0) { - return CallHtml; + return Response.CallHtml; } // We assume linked documents would produce the same component tag so just take the first var otherDocument = solution.GetAdditionalDocument(ids[0]); if (otherDocument is null) { - return CallHtml; + return Response.CallHtml; } var otherSnapshot = DocumentSnapshotFactory.GetOrCreate(otherDocument); @@ -94,15 +91,15 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar if (descriptor is null) { - return CallHtml; + return Response.CallHtml; } var tag = descriptor.TryGetComponentTag(); if (tag is null) { - return CallHtml; + return Response.CallHtml; } - return TextChange(new TextChange(span.ToTextSpan(sourceText), tag)); + return Response.Results(new TextChange(span.ToTextSpan(sourceText), tag)); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs index 686895b309b..28c4eed6c2e 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs @@ -63,15 +63,20 @@ internal class CohostDocumentHighlightEndpoint( private async Task HandleRequestAsync(DocumentHighlightParams request, TextDocument razorDocument, CancellationToken cancellationToken) { - var csharpResult = await _remoteServiceInvoker.TryInvokeAsync( + var csharpResult = await _remoteServiceInvoker.TryInvokeAsync>( razorDocument.Project.Solution, (service, solutionInfo, cancellationToken) => service.GetHighlightsAsync(solutionInfo, razorDocument.Id, request.Position.ToLinePosition(), cancellationToken), cancellationToken).ConfigureAwait(false); // If we got a response back, then either Razor or C# wants to do something with this, so we're good to go - if (csharpResult is not null) + if (csharpResult.Result is { } highlights) { - return csharpResult.Select(RemoteDocumentHighlight.ToLspDocumentHighlight).ToArray(); + return highlights.Select(RemoteDocumentHighlight.ToLspDocumentHighlight).ToArray(); + } + + if (!csharpResult.ShouldCallHtml) + { + return null; } // If we didn't get anything from Razor or Roslyn, lets ask Html what they want to do diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs index ac01c73ecbd..e80ca52de73 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; using Microsoft.CodeAnalysis.Razor.Remote; using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.Razor.LanguageClient.Extensions; @@ -63,13 +64,13 @@ internal class CohostUriPresentationEndpoint( private async Task HandleRequestAsync(VSInternalUriPresentationParams request, TextDocument razorDocument, CancellationToken cancellationToken) { - var data = await _remoteServiceInvoker.TryInvokeAsync( + var data = await _remoteServiceInvoker.TryInvokeAsync>( razorDocument.Project.Solution, (service, solutionInfo, cancellationToken) => service.GetPresentationAsync(solutionInfo, razorDocument.Id, request.Range.ToLinePositionSpan(), request.Uris, cancellationToken), cancellationToken).ConfigureAwait(false); // If we got a response back, then we're good to go - if (data.TextChange is { } textChange) + if (data.Result is { } textChange) { var sourceText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); @@ -90,7 +91,7 @@ internal class CohostUriPresentationEndpoint( } // If we didn't get anything from our logic, we might need to go and ask Html, but we also might have determined not to - if (!data.CallHtml) + if (!data.ShouldCallHtml) { return null; } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs index 6c8d68e415f..ef1c4e4777c 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs @@ -102,7 +102,7 @@ public async Task Html() } """; - await VerifyDocumentHighlightsAsync(input); + await VerifyDocumentHighlightsAsync(input, htmlResponse: [new DocumentHighlight()]); } [Fact] @@ -122,7 +122,7 @@ public async Task Razor() } """; - await VerifyDocumentHighlightsAsync(input, expectEmptyArray: true); + await VerifyDocumentHighlightsAsync(input); } [Fact] @@ -142,17 +142,16 @@ @inject [|IDis$$posable|] Disposable } """; - await VerifyDocumentHighlightsAsync(input, expectEmptyArray: true); + await VerifyDocumentHighlightsAsync(input); } - private async Task VerifyDocumentHighlightsAsync(string input, bool expectEmptyArray = false) + private async Task VerifyDocumentHighlightsAsync(string input, DocumentHighlight[]? htmlResponse = null) { TestFileMarkupParser.GetPositionAndSpans(input, out var source, out int cursorPosition, out ImmutableArray spans); var document = CreateProjectAndRazorDocument(source); var inputText = await document.GetTextAsync(DisposalToken); inputText.GetLineAndOffset(cursorPosition, out var lineIndex, out var characterIndex); - var htmlResponse = new[] { new DocumentHighlight() }; var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentDocumentHighlightName, htmlResponse)]); var endpoint = new CohostDocumentHighlightEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker); @@ -165,25 +164,14 @@ private async Task VerifyDocumentHighlightsAsync(string input, bool expectEmptyA var result = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken); - Assert.NotNull(result); - if (spans.Length == 0) { - if (expectEmptyArray) - { - // No spans and expecting an empty array means this result is in a Razor context - Assert.Empty(result); - } - else - { - // No spans but not expecting an empty array means we should have gotten a response from the Html server - // so we just verify we got our fake one - Assert.Same(htmlResponse, result); - } - + Assert.Same(result, htmlResponse); return; } + Assert.NotNull(result); + var actual = TestFileMarkupParser.CreateTestFile(source, cursorPosition, result.SelectAsArray(h => h.Range.ToTextSpan(inputText))); AssertEx.EqualOrDiff(input, actual); From 97c0acd9c9cd8d19b1f4544cf5f2f255f7435e53 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 20 Jul 2024 14:21:10 +1000 Subject: [PATCH 08/10] Rework remote response --- .../Remote/RemoteResponse.cs | 8 ++++---- .../Cohost/CohostDocumentHighlightPresentationEndpoint.cs | 2 +- .../Cohost/CohostUriPresentationEndpoint.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteResponse.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteResponse.cs index 2f7dcbe50cb..b22ade06a3b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteResponse.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteResponse.cs @@ -7,10 +7,10 @@ namespace Microsoft.CodeAnalysis.Razor.Remote; [DataContract] internal record struct RemoteResponse( - [property: DataMember(Order = 0)] bool ShouldCallHtml, + [property: DataMember(Order = 0)] bool StopHandling, [property: DataMember(Order = 1)] T Result) { - public static RemoteResponse CallHtml => new(ShouldCallHtml: true, Result: default!); - public static RemoteResponse NoFurtherHandling => new(ShouldCallHtml: false, Result: default!); - public static RemoteResponse Results(T result) => new(ShouldCallHtml: false, Result: result); + public static RemoteResponse CallHtml => new(StopHandling: false, Result: default!); + public static RemoteResponse NoFurtherHandling => new(StopHandling: true, Result: default!); + public static RemoteResponse Results(T result) => new(StopHandling: false, Result: result); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs index 28c4eed6c2e..568382eaa3d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightPresentationEndpoint.cs @@ -74,7 +74,7 @@ internal class CohostDocumentHighlightEndpoint( return highlights.Select(RemoteDocumentHighlight.ToLspDocumentHighlight).ToArray(); } - if (!csharpResult.ShouldCallHtml) + if (csharpResult.StopHandling) { return null; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs index e80ca52de73..9dc498ad081 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs @@ -91,7 +91,7 @@ internal class CohostUriPresentationEndpoint( } // If we didn't get anything from our logic, we might need to go and ask Html, but we also might have determined not to - if (!data.ShouldCallHtml) + if (data.StopHandling) { return null; } From 1371cb5ed935b19ecd0f677b15aa70400888a5af Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 20 Jul 2024 14:25:28 +1000 Subject: [PATCH 09/10] Update src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs --- .../Extensions/LinePositionSpanExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs index c9027db4f32..b446affd1be 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs @@ -17,11 +17,11 @@ public static Range ToRange(this LinePositionSpan linePositionSpan) }; public static RLSP.Range ToRLSPRange(this LinePositionSpan linePositionSpan) - => new RLSP.Range - { - Start = linePositionSpan.Start.ToRLSPPosition(), - End = linePositionSpan.End.ToRLSPPosition() - }; + => new RLSP.Range + { + Start = linePositionSpan.Start.ToRLSPPosition(), + End = linePositionSpan.End.ToRLSPPosition() + }; public static TextSpan ToTextSpan(this LinePositionSpan linePositionSpan, SourceText sourceText) => sourceText.GetTextSpan(linePositionSpan.Start.Line, linePositionSpan.Start.Character, linePositionSpan.End.Line, linePositionSpan.End.Character); From 761afd28f7d4750ff014d2589762a048597a8c05 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 20 Jul 2024 14:51:08 +1000 Subject: [PATCH 10/10] Nullability --- .../RazorServicesTest.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs index 8bfbdd8ba1c..40215a782ac 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs @@ -40,15 +40,19 @@ public void RazorServicesContainsAllServices() var services = new HashSet(RazorServices.MessagePackServices.Select(s => s.Item1.Name)); services.UnionWith(RazorServices.JsonServices.Select(s => s.Item1.Name)); var serviceNodes = s_servicesFile.SelectNodes("/Project/ItemGroup/ServiceHubService"); + Assert.NotNull(serviceNodes); foreach (XmlNode serviceNode in serviceNodes) { - var serviceEntry = serviceNode.Attributes["Include"].Value; - var factoryName = serviceNode.Attributes["ClassName"].Value; + Assert.NotNull(serviceNode); + Assert.NotNull(serviceNode.Attributes); + + var serviceEntry = serviceNode.Attributes["Include"]!.Value; + var factoryName = serviceNode.Attributes["ClassName"]!.Value; var factoryType = typeof(ServiceArgs).Assembly.GetType(factoryName); AssertEx.NotNull(factoryType, $"Could not load type for factory '{factoryName}'"); - var interfaceType = factoryType.BaseType.GetGenericArguments()[0]; + var interfaceType = factoryType.BaseType!.GetGenericArguments()[0]; Assert.True(services.Contains(interfaceType.Name), $"Service '{interfaceType.Name}' is not listed in RazorServices"); } } @@ -94,7 +98,7 @@ private static void VerifyService(Type serviceType, Type? callbackType) AssertEx.NotNull(serviceNode, $"Expected entry in Services.props for {servicePropsEntry}"); var serviceImplName = $"Microsoft.CodeAnalysis.Remote.Razor.Remote{shortName}Service"; - var factoryName = serviceNode.Attributes["ClassName"].Value; + var factoryName = serviceNode.Attributes!["ClassName"]!.Value; Assert.Equal($"{serviceImplName}+Factory", factoryName); var serviceImplType = typeof(ServiceArgs).Assembly.GetType(serviceImplName); @@ -105,7 +109,7 @@ private static void VerifyService(Type serviceType, Type? callbackType) Assert.True(serviceType.IsAssignableFrom(serviceImplType)); - var interfaceType = factoryType.BaseType.GetGenericArguments()[0]; + var interfaceType = factoryType.BaseType!.GetGenericArguments()[0]; Assert.Equal(serviceType, interfaceType); } }