Skip to content

Commit ef490e4

Browse files
authored
Allow cohosting quick info to show html tag information even when on a taghelper or component tag. (#12415)
* Allow cohosting quick info to show html tag information even when on a taghelper or component tag. Fixes https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_workitems/edit/2556384 This change allows cohosting quick info to call into the html LSP after getting a result from the razor GetHoverAsync call. If both razor and html returned items, then the code merges the results with html items shown above the razor items (as the legacy editor does).
1 parent 177e962 commit ef490e4

File tree

4 files changed

+138
-17
lines changed

4 files changed

+138
-17
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Hover/CohostHoverEndpoint.cs

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Features;
1111
using Microsoft.CodeAnalysis.Razor.Cohost;
1212
using Microsoft.CodeAnalysis.Razor.Remote;
13+
using Roslyn.Text.Adornments;
1314

1415
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
1516

@@ -54,29 +55,87 @@ public ImmutableArray<Registration> GetRegistrations(VSInternalClientCapabilitie
5455
{
5556
var position = LspFactory.CreatePosition(request.Position.ToLinePosition());
5657

57-
var response = await _remoteServiceInvoker
58+
var razorResponse = await _remoteServiceInvoker
5859
.TryInvokeAsync<IRemoteHoverService, RemoteResponse<LspHover?>>(
5960
razorDocument.Project.Solution,
6061
(service, solutionInfo, cancellationToken) =>
6162
service.GetHoverAsync(solutionInfo, razorDocument.Id, position, cancellationToken),
6263
cancellationToken)
6364
.ConfigureAwait(false);
6465

65-
if (response.Result is LspHover hover)
66+
if (razorResponse.StopHandling)
6667
{
67-
return hover;
68+
return razorResponse.Result;
6869
}
6970

70-
if (response.StopHandling)
71-
{
72-
return null;
73-
}
74-
75-
return await _requestInvoker.MakeHtmlLspRequestAsync<TextDocumentPositionParams, LspHover>(
71+
var htmlHover = await _requestInvoker.MakeHtmlLspRequestAsync<TextDocumentPositionParams, LspHover>(
7672
razorDocument,
7773
Methods.TextDocumentHoverName,
7874
request,
7975
cancellationToken).ConfigureAwait(false);
76+
77+
return MergeHtmlAndRazorHoverResponses(razorResponse.Result, htmlHover);
78+
}
79+
80+
private static LspHover? MergeHtmlAndRazorHoverResponses(LspHover? razorHover, LspHover? htmlHover)
81+
{
82+
if (razorHover is null)
83+
{
84+
return htmlHover;
85+
}
86+
87+
if (htmlHover is null
88+
|| htmlHover.Range != razorHover.Range)
89+
{
90+
return razorHover;
91+
}
92+
93+
var htmlStringResponse = htmlHover.Contents.Match(
94+
static s => s,
95+
static markedString => null,
96+
static stringOrMarkedStringArray => null,
97+
static markupContent => markupContent.Value
98+
);
99+
100+
if (htmlStringResponse is not null)
101+
{
102+
// This logic is to prepend HTML hover content to the razor hover content if both exist.
103+
// The razor content comes through as a ContainerElement, while the html content comes
104+
// through as MarkupContent. We need to extract the html content and insert it at the
105+
// start of the combined ContainerElement.
106+
if (razorHover is VSInternalHover razorVsInternalHover
107+
&& razorVsInternalHover.RawContent is ContainerElement razorContainerElement)
108+
{
109+
var htmlStringClassifiedTextElement = ClassifiedTextElement.CreatePlainText(htmlStringResponse);
110+
var verticalSpacingTextElement = ClassifiedTextElement.CreatePlainText(string.Empty);
111+
var htmlContainerElement = new ContainerElement(
112+
ContainerElementStyle.Stacked,
113+
[htmlStringClassifiedTextElement, verticalSpacingTextElement]);
114+
115+
// Modify the existing hover's RawContent to prepend the HTML content.
116+
razorVsInternalHover.RawContent = new ContainerElement(razorContainerElement.Style, [htmlContainerElement, .. razorContainerElement.Elements]);
117+
}
118+
else
119+
{
120+
var razorStringResponse = razorHover.Contents.Match(
121+
static s => s,
122+
static markedString => null,
123+
static stringOrMarkedStringArray => null,
124+
static markupContent => markupContent.Value
125+
);
126+
127+
if (razorStringResponse is not null)
128+
{
129+
razorHover.Contents = new MarkupContent()
130+
{
131+
Kind = MarkupKind.Markdown,
132+
Value = htmlStringResponse + "\n\n---\n\n" + razorStringResponse
133+
};
134+
}
135+
}
136+
}
137+
138+
return razorHover;
80139
}
81140

82141
internal TestAccessor GetTestAccessor() => new(this);

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ protected override IRemoteHoverService CreateService(in ServiceArgs args)
111111
}
112112
}
113113

114-
return Results(csharpHover);
114+
// As there is a C# hover, stop further handling.
115+
return new RemoteResponse<Hover?>(StopHandling: true, Result: csharpHover);
115116
}
116117

