diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Hover/CohostHoverEndpoint.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Hover/CohostHoverEndpoint.cs index e31ec4e1f0c..fcc5f20da3b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Hover/CohostHoverEndpoint.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Hover/CohostHoverEndpoint.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor.Features; using Microsoft.CodeAnalysis.Razor.Cohost; using Microsoft.CodeAnalysis.Razor.Remote; +using Roslyn.Text.Adornments; namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; @@ -54,7 +55,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie { var position = LspFactory.CreatePosition(request.Position.ToLinePosition()); - var response = await _remoteServiceInvoker + var razorResponse = await _remoteServiceInvoker .TryInvokeAsync>( razorDocument.Project.Solution, (service, solutionInfo, cancellationToken) => @@ -62,21 +63,79 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie cancellationToken) .ConfigureAwait(false); - if (response.Result is LspHover hover) + if (razorResponse.StopHandling) { - return hover; + return razorResponse.Result; } - if (response.StopHandling) - { - return null; - } - - return await _requestInvoker.MakeHtmlLspRequestAsync( + var htmlHover = await _requestInvoker.MakeHtmlLspRequestAsync( razorDocument, Methods.TextDocumentHoverName, request, cancellationToken).ConfigureAwait(false); + + return MergeHtmlAndRazorHoverResponses(razorResponse.Result, htmlHover); + } + + private static LspHover? MergeHtmlAndRazorHoverResponses(LspHover? razorHover, LspHover? htmlHover) + { + if (razorHover is null) + { + return htmlHover; + } + + if (htmlHover is null + || htmlHover.Range != razorHover.Range) + { + return razorHover; + } + + var htmlStringResponse = htmlHover.Contents.Match( + static s => s, + static markedString => null, + static stringOrMarkedStringArray => null, + static markupContent => markupContent.Value + ); + + if (htmlStringResponse is not null) + { + // This logic is to prepend HTML hover content to the razor hover content if both exist. + // The razor content comes through as a ContainerElement, while the html content comes + // through as MarkupContent. We need to extract the html content and insert it at the + // start of the combined ContainerElement. + if (razorHover is VSInternalHover razorVsInternalHover + && razorVsInternalHover.RawContent is ContainerElement razorContainerElement) + { + var htmlStringClassifiedTextElement = ClassifiedTextElement.CreatePlainText(htmlStringResponse); + var verticalSpacingTextElement = ClassifiedTextElement.CreatePlainText(string.Empty); + var htmlContainerElement = new ContainerElement( + ContainerElementStyle.Stacked, + [htmlStringClassifiedTextElement, verticalSpacingTextElement]); + + // Modify the existing hover's RawContent to prepend the HTML content. + razorVsInternalHover.RawContent = new ContainerElement(razorContainerElement.Style, [htmlContainerElement, .. razorContainerElement.Elements]); + } + else + { + var razorStringResponse = razorHover.Contents.Match( + static s => s, + static markedString => null, + static stringOrMarkedStringArray => null, + static markupContent => markupContent.Value + ); + + if (razorStringResponse is not null) + { + razorHover.Contents = new MarkupContent() + { + Kind = MarkupKind.Markdown, + Value = htmlStringResponse + "\n\n---\n\n" + razorStringResponse + }; + } + } + } + + return razorHover; } internal TestAccessor GetTestAccessor() => new(this); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs index 8e417f0a431..ab28c512071 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs @@ -111,7 +111,8 @@ protected override IRemoteHoverService CreateService(in ServiceArgs args) } } - return Results(csharpHover); + // As there is a C# hover, stop further handling. + return new RemoteResponse(StopHandling: true, Result: csharpHover); } if (positionInfo.LanguageKind is not (RazorLanguageKind.Html or RazorLanguageKind.Razor)) @@ -152,7 +153,7 @@ protected override IRemoteHoverService CreateService(in ServiceArgs args) /// /// Once Razor moves wholly over to Roslyn.LanguageServer.Protocol, this method can be removed. /// - private Hover ConvertHover(Hover hover) + private static Hover ConvertHover(Hover hover) { // Note: Razor only ever produces a Hover with MarkupContent or a VSInternalHover with RawContents. // Both variants return a Range. diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostHoverEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostHoverEndpointTest.cs index 749a158cb26..01a033f2870 100644 --- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostHoverEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostHoverEndpointTest.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor; @@ -73,6 +74,57 @@ public async Task Html() await VerifyHoverAsync(code, htmlResponse, h => Assert.Same(htmlResponse, h)); } + [Fact] + public async Task Html_TagHelper() + { + TestCode code = """ + <[|bo$$dy|]> + """; + + // This verifies Hover calls into both razor and HTML, aggregating their results + const string BodyDescription = "body description"; + var htmlResponse = new VSInternalHover + { + Range = new LspRange() + { + Start = new Position(0, 1), + End = new Position(0, " + { + await VerifyRangeAsync(hover, code.Span, document); + + hover.VerifyContents( + Container( + Container( + ClassifiedText( + Text(BodyDescription)), + ClassifiedText( + Text(string.Empty))), + Container( + Image, + ClassifiedText( + Text("Microsoft"), + Punctuation("."), + Text("AspNetCore"), + Punctuation("."), + Text("Mvc"), + Punctuation("."), + Text("Razor"), + Punctuation("."), + Text("TagHelpers"), + Punctuation("."), + Type("BodyTagHelper"))))); + }); + } + [Fact] public async Task Html_EndTag() { @@ -310,10 +362,13 @@ await VerifyHoverAsync(code, async (hover, document) => }); } - private async Task VerifyHoverAsync(TestCode input, Func verifyHover) + private Task VerifyHoverAsync(TestCode input, Func verifyHover) + => VerifyHoverAsync(input, fileKind: null, htmlResponse: null, verifyHover); + + private async Task VerifyHoverAsync(TestCode input, RazorFileKind? fileKind, Hover? htmlResponse, Func verifyHover) { - var document = CreateProjectAndRazorDocument(input.Text); - var result = await GetHoverResultAsync(document, input); + var document = CreateProjectAndRazorDocument(input.Text, fileKind); + var result = await GetHoverResultAsync(document, input, htmlResponse); Assert.NotNull(result); await verifyHover(result, document); diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/HoverAssertions.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/HoverAssertions.cs index ad7ba56516d..2674d8ef393 100644 --- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/HoverAssertions.cs +++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/HoverAssertions.cs @@ -3,7 +3,6 @@ using System.Collections.Immutable; using Roslyn.Test.Utilities; -using Xunit; namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; @@ -12,8 +11,15 @@ internal static class HoverAssertions public static void VerifyContents(this LspHover hover, object expected) { var markup = hover.Contents.Fourth; - Assert.Equal(MarkupKind.PlainText, markup.Kind); - AssertEx.EqualOrDiff(expected.ToString(), markup.Value.TrimEnd('\r', '\n')); + + var actual = markup.Value.TrimEnd('\r', '\n'); + if (markup.Kind == MarkupKind.Markdown) + { + // Remove any horizontal rules we may have added to separate HTML and Razor content + actual = actual.Replace("\n\n---\n\n", string.Empty); + } + + AssertEx.EqualOrDiff(expected.ToString(), actual); } // Our VS Code test only produce plain text hover content, so these methods are complete overkill,