Skip to content

Commit

Permalink
Push hover logic to Workspaces layer (#11119)
Browse files Browse the repository at this point in the history
This change moves all of the logic that computes a `VSInternalHover` to
the Workspaces layer. The file diff shouldn't be too bad, but a lot of
code is moved without much additional clean up. So, it might be easier
to go commit-by-commit.
  • Loading branch information
DustinCampbell authored Oct 30, 2024
2 parents 41693de + aa3209a commit 51c7d56
Show file tree
Hide file tree
Showing 17 changed files with 1,688 additions and 1,775 deletions.
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

0 comments on commit 51c7d56

Please sign in to comment.