From 87e93c7f8a0cd50381a73dda9a746820992b508d Mon Sep 17 00:00:00 2001 From: "Alex Gavrilov (DEV PROD)" Date: Tue, 12 Nov 2024 20:31:54 -0800 Subject: [PATCH 1/2] Skeleton implementation --- .../CohostDocumentCompletionEndpoint.cs | 2 +- ...CohostDocumentCompletionResolveEndpoint.cs | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveEndpoint.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs index 2783ba7b8d9..4d4ed6d3c7b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs @@ -64,7 +64,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie Method = Methods.TextDocumentCompletionName, RegisterOptions = new CompletionRegistrationOptions() { - ResolveProvider = false, // TODO - change to true when Resolve is implemented + ResolveProvider = true, // TODO - change to true when Resolve is implemented TriggerCharacters = CompletionTriggerAndCommitCharacters.AllTriggerCharacters, AllCommitCharacters = CompletionTriggerAndCommitCharacters.AllCommitCharacters } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveEndpoint.cs new file mode 100644 index 00000000000..593312b4dc9 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveEndpoint.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using RoslynVSInternalCompletionItem = Roslyn.LanguageServer.Protocol.VSInternalCompletionItem; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(Methods.TextDocumentCompletionResolveName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostDocumentCompletionResolveEndpoint))] +#pragma warning restore RS0030 // Do not use banned APIs +internal sealed class CohostDocumentCompletionResolveEndpoint : AbstractRazorCohostRequestHandler, IDynamicRegistrationProvider +{ + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) + { + if (clientCapabilities.TextDocument?.Completion?.DynamicRegistration is true) + { + return [new Registration() + { + Method = Methods.TextDocumentCompletionResolveName + }]; + } + + return []; + } + + protected override Task HandleRequestAsync(RoslynVSInternalCompletionItem request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(request); + + private Task HandleRequestAsync(RoslynVSInternalCompletionItem request) + { + return Task.FromResult(request); + } +} From 2058042b79455db0310890ac23df8b090c861e7f Mon Sep 17 00:00:00 2001 From: "Alex Gavrilov (DEV PROD)" Date: Mon, 18 Nov 2024 14:12:31 -0800 Subject: [PATCH 2/2] Making resolve request handler callable in cohosting - Making the resolve completion use document for cohosting - Serializing TextDocument property into Data member of completion items so that Roslyn will forward the request to us - Basic sanity test (shows we are getting called now) --- .../CohostDocumentCompletionEndpoint.cs | 22 ++++++- ...CohostDocumentCompletionResolveEndpoint.cs | 35 +++++++++-- .../CohostDocumentCompletionResolveParams.cs | 52 +++++++++++++++++ ...stDocumentCompletionResolveEndpointTest.cs | 58 +++++++++++++++++++ 4 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveParams.cs create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentCompletionResolveEndpointTest.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs index 4d4ed6d3c7b..edd52b76e7e 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs @@ -25,6 +25,7 @@ using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; using RoslynCompletionParams = Roslyn.LanguageServer.Protocol.CompletionParams; using RoslynLspExtensions = Roslyn.LanguageServer.Protocol.RoslynLspExtensions; +using RoslynTextDocumentIdentifier = Roslyn.LanguageServer.Protocol.TextDocumentIdentifier; namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; @@ -64,7 +65,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie Method = Methods.TextDocumentCompletionName, RegisterOptions = new CompletionRegistrationOptions() { - ResolveProvider = true, // TODO - change to true when Resolve is implemented + ResolveProvider = false, // TODO - change to true when Resolve is implemented TriggerCharacters = CompletionTriggerAndCommitCharacters.AllTriggerCharacters, AllCommitCharacters = CompletionTriggerAndCommitCharacters.AllCommitCharacters } @@ -88,8 +89,11 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie return null; } + // Save as it may be modified if we forward request to HTML language server + var originalTextDocumentIdentifier = request.TextDocument; + // Return immediately if this is auto-shown completion but auto-shown completion is disallowed in settings - var clientSettings = _clientSettingsManager.GetClientSettings(); + var clientSettings = _clientSettingsManager.GetClientSettings(); var autoShownCompletion = completionContext.TriggerKind != CompletionTriggerKind.Invoked; if (autoShownCompletion && !clientSettings.ClientCompletionSettings.AutoShowCompletion) { @@ -187,6 +191,11 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie completionContext.TriggerCharacter); } + if (combinedCompletionList != null) + { + AddResolutionParams(combinedCompletionList, originalTextDocumentIdentifier); + } + return combinedCompletionList; } @@ -273,6 +282,15 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie return completionList; } + private static void AddResolutionParams(VSInternalCompletionList completionList, RoslynTextDocumentIdentifier textDocumentIdentifier) + { + foreach (var item in completionList.Items) + { + var resolutionParams = CohostDocumentCompletionResolveParams.Create(textDocumentIdentifier); + item.Data = JsonSerializer.SerializeToElement(resolutionParams); + } + } + internal TestAccessor GetTestAccessor() => new(this); internal readonly struct TestAccessor(CohostDocumentCompletionEndpoint instance) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveEndpoint.cs index 593312b4dc9..432ca46b3cb 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveEndpoint.cs @@ -17,7 +17,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; [Export(typeof(IDynamicRegistrationProvider))] [ExportCohostStatelessLspService(typeof(CohostDocumentCompletionResolveEndpoint))] #pragma warning restore RS0030 // Do not use banned APIs -internal sealed class CohostDocumentCompletionResolveEndpoint : AbstractRazorCohostRequestHandler, IDynamicRegistrationProvider +internal sealed class CohostDocumentCompletionResolveEndpoint : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider { protected override bool MutatesSolutionState => false; @@ -29,18 +29,45 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie { return [new Registration() { - Method = Methods.TextDocumentCompletionResolveName + Method = Methods.TextDocumentCompletionResolveName, + RegisterOptions = new CompletionRegistrationOptions() + { + ResolveProvider = true + } }]; } return []; } + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(RoslynVSInternalCompletionItem request) + { + var completionResolveParams = CohostDocumentCompletionResolveParams.GetCohostDocumentCompletionResolveParams(request); + return Roslyn.LanguageServer.Protocol.RoslynLspExtensions.ToRazorTextDocumentIdentifier(completionResolveParams.TextDocument); + } + protected override Task HandleRequestAsync(RoslynVSInternalCompletionItem request, RazorCohostRequestContext context, CancellationToken cancellationToken) - => HandleRequestAsync(request); + => HandleRequestAsync(request, cancellationToken); - private Task HandleRequestAsync(RoslynVSInternalCompletionItem request) + private Task HandleRequestAsync(RoslynVSInternalCompletionItem request, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromResult(request); + } + + // TODO: actual request processing code + return Task.FromResult(request); } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostDocumentCompletionResolveEndpoint instance) + { + public Task HandleRequestAsync( + RoslynVSInternalCompletionItem request, + CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, cancellationToken); + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveParams.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveParams.cs new file mode 100644 index 00000000000..f07f7a7a635 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionResolveParams.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Text.Json; +using System; +using System.Text.Json.Serialization; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +// Data that's getting sent with each completion item so that we can provide document ID +// to Roslyn language server which will use the URI to figure out that language of the request +// and forward the request to us. It gets serialized as Data member of the completion item. +// Without it, Roslyn won't forward the completion resolve request to us. +internal sealed class CohostDocumentCompletionResolveParams +{ + // NOTE: Capital T here is required to match Roslyn's DocumentResolveData structure, so that the Roslyn + // language server can correctly route requests to us in cohosting. In future when we normalize + // on to Roslyn types, we should inherit from that class so we don't have to remember to do this. + [JsonPropertyName("TextDocument")] + public required VSTextDocumentIdentifier TextDocument { get; set; } + + public static CohostDocumentCompletionResolveParams Create(TextDocumentIdentifier textDocumentIdentifier) + { + var vsTextDocumentIdentifier = textDocumentIdentifier is VSTextDocumentIdentifier vsTextDocumentIdentifierValue + ? vsTextDocumentIdentifierValue + : new VSTextDocumentIdentifier() { Uri = textDocumentIdentifier.Uri }; + + var resolutionParams = new CohostDocumentCompletionResolveParams() + { + TextDocument = vsTextDocumentIdentifier + }; + + return resolutionParams; + } + + public static CohostDocumentCompletionResolveParams GetCohostDocumentCompletionResolveParams(VSInternalCompletionItem request) + { + if (request.Data is not JsonElement paramsObj) + { + throw new InvalidOperationException($"Invalid Completion Resolve Request Received"); + } + + var resolutionParams = paramsObj.Deserialize(); + if (resolutionParams is null) + { + throw new InvalidOperationException($"request.Data should be convertible to {nameof(CohostDocumentCompletionResolveParams)}"); + } + + return resolutionParams; + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentCompletionResolveEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentCompletionResolveEndpointTest.cs new file mode 100644 index 00000000000..1777ee8715b --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentCompletionResolveEndpointTest.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Xunit; +using Xunit.Abstractions; +using RoslynTextDocumentIdentifier = Roslyn.LanguageServer.Protocol.TextDocumentIdentifier; +using RoslynVSInternalCompletionItem = Roslyn.LanguageServer.Protocol.VSInternalCompletionItem; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostDocumentCompletionResolveEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task ResolveReturnsSelf() + { + await VerifyCompletionItemResolveAsync( + input: """ + This is a Razor document. + +
+ + The end. + """, + initialItemLabel: "TestItem1", + expectedItemLabel: "TestItem1"); + } + + private async Task VerifyCompletionItemResolveAsync( + TestCode input, + string initialItemLabel, + string expectedItemLabel) + { + var document = await CreateProjectAndRazorDocumentAsync(input.Text); + + var endpoint = new CohostDocumentCompletionResolveEndpoint(); + + var textDocumentIdentifier = new RoslynTextDocumentIdentifier() + { + Uri = document.CreateUri() + }; + + var resolutionParams = CohostDocumentCompletionResolveParams.Create(textDocumentIdentifier); + + var request = new RoslynVSInternalCompletionItem() + { + Data = JsonSerializer.SerializeToElement(resolutionParams), + Label = initialItemLabel + }; + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(request, DisposalToken); + + Assert.Equal(result.Label, expectedItemLabel); + } +}