Skip to content

Commit 5796653

Browse files
Add co-hosting support for hover (#11150)
Fixes #10839 Now that all of the infrastructure is in place, adding a co-hosting endpoint and remote service for Hover is mostly boilerplate. Similar to the signature help endpoint, the result type might be a Roslyn LSP Hover or a VS LSP Hover. The remote service always returns a Roslyn LSP Hover, but HTML will return a VS LSP Hover. So, we join the possibilities together with a SumType.
2 parents bcb8166 + be89fc9 commit 5796653

File tree

11 files changed

+638
-13
lines changed

11 files changed

+638
-13
lines changed

eng/targets/Services.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToImplementation" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToImplementationService+Factory" />
3434
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SpellCheck" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSpellCheckService+Factory" />
3535
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Diagnostics" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteDiagnosticsService+Factory" />
36+
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Hover" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteHoverService+Factory" />
3637
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Completion" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteCompletionService+Factory" />
3738
</ItemGroup>
3839
</Project>

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ protected override ILspServices ConstructLspServices()
131131
services.AddFormattingServices(featureOptions);
132132
services.AddCodeActionsServices();
133133
services.AddOptionsServices(_lspOptions);
134-
services.AddHoverServices();
135134
services.AddTextDocumentServices(featureOptions);
136135

137136
if (!featureOptions.UseRazorCohostServer)
@@ -156,6 +155,9 @@ protected override ILspServices ConstructLspServices()
156155
services.AddSingleton<IRazorFoldingRangeProvider, UsingsFoldingRangeProvider>();
157156

158157
services.AddSingleton<IFoldingRangeService, FoldingRangeService>();
158+
159+
// Hover
160+
services.AddHoverServices();
159161
}
160162

