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

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

@ -3,12 +3,13 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text;
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert.RemoteAutoInsertTextEdit?>; using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert.RemoteAutoInsertTextEdit?>;
namespace Microsoft.CodeAnalysis.Razor.Remote;
internal interface IRemoteAutoInsertService internal interface IRemoteAutoInsertService
{ {
ValueTask<Response> GetAutoInsertTextEditAsync( ValueTask<Response> GetAutoInsertTextEditAsync(
@ -16,9 +17,6 @@ internal interface IRemoteAutoInsertService
DocumentId documentId, DocumentId documentId,
LinePosition position, LinePosition position,
string character, string character,
bool autoCloseTags, RemoteAutoInsertOptions options,
bool formatOnType,
bool indentWithTabs,
int indentSize,
CancellationToken cancellationToken); 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.Logging;
using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert; using Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.LanguageServer.Protocol; using Roslyn.LanguageServer.Protocol;
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert.RemoteAutoInsertTextEdit?>; 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; using RoslynInsertTextFormat = Roslyn.LanguageServer.Protocol.InsertTextFormat;
namespace Microsoft.CodeAnalysis.Remote.Razor; namespace Microsoft.CodeAnalysis.Remote.Razor;
@ -42,10 +42,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
DocumentId documentId, DocumentId documentId,
LinePosition linePosition, LinePosition linePosition,
string character, string character,
bool autoCloseTags, RemoteAutoInsertOptions options,
bool formatOnType,
bool indentWithTabs,
int indentSize,
CancellationToken cancellationToken) CancellationToken cancellationToken)
=> RunServiceAsync( => RunServiceAsync(
solutionInfo, solutionInfo,
@ -54,10 +51,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
context, context,
linePosition, linePosition,
character, character,
autoCloseTags, options,
formatOnType,
indentWithTabs,
indentSize,
cancellationToken), cancellationToken),
cancellationToken); cancellationToken);
@ -65,10 +59,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
RemoteDocumentContext remoteDocumentContext, RemoteDocumentContext remoteDocumentContext,
LinePosition linePosition, LinePosition linePosition,
string character, string character,
bool autoCloseTags, RemoteAutoInsertOptions options,
bool formatOnType,
bool indentWithTabs,
int indentSize,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var sourceText = await remoteDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); var sourceText = await remoteDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
@ -86,7 +77,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
codeDocument, codeDocument,
VsLspExtensions.ToPosition(linePosition), VsLspExtensions.ToPosition(linePosition),
character, character,
autoCloseTags, options.EnableAutoClosingTags,
out var insertTextEdit)) out var insertTextEdit))
{ {
return Response.Results(RemoteAutoInsertTextEdit.FromLspInsertTextEdit(insertTextEdit)); return Response.Results(RemoteAutoInsertTextEdit.FromLspInsertTextEdit(insertTextEdit));
@ -110,9 +101,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
remoteDocumentContext, remoteDocumentContext,
mappedPosition, mappedPosition,
character, character,
formatOnType, options,
indentWithTabs,
indentSize,
cancellationToken); cancellationToken);
default: default:
Logger.LogError($"Unsupported language {languageKind} in {nameof(RemoteAutoInsertService)}"); Logger.LogError($"Unsupported language {languageKind} in {nameof(RemoteAutoInsertService)}");
@ -124,9 +113,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
RemoteDocumentContext remoteDocumentContext, RemoteDocumentContext remoteDocumentContext,
LinePosition mappedPosition, LinePosition mappedPosition,
string character, string character,
bool formatOnType, RemoteAutoInsertOptions options,
bool indentWithTabs,
int indentSize,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// Special case for C# where we use AutoInsert for two purposes: // 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 // 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 // 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!) // having to restart VS. Not the worst compromise (hopefully!)
if (!formatOnType) if (!options.FormatOnType)
{ {
return Response.NoFurtherHandling; return Response.NoFurtherHandling;
} }
@ -151,17 +138,12 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
} }
var generatedDocument = await remoteDocumentContext.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); var generatedDocument = await remoteDocumentContext.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false);
var formattingOptions = new RoslynFormattingOptions()
{
InsertSpaces = !indentWithTabs,
TabSize = indentSize
};
var autoInsertResponseItem = await OnAutoInsert.GetOnAutoInsertResponseAsync( var autoInsertResponseItem = await OnAutoInsert.GetOnAutoInsertResponseAsync(
generatedDocument, generatedDocument,
mappedPosition, mappedPosition,
character, character,
formattingOptions, options.FormattingOptions.ToRoslynFormattingOptions(),
cancellationToken cancellationToken
); );
@ -170,11 +152,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args)
return Response.NoFurtherHandling; return Response.NoFurtherHandling;
} }
var razorFormattingOptions = new RazorFormattingOptions() var razorFormattingOptions = options.FormattingOptions;
{
InsertSpaces = !indentWithTabs,
TabSize = indentSize
};
var vsLspTextEdit = VsLspFactory.CreateTextEdit( var vsLspTextEdit = VsLspFactory.CreateTextEdit(
autoInsertResponseItem.TextEdit.Range.ToLinePositionSpan(), autoInsertResponseItem.TextEdit.Range.ToLinePositionSpan(),

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

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