|
| 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