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|]>
+ {
+ 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,