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

Add support for remapping TextEdits & AdditionalTextEdits at resolve time. #6626

Merged
Merged
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 @@ -5,18 +5,27 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
using Microsoft.AspNetCore.Razor.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation
{
internal class DelegatedCompletionItemResolver : CompletionItemResolver
{
private readonly DocumentContextFactory _documentContextFactory;
private readonly RazorFormattingService _formattingService;
private readonly ClientNotifierServiceBase _languageServer;

public DelegatedCompletionItemResolver(ClientNotifierServiceBase languageServer)
public DelegatedCompletionItemResolver(
DocumentContextFactory documentContextFactory,
RazorFormattingService formattingService,
ClientNotifierServiceBase languageServer)
{
_documentContextFactory = documentContextFactory;
_formattingService = formattingService;
_languageServer = languageServer;
}

Expand Down Expand Up @@ -49,6 +58,78 @@ public DelegatedCompletionItemResolver(ClientNotifierServiceBase languageServer)
delegatedParams.ProjectedKind);
var delegatedRequest = await _languageServer.SendRequestAsync(LanguageServerConstants.RazorCompletionResolveEndpointName, delegatedResolveParams).ConfigureAwait(false);
var resolvedCompletionItem = await delegatedRequest.Returning<VSInternalCompletionItem?>(cancellationToken).ConfigureAwait(false);

if (resolvedCompletionItem is not null)
{
resolvedCompletionItem = await PostProcessCompletionItemAsync(resolutionContext, resolvedCompletionItem, cancellationToken).ConfigureAwait(false);
}

return resolvedCompletionItem;
}

private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(
DelegatedCompletionResolutionContext context,
VSInternalCompletionItem resolvedCompletionItem,
CancellationToken cancellationToken)
{
if (context.OriginalRequestParams.ProjectedKind != RazorLanguageKind.CSharp)
{
// We currently don't do any post-processing for non-C# items.
return resolvedCompletionItem;
}

if (!resolvedCompletionItem.VsResolveTextEditOnCommit)
{
// Resolve doesn't typically handle text edit resolution; however, in VS cases it does.
return resolvedCompletionItem;
}

if (resolvedCompletionItem.TextEdit is null && resolvedCompletionItem.AdditionalTextEdits is null)
{
// Only post-processing work we have to do is formatting text edits on resolution.
return resolvedCompletionItem;
}

var hostDocumentUri = context.OriginalRequestParams.HostDocument.Uri;
var documentContext = await _documentContextFactory.TryCreateAsync(hostDocumentUri, cancellationToken).ConfigureAwait(false);
if (documentContext is null)
{
return resolvedCompletionItem;
}

// TODO: Pull active formatting options from client.
var formattingOptions = new FormattingOptions()
{
InsertSpaces = true,
TabSize = 4,
};

if (resolvedCompletionItem.TextEdit is not null)
{
var formattedTextEdit = await _formattingService.FormatSnippetAsync(
hostDocumentUri,
documentContext.Snapshot,
RazorLanguageKind.CSharp,
new[] { resolvedCompletionItem.TextEdit },
formattingOptions,
cancellationToken).ConfigureAwait(false);

resolvedCompletionItem.TextEdit = formattedTextEdit.FirstOrDefault();
}

if (resolvedCompletionItem.AdditionalTextEdits is not null)
{
var formattedTextEdits = await _formattingService.FormatSnippetAsync(
hostDocumentUri,
documentContext.Snapshot,
RazorLanguageKind.CSharp,
resolvedCompletionItem.AdditionalTextEdits,
formattingOptions,
cancellationToken).ConfigureAwait(false);

resolvedCompletionItem.AdditionalTextEdits = formattedTextEdits;
}

return resolvedCompletionItem;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
using Microsoft.AspNetCore.Razor.LanguageServer.Serialization;
using Microsoft.AspNetCore.Razor.LanguageServer.Test.Common;
using Microsoft.CodeAnalysis;
Expand All @@ -35,7 +36,7 @@ public LanguageServerTestBase()
var logger = new Mock<ILogger>(MockBehavior.Strict).Object;
Mock.Get(logger).Setup(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<EventId>(), It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), It.IsAny<Func<It.IsAnyType, Exception?, string>>())).Verifiable();
Mock.Get(logger).Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).Returns(false);
LoggerFactory = Mock.Of<ILoggerFactory>(factory => factory.CreateLogger(It.IsAny<string>()) == logger, MockBehavior.Strict);
LoggerFactory = TestLoggerFactory.Instance;
Serializer = new LspSerializer();
Serializer.RegisterRazorConverters();
Serializer.RegisterVSInternalExtensionConverters();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public override Task<RazorCodeDocument> GetGeneratedOutputAsync()

public override IReadOnlyList<DocumentSnapshot> GetImports()
{
throw new NotImplementedException();
return Array.Empty<DocumentSnapshot>();
}

