Skip to content

Commit

Permalink
Cohost: Support signature help (#10595)
Browse files Browse the repository at this point in the history
Requires dotnet/roslyn#74280, and won't even
compile without it.
Part of #9519

This brings signature help to Cohosting. It's a pretty simply PR, for a
pretty simple endpoint, as we just delegate, and there is no translation
of delegated info. The interesting part here is that we use
`System.Text.Json` for the remote signature help service, because it
makes more sense to take advantage of the existing Json converters for
the potential complexity of the `SignatureHelp` result type.
  • Loading branch information
davidwengier authored Jul 19, 2024
2 parents f75ab60 + 45798d4 commit 07ae1eb
Show file tree
Hide file tree
Showing 15 changed files with 439 additions and 14 deletions.
1 change: 1 addition & 0 deletions eng/targets/Services.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
<ServiceHubService Include="Microsoft.VisualStudio.Razor.ClientInitialization" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteClientInitializationService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.UriPresentation" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteUriPresentationService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.FoldingRange" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFoldingRangeService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SignatureHelp" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSignatureHelpService+Factory" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ public static SemanticTokensOptions EnableSemanticTokens(this SemanticTokensOpti
return options;
}

public static void EnableSignatureHelp(this VSInternalServerCapabilities serverCapabilities)
{
serverCapabilities.SignatureHelpProvider = new SignatureHelpOptions().EnableSignatureHelp();
}

public static SignatureHelpOptions EnableSignatureHelp(this SignatureHelpOptions options)
{
options.TriggerCharacters = ["(", ",", "<"];
options.RetriggerCharacters = [">", ")"];

return options;
}

public static void EnableHoverProvider(this VSInternalServerCapabilities serverCapabilities)
{
serverCapabilities.HoverProvider = new HoverOptions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption
services.AddTransient<IOnInitialized>(sp => sp.GetRequiredService<RazorConfigurationEndpoint>());

services.AddHandlerWithCapabilities<ImplementationEndpoint>();
services.AddHandlerWithCapabilities<SignatureHelpEndpoint>();
services.AddHandlerWithCapabilities<DocumentHighlightEndpoint>();
services.AddHandlerWithCapabilities<OnAutoInsertEndpoint>();

Expand All @@ -187,6 +186,7 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption

if (!featureOptions.UseRazorCohostServer)
{
services.AddHandlerWithCapabilities<SignatureHelpEndpoint>();
services.AddHandlerWithCapabilities<LinkedEditingRangeEndpoint>();
services.AddHandlerWithCapabilities<FoldingRangeEndpoint>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using LS = Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.SignatureHelp;

using SignatureHelp = VisualStudio.LanguageServer.Protocol.SignatureHelp;

[RazorLanguageServerEndpoint(Methods.TextDocumentSignatureHelpName)]
internal sealed class SignatureHelpEndpoint(
LanguageServerFeatureOptions languageServerFeatureOptions,
IRazorDocumentMappingService documentMappingService,
IClientConnection clientConnection,
RazorLSPOptionsMonitor optionsMonitor,
ILoggerFactory loggerProvider)
: AbstractRazorDelegatingEndpoint<SignatureHelpParams, LS.SignatureHelp?>(
: AbstractRazorDelegatingEndpoint<SignatureHelpParams, SignatureHelp?>(
languageServerFeatureOptions,
documentMappingService,
clientConnection,
Expand All @@ -33,11 +34,7 @@ internal sealed class SignatureHelpEndpoint(

public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
{
serverCapabilities.SignatureHelpProvider = new SignatureHelpOptions()
{
TriggerCharacters = new[] { "(", ",", "<" },
RetriggerCharacters = new[] { ">", ")" }
};
serverCapabilities.EnableSignatureHelp();
}

protected override Task<IDelegatedParams?> CreateDelegatedParamsAsync(SignatureHelpParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

namespace Microsoft.CodeAnalysis.Razor.Remote;

/// <summary>
/// Marker interface to indicate that an OOP service should use Json for communication
/// </summary>
internal interface IRemoteJsonService
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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 Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.Remote;

using SignatureHelp = Roslyn.LanguageServer.Protocol.SignatureHelp;

internal interface IRemoteSignatureHelpService : IRemoteJsonService
{
ValueTask<SignatureHelp?> GetSignatureHelpAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId documentId, Position linePosition, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,13 @@ internal static class RazorServices
(typeof(IRemoteUriPresentationService), null),
(typeof(IRemoteFoldingRangeService), null)
]);

public static readonly RazorServiceDescriptorsWrapper JsonDescriptors = new(
ComponentName, // Needs to match the above because so much of our ServiceHub infrastructure is convention based
featureDisplayNameProvider: feature => $"Razor {feature} Feature",
jsonConverters: RazorServiceDescriptorsWrapper.GetLspConverters(),
interfaces:
[
(typeof(IRemoteSignatureHelpService), null),
]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ public async Task<object> CreateAsync(

var pipe = stream.UsePipe();

var descriptor = RazorServices.Descriptors.GetDescriptorForServiceFactory(typeof(TService));
var descriptor = typeof(IRemoteJsonService).IsAssignableFrom(typeof(TService))
? RazorServices.JsonDescriptors.GetDescriptorForServiceFactory(typeof(TService))
: RazorServices.Descriptors.GetDescriptorForServiceFactory(typeof(TService));
var serverConnection = descriptor.WithTraceSource(traceSource).ConstructRpcConnection(pipe);

var args = new ServiceArgs(serviceBroker, exportProvider, targetLoggerFactory, serverConnection, brokeredServiceData?.Interceptor);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using Microsoft.CodeAnalysis.ExternalAccess.Razor.Api;

namespace Microsoft.CodeAnalysis.Remote.Razor;

internal static class RemoteWorkspaceAccessor
{
/// <summary>
/// Gets the remote workspace used in the Roslyn OOP process
/// </summary>
/// <remarks>
/// Normally getting a workspace is possible from a document, project or solution snapshot but in the Roslyn OOP
/// process that is explicitly denied via an exception. This method serves as a workaround when a workspace is
/// needed (eg, the Go To Definition API requires one).
///
/// This should be used sparingly nad carefully, and no updates should be made to the workspace.
/// </remarks>
public static Workspace GetWorkspace()
=> RazorBrokeredServiceImplementation.GetWorkspace();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;

namespace Microsoft.CodeAnalysis.Remote.Razor;

using SignatureHelp = Roslyn.LanguageServer.Protocol.SignatureHelp;

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

private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IRazorDocumentMappingService>();

public ValueTask<SignatureHelp?> GetSignatureHelpAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId documentId, Position position, CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetSignatureHelpsAsync(context, position, cancellationToken),
cancellationToken);

private async ValueTask<SignatureHelp?> GetSignatureHelpsAsync(RemoteDocumentContext context, Position position, CancellationToken cancellationToken)
{
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
var linePosition = new LinePosition(position.Line, position.Character);
var absoluteIndex = linePosition.GetRequiredAbsoluteIndex(codeDocument.Source.Text, logger: null);

var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);

if (_documentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), absoluteIndex, out var mappedPosition, out _))
{
return await ExternalHandlers.SignatureHelp.GetSignatureHelpAsync(generatedDocument, mappedPosition, supportsVisualStudioExtensions: true, cancellationToken).ConfigureAwait(false);
}

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

using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.Razor.LanguageClient;
using Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
using Microsoft.VisualStudio.Razor.LanguageClient.Extensions;
using Microsoft.VisualStudio.Razor.Settings;
using RLSP = Roslyn.LanguageServer.Protocol;

namespace Microsoft.VisualStudio.LanguageServices.Razor.LanguageClient.Cohost;

#pragma warning disable RS0030 // Do not use banned APIs
[Shared]
[CohostEndpoint(Methods.TextDocumentSignatureHelpName)]
[Export(typeof(IDynamicRegistrationProvider))]
[ExportCohostStatelessLspService(typeof(CohostSignatureHelpEndpoint))]
[method: ImportingConstructor]
#pragma warning restore RS0030 // Do not use banned APIs
internal class CohostSignatureHelpEndpoint(
IRemoteServiceInvoker remoteServiceInvoker,
IClientSettingsManager clientSettingsManager,
IHtmlDocumentSynchronizer htmlDocumentSynchronizer,
LSPRequestInvoker requestInvoker,
ILoggerFactory loggerFactory)
: AbstractRazorCohostDocumentRequestHandler<SignatureHelpParams, SumType<SignatureHelp, RLSP.SignatureHelp>?>, IDynamicRegistrationProvider
{
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager;
private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer;
private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostFoldingRangeEndpoint>();

protected override bool MutatesSolutionState => false;

protected override bool RequiresLSPSolution => true;

public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext)
{
if (clientCapabilities.TextDocument?.SignatureHelp?.DynamicRegistration == true)
{
return new Registration()
{
Method = Methods.TextDocumentSignatureHelpName,
RegisterOptions = new SignatureHelpRegistrationOptions()
{
DocumentSelector = filter
}.EnableSignatureHelp()
};
}

return null;
}

protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(SignatureHelpParams request)
=> new RazorTextDocumentIdentifier(request.TextDocument.Uri, (request.TextDocument as VSTextDocumentIdentifier)?.ProjectContext?.Id);

// NOTE: The use of SumType here is a little odd, but it allows us to return Roslyn LSP types from the Roslyn call, and VS LSP types from the Html
// call. It works because both sets of types are attributed the right way, so the Json ends up looking the same and the client doesn't
// care. Ideally eventually we will be able to move all of this to just Roslyn LSP types, but we might have to wait for Web Tools
protected override Task<SumType<SignatureHelp, RLSP.SignatureHelp>?> HandleRequestAsync(SignatureHelpParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
=> HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken);

private async Task<SumType<SignatureHelp, RLSP.SignatureHelp>?> HandleRequestAsync(SignatureHelpParams request, TextDocument razorDocument, CancellationToken cancellationToken)
{
// Return nothing if "Parameter Information" option is disabled unless signature help is invoked explicitly via command as opposed to typing or content change
if (request.Context is { TriggerKind: not SignatureHelpTriggerKind.Invoked } &&
!_clientSettingsManager.GetClientSettings().ClientCompletionSettings.AutoListParams)
{
return null;
}

var data = await _remoteServiceInvoker.TryInvokeAsync<IRemoteSignatureHelpService, RLSP.SignatureHelp?>(
razorDocument.Project.Solution,
(service, solutionInfo, cancellationToken) => service.GetSignatureHelpAsync(solutionInfo, razorDocument.Id, new RLSP.Position(request.Position.Line, request.Position.Character), cancellationToken),
cancellationToken)
.ConfigureAwait(false);

// If we got a response back, then either Razor or C# wants to do something with this, so we're good to go
if (data is { } signatureHelp)
{
return signatureHelp;
}

// If we didn't get anything from Razor or Roslyn, lets ask Html what they want to do
var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
if (htmlDocument is null)
{
return null;
}

request.TextDocument = request.TextDocument.WithUri(htmlDocument.Uri);

var result = await _requestInvoker.ReinvokeRequestOnServerAsync<SignatureHelpParams, SignatureHelp?>(
htmlDocument.Buffer,
Methods.TextDocumentSignatureHelpName,
RazorLSPConstants.HtmlLanguageServerName,
request,
cancellationToken)
.ConfigureAwait(false);

return result?.Response;
}

internal TestAccessor GetTestAccessor() => new(this);

internal readonly struct TestAccessor(CohostSignatureHelpEndpoint instance)
{
internal async Task<string[]?> HandleRequestAndGetLabelsAsync(SignatureHelpParams request, TextDocument document, CancellationToken cancellationToken)
{
// Our tests don't have IVT to Roslyn.LanguageServer.Protocol (yet!?) so we can't expose the return from HandleRequestAsync directly,
// but rather need to do a little test code here.
var result = await instance.HandleRequestAsync(request, document, cancellationToken);

if (result is not { } signatureHelp)
{
return null;
}

if (signatureHelp.TryGetFirst(out var sigHelp1))
{
return sigHelp1.Signatures.Select(s => s.Label).ToArray();
}
else if (signatureHelp.TryGetSecond(out var sigHelp2))
{
return sigHelp2.Signatures.Select(s => s.Label).ToArray();
}

Assumed.Unreachable();
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ internal sealed class RemoteServiceInvoker(
[CallerMemberName] string? callerMemberName = null)
where TService : class
{
var client = await TryGetClientAsync(cancellationToken).ConfigureAwait(false);
var client = typeof(IRemoteJsonService).IsAssignableFrom(typeof(TService))
? await TryGetJsonClientAsync(cancellationToken).ConfigureAwait(false)
: await TryGetClientAsync(cancellationToken).ConfigureAwait(false);
if (client is null)
{
_logger.LogError($"Couldn't get remote client for {typeof(TService).Name} service");
Expand Down Expand Up @@ -97,6 +99,24 @@ internal sealed class RemoteServiceInvoker(
return remoteClient;
}

private async Task<RazorRemoteHostClient?> TryGetJsonClientAsync(CancellationToken cancellationToken)
{
// Even if we're getting a service that wants to use Json, we still have to initialize the OOP client
// so we get the regular (MessagePack) client too.
if (!_fullyInitialized)
{
_ = await TryGetClientAsync(cancellationToken).ConfigureAwait(false);
}

var workspace = _workspaceProvider.GetWorkspace();

return await RazorRemoteHostClient.TryGetClientAsync(
workspace.Services,
RazorServices.JsonDescriptors,
RazorRemoteServiceCallbackDispatcherRegistry.Empty,
cancellationToken).ConfigureAwait(false);
}

private async Task InitializeRemoteClientAsync(RazorRemoteHostClient remoteClient, CancellationToken cancellationToken)
{
if (_fullyInitialized)
Expand Down
Loading

0 comments on commit 07ae1eb

Please sign in to comment.