117118
if (positionInfo.LanguageKind is not (RazorLanguageKind.Html or RazorLanguageKind.Razor))
@@ -152,7 +153,7 @@ protected override IRemoteHoverService CreateService(in ServiceArgs args)
152153
/// <remarks>
153154
/// Once Razor moves wholly over to Roslyn.LanguageServer.Protocol, this method can be removed.
154155
/// </remarks>
155-
private Hover ConvertHover(Hover hover)
156+
private static Hover ConvertHover(Hover hover)
156157
{
157158
// Note: Razor only ever produces a Hover with MarkupContent or a VSInternalHover with RawContents.
158159
// Both variants return a Range.

src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostHoverEndpointTest.cs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Razor.Language;
67
using Microsoft.AspNetCore.Razor.Test.Common;
78
using Microsoft.CodeAnalysis;
89
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
@@ -73,6 +74,57 @@ public async Task Html()
7374
await VerifyHoverAsync(code, htmlResponse, h => Assert.Same(htmlResponse, h));
7475
}
7576

77+
[Fact]
78+
public async Task Html_TagHelper()
79+
{
80+
TestCode code = """
81+
<[|bo$$dy|]></body>
82+
""";
83+
84+
// This verifies Hover calls into both razor and HTML, aggregating their results
85+
const string BodyDescription = "body description";
86+
var htmlResponse = new VSInternalHover
87+
{
88+
Range = new LspRange()
89+
{
90+
Start = new Position(0, 1),
91+
End = new Position(0, "<body".Length),
92+
},
93+
Contents = new MarkupContent()
94+
{
95+
Kind = MarkupKind.Markdown,
96+
Value = BodyDescription,
97+
}
98+
};
99+
100+
await VerifyHoverAsync(code, RazorFileKind.Legacy, htmlResponse, async (hover, document) =>
101+
{
102+
await VerifyRangeAsync(hover, code.Span, document);
103+
104+
hover.VerifyContents(
105+
Container(
106+
Container(
107+
ClassifiedText(
108+
Text(BodyDescription)),
109+
ClassifiedText(
110+
Text(string.Empty))),
111+
Container(
112+
Image,
113+
ClassifiedText(
114+
Text("Microsoft"),
115+
Punctuation("."),
116+
Text("AspNetCore"),
117+
Punctuation("."),
118+
Text("Mvc"),
119+
Punctuation("."),
120+
Text("Razor"),
121+
Punctuation("."),
122+
Text("TagHelpers"),
123+
Punctuation("."),
124+
Type("BodyTagHelper")))));
125+
});
126+
}
127+
76128
[Fact]
77129
public async Task Html_EndTag()
78130
{
@@ -310,10 +362,13 @@ await VerifyHoverAsync(code, async (hover, document) =>
310362
});
311363
}
312364

313-
private async Task VerifyHoverAsync(TestCode input, Func<Hover, TextDocument, Task> verifyHover)
365+
private Task VerifyHoverAsync(TestCode input, Func<Hover, TextDocument, Task> verifyHover)
366+
=> VerifyHoverAsync(input, fileKind: null, htmlResponse: null, verifyHover);
367+
368+
private async Task VerifyHoverAsync(TestCode input, RazorFileKind? fileKind, Hover? htmlResponse, Func<Hover, TextDocument, Task> verifyHover)
314369
{
315-
var document = CreateProjectAndRazorDocument(input.Text);
316-
var result = await GetHoverResultAsync(document, input);
370+
var document = CreateProjectAndRazorDocument(input.Text, fileKind);
371+
var result = await GetHoverResultAsync(document, input, htmlResponse);
317372

318373
Assert.NotNull(result);
319374
await verifyHover(result, document);

src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/HoverAssertions.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Collections.Immutable;
55
using Roslyn.Test.Utilities;
6-
using Xunit;
76

87
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
98

@@ -12,8 +11,15 @@ internal static class HoverAssertions
1211
public static void VerifyContents(this LspHover hover, object expected)
1312
{
1413
var markup = hover.Contents.Fourth;
15-
Assert.Equal(MarkupKind.PlainText, markup.Kind);
16-
AssertEx.EqualOrDiff(expected.ToString(), markup.Value.TrimEnd('\r', '\n'));
14+
15+
var actual = markup.Value.TrimEnd('\r', '\n');
16+
if (markup.Kind == MarkupKind.Markdown)
17+
{
18+
// Remove any horizontal rules we may have added to separate HTML and Razor content
19+
actual = actual.Replace("\n\n---\n\n", string.Empty);
20+
}
21+
22+
AssertEx.EqualOrDiff(expected.ToString(), actual);
1723
}
1824

1925
// Our VS Code test only produce plain text hover content, so these methods are complete overkill,

0 commit comments

Comments
 (0)