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)
This commit is contained in:
Alex Gavrilov (DEV PROD) 2024-11-18 14:12:31 -08:00
Родитель 87e93c7f8a
Коммит 2058042b79
4 изменённых файлов: 163 добавлений и 8 удалений

Просмотреть файл

@ -25,6 +25,7 @@ using Microsoft.VisualStudio.Razor.Snippets;
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.VisualStudio.LanguageServer.Protocol.VSInternalCompletionList?>; using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.VisualStudio.LanguageServer.Protocol.VSInternalCompletionList?>;
using RoslynCompletionParams = Roslyn.LanguageServer.Protocol.CompletionParams; using RoslynCompletionParams = Roslyn.LanguageServer.Protocol.CompletionParams;
using RoslynLspExtensions = Roslyn.LanguageServer.Protocol.RoslynLspExtensions; using RoslynLspExtensions = Roslyn.LanguageServer.Protocol.RoslynLspExtensions;
using RoslynTextDocumentIdentifier = Roslyn.LanguageServer.Protocol.TextDocumentIdentifier;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
@ -64,7 +65,7 @@ internal sealed class CohostDocumentCompletionEndpoint(
Method = Methods.TextDocumentCompletionName, Method = Methods.TextDocumentCompletionName,
RegisterOptions = new CompletionRegistrationOptions() 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, TriggerCharacters = CompletionTriggerAndCommitCharacters.AllTriggerCharacters,
AllCommitCharacters = CompletionTriggerAndCommitCharacters.AllCommitCharacters AllCommitCharacters = CompletionTriggerAndCommitCharacters.AllCommitCharacters
} }
@ -88,8 +89,11 @@ internal sealed class CohostDocumentCompletionEndpoint(
return null; 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 // 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; var autoShownCompletion = completionContext.TriggerKind != CompletionTriggerKind.Invoked;
if (autoShownCompletion && !clientSettings.ClientCompletionSettings.AutoShowCompletion) if (autoShownCompletion && !clientSettings.ClientCompletionSettings.AutoShowCompletion)
{ {
@ -187,6 +191,11 @@ internal sealed class CohostDocumentCompletionEndpoint(
completionContext.TriggerCharacter); completionContext.TriggerCharacter);
} }
if (combinedCompletionList != null)
{
AddResolutionParams(combinedCompletionList, originalTextDocumentIdentifier);
}
return combinedCompletionList; return combinedCompletionList;
} }
@ -273,6 +282,15 @@ internal sealed class CohostDocumentCompletionEndpoint(
return completionList; 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 TestAccessor GetTestAccessor() => new(this);
internal readonly struct TestAccessor(CohostDocumentCompletionEndpoint instance) internal readonly struct TestAccessor(CohostDocumentCompletionEndpoint instance)

Просмотреть файл

@ -17,7 +17,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
[Export(typeof(IDynamicRegistrationProvider))] [Export(typeof(IDynamicRegistrationProvider))]
[ExportCohostStatelessLspService(typeof(CohostDocumentCompletionResolveEndpoint))] [ExportCohostStatelessLspService(typeof(CohostDocumentCompletionResolveEndpoint))]
#pragma warning restore RS0030 // Do not use banned APIs #pragma warning restore RS0030 // Do not use banned APIs
internal sealed class CohostDocumentCompletionResolveEndpoint : AbstractRazorCohostRequestHandler<RoslynVSInternalCompletionItem, RoslynVSInternalCompletionItem>, IDynamicRegistrationProvider internal sealed class CohostDocumentCompletionResolveEndpoint : AbstractRazorCohostDocumentRequestHandler<RoslynVSInternalCompletionItem, RoslynVSInternalCompletionItem>, IDynamicRegistrationProvider
{ {
protected override bool MutatesSolutionState => false; protected override bool MutatesSolutionState => false;
@ -29,18 +29,45 @@ internal sealed class CohostDocumentCompletionResolveEndpoint : AbstractRazorCoh
{ {
return [new Registration() return [new Registration()
{ {
Method = Methods.TextDocumentCompletionResolveName Method = Methods.TextDocumentCompletionResolveName,
RegisterOptions = new CompletionRegistrationOptions()
{
ResolveProvider = true
}
}]; }];
} }
return []; return [];
} }
protected override Task<RoslynVSInternalCompletionItem> HandleRequestAsync(RoslynVSInternalCompletionItem request, RazorCohostRequestContext context, CancellationToken cancellationToken) protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(RoslynVSInternalCompletionItem request)
=> HandleRequestAsync(request);
private Task<RoslynVSInternalCompletionItem> HandleRequestAsync(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); 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);
}
} }

Просмотреть файл

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

Просмотреть файл

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