diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 9990daed115..068c5f2e0ab 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -22,5 +22,6 @@ + diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/VSInternalServerCapabilitiesExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/VSInternalServerCapabilitiesExtensions.cs index 62785fb5267..83b32a3dd23 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/VSInternalServerCapabilitiesExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/VSInternalServerCapabilitiesExtensions.cs @@ -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() diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 17c412bd72b..d28cb50e47c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -178,7 +178,6 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddTransient(sp => sp.GetRequiredService()); services.AddHandlerWithCapabilities(); - services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); @@ -187,6 +186,7 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption if (!featureOptions.UseRazorCohostServer) { + services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SignatureHelp/SignatureHelpEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SignatureHelp/SignatureHelpEndpoint.cs index baa5d4b0000..790cd537f07 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SignatureHelp/SignatureHelpEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SignatureHelp/SignatureHelpEndpoint.cs @@ -11,10 +11,11 @@ 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, @@ -22,7 +23,7 @@ internal sealed class SignatureHelpEndpoint( IClientConnection clientConnection, RazorLSPOptionsMonitor optionsMonitor, ILoggerFactory loggerProvider) - : AbstractRazorDelegatingEndpoint( + : AbstractRazorDelegatingEndpoint( languageServerFeatureOptions, documentMappingService, clientConnection, @@ -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 CreateDelegatedParamsAsync(SignatureHelpParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteJsonService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteJsonService.cs new file mode 100644 index 00000000000..4e193d31649 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteJsonService.cs @@ -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; + +/// +/// Marker interface to indicate that an OOP service should use Json for communication +/// +internal interface IRemoteJsonService +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteSignatureHelpService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteSignatureHelpService.cs new file mode 100644 index 00000000000..82163933eef --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteSignatureHelpService.cs @@ -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 GetSignatureHelpAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId documentId, Position linePosition, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index a33f9dd3fa9..5b79b2b5005 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -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), + ]); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs index f3c1b31d6db..f9cd31f160e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs @@ -62,7 +62,9 @@ public async Task 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); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteWorkspaceAccessor.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteWorkspaceAccessor.cs new file mode 100644 index 00000000000..f8e3087df91 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteWorkspaceAccessor.cs @@ -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 +{ + /// + /// Gets the remote workspace used in the Roslyn OOP process + /// + /// + /// 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. + /// + public static Workspace GetWorkspace() + => RazorBrokeredServiceImplementation.GetWorkspace(); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SignatureHelp/RemoteSignatureHelpService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SignatureHelp/RemoteSignatureHelpService.cs new file mode 100644 index 00000000000..dc94aa76584 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SignatureHelp/RemoteSignatureHelpService.cs @@ -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 + { + protected override IRemoteSignatureHelpService CreateService(in ServiceArgs args) + => new RemoteSignatureHelpService(in args); + } + + private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue(); + private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); + + public ValueTask GetSignatureHelpAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId documentId, Position position, CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => GetSignatureHelpsAsync(context, position, cancellationToken), + cancellationToken); + + private async ValueTask 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; + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSignatureHelpEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSignatureHelpEndpoint.cs new file mode 100644 index 00000000000..1461c58f588 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSignatureHelpEndpoint.cs @@ -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?>, 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(); + + 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?> HandleRequestAsync(SignatureHelpParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken); + + private async Task?> 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( + 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( + 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 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; + } + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs index e60330a4d91..ac71f62e1f3 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs @@ -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"); @@ -97,6 +99,24 @@ internal sealed class RemoteServiceInvoker( return remoteClient; } + private async Task 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) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index 3a6e17f38fc..9a2593fa3bb 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.Remote; using Microsoft.CodeAnalysis.Remote.Razor; @@ -19,7 +18,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; -public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper) : WorkspaceTestBase(testOutputHelper) +public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper) : ToolingTestBase(testOutputHelper) { private const string CSharpVirtualDocumentSuffix = ".g.cs"; private ExportProvider? _exportProvider; @@ -87,7 +86,10 @@ protected TextDocument CreateProjectAndRazorDocument(string contents, string? fi documentFilePath) .WithMetadataReferences(AspNet80.ReferenceInfos.All.Select(r => r.Reference)); - var solution = Workspace.CurrentSolution.AddProject(projectInfo); + // Importantly, we use Roslyn's remote workspace here so that when our OOP services call into Roslyn, their code + // will be able to access their services. + var workspace = RemoteWorkspaceAccessor.GetWorkspace(); + var solution = workspace.CurrentSolution.AddProject(projectInfo); solution = solution .AddAdditionalDocument( diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFoldingRangeEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFoldingRangeEndpointTest.cs index f63d412c10e..2f8a9be3573 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFoldingRangeEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFoldingRangeEndpointTest.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSignatureHelpEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSignatureHelpEndpointTest.cs new file mode 100644 index 00000000000..35c070ae769 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSignatureHelpEndpointTest.cs @@ -0,0 +1,136 @@ +// 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.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Settings; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServices.Razor.LanguageClient.Cohost; +using Microsoft.VisualStudio.Razor.Settings; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostSignatureHelpEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task CSharpMethodCSharp() + { + var input = """ +
+ + @{ + string M1(int i) => throw new NotImplementedException(); + + void Act() + { + M1($$); + } + } + """; + + await VerifySignatureHelpAsync(input, "string M1(int i)"); + } + + [Fact] + public async Task CSharpMethodInRazor() + { + var input = """ +
@GetDiv($$)
+ + @{ + string GetDiv() => ""; + } + """; + + await VerifySignatureHelpAsync(input, "string GetDiv()"); + } + + [Fact] + public async Task AutoListParamsOff_Invoked_ReturnsResult() + { + var input = """ +
+ + @{ + string M1(int i) => throw new NotImplementedException(); + + void Act() + { + M1($$); + } + } + """; + + await VerifySignatureHelpAsync(input, "string M1(int i)", autoListParams: false, triggerKind: SignatureHelpTriggerKind.Invoked); + } + + [Fact] + public async Task AutoListParamsOff_NotInvoked_ReturnsNoResult() + { + var input = """ +
+ + @{ + string M1(int i) => throw new NotImplementedException(); + + void Act() + { + M1($$); + } + } + """; + + await VerifySignatureHelpAsync(input, "", autoListParams: false, triggerKind: SignatureHelpTriggerKind.ContentChange); + } + + private async Task VerifySignatureHelpAsync(string input, string expected, bool autoListParams = true, SignatureHelpTriggerKind? triggerKind = null) + { + TestFileMarkupParser.GetPosition(input, out input, out var cursorPosition); + var document = CreateProjectAndRazorDocument(input); + var sourceText = await document.GetTextAsync(DisposalToken); + sourceText.GetLineAndOffset(cursorPosition, out var lineIndex, out var characterIndex); + + var clientSettingsManager = new ClientSettingsManager([], null, null); + clientSettingsManager.Update(ClientCompletionSettings.Default with { AutoListParams = autoListParams }); + + var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentSignatureHelpName, null)]); + + var endpoint = new CohostSignatureHelpEndpoint(RemoteServiceInvoker, clientSettingsManager, TestHtmlDocumentSynchronizer.Instance, requestInvoker, LoggerFactory); + + var signatureHelpContext = new SignatureHelpContext() + { + TriggerKind = triggerKind ?? SignatureHelpTriggerKind.Invoked + }; + + var request = new SignatureHelpParams() + { + TextDocument = new TextDocumentIdentifier() + { + Uri = document.CreateUri() + }, + Position = new Position() + { + Line = lineIndex, + Character = characterIndex + }, + Context = signatureHelpContext + }; + + var result = await endpoint.GetTestAccessor().HandleRequestAndGetLabelsAsync(request, document, DisposalToken); + + // Assert + if (expected.Length == 0) + { + Assert.Null(result); + return; + } + + var actual = Assert.Single(result.AssumeNotNull()); + Assert.Equal(expected, actual); + } +}