* OnAutoInsert Cohosting Tests

* Fixing C# case (and correcting others)

All text should already be in the document/buffer when OnAutoInsert is being executed. Tigger character is not being added to the buffer, it should already be in the buffer.

* PR feedback

Switching to applying edit instead of verifying edit contents and range. Switching from Theories to separate Facts where input was complex. Other misc cleanup.

* Fixing options source and adding options tests

* Tests for all options

* Switching to use TestCode class

* Create options object for cohost OnAutoInsert to combined individual options passed to the remove service.

* More PR feedback

* Switching to nested RazorFormattingOptions
This commit is contained in:
Alex Gavrilov 2024-09-06 15:31:13 -07:00 коммит произвёл GitHub
Родитель 07e138222c
Коммит e9cd0b2e2e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
6 изменённых файлов: 341 добавлений и 48 удалений

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

@ -4,6 +4,7 @@
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using RoslynFormattingOptions = Roslyn.LanguageServer.Protocol.FormattingOptions;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
@ -36,4 +37,11 @@ internal readonly record struct RazorFormattingOptions
UseTabs: !InsertSpaces,
TabSize: TabSize,
IndentationSize: TabSize);
public RoslynFormattingOptions ToRoslynFormattingOptions()
=> new RoslynFormattingOptions()
{
InsertSpaces = InsertSpaces,
TabSize = TabSize
};
}

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

@ -3,12 +3,13 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Text;
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert.RemoteAutoInsertTextEdit?>;
namespace Microsoft.CodeAnalysis.Razor.Remote;
internal interface IRemoteAutoInsertService
{
ValueTask<Response> GetAutoInsertTextEditAsync(
@ -16,9 +17,6 @@ internal interface IRemoteAutoInsertService
DocumentId documentId,
LinePosition position,
string character,
bool autoCloseTags,
bool formatOnType,
bool indentWithTabs,
int indentSize,
RemoteAutoInsertOptions options,
CancellationToken cancellationToken);
}

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

@ -0,0 +1,35 @@
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.Settings;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.Razor.Remote;
[DataContract]
internal readonly record struct RemoteAutoInsertOptions
{
[DataMember(Order = 0)]
public bool EnableAutoClosingTags { get; init; } = true;
[DataMember(Order = 1)]
public bool FormatOnType { get; init; } = true;
[DataMember(Order = 2)]
public RazorFormattingOptions FormattingOptions { get; init; } = new RazorFormattingOptions()
{
InsertSpaces = true,
TabSize = 4
};
public RemoteAutoInsertOptions()
{
}
public static RemoteAutoInsertOptions From(ClientSettings clientSettings, FormattingOptions formattingOptions)
=> new()
{
EnableAutoClosingTags = clientSettings.AdvancedSettings.AutoClosingTags,
FormatOnType = clientSettings.AdvancedSettings.FormatOnType,
FormattingOptions = RazorFormattingOptions.From(formattingOptions, codeBlockBraceOnNextLine: false)
};
}

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

