Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Push hover logic to Workspaces layer #11119

Merged
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,17 @@

namespace Microsoft.AspNetCore.Razor.LanguageServer;

internal abstract class AbstractRazorDelegatingEndpoint<TRequest, TResponse> : IRazorRequestHandler<TRequest, TResponse?>
where TRequest : ITextDocumentPositionParams
internal abstract class AbstractRazorDelegatingEndpoint<TRequest, TResponse>(
LanguageServerFeatureOptions languageServerFeatureOptions,
IDocumentMappingService documentMappingService,
IClientConnection clientConnection,
ILogger logger)
: IRazorRequestHandler<TRequest, TResponse?> where TRequest : ITextDocumentPositionParams
{
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions;
private readonly IDocumentMappingService _documentMappingService;
private readonly IClientConnection _clientConnection;
protected readonly ILogger Logger;

protected AbstractRazorDelegatingEndpoint(
LanguageServerFeatureOptions languageServerFeatureOptions,
IDocumentMappingService documentMappingService,
IClientConnection clientConnection,
ILogger logger)
{
_languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions));
_documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService));
_clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection));

Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;
protected readonly IDocumentMappingService DocumentMappingService = documentMappingService;
private readonly IClientConnection _clientConnection = clientConnection;
protected readonly ILogger Logger = logger;

/// <summary>
/// The strategy to use to project the incoming caret position onto the generated C#/Html document
Expand Down Expand Up @@ -119,7 +110,7 @@ protected virtual Task<TResponse> HandleDelegatedResponseAsync(TResponse delegat
return default;
}

var positionInfo = DocumentPositionInfoStrategy.GetPositionInfo(_documentMappingService, codeDocument, absoluteIndex);
var positionInfo = DocumentPositionInfoStrategy.GetPositionInfo(DocumentMappingService, codeDocument, absoluteIndex);

var response = await TryHandleAsync(request, requestContext, positionInfo, cancellationToken).ConfigureAwait(false);
if (response is not null && response is not ISumType { Value: null })
Expand All @@ -143,7 +134,7 @@ protected virtual Task<TResponse> HandleDelegatedResponseAsync(TResponse delegat
// Sometimes Html can actually be mapped to C#, like for example component attributes, which map to
// C# properties, even though they appear entirely in a Html context. Since remapping is pretty cheap
// it's easier to just try mapping, and see what happens, rather than checking for specific syntax nodes.
if (_documentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out Position? csharpPosition, out _))
if (DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out Position? csharpPosition, out _))
{
// We're just gonna pretend this mapped perfectly normally onto C#. Moving this logic to the actual position info
// calculating code is possible, but could have untold effects, so opt-in is better (for now?)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,6 @@ public static void AddDiagnosticServices(this IServiceCollection services)
public static void AddHoverServices(this IServiceCollection services)
{
services.AddHandlerWithCapabilities<HoverEndpoint>();

services.AddSingleton<IHoverService, HoverService>();
}

public static void AddSemanticTokensServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
// 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 System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Hover;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover;

[RazorLanguageServerEndpoint(Methods.TextDocumentHoverName)]
internal sealed class HoverEndpoint : AbstractRazorDelegatingEndpoint<TextDocumentPositionParams, VSInternalHover?>, ICapabilitiesProvider
internal sealed class HoverEndpoint(
IProjectSnapshotManager projectManager,
IClientCapabilitiesService clientCapabilitiesService,
LanguageServerFeatureOptions languageServerFeatureOptions,
IDocumentMappingService documentMappingService,
IClientConnection clientConnection,
ILoggerFactory loggerFactory)
: AbstractRazorDelegatingEndpoint<TextDocumentPositionParams, VSInternalHover?>(
languageServerFeatureOptions,
documentMappingService,
clientConnection,
loggerFactory.GetOrCreateLogger<HoverEndpoint>()), ICapabilitiesProvider
{
private readonly IHoverService _hoverService;

public HoverEndpoint(
IHoverService hoverService,
LanguageServerFeatureOptions languageServerFeatureOptions,
IDocumentMappingService documentMappingService,
IClientConnection clientConnection,
ILoggerFactory loggerFactory)
: base(languageServerFeatureOptions, documentMappingService, clientConnection, loggerFactory.GetOrCreateLogger<HoverEndpoint>())
{
_hoverService = hoverService ?? throw new ArgumentNullException(nameof(hoverService));
}
private readonly IProjectSnapshotManager _projectManager = projectManager;
private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService;

public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
{
Expand All @@ -56,33 +61,81 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V
positionInfo.LanguageKind));
}

protected override Task<VSInternalHover?> TryHandleAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
protected override async Task<VSInternalHover?> TryHandleAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
{
var documentContext = requestContext.DocumentContext;
if (documentContext is null)
{
return SpecializedTasks.Null<VSInternalHover>();
return null;
}

// HTML can still sometimes be handled by razor. For example hovering over
// a component tag like <Counter /> will still be in an html context
if (positionInfo.LanguageKind == RazorLanguageKind.CSharp)
{
return null;
}

return _hoverService.GetRazorHoverInfoAsync(
documentContext,
positionInfo,
request.Position,
cancellationToken);
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);

// Sometimes what looks like a html attribute can actually map to C#, in which case its better to let Roslyn try to handle this.
// We can only do this if we're in single server mode though, otherwise we won't be delegating to Roslyn at all
if (SingleServerSupport && DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out _, out _))
{
return null;
}

var options = HoverDisplayOptions.From(_clientCapabilitiesService.ClientCapabilities);

return await HoverFactory.GetHoverAsync(
codeDocument,
documentContext.FilePath,
positionInfo.HostDocumentIndex,
options,
_projectManager.GetQueryOperations(),
cancellationToken)
.ConfigureAwait(false);
}

protected override Task<VSInternalHover?> HandleDelegatedResponseAsync(VSInternalHover? response, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
protected override async Task<VSInternalHover?> HandleDelegatedResponseAsync(VSInternalHover? response, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
{
var documentContext = requestContext.DocumentContext;
if (documentContext is null)
{
return SpecializedTasks.Null<VSInternalHover>();
return null;
}

if (response?.Range is null)
{
return response;
}

var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);

// If we don't include the originally requested position in our response, the client may not show it, so we extend the range to ensure it is in there.
// eg for hovering at @bind-Value:af$$ter, we want to show people the hover for the Value property, so Roslyn will return to us the range for just the
// portion of the attribute that says "Value".
if (RazorSyntaxFacts.TryGetFullAttributeNameSpan(codeDocument, positionInfo.HostDocumentIndex, out var originalAttributeRange))
{
response.Range = codeDocument.Source.Text.GetRange(originalAttributeRange);
}
else if (positionInfo.LanguageKind == RazorLanguageKind.CSharp)
{
if (DocumentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), response.Range, out var projectedRange))
{
response.Range = projectedRange;
}
else
{
// We couldn't remap the range back from Roslyn, but we have to do something with it, because it definitely won't
// be correct, and if the Razor document is small, will be completely outside the valid range for the file, which
// would cause the client to error.
// Returning null here will still show the hover, just there won't be any extra visual indication, like
// a background color, applied by the client.
response.Range = null;
}
}

return _hoverService.TranslateDelegatedResponseAsync(
response,
documentContext,
positionInfo,
cancellationToken);
return response;
}
}

This file was deleted.

Loading