diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs index a0397ec80f5..41e1e855a55 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.VisualStudio.LanguageServer.Protocol; +using RoslynFormattingOptions = Roslyn.LanguageServer.Protocol.FormattingOptions; namespace Microsoft.CodeAnalysis.Razor.Formatting; @@ -33,4 +34,11 @@ public RazorIndentationOptions ToIndentationOptions() UseTabs: !InsertSpaces, TabSize: TabSize, IndentationSize: TabSize); + + public RoslynFormattingOptions ToRoslynFormattingOptions() + => new RoslynFormattingOptions() + { + InsertSpaces = InsertSpaces, + TabSize = TabSize + }; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteAutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteAutoInsertService.cs index 3872965c0f7..5130487c88f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteAutoInsertService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteAutoInsertService.cs @@ -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; +namespace Microsoft.CodeAnalysis.Razor.Remote; + internal interface IRemoteAutoInsertService { ValueTask GetAutoInsertTextEditAsync( @@ -16,9 +17,6 @@ ValueTask GetAutoInsertTextEditAsync( DocumentId documentId, LinePosition position, string character, - bool autoCloseTags, - bool formatOnType, - bool indentWithTabs, - int indentSize, + RemoteAutoInsertOptions options, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteAutoInsertOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteAutoInsertOptions.cs new file mode 100644 index 00000000000..9281506849c --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteAutoInsertOptions.cs @@ -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) + }; +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs index 9793c86b68e..b3d7d0d23f6 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs @@ -11,13 +11,13 @@ 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; -using RoslynFormattingOptions = Roslyn.LanguageServer.Protocol.FormattingOptions; using RoslynInsertTextFormat = Roslyn.LanguageServer.Protocol.InsertTextFormat; namespace Microsoft.CodeAnalysis.Remote.Razor; @@ -42,10 +42,7 @@ public ValueTask GetAutoInsertTextEditAsync( DocumentId documentId, LinePosition linePosition, string character, - bool autoCloseTags, - bool formatOnType, - bool indentWithTabs, - int indentSize, + RemoteAutoInsertOptions options, CancellationToken cancellationToken) => RunServiceAsync( solutionInfo, @@ -54,10 +51,7 @@ public ValueTask GetAutoInsertTextEditAsync( context, linePosition, character, - autoCloseTags, - formatOnType, - indentWithTabs, - indentSize, + options, cancellationToken), cancellationToken); @@ -65,10 +59,7 @@ private async ValueTask TryResolveInsertionAsync( 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 @@ private async ValueTask TryResolveInsertionAsync( codeDocument, VsLspExtensions.ToPosition(linePosition), character, - autoCloseTags, + options.EnableAutoClosingTags, out var insertTextEdit)) { return Response.Results(RemoteAutoInsertTextEdit.FromLspInsertTextEdit(insertTextEdit)); @@ -110,9 +101,7 @@ private async ValueTask TryResolveInsertionAsync( remoteDocumentContext, mappedPosition, character, - formatOnType, - indentWithTabs, - indentSize, + options, cancellationToken); default: Logger.LogError($"Unsupported language {languageKind} in {nameof(RemoteAutoInsertService)}"); @@ -124,9 +113,7 @@ private async ValueTask TryResolveInsertionInCSharpAsync( 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 @@ private async ValueTask TryResolveInsertionInCSharpAsync( // 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 @@ private async ValueTask TryResolveInsertionInCSharpAsync( } 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 @@ private async ValueTask TryResolveInsertionInCSharpAsync( return Response.NoFurtherHandling; } - var razorFormattingOptions = new RazorFormattingOptions() - { - InsertSpaces = !indentWithTabs, - TabSize = indentSize - }; + var razorFormattingOptions = options.FormattingOptions; var vsLspTextEdit = VsLspFactory.CreateTextEdit( autoInsertResponseItem.TextEdit.Range.ToLinePositionSpan(), diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs index 1b90bd110ac..db0d7db8cd0 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs @@ -85,17 +85,15 @@ private static ImmutableArray CalculateTriggerChars(IEnumerable request.TextDocument.ToRazorTextDocumentIdentifier(); - protected override async Task HandleRequestAsync(VSInternalDocumentOnAutoInsertParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) - { - var razorDocument = context.TextDocument.AssumeNotNull(); + protected override Task HandleRequestAsync(VSInternalDocumentOnAutoInsertParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken); + private async Task 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( @@ -106,10 +104,7 @@ private static ImmutableArray CalculateTriggerChars(IEnumerable CalculateTriggerChars(IEnumerable new(this); + + internal readonly struct TestAccessor(CohostOnAutoInsertEndpoint instance) + { + public Task HandleRequestAsync( + VSInternalDocumentOnAutoInsertParams request, + TextDocument razorDocument, + CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, razorDocument, cancellationToken); + } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnAutoInsertEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnAutoInsertEndpointTest.cs new file mode 100644 index 00000000000..b7dbb70194a --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnAutoInsertEndpointTest.cs @@ -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 + + 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. + + + + The end. + """, + output: $""" + This is a Razor document. + + + + The end. + """, + triggerCharacter: "=", + delegatedResponseText: "\"$0\""); + } + + [Fact] + public async Task CSharp_OnForwardSlash() + { + await VerifyOnAutoInsertAsync( + input: """ + @code { + ///$$ + void TestMethod() {} + } + """, + output: """ + @code { + /// + /// $0 + /// + 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()); + } +}