public override bool TryGetGeneratedOutput(out RazorCodeDocument result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public override DocumentSnapshot GetDocument(string filePath)

public override RazorProjectEngine GetProjectEngine()
{
throw new NotImplementedException();
return RazorProjectEngine.Create(RazorConfiguration.Default, RazorProjectFileSystem.Create("C:/"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@

#nullable disable

using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.AspNetCore.Razor.LanguageServer.Extensions;
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
using Microsoft.AspNetCore.Razor.LanguageServer.Protocol;
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
using Microsoft.AspNetCore.Razor.LanguageServer.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions;
using System.Linq;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using Microsoft.AspNetCore.Razor.Language;
using Xunit.Sdk;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation
Expand All @@ -44,6 +46,9 @@ public DelegatedCompletionItemResolverTest()
var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml");
CSharpCompletionParams = new DelegatedCompletionParams(documentContext.Identifier, new Position(10, 6), RazorLanguageKind.CSharp, new VSInternalCompletionContext(), ProvisionalTextEdit: null);
HtmlCompletionParams = new DelegatedCompletionParams(documentContext.Identifier, new Position(0, 0), RazorLanguageKind.Html, new VSInternalCompletionContext(), ProvisionalTextEdit: null);
DocumentContextFactory = new TestDocumentContextFactory();
FormattingService = TestRazorFormattingService.Instance;
MappingService = new DefaultRazorDocumentMappingService(LoggerFactory);
}

private VSInternalClientCapabilities ClientCapabilities { get; }
Expand All @@ -52,12 +57,18 @@ public DelegatedCompletionItemResolverTest()

private DelegatedCompletionParams HtmlCompletionParams { get; }

private DocumentContextFactory DocumentContextFactory { get; }

private RazorFormattingService FormattingService { get; }

private RazorDocumentMappingService MappingService { get; }

[Fact]
public async Task ResolveAsync_CanNotFindCompletionItem_Noops()
{
// Arrange
var server = TestDelegatedCompletionItemResolverServer.Create();
var resolver = new DelegatedCompletionItemResolver(server);
var resolver = new DelegatedCompletionItemResolver(DocumentContextFactory, FormattingService, server);
var item = new VSInternalCompletionItem();
var notContainingCompletionList = new VSInternalCompletionList();
var originalRequestContext = new object();
Expand All @@ -74,7 +85,7 @@ public async Task ResolveAsync_UnknownRequestContext_Noops()
{
// Arrange
var server = TestDelegatedCompletionItemResolverServer.Create();
var resolver = new DelegatedCompletionItemResolver(server);
var resolver = new DelegatedCompletionItemResolver(DocumentContextFactory, FormattingService, server);
var item = new VSInternalCompletionItem();
var containingCompletionList = new VSInternalCompletionList() { Items = new[] { item, } };
var originalRequestContext = new object();
Expand All @@ -91,7 +102,7 @@ public async Task ResolveAsync_UsesItemsData()
{
// Arrange
var server = TestDelegatedCompletionItemResolverServer.Create();
var resolver = new DelegatedCompletionItemResolver(server);
var resolver = new DelegatedCompletionItemResolver(DocumentContextFactory, FormattingService, server);
var expectedData = new object();
var item = new VSInternalCompletionItem()
{
Expand All @@ -112,7 +123,7 @@ public async Task ResolveAsync_InheritsOriginalCompletionListData()
{
// Arrange
var server = TestDelegatedCompletionItemResolverServer.Create();
var resolver = new DelegatedCompletionItemResolver(server);
var resolver = new DelegatedCompletionItemResolver(DocumentContextFactory, FormattingService, server);
var item = new VSInternalCompletionItem();
var containingCompletionList = new VSInternalCompletionList() { Items = new[] { item, }, Data = new object() };
var expectedData = new object();
Expand All @@ -135,13 +146,47 @@ public async Task ResolveAsync_CSharp_Resolves()
Assert.NotNull(resolvedItem.Description);
}

[Fact]
public async Task ResolveAsync_CSharp_RemapAndFormatsTextEdit()
{
// Arrange
var input =
"""
@{
Task FooAsync()
{
awai$$
}
}
""";
TestFileMarkupParser.GetPosition(input, out var documentContent, out _);
var originalSourceText = SourceText.From(documentContent);
var expectedSourceText = SourceText.From(
"""
@{
async Task FooAsync()
{
await
}
}
""");

// Act
var resolvedItem = await ResolveCompletionItemAsync(input, itemToResolve: "await", CancellationToken.None).ConfigureAwait(false);

// Assert
var textChange = resolvedItem.TextEdit.AsTextChange(originalSourceText);
var actualSourceText = originalSourceText.WithChanges(textChange);
Assert.True(expectedSourceText.ContentEquals(actualSourceText));
}

[Fact]
public async Task ResolveAsync_Html_Resolves()
{
// Arrange
var expectedResolvedItem = new VSInternalCompletionItem();
var server = TestDelegatedCompletionItemResolverServer.Create(expectedResolvedItem);
var resolver = new DelegatedCompletionItemResolver(server);
var resolver = new DelegatedCompletionItemResolver(DocumentContextFactory, FormattingService, server);
var item = new VSInternalCompletionItem();
var containingCompletionList = new VSInternalCompletionList() { Items = new[] { item, } };
var originalRequestContext = new DelegatedCompletionResolutionContext(HtmlCompletionParams, new object());
Expand All @@ -155,14 +200,15 @@ public async Task ResolveAsync_Html_Resolves()
Assert.Same(expectedResolvedItem, resolvedItem);
}

private async Task<VSInternalCompletionItem> ResolveCompletionItemAsync(string content, string itemToResolve, CancellationToken none)
private async Task<VSInternalCompletionItem> ResolveCompletionItemAsync(string content, string itemToResolve, CancellationToken cancellationToken)
{
TestFileMarkupParser.GetPosition(content, out var documentContent, out var cursorPosition);
var codeDocument = CreateCodeDocument(documentContent);
await using var csharpServer = await CreateCSharpServerAsync(codeDocument).ConfigureAwait(false);

var server = TestDelegatedCompletionItemResolverServer.Create(csharpServer);
var resolver = new DelegatedCompletionItemResolver(server);
var documentContextFactory = new TestDocumentContextFactory("C:/path/to/file.razor", codeDocument);
var resolver = new DelegatedCompletionItemResolver(documentContextFactory, FormattingService, server);
var (containingCompletionList, csharpCompletionParams) = await GetCompletionListAndOriginalParamsAsync(cursorPosition, codeDocument, csharpServer).ConfigureAwait(false);

var originalRequestContext = new DelegatedCompletionResolutionContext(csharpCompletionParams, containingCompletionList.Data);
Expand All @@ -173,7 +219,7 @@ private async Task<VSInternalCompletionItem> ResolveCompletionItemAsync(string c
throw new XunitException($"Could not locate completion item '{item.Label}' for completion resolve test");
}

var resolvedItem = await resolver.ResolveAsync(item, containingCompletionList, originalRequestContext, ClientCapabilities, CancellationToken.None).ConfigureAwait(false);
var resolvedItem = await resolver.ResolveAsync(item, containingCompletionList, originalRequestContext, ClientCapabilities, cancellationToken).ConfigureAwait(false);
return resolvedItem;
}

Expand Down Expand Up @@ -216,7 +262,7 @@ internal class TestDelegatedCompletionItemResolverServer : TestOmnisharpLanguage

private TestDelegatedCompletionItemResolverServer(CompletionResolveRequestResponseFactory requestHandler) : base(new Dictionary<string, Func<object, Task<object>>>()
{
[LanguageServerConstants.RazorCompletionResolveEndpointName] = requestHandler.OnDelegationAsync,
[LanguageServerConstants.RazorCompletionResolveEndpointName] = requestHandler.OnCompletionResolveDelegationAsync,
})
{
_requestHandler = requestHandler;
Expand Down Expand Up @@ -251,7 +297,7 @@ public StaticCompletionResolveRequestHandler(VSInternalCompletionItem resolveRes

public override DelegatedCompletionItemResolveParams DelegatedParams => _delegatedParams;

public override Task<object> OnDelegationAsync(object parameters)
public override Task<object> OnCompletionResolveDelegationAsync(object parameters)
{
var resolveParams = (DelegatedCompletionItemResolveParams)parameters;
_delegatedParams = resolveParams;
Expand All @@ -272,11 +318,11 @@ public DelegatedCSharpCompletionRequestHandler(CSharpTestLspServer csharpServer)

public override DelegatedCompletionItemResolveParams DelegatedParams => _delegatedParams;

public override async Task<object> OnDelegationAsync(object parameters)
public override async Task<object> OnCompletionResolveDelegationAsync(object parameters)
{
var resolveParams = (DelegatedCompletionItemResolveParams)parameters;
_delegatedParams = resolveParams;

var resolvedCompletionItem = await _csharpServer.ExecuteRequestAsync<VSInternalCompletionItem, VSInternalCompletionItem>(
Methods.TextDocumentCompletionResolveName,
_delegatedParams.CompletionItem,
Expand All @@ -290,7 +336,7 @@ private abstract class CompletionResolveRequestResponseFactory
{
public abstract DelegatedCompletionItemResolveParams DelegatedParams { get; }

public abstract Task<object> OnDelegationAsync(object parameters);
public abstract Task<object> OnCompletionResolveDelegationAsync(object parameters);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal static IOptionsMonitor<RazorLSPOptions> GetOptionsMonitor(bool enableFo
return monitor.Object;
}

internal class TestRazorFormattingService : RazorFormattingService
internal class DummyRazorFormattingService : RazorFormattingService
{
public bool Called { get; private set; }

Expand Down
Loading