161163
// Other
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.VisualStudio.LanguageServer.Protocol;
7+
8+
internal static class ClientCapabilitiesExtensions
9+
{
10+
public static MarkupKind GetMarkupKind(this ClientCapabilities clientCapabilities)
11+
{
12+
// If MarkDown is supported, we'll use that.
13+
if (clientCapabilities.TextDocument?.Hover?.ContentFormat is MarkupKind[] contentFormat &&
14+
Array.IndexOf(contentFormat, MarkupKind.Markdown) >= 0)
15+
{
16+
return MarkupKind.Markdown;
17+
}
18+
19+
return MarkupKind.PlainText;
20+
}
21+
22+
public static bool SupportsMarkdown(this ClientCapabilities clientCapabilities)
23+
{
24+
return clientCapabilities.GetMarkupKind() == MarkupKind.Markdown;
25+
}
26+
27+
public static bool SupportsVisualStudioExtensions(this ClientCapabilities clientCapabilities)
28+
{
29+
return (clientCapabilities as VSInternalClientCapabilities)?.SupportsVisualStudioExtensions ?? false;
30+
}
31+
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverDisplayOptions.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

4-
using System;
54
using Microsoft.VisualStudio.LanguageServer.Protocol;
65

76
namespace Microsoft.CodeAnalysis.Razor.Hover;
@@ -10,16 +9,8 @@ internal readonly record struct HoverDisplayOptions(MarkupKind MarkupKind, bool
109
{
1110
public static HoverDisplayOptions From(ClientCapabilities clientCapabilities)
1211
{
13-
var markupKind = MarkupKind.PlainText;
14-
15-
// If MarkDown is supported, we'll use that.
16-
if (clientCapabilities.TextDocument?.Hover?.ContentFormat is MarkupKind[] contentFormat &&
17-
Array.IndexOf(contentFormat, MarkupKind.Markdown) >= 0)
18-
{
19-
markupKind = MarkupKind.Markdown;
20-
}
21-
22-
var supportsVisualStudioExtensions = (clientCapabilities as VSInternalClientCapabilities)?.SupportsVisualStudioExtensions ?? false;
12+
var markupKind = clientCapabilities.GetMarkupKind();
13+
var supportsVisualStudioExtensions = clientCapabilities.SupportsVisualStudioExtensions();
2314

2415
return new(markupKind, supportsVisualStudioExtensions);
2516
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
7+
using RoslynHover = Roslyn.LanguageServer.Protocol.Hover;
8+
using RoslynPosition = Roslyn.LanguageServer.Protocol.Position;
9+
10+
namespace Microsoft.CodeAnalysis.Razor.Remote;
11+
12+
internal interface IRemoteHoverService : IRemoteJsonService
13+
{
14+
ValueTask<RemoteResponse<RoslynHover?>> GetHoverAsync(
15+
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
16+
JsonSerializableDocumentId documentId,
17+
RoslynPosition position,
18+
CancellationToken cancellationToken);
19+
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ internal static class RazorServices
3030
[
3131
(typeof(IRemoteClientInitializationService), null),
3232
(typeof(IRemoteGoToDefinitionService), null),
33+
(typeof(IRemoteHoverService), null),
3334
(typeof(IRemoteSignatureHelpService), null),
3435
(typeof(IRemoteInlayHintService), null),
3536
(typeof(IRemoteDocumentSymbolService), null),
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Diagnostics;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Razor;
9+
using Microsoft.AspNetCore.Razor.Language;
10+
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
11+
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
12+
using Microsoft.CodeAnalysis.Razor.Hover;
13+
using Microsoft.CodeAnalysis.Razor.Protocol;
14+
using Microsoft.CodeAnalysis.Razor.Remote;
15+
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
16+
using Roslyn.LanguageServer.Protocol;
17+
using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Roslyn.LanguageServer.Protocol.Hover?>;
18+
using static Microsoft.VisualStudio.LanguageServer.Protocol.ClientCapabilitiesExtensions;
19+
using static Microsoft.VisualStudio.LanguageServer.Protocol.VsLspExtensions;
20+
using static Roslyn.LanguageServer.Protocol.RoslynLspExtensions;
21+
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
22+
using Range = Roslyn.LanguageServer.Protocol.Range;
23+
using VsLsp = Microsoft.VisualStudio.LanguageServer.Protocol;
24+
25+
namespace Microsoft.CodeAnalysis.Remote.Razor;
26+
27+
internal sealed class RemoteHoverService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteHoverService
28+
{
29+
internal sealed class Factory : FactoryBase<IRemoteHoverService>
30+
{
31+
protected override IRemoteHoverService CreateService(in ServiceArgs args)
32+
=> new RemoteHoverService(in args);
33+
}
34+
35+
private readonly IClientCapabilitiesService _clientCapabilitiesService = args.ExportProvider.GetExportedValue<IClientCapabilitiesService>();
36+
37+
protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance;
38+
39+
public ValueTask<RemoteResponse<Hover?>> GetHoverAsync(
40+
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
41+
JsonSerializableDocumentId documentId,
42+
Position position,
43+
CancellationToken cancellationToken)
44+
=> RunServiceAsync(
45+
solutionInfo,
46+
documentId,
47+
context => GetHoverAsync(context, position, cancellationToken),
48+
cancellationToken);
49+
50+
private async ValueTask<RemoteResponse<Hover?>> GetHoverAsync(
51+
RemoteDocumentContext context,
52+
Position position,
53+
CancellationToken cancellationToken)
54+
{
55+
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
56+
57+
if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex))
58+
{
59+
return NoFurtherHandling;
60+
}
61+
62+
var clientCapabilities = _clientCapabilitiesService.ClientCapabilities;
63+
var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true);
64+
65+
if (positionInfo.LanguageKind == RazorLanguageKind.CSharp)
66+
{
67+
var generatedDocument = await context.Snapshot
68+
.GetGeneratedDocumentAsync(cancellationToken)
69+
.ConfigureAwait(false);
70+
71+
var csharpHover = await ExternalHandlers.Hover
72+
.GetHoverAsync(
73+
generatedDocument,
74+
positionInfo.Position.ToLinePosition(),
75+
clientCapabilities.SupportsVisualStudioExtensions(),
76+
clientCapabilities.SupportsMarkdown(),
77+
cancellationToken)
78+
.ConfigureAwait(false);
79+
80+
// Roslyn couldn't provide a hover, so we're done.
81+
if (csharpHover is null)
82+
{
83+
return NoFurtherHandling;
84+
}
85+
86+
// Map the hover range back to the host document
87+
if (csharpHover.Range is { } range &&
88+
DocumentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), range.ToLinePositionSpan(), out var hostDocumentSpan))
89+
{
90+
csharpHover.Range = RoslynLspFactory.CreateRange(hostDocumentSpan);
91+
}
92+
93+
return Results(csharpHover);
94+
}
95+
96+
if (positionInfo.LanguageKind is not (RazorLanguageKind.Html or RazorLanguageKind.Razor))
97+
{
98+
Debug.Fail($"Encountered an unexpected {nameof(RazorLanguageKind)}: {positionInfo.LanguageKind}");
99+
return NoFurtherHandling;
100+
}
101+
102+
// If this is Html or Razor, try to retrieve a hover from Razor.
103+
var options = HoverDisplayOptions.From(clientCapabilities);
104+
105+
var razorHover = await HoverFactory
106+
.GetHoverAsync(codeDocument, hostDocumentIndex, options, context.GetSolutionQueryOperations(), cancellationToken)
107+
.ConfigureAwait(false);
108+
109+
// Roslyn couldn't provide a hover, so we're done.
110+
if (razorHover is null)
111+
{
112+
return CallHtml;
113+
}
114+
115+
// Ensure that we convert our Hover to a Roslyn Hover.
116+
var resultHover = ConvertHover(razorHover);
117+
118+
return Results(resultHover);
119+
}
120+
121+
/// <summary>
122+
/// Converts a <see cref="VsLsp.Hover"/> to a <see cref="Hover"/>.
123+
/// </summary>
124+
/// <remarks>
125+
/// Once Razor moves wholly over to Roslyn.LanguageServer.Protocol, this method can be removed.
126+
/// </remarks>
127+
private Hover ConvertHover(VsLsp.Hover hover)
128+
{
129+
// Note: Razor only ever produces a Hover with MarkupContent or a VSInternalHover with RawContents.
130+
// Both variants return a Range.
131+
132+
return hover switch
133+
{
134+
VsLsp.VSInternalHover { Range: var range, RawContent: { } rawContent } => new VSInternalHover()
135+
{
136+
Range = ConvertRange(range),
137+
Contents = string.Empty,
138+
RawContent = ConvertVsContent(rawContent)
139+
},
140+
VsLsp.Hover { Range: var range, Contents.Fourth: VsLsp.MarkupContent contents } => new Hover()
141+
{
142+
Range = ConvertRange(range),
143+
Contents = ConvertMarkupContent(contents)
144+
},
145+
_ => Assumed.Unreachable<Hover>(),
146+
};
147+
148+
static Range? ConvertRange(VsLsp.Range? range)
149+
{
150+
return range is not null
151+
? RoslynLspFactory.CreateRange(range.ToLinePositionSpan())
152+
: null;
153+
}
154+
155+
static object ConvertVsContent(object obj)
156+
{
157+
return obj switch
158+
{
159+
VisualStudio.Core.Imaging.ImageId imageId => ConvertImageId(imageId),
160+
VisualStudio.Text.Adornments.ImageElement element => ConvertImageElement(element),
161+
VisualStudio.Text.Adornments.ClassifiedTextRun run => ConvertClassifiedTextRun(run),
162+
VisualStudio.Text.Adornments.ClassifiedTextElement element => ConvertClassifiedTextElement(element),
163+
VisualStudio.Text.Adornments.ContainerElement element => ConvertContainerElement(element),
164+
_ => Assumed.Unreachable<object>()
165+
};
166+
167+
static Roslyn.Core.Imaging.ImageId ConvertImageId(VisualStudio.Core.Imaging.ImageId imageId)
168+
{
169+
return new(imageId.Guid, imageId.Id);
170+
}
171+
172+
static Roslyn.Text.Adornments.ImageElement ConvertImageElement(VisualStudio.Text.Adornments.ImageElement element)
173+
{
174+
return new(ConvertImageId(element.ImageId), element.AutomationName);
175+
}
176+
177+
static Roslyn.Text.Adornments.ClassifiedTextRun ConvertClassifiedTextRun(VisualStudio.Text.Adornments.ClassifiedTextRun run)
178+
{
179+
return new(
180+
run.ClassificationTypeName,
181+
run.Text,
182+
(Roslyn.Text.Adornments.ClassifiedTextRunStyle)run.Style,
183+
run.MarkerTagType,
184+
run.NavigationAction,
185+
run.Tooltip);
186+
}
187+
188+
static Roslyn.Text.Adornments.ClassifiedTextElement ConvertClassifiedTextElement(VisualStudio.Text.Adornments.ClassifiedTextElement element)
189+
{
190+
return new(element.Runs.Select(ConvertClassifiedTextRun));
191+
}
192+
193+
static Roslyn.Text.Adornments.ContainerElement ConvertContainerElement(VisualStudio.Text.Adornments.ContainerElement element)
194+
{
195+
return new((Roslyn.Text.Adornments.ContainerElementStyle)element.Style, element.Elements.Select(ConvertVsContent));
196+
}
197+
}
198+
199+
static MarkupContent ConvertMarkupContent(VsLsp.MarkupContent value)
200+
{
201+
return new()
202+
{
203+
Kind = ConvertMarkupKind(value.Kind),
204+
Value = value.Value
205+
};
206+
}
207+
208+
static MarkupKind ConvertMarkupKind(VsLsp.MarkupKind value)
209+
{
210+
return value == VsLsp.MarkupKind.Markdown
211+
? MarkupKind.Markdown
212+
: MarkupKind.PlainText;
213+
}
214+
}
215+
}

0 commit comments

Comments
 (0)