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

Completion resolve endpoint skeleton #11226

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.VisualStudio.LanguageServer.Protocol.VSInternalCompletionList?>;
using RoslynCompletionParams = Roslyn.LanguageServer.Protocol.CompletionParams;
using RoslynLspExtensions = Roslyn.LanguageServer.Protocol.RoslynLspExtensions;
using RoslynTextDocumentIdentifier = Roslyn.LanguageServer.Protocol.TextDocumentIdentifier;

namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;

Expand Down Expand Up @@ -88,8 +89,11 @@ public ImmutableArray<Registration> 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)
{
Expand Down Expand Up @@ -187,6 +191,11 @@ public ImmutableArray<Registration> GetRegistrations(VSInternalClientCapabilitie
completionContext.TriggerCharacter);
}

if (combinedCompletionList != null)
{
AddResolutionParams(combinedCompletionList, originalTextDocumentIdentifier);
}

return combinedCompletionList;
}

Expand Down Expand Up @@ -273,6 +282,15 @@ public ImmutableArray<Registration> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 : AbstractRazorCohostDocumentRequestHandler<RoslynVSInternalCompletionItem, RoslynVSInternalCompletionItem>, IDynamicRegistrationProvider
{
protected override bool MutatesSolutionState => false;

protected override bool RequiresLSPSolution => true;

public ImmutableArray<Registration> GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext)
{
if (clientCapabilities.TextDocument?.Completion?.DynamicRegistration is true)
{
return [new Registration()
{
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<RoslynVSInternalCompletionItem> HandleRequestAsync(RoslynVSInternalCompletionItem request, RazorCohostRequestContext context, CancellationToken cancellationToken)
=> HandleRequestAsync(request, cancellationToken);

private Task<RoslynVSInternalCompletionItem> 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<RoslynVSInternalCompletionItem> HandleRequestAsync(
RoslynVSInternalCompletionItem request,
CancellationToken cancellationToken)
=> instance.HandleRequestAsync(request, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -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<CohostDocumentCompletionResolveParams>();
if (resolutionParams is null)
{
throw new InvalidOperationException($"request.Data should be convertible to {nameof(CohostDocumentCompletionResolveParams)}");
}

return resolutionParams;
}
}
Original file line number Diff line number Diff line change
@@ -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.

<div st$$></div>

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);
}
}