Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/targets/Services.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToImplementation" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToImplementationService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SpellCheck" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSpellCheckService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Diagnostics" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteDiagnosticsService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Hover" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteHoverService+Factory" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ protected override ILspServices ConstructLspServices()
services.AddFormattingServices(featureOptions);
services.AddCodeActionsServices();
services.AddOptionsServices(_lspOptions);
services.AddHoverServices();
services.AddTextDocumentServices(featureOptions);

if (!featureOptions.UseRazorCohostServer)
Expand All @@ -154,6 +153,9 @@ protected override ILspServices ConstructLspServices()
services.AddSingleton<IRazorFoldingRangeProvider, UsingsFoldingRangeProvider>();

services.AddSingleton<IFoldingRangeService, FoldingRangeService>();

// Hover
services.AddHoverServices();
}

// Other
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;

namespace Microsoft.VisualStudio.LanguageServer.Protocol;

internal static class ClientCapabilitiesExtensions
{
public static MarkupKind GetMarkupKind(this ClientCapabilities clientCapabilities)
{
// If MarkDown is supported, we'll use that.
if (clientCapabilities.TextDocument?.Hover?.ContentFormat is MarkupKind[] contentFormat &&
Array.IndexOf(contentFormat, MarkupKind.Markdown) >= 0)
{
return MarkupKind.Markdown;
}

return MarkupKind.PlainText;
}

public static bool SupportsMarkdown(this ClientCapabilities clientCapabilities)
{
return clientCapabilities.GetMarkupKind() == MarkupKind.Markdown;
}

public static bool SupportsVisualStudioExtensions(this ClientCapabilities clientCapabilities)
{
return (clientCapabilities as VSInternalClientCapabilities)?.SupportsVisualStudioExtensions ?? false;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.Hover;
Expand All @@ -10,16 +9,8 @@ internal readonly record struct HoverDisplayOptions(MarkupKind MarkupKind, bool
{
public static HoverDisplayOptions From(ClientCapabilities clientCapabilities)
{
var markupKind = MarkupKind.PlainText;

// If MarkDown is supported, we'll use that.
if (clientCapabilities.TextDocument?.Hover?.ContentFormat is MarkupKind[] contentFormat &&
Array.IndexOf(contentFormat, MarkupKind.Markdown) >= 0)
{
markupKind = MarkupKind.Markdown;
}

var supportsVisualStudioExtensions = (clientCapabilities as VSInternalClientCapabilities)?.SupportsVisualStudioExtensions ?? false;
var markupKind = clientCapabilities.GetMarkupKind();
var supportsVisualStudioExtensions = clientCapabilities.SupportsVisualStudioExtensions();

return new(markupKind, supportsVisualStudioExtensions);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using RoslynHover = Roslyn.LanguageServer.Protocol.Hover;
using RoslynPosition = Roslyn.LanguageServer.Protocol.Position;

namespace Microsoft.CodeAnalysis.Razor.Remote;

internal interface IRemoteHoverService : IRemoteJsonService
{
ValueTask<RemoteResponse<RoslynHover?>> GetHoverAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
RoslynPosition position,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal static class RazorServices
[
(typeof(IRemoteClientInitializationService), null),
(typeof(IRemoteGoToDefinitionService), null),
(typeof(IRemoteHoverService), null),
(typeof(IRemoteSignatureHelpService), null),
(typeof(IRemoteInlayHintService), null),
(typeof(IRemoteDocumentSymbolService), null),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Hover;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Roslyn.LanguageServer.Protocol;
using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Roslyn.LanguageServer.Protocol.Hover?>;
using static Microsoft.VisualStudio.LanguageServer.Protocol.ClientCapabilitiesExtensions;
using static Microsoft.VisualStudio.LanguageServer.Protocol.VsLspExtensions;
using static Roslyn.LanguageServer.Protocol.RoslynLspExtensions;
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
using Range = Roslyn.LanguageServer.Protocol.Range;
using VsLsp = Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Remote.Razor;

internal sealed class RemoteHoverService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteHoverService
{
internal sealed class Factory : FactoryBase<IRemoteHoverService>
{
protected override IRemoteHoverService CreateService(in ServiceArgs args)
=> new RemoteHoverService(in args);
}

private readonly IClientCapabilitiesService _clientCapabilitiesService = args.ExportProvider.GetExportedValue<IClientCapabilitiesService>();

protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance;

public ValueTask<RemoteResponse<Hover?>> GetHoverAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
Position position,
CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetHoverAsync(context, position, cancellationToken),
cancellationToken);

private async ValueTask<RemoteResponse<Hover?>> GetHoverAsync(
RemoteDocumentContext context,
Position position,
CancellationToken cancellationToken)
{
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);

if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex))
{
return NoFurtherHandling;
}

var clientCapabilities = _clientCapabilitiesService.ClientCapabilities;

var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true);

if (positionInfo.LanguageKind is RazorLanguageKind.Html or RazorLanguageKind.Razor)
{
// Sometimes what looks like a html attribute can actually map to C#, in which case its better to let Roslyn try to handle this.
if (!DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out _, out _))
{
// Acquire from client capabilities
var options = HoverDisplayOptions.From(clientCapabilities);

var razorHover = await HoverFactory
.GetHoverAsync(codeDocument, hostDocumentIndex, options, context.GetSolutionQueryOperations(), cancellationToken)
.ConfigureAwait(false);

if (razorHover is null)
{
return CallHtml;
}

// Ensure that we convert our Hover to a Roslyn Hover.
var resultHover = ConvertHover(razorHover);

return Results(resultHover);
}
}

var csharpDocument = codeDocument.GetCSharpDocument();
if (!DocumentMappingService.TryMapToGeneratedDocumentPosition(csharpDocument, positionInfo.HostDocumentIndex, out var mappedPosition, out _))
{
// If we can't map to the generated C# file, we're done.
return NoFurtherHandling;
}

// Finally, call into C#.
var generatedDocument = await context.Snapshot
.GetGeneratedDocumentAsync(cancellationToken)
.ConfigureAwait(false);

var csharpHover = await ExternalHandlers.Hover
.GetHoverAsync(
generatedDocument,
mappedPosition,
clientCapabilities.SupportsVisualStudioExtensions(),
clientCapabilities.SupportsMarkdown(),
cancellationToken)
.ConfigureAwait(false);

if (csharpHover is null)
{
return NoFurtherHandling;
}

// Map range back to host document
if (csharpHover.Range is { } range &&
DocumentMappingService.TryMapToHostDocumentRange(csharpDocument, range.ToLinePositionSpan(), out var hostDocumentSpan))
{
csharpHover.Range = RoslynLspFactory.CreateRange(hostDocumentSpan);
}

return Results(csharpHover);
}

/// <summary>
/// Converts a <see cref="VsLsp.Hover"/> to a <see cref="Hover"/>.
/// </summary>
/// <remarks>
/// Once Razor moves wholly over to Roslyn.LanguageServer.Protocol, this method can be removed.
/// </remarks>
private Hover ConvertHover(VsLsp.Hover hover)
{
// Note: Razor only ever produces a Hover with MarkupContent or a VSInternalHover with RawContents.
// Both variants return a Range.

return hover switch
{
VsLsp.VSInternalHover { Range: var range, RawContent: { } rawContent } => new VSInternalHover()
{
Range = ConvertRange(range),
Contents = string.Empty,
RawContent = ConvertVsContent(rawContent)
},
VsLsp.Hover { Range: var range, Contents.Fourth: VsLsp.MarkupContent contents } => new Hover()
{
Range = ConvertRange(range),
Contents = ConvertMarkupContent(contents)
},
_ => Assumed.Unreachable<Hover>(),
};

static Range? ConvertRange(VsLsp.Range? range)
{
return range is not null
? RoslynLspFactory.CreateRange(range.ToLinePositionSpan())
: null;
}

static object ConvertVsContent(object obj)
{
return obj switch
{
VisualStudio.Core.Imaging.ImageId imageId => ConvertImageId(imageId),
VisualStudio.Text.Adornments.ImageElement element => ConvertImageElement(element),
VisualStudio.Text.Adornments.ClassifiedTextRun run => ConvertClassifiedTextRun(run),
VisualStudio.Text.Adornments.ClassifiedTextElement element => ConvertClassifiedTextElement(element),
VisualStudio.Text.Adornments.ContainerElement element => ConvertContainerElement(element),
_ => Assumed.Unreachable<object>()
};

static Roslyn.Core.Imaging.ImageId ConvertImageId(VisualStudio.Core.Imaging.ImageId imageId)
{
return new(imageId.Guid, imageId.Id);
}

static Roslyn.Text.Adornments.ImageElement ConvertImageElement(VisualStudio.Text.Adornments.ImageElement element)
{
return new(ConvertImageId(element.ImageId), element.AutomationName);
}

static Roslyn.Text.Adornments.ClassifiedTextRun ConvertClassifiedTextRun(VisualStudio.Text.Adornments.ClassifiedTextRun run)
{
return new(
run.ClassificationTypeName,
run.Text,
(Roslyn.Text.Adornments.ClassifiedTextRunStyle)run.Style,
run.MarkerTagType,
run.NavigationAction,
run.Tooltip);
}

static Roslyn.Text.Adornments.ClassifiedTextElement ConvertClassifiedTextElement(VisualStudio.Text.Adornments.ClassifiedTextElement element)
{
return new(element.Runs.Select(ConvertClassifiedTextRun));
}

static Roslyn.Text.Adornments.ContainerElement ConvertContainerElement(VisualStudio.Text.Adornments.ContainerElement element)
{
return new((Roslyn.Text.Adornments.ContainerElementStyle)element.Style, element.Elements.Select(ConvertVsContent));
}
}

static MarkupContent ConvertMarkupContent(VsLsp.MarkupContent value)
{
return new()
{
Kind = ConvertMarkupKind(value.Kind),
Value = value.Value
};
}

static MarkupKind ConvertMarkupKind(VsLsp.MarkupKind value)
{
return value == VsLsp.MarkupKind.Markdown
? MarkupKind.Markdown
: MarkupKind.PlainText;
}
}
}
Loading