Skip to content

Commit

Permalink
Add support for remapping TextEdits & AdditionalTextEdits at resolve …
Browse files Browse the repository at this point in the history
…time.

- Migrated our old `CompletionResolutionHandler` logic for post-processing C# completion items to our new single server completion system.
    - One current gap is that the old system used to lookup active formatting options on the client to understand if snippets should be formatted with/without tabs etc. For now I'm using defaults but in a follow up PR i'll light up the real formatting options acquisition logic.
- As part of this PR there were several pieces of code that could be re-used so I refactored them out. The `TestRazorFormattingService` is a prime example (especially now that completion resolve depends on it).
    - Did a few updates to the API so it was clear what type of formatting service you'd be getting (aka should it be HTML enabled?).
- Added a test that validates that we get and remap text edit completions properly (I use `await`). I couldn't find a corresponding C# completion item that utilizes `AdditionalTextEdit`s.

## Enables

![gif of await and override completion working](https://i.imgur.com/EAwqM3j.gif)

Most of #6618
  • Loading branch information
NTaylorMullen committed Jul 22, 2022
1 parent c485c34 commit 5e5db5f
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 60 deletions.
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

0 comments on commit 5e5db5f

Please sign in to comment.