@ -11,13 +11,13 @@ using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.LanguageServer.Protocol;
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert.RemoteAutoInsertTextEdit?>;
using RoslynFormattingOptions = Roslyn.LanguageServer.Protocol.FormattingOptions;
using RoslynInsertTextFormat = Roslyn.LanguageServer.Protocol.InsertTextFormat;
namespace Microsoft.CodeAnalysis.Remote.Razor;
@ -42,10 +42,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
DocumentId documentId,
LinePosition linePosition,
string character,
bool autoCloseTags,
bool formatOnType,
bool indentWithTabs,
int indentSize,
RemoteAutoInsertOptions options,
CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
@ -54,10 +51,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
context,
linePosition,
character,
autoCloseTags,
formatOnType,
indentWithTabs,
indentSize,
options,
cancellationToken),
cancellationToken);
@ -65,10 +59,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
RemoteDocumentContext remoteDocumentContext,
LinePosition linePosition,
string character,
bool autoCloseTags,
bool formatOnType,
bool indentWithTabs,
int indentSize,
RemoteAutoInsertOptions options,
CancellationToken cancellationToken)
{
var sourceText = await remoteDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
@ -86,7 +77,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
codeDocument,
VsLspExtensions.ToPosition(linePosition),
character,
autoCloseTags,
options.EnableAutoClosingTags,
out var insertTextEdit))
{
return Response.Results(RemoteAutoInsertTextEdit.FromLspInsertTextEdit(insertTextEdit));
@ -110,9 +101,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
remoteDocumentContext,
mappedPosition,
character,
formatOnType,
indentWithTabs,
indentSize,
options,
cancellationToken);
default:
Logger.LogError($"Unsupported language {languageKind} in {nameof(RemoteAutoInsertService)}");
@ -124,9 +113,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
RemoteDocumentContext remoteDocumentContext,
LinePosition mappedPosition,
string character,
bool formatOnType,
bool indentWithTabs,
int indentSize,
RemoteAutoInsertOptions options,
CancellationToken cancellationToken)
{
// Special case for C# where we use AutoInsert for two purposes:
@ -140,7 +127,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
// Therefore we are just going to no-op if the user has turned off on type formatting. Maybe one day we can make this
// smarter, but at least the user can always turn the setting back on, type their "///", and turn it back off, without
// having to restart VS. Not the worst compromise (hopefully!)
if (!formatOnType)
if (!options.FormatOnType)
{
return Response.NoFurtherHandling;
}
@ -151,17 +138,12 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
}
var generatedDocument = await remoteDocumentContext.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false);
var formattingOptions = new RoslynFormattingOptions()
{
InsertSpaces = !indentWithTabs,
TabSize = indentSize
};
var autoInsertResponseItem = await OnAutoInsert.GetOnAutoInsertResponseAsync(
generatedDocument,
mappedPosition,
character,
formattingOptions,
options.FormattingOptions.ToRoslynFormattingOptions(),
cancellationToken
);
@ -170,11 +152,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
return Response.NoFurtherHandling;
}
var razorFormattingOptions = new RazorFormattingOptions()
{
InsertSpaces = !indentWithTabs,
TabSize = indentSize
};
var razorFormattingOptions = options.FormattingOptions;
var vsLspTextEdit = VsLspFactory.CreateTextEdit(
autoInsertResponseItem.TextEdit.Range.ToLinePositionSpan(),

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

@ -85,17 +85,15 @@ internal class CohostOnAutoInsertEndpoint(
protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSInternalDocumentOnAutoInsertParams request)
=> request.TextDocument.ToRazorTextDocumentIdentifier();
protected override async Task<VSInternalDocumentOnAutoInsertResponseItem?> HandleRequestAsync(VSInternalDocumentOnAutoInsertParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
{
var razorDocument = context.TextDocument.AssumeNotNull();
protected override Task<VSInternalDocumentOnAutoInsertResponseItem?> HandleRequestAsync(VSInternalDocumentOnAutoInsertParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
=> HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken);
private async Task<VSInternalDocumentOnAutoInsertResponseItem?> HandleRequestAsync(VSInternalDocumentOnAutoInsertParams request, TextDocument razorDocument, CancellationToken cancellationToken)
{
_logger.LogDebug($"Resolving auto-insertion for {razorDocument.FilePath}");
var clientSettings = _clientSettingsManager.GetClientSettings();
var enableAutoClosingTags = clientSettings.AdvancedSettings.AutoClosingTags;
var formatOnType = clientSettings.AdvancedSettings.FormatOnType;
var indentWithTabs = clientSettings.ClientSpaceSettings.IndentWithTabs;
var indentSize = clientSettings.ClientSpaceSettings.IndentSize;
var autoInsertOptions = RemoteAutoInsertOptions.From(clientSettings, request.Options);
_logger.LogDebug($"Calling OOP to resolve insertion at {request.Position} invoked by typing '{request.Character}'");
var data = await _remoteServiceInvoker.TryInvokeAsync<IRemoteAutoInsertService, Response>(
@ -106,10 +104,7 @@ internal class CohostOnAutoInsertEndpoint(
razorDocument.Id,
request.Position.ToLinePosition(),
request.Character,
enableAutoClosingTags,
formatOnType,
indentWithTabs,
indentSize,
autoInsertOptions,
cancellationToken),
cancellationToken).ConfigureAwait(false);
@ -173,4 +168,15 @@ internal class CohostOnAutoInsertEndpoint(
return result.Response;
}
internal TestAccessor GetTestAccessor() => new(this);
internal readonly struct TestAccessor(CohostOnAutoInsertEndpoint instance)
{
public Task<VSInternalDocumentOnAutoInsertResponseItem?> HandleRequestAsync(
VSInternalDocumentOnAutoInsertParams request,
TextDocument razorDocument,
CancellationToken cancellationToken)
=> instance.HandleRequestAsync(request, razorDocument, cancellationToken);
}
}

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

@ -0,0 +1,268 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodBeAnalysis.Remote.Razor.AutoInsert;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.AutoInsert;
using Microsoft.CodeAnalysis.Razor.Settings;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServices.Razor.LanguageClient.Cohost;
using Microsoft.VisualStudio.Razor.Settings;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
public class CohostOnAutoInsertEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
{
[Theory]
[InlineData("PageTitle")]
[InlineData("div")]
[InlineData("text")]
public async Task EndTag(string startTag)
{
await VerifyOnAutoInsertAsync(
input: $"""
This is a Razor document.
<{startTag}>$$
The end.
""",
output: $"""
This is a Razor document.
<{startTag}>$0</{startTag}>
The end.
""",
triggerCharacter: ">");
}
[Theory]
[InlineData("PageTitle")]
[InlineData("div")]
[InlineData("text")]
public async Task DoNotAutoInsertEndTag_DisabledAutoClosingTags(string startTag)
{
await VerifyOnAutoInsertAsync(
input: $"""
This is a Razor document.
<{startTag}>$$
The end.
""",
output: null,
triggerCharacter: ">",
autoClosingTags: false);
}
[Fact]
public async Task AttributeQuotes()
{
await VerifyOnAutoInsertAsync(
input: $"""
This is a Razor document.
<PageTitle style=$$></PageTitle>
The end.
""",
output: $"""
This is a Razor document.
<PageTitle style="$0"></PageTitle>
The end.
""",
triggerCharacter: "=",
delegatedResponseText: "\"$0\"");
}
[Fact]
public async Task CSharp_OnForwardSlash()
{
await VerifyOnAutoInsertAsync(
input: """
@code {
///$$
void TestMethod() {}
}
""",
output: """
@code {
/// <summary>
/// $0
/// </summary>
void TestMethod() {}
}
""",
triggerCharacter: "/");
}
[Fact]
public async Task DoNotAutoInsertCSharp_OnForwardSlashWithFormatOnTypeDisabled()
{
await VerifyOnAutoInsertAsync(
input: """
@code {
///$$
void TestMethod() {}
}
""",
output: null,
triggerCharacter: "/",
formatOnType: false);
}
[Fact]
public async Task CSharp_OnEnter()
{
await VerifyOnAutoInsertAsync(
input: """
@code {
void TestMethod() {
$$}
}
""",
output: """
@code {
void TestMethod()
{
$0
}
}
""",
triggerCharacter: "\n");
}
[Fact]
public async Task CSharp_OnEnter_TwoSpaceIndent()
{
await VerifyOnAutoInsertAsync(
input: """
@code {
void TestMethod() {
$$}
}
""",
output: """
@code {
void TestMethod()
{
$0
}
}
""",
triggerCharacter: "\n",
tabSize: 2);
}
[Fact]
public async Task CSharp_OnEnter_UseTabs()
{
const char tab = '\t';
await VerifyOnAutoInsertAsync(
input: """
@code {
void TestMethod() {
$$}
}
""",
output: $$"""
@code {
{{tab}}void TestMethod()
{{tab}}{
{{tab}}{{tab}}$0
{{tab}}}
}
""",
triggerCharacter: "\n",
insertSpaces: false);
}
private async Task VerifyOnAutoInsertAsync(
TestCode input,
string? output,
string triggerCharacter,
string? delegatedResponseText = null,
bool insertSpaces = true,
int tabSize = 4,
bool formatOnType = true,
bool autoClosingTags = true)
{
var document = CreateProjectAndRazorDocument(input.Text);
var sourceText = await document.GetTextAsync(DisposalToken);
var clientSettingsManager = new ClientSettingsManager([], null, null);
clientSettingsManager.Update(ClientAdvancedSettings.Default with { FormatOnType = formatOnType, AutoClosingTags = autoClosingTags });
IOnAutoInsertTriggerCharacterProvider[] onAutoInsertTriggerCharacterProviders = [
new RemoteAutoClosingTagOnAutoInsertProvider(),
new RemoteCloseTextTagOnAutoInsertProvider()];
VSInternalDocumentOnAutoInsertResponseItem? response = null;
if (delegatedResponseText is not null)
{
var start = sourceText.GetPosition(input.Position);
var end = start;
response = new VSInternalDocumentOnAutoInsertResponseItem()
{
TextEdit = new TextEdit() { NewText = delegatedResponseText, Range = new() { Start = start, End = end } },
TextEditFormat = InsertTextFormat.Snippet
};
}
var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.OnAutoInsertName, response)]);
var endpoint = new CohostOnAutoInsertEndpoint(
RemoteServiceInvoker,
clientSettingsManager,
onAutoInsertTriggerCharacterProviders,
TestHtmlDocumentSynchronizer.Instance,
requestInvoker,
LoggerFactory);
var formattingOptions = new FormattingOptions()
{
InsertSpaces = insertSpaces,
TabSize = tabSize
};
var request = new VSInternalDocumentOnAutoInsertParams()
{
TextDocument = new TextDocumentIdentifier()
{
Uri = document.CreateUri()
},
Position = sourceText.GetPosition(input.Position),
Character = triggerCharacter,
Options = formattingOptions
};
var result = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken);
if (output is not null)
{
Assert.NotNull(result);
}
else
{
Assert.Null(result);
return;
}
if (result is not null)
{
var change = sourceText.GetTextChange(result.TextEdit);
sourceText = sourceText.WithChanges(change);
}
AssertEx.EqualOrDiff(output, sourceText.ToString());
}
}