diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 6d9d80ecd5e..b2476eab94a 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -28,5 +28,6 @@ + diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/CloseTextTagOnAutoInsertProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/CloseTextTagOnAutoInsertProvider.cs deleted file mode 100644 index f6ff6ff95e1..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/CloseTextTagOnAutoInsertProvider.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Legacy; -using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -using Microsoft.CodeAnalysis.Razor.Formatting; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.AutoInsert; - -internal sealed class CloseTextTagOnAutoInsertProvider(RazorLSPOptionsMonitor optionsMonitor) : IOnAutoInsertProvider -{ - private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; - - public string TriggerCharacter => ">"; - - public bool TryResolveInsertion(Position position, FormattingContext context, [NotNullWhen(true)] out TextEdit? edit, out InsertTextFormat format) - { - if (!_optionsMonitor.CurrentValue.AutoClosingTags) - { - // We currently only support auto-closing tags our onType formatter. - format = default; - edit = default; - return false; - } - - if (!IsAtTextTag(context, position)) - { - format = default; - edit = default; - return false; - } - - // This is a text tag. - format = InsertTextFormat.Snippet; - edit = VsLspFactory.CreateTextEdit(position, $"$0"); - - return true; - } - - private static bool IsAtTextTag(FormattingContext context, Position position) - { - var syntaxTree = context.CodeDocument.GetSyntaxTree(); - - if (!context.SourceText.TryGetAbsoluteIndex(position, out var absoluteIndex)) - { - return false; - } - - var owner = syntaxTree.Root.FindToken(absoluteIndex - 1); - // Make sure the end tag doesn't already exist - if (owner?.Parent is MarkupStartTagSyntax - { - IsMarkupTransition: true, - Parent: MarkupElementSyntax { EndTag: null } - } startTag) - { - Debug.Assert(string.Equals(startTag.Name.Content, SyntaxConstants.TextTagName, StringComparison.Ordinal), "MarkupTransition that is not a tag."); - - return true; - } - - return false; - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/IOnAutoInsertProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/IOnAutoInsertProvider.cs deleted file mode 100644 index d45bfe02d8d..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/IOnAutoInsertProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -using Microsoft.CodeAnalysis.Razor.Formatting; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.AutoInsert; - -internal interface IOnAutoInsertProvider -{ - string TriggerCharacter { get; } - - bool TryResolveInsertion(Position position, FormattingContext context, [NotNullWhen(true)] out TextEdit? edit, out InsertTextFormat format); -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs index cbcef4ece99..93dd2e890e8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Frozen; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,6 +14,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Threading; +using Microsoft.CodeAnalysis.Razor.AutoInsert; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Logging; @@ -26,21 +29,18 @@ internal class OnAutoInsertEndpoint( LanguageServerFeatureOptions languageServerFeatureOptions, IDocumentMappingService documentMappingService, IClientConnection clientConnection, - IEnumerable onAutoInsertProvider, + IAutoInsertService autoInsertService, RazorLSPOptionsMonitor optionsMonitor, IAdhocWorkspaceFactory workspaceFactory, IRazorFormattingService razorFormattingService, ILoggerFactory loggerFactory) : AbstractRazorDelegatingEndpoint(languageServerFeatureOptions, documentMappingService, clientConnection, loggerFactory.GetOrCreateLogger()), ICapabilitiesProvider { - private static readonly HashSet s_htmlAllowedTriggerCharacters = new(StringComparer.Ordinal) { "=", }; - private static readonly HashSet s_cSharpAllowedTriggerCharacters = new(StringComparer.Ordinal) { "'", "/", "\n" }; - private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; private readonly IAdhocWorkspaceFactory _workspaceFactory = workspaceFactory; private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; - private readonly List _onAutoInsertProviders = onAutoInsertProvider.ToList(); + private readonly IAutoInsertService _autoInsertService = autoInsertService; protected override string CustomMessageTarget => CustomMessageNames.RazorOnAutoInsertEndpointName; @@ -53,17 +53,17 @@ internal class OnAutoInsertEndpoint( public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) { - var triggerCharacters = _onAutoInsertProviders.Select(provider => provider.TriggerCharacter); + var triggerCharacters = _autoInsertService.TriggerCharacters; if (_languageServerFeatureOptions.SingleServerSupport) { - triggerCharacters = triggerCharacters.Concat(s_htmlAllowedTriggerCharacters).Concat(s_cSharpAllowedTriggerCharacters); + triggerCharacters = [ + .. triggerCharacters, + .. AutoInsertService.HtmlAllowedAutoInsertTriggerCharacters, + .. AutoInsertService.CSharpAllowedAutoInsertTriggerCharacters]; } - serverCapabilities.OnAutoInsertProvider = new VSInternalDocumentOnAutoInsertOptions() - { - TriggerCharacters = triggerCharacters.Distinct().ToArray() - }; + serverCapabilities.EnableOnAutoInsert(triggerCharacters); } protected override async Task TryHandleAsync(VSInternalDocumentOnAutoInsertParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) @@ -84,36 +84,18 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V var character = request.Character; - using var applicableProviders = new PooledArrayBuilder(); - foreach (var provider in _onAutoInsertProviders) - { - if (provider.TriggerCharacter == character) - { - applicableProviders.Add(provider); - } - } - - if (applicableProviders.Count == 0) + if (_autoInsertService.TryResolveInsertion( + codeDocument, + request.Position, + character, + _optionsMonitor.CurrentValue.AutoClosingTags, + out var insertTextEdit)) { - // There's currently a bug in the LSP platform where other language clients OnAutoInsert trigger characters influence every language clients trigger characters. - // To combat this we need to preemptively return so we don't try having our providers handle characters that they can't. - return null; - } - - var uri = request.TextDocument.Uri; - var position = request.Position; - - using var formattingContext = FormattingContext.Create(uri, documentContext.Snapshot, codeDocument, request.Options, _workspaceFactory); - foreach (var provider in applicableProviders) - { - if (provider.TryResolveInsertion(position, formattingContext, out var textEdit, out var format)) + return new VSInternalDocumentOnAutoInsertResponseItem() { - return new VSInternalDocumentOnAutoInsertResponseItem() - { - TextEdit = textEdit, - TextEditFormat = format, - }; - } + TextEdit = insertTextEdit.TextEdit, + TextEditFormat = insertTextEdit.TextEditFormat, + }; } // No provider could handle the text edit. @@ -130,7 +112,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V if (positionInfo.LanguageKind == RazorLanguageKind.Html) { - if (!s_htmlAllowedTriggerCharacters.Contains(request.Character)) + if (!AutoInsertService.HtmlAllowedAutoInsertTriggerCharacters.Contains(request.Character)) { Logger.LogInformation($"Inapplicable HTML trigger char {request.Character}."); return SpecializedTasks.Null(); @@ -146,7 +128,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V } else if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) { - if (!s_cSharpAllowedTriggerCharacters.Contains(request.Character)) + if (!AutoInsertService.CSharpAllowedAutoInsertTriggerCharacters.Contains(request.Character)) { Logger.LogInformation($"Inapplicable C# trigger char {request.Character}."); return SpecializedTasks.Null(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspFormattingCodeDocumentProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspFormattingCodeDocumentProvider.cs new file mode 100644 index 00000000000..eaf9bfa091b --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspFormattingCodeDocumentProvider.cs @@ -0,0 +1,18 @@ +// 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.Language; +using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; + +internal sealed class LspFormattingCodeDocumentProvider : IFormattingCodeDocumentProvider +{ + public Task GetCodeDocumentAsync(IDocumentSnapshot snapshot) + { + var useDesignTimeGeneratedOutput = snapshot.Project.Configuration.LanguageServerFlags?.ForceRuntimeCodeGeneration ?? false; + return snapshot.GetGeneratedOutputAsync(useDesignTimeGeneratedOutput); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/VSInternalServerCapabilitiesExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/LspInitializationHelpers.cs similarity index 76% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/VSInternalServerCapabilitiesExtensions.cs rename to src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/LspInitializationHelpers.cs index cb9f96cecb3..ac141c81680 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/VSInternalServerCapabilitiesExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/LspInitializationHelpers.cs @@ -1,12 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis.Razor.SemanticTokens; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -internal static class VSInternalServerCapabilitiesExtensions +internal static class LspInitializationHelpers { public static void EnableInlayHints(this VSInternalServerCapabilities serverCapabilities) { @@ -65,4 +67,21 @@ public static void EnableMapCodeProvider(this VSInternalServerCapabilities serve { serverCapabilities.MapCodeProvider = true; } + + public static void EnableOnAutoInsert( + this VSInternalServerCapabilities serverCapabilities, + IEnumerable triggerCharacters) + { + serverCapabilities.OnAutoInsertProvider = new VSInternalDocumentOnAutoInsertOptions() + .EnableOnAutoInsert(triggerCharacters); + } + + public static VSInternalDocumentOnAutoInsertOptions EnableOnAutoInsert( + this VSInternalDocumentOnAutoInsertOptions options, + IEnumerable triggerCharacters) + { + options.TriggerCharacters = triggerCharacters.Distinct().ToArray(); + + return options; + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs index 0993fac51f9..76d61ba0eb8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.InlineCompletion; internal sealed class InlineCompletionEndpoint( IDocumentMappingService documentMappingService, IClientConnection clientConnection, + IFormattingCodeDocumentProvider formattingCodeDocumentProvider, IAdhocWorkspaceFactory adhocWorkspaceFactory, ILoggerFactory loggerFactory) : IRazorRequestHandler, ICapabilitiesProvider @@ -39,6 +40,7 @@ internal sealed class InlineCompletionEndpoint( private readonly IDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); + private readonly IFormattingCodeDocumentProvider _formattingCodeDocumentProvider = formattingCodeDocumentProvider; private readonly IAdhocWorkspaceFactory _adhocWorkspaceFactory = adhocWorkspaceFactory ?? throw new ArgumentNullException(nameof(adhocWorkspaceFactory)); private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); @@ -123,7 +125,13 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalInlineCompleti continue; } - using var formattingContext = FormattingContext.Create(request.TextDocument.Uri, documentContext.Snapshot, codeDocument, request.Options, _adhocWorkspaceFactory); + using var formattingContext = FormattingContext.Create( + request.TextDocument.Uri, + documentContext.Snapshot, + codeDocument, + request.Options, + _formattingCodeDocumentProvider, + _adhocWorkspaceFactory); if (!TryGetSnippetWithAdjustedIndentation(formattingContext, item.Text, hostDocumentIndex, out var newSnippetText)) { continue; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Mapping/RazorLanguageQueryEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Mapping/RazorLanguageQueryEndpoint.cs index 7c7bc03cc47..2ef39d5ab1f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Mapping/RazorLanguageQueryEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Mapping/RazorLanguageQueryEndpoint.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index d542644a9c8..d3162a0e5b1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; using Microsoft.AspNetCore.Razor.LanguageServer.FindAllReferences; using Microsoft.AspNetCore.Razor.LanguageServer.Folding; +using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.LanguageServer.Implementation; using Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; @@ -24,7 +25,9 @@ using Microsoft.AspNetCore.Razor.LanguageServer.SignatureHelp; using Microsoft.AspNetCore.Razor.LanguageServer.WrapWithTag; using Microsoft.AspNetCore.Razor.Telemetry; +using Microsoft.CodeAnalysis.Razor.AutoInsert; using Microsoft.CodeAnalysis.Razor.FoldingRanges; +using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.GoToDefinition; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Protocol.DocumentSymbols; @@ -119,6 +122,8 @@ protected override ILspServices ConstructLspServices() services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + var featureOptions = _featureOptions ?? new DefaultLanguageServerFeatureOptions(); services.AddSingleton(featureOptions); @@ -136,12 +141,14 @@ protected override ILspServices ConstructLspServices() services.AddHoverServices(); services.AddTextDocumentServices(featureOptions); - // Auto insert - services.AddSingleton(); - services.AddSingleton(); - if (!featureOptions.UseRazorCohostServer) { + // Auto insert + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + // Folding Range Providers services.AddSingleton(); services.AddSingleton(); @@ -178,7 +185,6 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddTransient(sp => sp.GetRequiredService()); services.AddHandlerWithCapabilities(); - services.AddHandlerWithCapabilities(); if (!featureOptions.UseRazorCohostServer) { @@ -188,6 +194,7 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddSingleton(); services.AddHandlerWithCapabilities(); + services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs similarity index 74% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs index ae288e14de1..e5e9febc604 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs @@ -7,14 +7,13 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -using Microsoft.CodeAnalysis.Razor.Formatting; -using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.AutoInsert; +using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; -internal sealed class AutoClosingTagOnAutoInsertProvider(RazorLSPOptionsMonitor optionsMonitor) : IOnAutoInsertProvider +namespace Microsoft.CodeAnalysis.Razor.AutoInsert; + +internal class AutoClosingTagOnAutoInsertProvider : IOnAutoInsertProvider { // From http://dev.w3.org/html5/spec/Overview.html#elements-0 private static readonly ImmutableHashSet s_voidElements = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, @@ -39,55 +38,58 @@ internal sealed class AutoClosingTagOnAutoInsertProvider(RazorLSPOptionsMonitor private static readonly ImmutableHashSet s_voidElementsCaseSensitive = s_voidElements.WithComparer(StringComparer.Ordinal); - private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; - public string TriggerCharacter => ">"; - public bool TryResolveInsertion(Position position, FormattingContext context, [NotNullWhen(true)] out TextEdit? edit, out InsertTextFormat format) + public bool TryResolveInsertion( + Position position, + RazorCodeDocument codeDocument, + bool enableAutoClosingTags, + [NotNullWhen(true)] out VSInternalDocumentOnAutoInsertResponseItem? autoInsertEdit) { - if (!_optionsMonitor.CurrentValue.AutoClosingTags) - { - format = default; - edit = default; - return false; - } + autoInsertEdit = null; - if (!context.SourceText.TryGetAbsoluteIndex(position, out var afterCloseAngleIndex)) + if (!(enableAutoClosingTags + && codeDocument.Source.Text is { } sourceText + && sourceText.TryGetAbsoluteIndex(position, out var afterCloseAngleIndex) + && TryResolveAutoClosingBehavior(codeDocument, afterCloseAngleIndex) is { } tagNameWithClosingBehavior)) { - format = default; - edit = default; return false; } - if (!TryResolveAutoClosingBehavior(context, afterCloseAngleIndex, out var tagName, out var autoClosingBehavior)) + if (tagNameWithClosingBehavior.AutoClosingBehavior == AutoClosingBehavior.EndTag) { - format = default; - edit = default; - return false; - } + var formatForEndTag = InsertTextFormat.Snippet; + var editForEndTag = VsLspFactory.CreateTextEdit(position, $"$0"); - if (autoClosingBehavior == AutoClosingBehavior.EndTag) - { - format = InsertTextFormat.Snippet; - edit = VsLspFactory.CreateTextEdit(position, $"$0"); + autoInsertEdit = new() + { + TextEdit = editForEndTag, + TextEditFormat = formatForEndTag + }; return true; } - Debug.Assert(autoClosingBehavior == AutoClosingBehavior.SelfClosing); + Debug.Assert(tagNameWithClosingBehavior.AutoClosingBehavior == AutoClosingBehavior.SelfClosing); - format = InsertTextFormat.Plaintext; + var format = InsertTextFormat.Plaintext; // Need to replace the `>` with ' />$0' or '/>$0' depending on if there's prefixed whitespace. - var insertionText = char.IsWhiteSpace(context.SourceText[afterCloseAngleIndex - 2]) ? "/" : " /"; - edit = VsLspFactory.CreateTextEdit(position.Line, position.Character - 1, insertionText); + var insertionText = char.IsWhiteSpace(sourceText[afterCloseAngleIndex - 2]) ? "/" : " /"; + var edit = VsLspFactory.CreateTextEdit(position.Line, position.Character - 1, insertionText); + + autoInsertEdit = new() + { + TextEdit = edit, + TextEditFormat = format + }; return true; } - private static bool TryResolveAutoClosingBehavior(FormattingContext context, int afterCloseAngleIndex, [NotNullWhen(true)] out string? name, out AutoClosingBehavior autoClosingBehavior) + private static TagNameWithClosingBehavior? TryResolveAutoClosingBehavior(RazorCodeDocument codeDocument, int afterCloseAngleIndex) { - var syntaxTree = context.CodeDocument.GetSyntaxTree(); + var syntaxTree = codeDocument.GetSyntaxTree(); var closeAngle = syntaxTree.Root.FindToken(afterCloseAngleIndex - 1); if (closeAngle.Parent is MarkupStartTagSyntax @@ -97,19 +99,17 @@ private static bool TryResolveAutoClosingBehavior(FormattingContext context, int } startTag) { var unescapedTagName = startTag.Name.Content; - autoClosingBehavior = InferAutoClosingBehavior(unescapedTagName, caseSensitive: false); + var autoClosingBehavior = InferAutoClosingBehavior(unescapedTagName, caseSensitive: false); if (autoClosingBehavior == AutoClosingBehavior.EndTag && !CouldAutoCloseParentOrSelf(unescapedTagName, htmlElement)) { // Auto-closing behavior is end-tag; however, we already have and end-tag therefore we don't need to do anything! - autoClosingBehavior = default; - name = null; - return false; + return default; } // Finally capture the entire tag name with the potential escape operator. - name = startTag.GetTagNameWithOptionalBang(); - return true; + var name = startTag.GetTagNameWithOptionalBang(); + return new TagNameWithClosingBehavior(name, autoClosingBehavior); } if (closeAngle.Parent is MarkupTagHelperStartTagSyntax @@ -118,9 +118,9 @@ private static bool TryResolveAutoClosingBehavior(FormattingContext context, int Parent: MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding } tagHelperElement } startTagHelper) { - name = startTagHelper.Name.Content; + var name = startTagHelper.Name.Content; - if (!TryGetTagHelperAutoClosingBehavior(binding, out autoClosingBehavior)) + if (!TryGetTagHelperAutoClosingBehavior(binding, out var autoClosingBehavior)) { autoClosingBehavior = InferAutoClosingBehavior(name, caseSensitive: true); } @@ -128,17 +128,13 @@ private static bool TryResolveAutoClosingBehavior(FormattingContext context, int if (autoClosingBehavior == AutoClosingBehavior.EndTag && !CouldAutoCloseParentOrSelf(name, tagHelperElement)) { // Auto-closing behavior is end-tag; however, we already have and end-tag therefore we don't need to do anything! - autoClosingBehavior = default; - name = null; - return false; + return default; } - return true; + return new TagNameWithClosingBehavior(name, autoClosingBehavior); } - autoClosingBehavior = default; - name = null; - return false; + return default; } private static AutoClosingBehavior InferAutoClosingBehavior(string name, bool caseSensitive) @@ -194,7 +190,7 @@ private static bool TryGetTagHelperAutoClosingBehavior(TagHelperBinding bindingR return false; } - private static bool CouldAutoCloseParentOrSelf(string currentTagName, SyntaxNode node) + private static bool CouldAutoCloseParentOrSelf(string currentTagName, RazorSyntaxNode node) { do { @@ -256,4 +252,6 @@ private enum AutoClosingBehavior EndTag, SelfClosing, } + + private readonly record struct TagNameWithClosingBehavior(string TagName, AutoClosingBehavior AutoClosingBehavior); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoInsertService.cs new file mode 100644 index 00000000000..d6b6e3e3a47 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoInsertService.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.AutoInsert; + +internal class AutoInsertService(IEnumerable onAutoInsertProviders) : IAutoInsertService +{ + private readonly ImmutableArray _onAutoInsertProviders = onAutoInsertProviders.ToImmutableArray(); + + public static FrozenSet HtmlAllowedAutoInsertTriggerCharacters { get; } + = new string[] { "=" }.ToFrozenSet(StringComparer.Ordinal); + public static FrozenSet CSharpAllowedAutoInsertTriggerCharacters { get; } + = new string[] { "'", "/", "\n" }.ToFrozenSet(StringComparer.Ordinal); + + private readonly ImmutableArray _triggerCharacters = CalculateTriggerCharacters(onAutoInsertProviders); + + private static ImmutableArray CalculateTriggerCharacters(IEnumerable onAutoInsertProviders) + { + using var builder = new PooledArrayBuilder(); + using var _ = StringHashSetPool.Ordinal.GetPooledObject(out var set); + foreach (var provider in onAutoInsertProviders) + { + var triggerCharacter = provider.TriggerCharacter; + if (set.Add(triggerCharacter)) + { + builder.Add(triggerCharacter); + } + } + + return builder.DrainToImmutable(); + } + + public ImmutableArray TriggerCharacters => _triggerCharacters; + + public bool TryResolveInsertion( + RazorCodeDocument codeDocument, + Position position, + string character, + bool autoCloseTags, + [NotNullWhen(true)] out VSInternalDocumentOnAutoInsertResponseItem? insertTextEdit) + { + using var applicableProviders = new PooledArrayBuilder(capacity: _onAutoInsertProviders.Length); + foreach (var provider in _onAutoInsertProviders) + { + if (provider.TriggerCharacter == character) + { + applicableProviders.Add(provider); + } + } + + if (applicableProviders.Count == 0) + { + // There's currently a bug in the LSP platform where other language clients OnAutoInsert trigger characters influence every language clients trigger characters. + // To combat this we need to preemptively return so we don't try having our providers handle characters that they can't. + insertTextEdit = null; + return false; + } + + foreach (var provider in applicableProviders) + { + if (provider.TryResolveInsertion(position, codeDocument, autoCloseTags, out insertTextEdit)) + { + return true; + } + } + + // No provider could handle the text edit. + insertTextEdit = null; + return false; + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/CloseTextTagOnAutoInsertProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/CloseTextTagOnAutoInsertProvider.cs new file mode 100644 index 00000000000..b2a791b6881 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/CloseTextTagOnAutoInsertProvider.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.AutoInsert; + +internal class CloseTextTagOnAutoInsertProvider : IOnAutoInsertProvider +{ + public string TriggerCharacter => ">"; + + public bool TryResolveInsertion( + Position position, + RazorCodeDocument codeDocument, + bool enableAutoClosingTags, + [NotNullWhen(true)] out VSInternalDocumentOnAutoInsertResponseItem? autoInsertEdit) + { + if (!(enableAutoClosingTags && IsAtTextTag(codeDocument, position))) + { + autoInsertEdit = null; + return false; + } + + // This is a text tag. + var format = InsertTextFormat.Snippet; + var edit = VsLspFactory.CreateTextEdit(position, $"$0"); + + autoInsertEdit = new() + { + TextEdit = edit, + TextEditFormat = format + }; + + return true; + } + + private static bool IsAtTextTag(RazorCodeDocument codeDocument, Position position) + { + var syntaxTree = codeDocument.GetSyntaxTree(); + + if (!(codeDocument.Source.Text is { } sourceText + && sourceText.TryGetAbsoluteIndex(position, out var absoluteIndex))) + { + return false; + } + + var owner = syntaxTree.Root.FindToken(absoluteIndex - 1); + // Make sure the end tag doesn't already exist + if (owner?.Parent is MarkupStartTagSyntax + { + IsMarkupTransition: true, + Parent: MarkupElementSyntax { EndTag: null } + } startTag) + { + Debug.Assert(string.Equals(startTag.Name.Content, SyntaxConstants.TextTagName, StringComparison.Ordinal), "MarkupTransition that is not a tag."); + + return true; + } + + return false; + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IAutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IAutoInsertService.cs new file mode 100644 index 00000000000..8bad0493d0f --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IAutoInsertService.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.AutoInsert; + +internal interface IAutoInsertService +{ + ImmutableArray TriggerCharacters { get; } + + bool TryResolveInsertion( + RazorCodeDocument codeDocument, + Position position, + string character, + bool autoCloseTags, + [NotNullWhen(true)] out VSInternalDocumentOnAutoInsertResponseItem? insertTextEdit); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IOnAutoInsertProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IOnAutoInsertProvider.cs new file mode 100644 index 00000000000..ba6458712a0 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IOnAutoInsertProvider.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.AutoInsert; + +internal interface IOnAutoInsertProvider : IOnAutoInsertTriggerCharacterProvider +{ + public bool TryResolveInsertion( + Position position, + RazorCodeDocument codeDocument, + bool enableAutoClosingTags, + [NotNullWhen(true)] out VSInternalDocumentOnAutoInsertResponseItem? autoInsertEdit); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IOnAutoInsertTriggerCharacterProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IOnAutoInsertTriggerCharacterProvider.cs new file mode 100644 index 00000000000..cfc1ffbed5e --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/IOnAutoInsertTriggerCharacterProvider.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Razor.AutoInsert; + +internal interface IOnAutoInsertTriggerCharacterProvider +{ + string TriggerCharacter { get; } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs index e061b305aaf..722b986ffee 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs @@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed class CSharpFormattingPass( +internal class CSharpFormattingPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) : CSharpFormattingPassBase(documentMappingService) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs index 7973e9d0df3..0b80da73d28 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs @@ -13,7 +13,7 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed class FormattingContentValidationPass( +internal class FormattingContentValidationPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) : FormattingPassBase(documentMappingService) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs index e5329eccbc7..bd0fd0db7b7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -21,16 +21,27 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal sealed class FormattingContext : IDisposable { private readonly IAdhocWorkspaceFactory _workspaceFactory; - private Document? _csharpWorkspaceDocument; + private readonly IFormattingCodeDocumentProvider _codeDocumentProvider; + private Document? _csharpWorkspaceDocument; private AdhocWorkspace? _csharpWorkspace; private IReadOnlyList? _formattingSpans; private IReadOnlyDictionary? _indentations; - private FormattingContext(IAdhocWorkspaceFactory workspaceFactory, Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, FormattingOptions options, - bool isFormatOnType, bool automaticallyAddUsings, int hostDocumentIndex, char triggerCharacter) + private FormattingContext( + IFormattingCodeDocumentProvider codeDocumentProvider, + IAdhocWorkspaceFactory workspaceFactory, + Uri uri, + IDocumentSnapshot originalSnapshot, + RazorCodeDocument codeDocument, + FormattingOptions options, + bool isFormatOnType, + bool automaticallyAddUsings, + int hostDocumentIndex, + char triggerCharacter) { + _codeDocumentProvider = codeDocumentProvider; _workspaceFactory = workspaceFactory; Uri = uri; OriginalSnapshot = originalSnapshot; @@ -268,11 +279,12 @@ public async Task WithTextAsync(SourceText changedText) var changedSnapshot = OriginalSnapshot.WithText(changedText); - var codeDocument = await changedSnapshot.GetFormatterCodeDocumentAsync().ConfigureAwait(false); + var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(changedSnapshot).ConfigureAwait(false); DEBUG_ValidateComponents(CodeDocument, codeDocument); var newContext = new FormattingContext( + _codeDocumentProvider, _workspaceFactory, Uri, OriginalSnapshot, @@ -309,12 +321,23 @@ public static FormattingContext CreateForOnTypeFormatting( IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, FormattingOptions options, + IFormattingCodeDocumentProvider codeDocumentProvider, IAdhocWorkspaceFactory workspaceFactory, bool automaticallyAddUsings, int hostDocumentIndex, char triggerCharacter) { - return CreateCore(uri, originalSnapshot, codeDocument, options, workspaceFactory, isFormatOnType: true, automaticallyAddUsings, hostDocumentIndex, triggerCharacter); + return CreateCore( + uri, + originalSnapshot, + codeDocument, + options, + codeDocumentProvider, + workspaceFactory, + isFormatOnType: true, + automaticallyAddUsings, + hostDocumentIndex, + triggerCharacter); } public static FormattingContext Create( @@ -322,9 +345,20 @@ public static FormattingContext Create( IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, FormattingOptions options, + IFormattingCodeDocumentProvider codeDocumentProvider, IAdhocWorkspaceFactory workspaceFactory) { - return CreateCore(uri, originalSnapshot, codeDocument, options, workspaceFactory, isFormatOnType: false, automaticallyAddUsings: false, hostDocumentIndex: 0, triggerCharacter: '\0'); + return CreateCore( + uri, + originalSnapshot, + codeDocument, + options, + codeDocumentProvider, + workspaceFactory, + isFormatOnType: false, + automaticallyAddUsings: false, + hostDocumentIndex: 0, + triggerCharacter: '\0'); } private static FormattingContext CreateCore( @@ -332,6 +366,7 @@ private static FormattingContext CreateCore( IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, FormattingOptions options, + IFormattingCodeDocumentProvider codeDocumentProvider, IAdhocWorkspaceFactory workspaceFactory, bool isFormatOnType, bool automaticallyAddUsings, @@ -367,6 +402,7 @@ private static FormattingContext CreateCore( Debug.Assert(isFormatOnType || (hostDocumentIndex == 0 && triggerCharacter == '\0' && automaticallyAddUsings == false)); var result = new FormattingContext( + codeDocumentProvider, workspaceFactory, uri, originalSnapshot, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs index 64bf765dd73..8515f5ff13b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed class FormattingDiagnosticValidationPass( +internal class FormattingDiagnosticValidationPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) : FormattingPassBase(documentMappingService) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingCodeDocumentProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingCodeDocumentProvider.cs new file mode 100644 index 00000000000..0d1c25da983 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingCodeDocumentProvider.cs @@ -0,0 +1,13 @@ +// 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.Language; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor.Formatting; + +internal interface IFormattingCodeDocumentProvider +{ + Task GetCodeDocumentAsync(IDocumentSnapshot snapshot); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 79d03f61986..15ef27643c9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -21,10 +21,12 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal class RazorFormattingService : IRazorFormattingService { private readonly List _formattingPasses; + private readonly IFormattingCodeDocumentProvider _codeDocumentProvider; private readonly IAdhocWorkspaceFactory _workspaceFactory; public RazorFormattingService( IEnumerable formattingPasses, + IFormattingCodeDocumentProvider codeDocumentProvider, IAdhocWorkspaceFactory workspaceFactory) { if (formattingPasses is null) @@ -33,6 +35,7 @@ public RazorFormattingService( } _formattingPasses = formattingPasses.OrderBy(f => f.Order).ToList(); + _codeDocumentProvider = codeDocumentProvider ?? throw new ArgumentNullException(nameof(codeDocumentProvider)); _workspaceFactory = workspaceFactory ?? throw new ArgumentNullException(nameof(workspaceFactory)); } @@ -42,7 +45,7 @@ public async Task FormatAsync( FormattingOptions options, CancellationToken cancellationToken) { - var codeDocument = await documentContext.Snapshot.GetFormatterCodeDocumentAsync().ConfigureAwait(false); + var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(documentContext.Snapshot).ConfigureAwait(false); // Range formatting happens on every paste, and if there are Razor diagnostics in the file // that can make some very bad results. eg, given: @@ -70,7 +73,13 @@ public async Task FormatAsync( var uri = documentContext.Uri; var documentSnapshot = documentContext.Snapshot; var hostDocumentVersion = documentContext.Snapshot.Version; - using var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, _workspaceFactory); + using var context = FormattingContext.Create( + uri, + documentSnapshot, + codeDocument, + options, + _codeDocumentProvider, + _workspaceFactory); var originalText = context.SourceText; var result = new FormattingResult([]); @@ -156,7 +165,16 @@ private async Task ApplyFormattedEditsAsync( var documentSnapshot = documentContext.Snapshot; var uri = documentContext.Uri; var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); - using var context = FormattingContext.CreateForOnTypeFormatting(uri, documentSnapshot, codeDocument, options, _workspaceFactory, automaticallyAddUsings: automaticallyAddUsings, hostDocumentIndex, triggerCharacter); + using var context = FormattingContext.CreateForOnTypeFormatting( + uri, + documentSnapshot, + codeDocument, + options, + _codeDocumentProvider, + _workspaceFactory, + automaticallyAddUsings: automaticallyAddUsings, + hostDocumentIndex, + triggerCharacter); var result = new FormattingResult(formattedEdits, kind); foreach (var pass in _formattingPasses) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs index 00d3e9fa182..7a145bf3ee1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs @@ -36,12 +36,6 @@ public Task GetTextAsync() public Task GetTextVersionAsync() => State.GetTextVersionAsync(); - public virtual async Task GetGeneratedOutputAsync() - { - var (output, _) = await State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).ConfigureAwait(false); - return output; - } - public bool TryGetText([NotNullWhen(true)] out SourceText? result) => State.TryGetText(out result); @@ -69,8 +63,27 @@ public IDocumentSnapshot WithText(SourceText text) public async Task GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken) { - var codeDocument = await GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: false).ConfigureAwait(false); var csharpText = codeDocument.GetCSharpSourceText(); return CSharpSyntaxTree.ParseText(csharpText, cancellationToken: cancellationToken); } + + public virtual async Task GetGeneratedOutputAsync(bool forceDesignTimeGeneratedOutput) + { + if (forceDesignTimeGeneratedOutput) + { + return await GetDesignTimeGeneratedOutputAsync().ConfigureAwait(false); + } + + var (output, _) = await State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).ConfigureAwait(false); + return output; + } + + private async Task GetDesignTimeGeneratedOutputAsync() + { + var tagHelpers = await Project.GetTagHelpersAsync(CancellationToken.None).ConfigureAwait(false); + var projectEngine = Project.GetProjectEngine(); + var imports = await DocumentState.GetImportsAsync(this, projectEngine).ConfigureAwait(false); + return await DocumentState.GenerateCodeDocumentAsync(this, projectEngine, imports, tagHelpers, forceRuntimeCodeGeneration: false).ConfigureAwait(false); + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs index 3e226a3eda9..c3e7f78f698 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs @@ -21,7 +21,7 @@ internal interface IDocumentSnapshot Task GetTextAsync(); Task GetTextVersionAsync(); - Task GetGeneratedOutputAsync(); + Task GetGeneratedOutputAsync(bool forceDesignTimeGeneratedOutput); /// /// Gets the Roslyn syntax tree for the generated C# for this Razor document diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshotExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshotExtensions.cs index ef8b26ca9e9..6b6ec6a9360 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshotExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshotExtensions.cs @@ -59,29 +59,9 @@ public static bool IsPathCandidateForComponent(this IDocumentSnapshot documentSn return fileName.AsSpan().Equals(path.Span, FilePathComparison.Instance); } - public static Task GetFormatterCodeDocumentAsync(this IDocumentSnapshot documentSnapshot) + public static Task GetGeneratedOutputAsync(this IDocumentSnapshot documentSnapshot) { - var forceRuntimeCodeGeneration = documentSnapshot.Project.Configuration.LanguageServerFlags?.ForceRuntimeCodeGeneration ?? false; - if (!forceRuntimeCodeGeneration) - { - return documentSnapshot.GetGeneratedOutputAsync(); - } - - // if forceRuntimeCodeGeneration is on, GetGeneratedOutputAsync will get runtime code. As of now - // the formatting service doesn't expect the form of code generated to be what the compiler does with - // runtime. For now force usage of design time and avoid the cache. There may be a slight perf hit - // but either the user is typing (which will invalidate the cache) or the user is manually attempting to - // format. We expect formatting to invalidate the cache if it changes things and consider this an - // acceptable overhead for now. - return GetDesignTimeDocumentAsync(documentSnapshot); + return documentSnapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: false); } - private static async Task GetDesignTimeDocumentAsync(IDocumentSnapshot documentSnapshot) - { - var project = documentSnapshot.Project; - var tagHelpers = await project.GetTagHelpersAsync(CancellationToken.None).ConfigureAwait(false); - var projectEngine = project.GetProjectEngine(); - var imports = await DocumentState.GetImportsAsync(documentSnapshot, projectEngine).ConfigureAwait(false); - return await DocumentState.GenerateCodeDocumentAsync(documentSnapshot, project.GetProjectEngine(), imports, tagHelpers, false).ConfigureAwait(false); - } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs index e77a615ed5c..213a2f2352c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs @@ -35,9 +35,6 @@ public ImportDocumentSnapshot(IProjectSnapshot project, RazorProjectItem item) public int Version => 1; - public Task GetGeneratedOutputAsync() - => throw new NotSupportedException(); - public async Task GetTextAsync() { using (var stream = _importItem.Read()) @@ -50,6 +47,9 @@ public async Task GetTextAsync() return _sourceText; } + public Task GetGeneratedOutputAsync(bool _) + => throw new NotSupportedException(); + public Task GetTextVersionAsync() { return Task.FromResult(_version); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/AutoInsert/RemoteAutoInsertTextEdit.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/AutoInsert/RemoteAutoInsertTextEdit.cs new file mode 100644 index 00000000000..faaf69dc274 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/AutoInsert/RemoteAutoInsertTextEdit.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Runtime.Serialization; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using static Microsoft.VisualStudio.LanguageServer.Protocol.VsLspExtensions; +using static Roslyn.LanguageServer.Protocol.RoslynLspExtensions; +using RoslynInsertTextFormat = Roslyn.LanguageServer.Protocol.InsertTextFormat; + +namespace Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert; + +[DataContract] +internal readonly record struct RemoteAutoInsertTextEdit( + [property: DataMember(Order = 0)] LinePositionSpan LinePositionSpan, + [property: DataMember(Order = 1)] string NewText, + [property: DataMember(Order = 2)] RoslynInsertTextFormat InsertTextFormat) +{ + public static RemoteAutoInsertTextEdit FromLspInsertTextEdit(VSInternalDocumentOnAutoInsertResponseItem edit) + => new ( + edit.TextEdit.Range.ToLinePositionSpan(), + edit.TextEdit.NewText, + (RoslynInsertTextFormat)edit.TextEditFormat); + + public static VSInternalDocumentOnAutoInsertResponseItem ToLspInsertTextEdit(RemoteAutoInsertTextEdit edit) + => new() + { + TextEdit = VsLspFactory.CreateTextEdit(edit.LinePositionSpan, edit.NewText), + TextEditFormat = (InsertTextFormat)edit.InsertTextFormat, + }; + + public override string ToString() + { + return $"({LinePositionSpan}), '{NewText}', {InsertTextFormat}"; + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteAutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteAutoInsertService.cs new file mode 100644 index 00000000000..3872965c0f7 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteAutoInsertService.cs @@ -0,0 +1,24 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Text; + +using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; + +internal interface IRemoteAutoInsertService +{ + ValueTask GetAutoInsertTextEditAsync( + RazorPinnedSolutionInfoWrapper solutionInfo, + DocumentId documentId, + LinePosition position, + string character, + bool autoCloseTags, + bool formatOnType, + bool indentWithTabs, + int indentSize, + CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index b99ef863b7e..558b737d4c4 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -21,6 +21,7 @@ internal static class RazorServices (typeof(IRemoteUriPresentationService), null), (typeof(IRemoteFoldingRangeService), null), (typeof(IRemoteDocumentHighlightService), null), + (typeof(IRemoteAutoInsertService), null), ]; // Internal for testing diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/OOPAutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/OOPAutoInsertService.cs new file mode 100644 index 00000000000..6e19d616fcf --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/OOPAutoInsertService.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Composition; +using Microsoft.CodeAnalysis.Razor.AutoInsert; + +namespace Microsoft.CodeAnalysis.Remote.Razor.AutoInsert; + +[Export(typeof(IAutoInsertService)), Shared] +[method: ImportingConstructor] +internal sealed class OOPAutoInsertService([ImportMany] IEnumerable providers) : AutoInsertService(providers) +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs new file mode 100644 index 00000000000..4c009c6b7ad --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs @@ -0,0 +1,212 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; +using Microsoft.CodeAnalysis.Razor.AutoInsert; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +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.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; +using VsLspFormattingOptions = Microsoft.VisualStudio.LanguageServer.Protocol.FormattingOptions; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed class RemoteAutoInsertService(in ServiceArgs args) + : RazorDocumentServiceBase(in args), IRemoteAutoInsertService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteAutoInsertService CreateService(in ServiceArgs args) + => new RemoteAutoInsertService(in args); + } + + private readonly IAutoInsertService _autoInsertService = args.ExportProvider.GetExportedValue(); + private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue(); + private readonly IRazorFormattingService _razorFormattingService = args.ExportProvider.GetExportedValue(); + + protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferHtmlInAttributeValuesDocumentPositionInfoStrategy.Instance; + + public ValueTask GetAutoInsertTextEditAsync( + RazorPinnedSolutionInfoWrapper solutionInfo, + DocumentId documentId, + LinePosition linePosition, + string character, + bool autoCloseTags, + bool formatOnType, + bool indentWithTabs, + int indentSize, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => TryResolveInsertionAsync( + context, + linePosition, + character, + autoCloseTags, + formatOnType, + indentWithTabs, + indentSize, + cancellationToken), + cancellationToken); + + private async ValueTask TryResolveInsertionAsync( + RemoteDocumentContext remoteDocumentContext, + LinePosition linePosition, + string character, + bool autoCloseTags, + bool formatOnType, + bool indentWithTabs, + int indentSize, + CancellationToken cancellationToken) + { + var sourceText = await remoteDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); + if (!sourceText.TryGetAbsoluteIndex(linePosition, out var index)) + { + return Response.NoFurtherHandling; + } + + var codeDocument = await remoteDocumentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + // Always try our own service first, regardless of language + // E.g. if ">" is typed for html tag, it's actually our auto-insert provider + // that adds closing tag instead of HTML even though we are in HTML + if (_autoInsertService.TryResolveInsertion( + codeDocument, + VsLspExtensions.ToPosition(linePosition), + character, + autoCloseTags, + out var insertTextEdit)) + { + return Response.Results(RemoteAutoInsertTextEdit.FromLspInsertTextEdit(insertTextEdit)); + } + + var positionInfo = GetPositionInfo(codeDocument, index); + var languageKind = positionInfo.LanguageKind; + + switch (languageKind) + { + case RazorLanguageKind.Razor: + // If we are in Razor and got no edit from our own service, there is nothing else to do + return Response.NoFurtherHandling; + case RazorLanguageKind.Html: + return AutoInsertService.HtmlAllowedAutoInsertTriggerCharacters.Contains(character) + ? Response.CallHtml + : Response.NoFurtherHandling; + case RazorLanguageKind.CSharp: + var mappedPosition = positionInfo.Position.ToLinePosition(); + return await TryResolveInsertionInCSharpAsync( + remoteDocumentContext, + mappedPosition, + character, + formatOnType, + indentWithTabs, + indentSize, + cancellationToken); + default: + Logger.LogError($"Unsupported language {languageKind} in {nameof(RemoteAutoInsertService)}"); + return Response.NoFurtherHandling; + } + } + + private async ValueTask TryResolveInsertionInCSharpAsync( + RemoteDocumentContext remoteDocumentContext, + LinePosition mappedPosition, + string character, + bool formatOnType, + bool indentWithTabs, + int indentSize, + CancellationToken cancellationToken) + { + // Special case for C# where we use AutoInsert for two purposes: + // 1. For XML documentation comments (filling out the template when typing "///") + // 2. For "on type formatting" style behavior, like adjusting indentation when pressing Enter inside empty braces + // + // If users have turned off on-type formatting, they don't want the behavior of number 2, but its impossible to separate + // that out from number 1. Typing "///" could just as easily adjust indentation on some unrelated code higher up in the + // file, which is exactly the behavior users complain about. + // + // 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) + { + return Response.NoFurtherHandling; + } + + if (!AutoInsertService.CSharpAllowedAutoInsertTriggerCharacters.Contains(character)) + { + return Response.NoFurtherHandling; + } + + 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, + cancellationToken + ); + + if (autoInsertResponseItem is null) + { + return Response.NoFurtherHandling; + } + + var razorFormattingOptions = new VsLspFormattingOptions() + { + InsertSpaces = !indentWithTabs, + TabSize = indentSize + }; + + var vsLspTextEdit = VsLspFactory.CreateTextEdit( + autoInsertResponseItem.TextEdit.Range.ToLinePositionSpan(), + autoInsertResponseItem.TextEdit.NewText); + var mappedEdits = autoInsertResponseItem.TextEditFormat == RoslynInsertTextFormat.Snippet + ? await _razorFormattingService.FormatSnippetAsync( + remoteDocumentContext, + RazorLanguageKind.CSharp, + [vsLspTextEdit], + razorFormattingOptions, + cancellationToken) + .ConfigureAwait(false) + : await _razorFormattingService.FormatOnTypeAsync( + remoteDocumentContext, + RazorLanguageKind.CSharp, + [vsLspTextEdit], + razorFormattingOptions, + hostDocumentIndex: 0, + triggerCharacter: '\0', + cancellationToken) + .ConfigureAwait(false); + + if (mappedEdits is not [{ } edit]) + { + return Response.NoFurtherHandling; + } + + return Response.Results( + new RemoteAutoInsertTextEdit( + edit.Range.ToLinePositionSpan(), + edit.NewText, + autoInsertResponseItem.TextEditFormat)); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteOnAutoInsertProviders.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteOnAutoInsertProviders.cs new file mode 100644 index 00000000000..e619a1aa100 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteOnAutoInsertProviders.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.Razor.AutoInsert; + +namespace Microsoft.CodBeAnalysis.Remote.Razor.AutoInsert; + +[Shared] +[Export(typeof(IOnAutoInsertProvider))] +internal sealed class RemoteAutoClosingTagOnAutoInsertProvider + : AutoClosingTagOnAutoInsertProvider; + +[Shared] +[Export(typeof(IOnAutoInsertProvider))] +internal sealed class RemoteCloseTextTagOnAutoInsertProvider + : CloseTextTagOnAutoInsertProvider; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs index 50fbc2fec25..0d3a170efac 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteCSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteCSharpOnTypeFormattingPass.cs index 6d4e33d0af8..638aa40b213 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteCSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteCSharpOnTypeFormattingPass.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Diagnostics; +using System.Composition; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; +[Export(typeof(IFormattingPass)), Shared] +[method: ImportingConstructor] internal sealed class RemoteCSharpOnTypeFormattingPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) @@ -20,7 +22,8 @@ internal sealed class RemoteCSharpOnTypeFormattingPass( { protected override Task AddUsingStatementEditsIfNecessaryAsync(CodeAnalysis.Razor.Formatting.FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken) { - Debug.Fail("Implement this when code actions are migrated to cohosting"); + // Implement this when code actions are migrated to cohosting, + // probably will be able to move it back into base class and make that non-abstract. return Task.FromResult(finalEdits); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingCodeDocumentProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingCodeDocumentProvider.cs new file mode 100644 index 00000000000..92a0ccd1ae6 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingCodeDocumentProvider.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Formatting; + +[Export(typeof(IFormattingCodeDocumentProvider)), Shared] +internal sealed class RemoteFormattingCodeDocumentProvider : IFormattingCodeDocumentProvider +{ + public Task GetCodeDocumentAsync(IDocumentSnapshot snapshot) + { + return snapshot.GetGeneratedOutputAsync(); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingPasses.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingPasses.cs new file mode 100644 index 00000000000..2ca3bda8a99 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingPasses.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.Logging; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Formatting; + +[Export(typeof(IFormattingPass)), Shared] +[method: ImportingConstructor] +internal sealed class RemoteCSharpFormattingPass( + IDocumentMappingService documentMappingService, + ILoggerFactory loggerFactory) + : CSharpFormattingPass(documentMappingService, loggerFactory); + +[Export(typeof(IFormattingPass)), Shared] +[method: ImportingConstructor] +internal sealed class RemoteFormattingContentValidationPass( + IDocumentMappingService documentMappingService, + ILoggerFactory loggerFactory) + : FormattingContentValidationPass(documentMappingService, loggerFactory); + +[Export(typeof(IFormattingPass)), Shared] +[method: ImportingConstructor] +internal sealed class RemoteFormattingDiagnosticValidationPass( + IDocumentMappingService documentMappingService, + ILoggerFactory loggerFactory) + : FormattingDiagnosticValidationPass(documentMappingService, loggerFactory); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingPass.cs index 500d610a99e..0347e5591c9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingPass.cs @@ -1,15 +1,18 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Composition; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Formatting; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; +[Export(typeof(IFormattingPass)), Shared] +[method: ImportingConstructor] internal sealed class RemoteRazorFormattingPass( IDocumentMappingService documentMappingService) : RazorFormattingPassBase(documentMappingService) { // TODO: properly plumb this through - protected override bool CodeBlockBraceOnNextLine => true; + protected override bool CodeBlockBraceOnNextLine => false; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs new file mode 100644 index 00000000000..bff121bd4c0 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Composition; +using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.Workspaces; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Formatting; + +[Export(typeof(IRazorFormattingService)), Shared] +[method: ImportingConstructor] +internal class RemoteRazorFormattingService( + [ImportMany] IEnumerable formattingPasses, + IFormattingCodeDocumentProvider codeDocumentProvider, + IAdhocWorkspaceFactory adhocWorkspaceFactory) + : RazorFormattingService( + formattingPasses, + codeDocumentProvider, + adhocWorkspaceFactory) +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteDocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteDocumentSnapshot.cs index e4e413bc5a6..bf65778cd50 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteDocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteDocumentSnapshot.cs @@ -46,7 +46,7 @@ internal class RemoteDocumentSnapshot(TextDocument textDocument, RemoteProjectSn public bool TryGetTextVersion(out VersionStamp result) => _textDocument.TryGetTextVersion(out result); - public async Task GetGeneratedOutputAsync() + public async Task GetGeneratedOutputAsync(bool _) { // TODO: We don't need to worry about locking if we get called from the didOpen/didChange LSP requests, as CLaSP // takes care of that for us, and blocks requests until those are complete. If that doesn't end up happening, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteAdhocWorkspaceFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteAdhocWorkspaceFactory.cs index 232561f871f..9abf7da6394 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteAdhocWorkspaceFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteAdhocWorkspaceFactory.cs @@ -2,20 +2,23 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Immutable; +using System.Composition; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor.Workspaces; namespace Microsoft.CodeAnalysis.Remote.Razor; -internal sealed class RemoteAdhocWorkspaceFactory(HostServices hostServices) : IAdhocWorkspaceFactory +[Export(typeof(IAdhocWorkspaceFactory)), Shared] +internal sealed class RemoteAdhocWorkspaceFactory() : IAdhocWorkspaceFactory { public AdhocWorkspace Create(params IWorkspaceService[] workspaceServices) { workspaceServices ??= []; + var hostServices = RemoteWorkspaceAccessor.GetWorkspace().Services.HostServices; var services = AdhocServices.Create( workspaceServices: workspaceServices.ToImmutableArray(), - languageServices: ImmutableArray.Empty, + languageServices: [], fallbackServices: hostServices); return new AdhocWorkspace(services); 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 new file mode 100644 index 00000000000..1b90bd110ac --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs @@ -0,0 +1,176 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.AutoInsert; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.LanguageClient.Cohost; +using Microsoft.VisualStudio.Razor.Settings; +using RazorLSPConstants = Microsoft.VisualStudio.Razor.LanguageClient.RazorLSPConstants; +using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(VSInternalMethods.OnAutoInsertName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostOnAutoInsertEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal class CohostOnAutoInsertEndpoint( + IRemoteServiceInvoker remoteServiceInvoker, + IClientSettingsManager clientSettingsManager, +#pragma warning disable RS0030 // Do not use banned APIs + [ImportMany] IEnumerable onAutoInsertTriggerCharacterProviders, +#pragma warning restore RS0030 // Do not use banned APIs + IHtmlDocumentSynchronizer htmlDocumentSynchronizer, + LSPRequestInvoker requestInvoker, + ILoggerFactory loggerFactory) + : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager; + private readonly IEnumerable _onAutoInsertTriggerCharacterProviders = onAutoInsertTriggerCharacterProviders; + private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); + + private readonly ImmutableArray _triggerCharacters = CalculateTriggerChars(onAutoInsertTriggerCharacterProviders); + + private static ImmutableArray CalculateTriggerChars(IEnumerable onAutoInsertTriggerCharacterProviders) + { + var providerTriggerCharacters = onAutoInsertTriggerCharacterProviders.Select((provider) => provider.TriggerCharacter).Distinct(); + + ImmutableArray _triggerCharacters = [ + .. providerTriggerCharacters, + .. AutoInsertService.HtmlAllowedAutoInsertTriggerCharacters, + .. AutoInsertService.CSharpAllowedAutoInsertTriggerCharacters ]; + + return _triggerCharacters; + } + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + { + if (clientCapabilities.SupportsVisualStudioExtensions) + { + return new Registration + { + Method = VSInternalMethods.OnAutoInsertName, + RegisterOptions = new VSInternalDocumentOnAutoInsertOptions() + .EnableOnAutoInsert(_triggerCharacters) + }; + } + + return null; + } + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSInternalDocumentOnAutoInsertParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + protected override async Task HandleRequestAsync(VSInternalDocumentOnAutoInsertParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + { + var razorDocument = context.TextDocument.AssumeNotNull(); + + _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; + + _logger.LogDebug($"Calling OOP to resolve insertion at {request.Position} invoked by typing '{request.Character}'"); + var data = await _remoteServiceInvoker.TryInvokeAsync( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) + => service.GetAutoInsertTextEditAsync( + solutionInfo, + razorDocument.Id, + request.Position.ToLinePosition(), + request.Character, + enableAutoClosingTags, + formatOnType, + indentWithTabs, + indentSize, + cancellationToken), + cancellationToken).ConfigureAwait(false); + + if (data.Result is { } remoteInsertTextEdit) + { + _logger.LogDebug($"Got insert text edit from OOP {remoteInsertTextEdit}"); + return RemoteAutoInsertTextEdit.ToLspInsertTextEdit(remoteInsertTextEdit); + } + + if (data.StopHandling) + { + return null; + } + + // Got no data but no signal to stop handling + + return await TryResolveHtmlInsertionAsync( + razorDocument, + request, + clientSettings.AdvancedSettings.AutoInsertAttributeQuotes, + cancellationToken) + .ConfigureAwait(false); + } + + private async Task TryResolveHtmlInsertionAsync( + TextDocument razorDocument, + VSInternalDocumentOnAutoInsertParams request, + bool autoInsertAttributeQuotes, + CancellationToken cancellationToken) + { + if (!autoInsertAttributeQuotes && request.Character == "=") + { + // Use Razor setting for auto insert attribute quotes. HTML Server doesn't have a way to pass that + // information along so instead we just don't delegate the request. + _logger.LogTrace($"Not delegating to HTML completion because AutoInsertAttributeQuotes is disabled"); + return null; + } + + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); + if (htmlDocument is null) + { + return null; + } + + request.TextDocument = request.TextDocument.WithUri(htmlDocument.Uri); + + _logger.LogDebug($"Resolving auto-insertion edit for {htmlDocument.Uri}"); + + var result = await _requestInvoker.ReinvokeRequestOnServerAsync( + htmlDocument.Buffer, + VSInternalMethods.OnAutoInsertName, + RazorLSPConstants.HtmlLanguageServerName, + request, + cancellationToken).ConfigureAwait(false); + + if (result?.Response is null) + { + _logger.LogDebug($"Didn't get insert edit back from Html."); + return null; + } + + return result.Response; + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertTriggerCharacterProviders.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertTriggerCharacterProviders.cs new file mode 100644 index 00000000000..84a7207e1cd --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertTriggerCharacterProviders.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Razor.AutoInsert; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.LanguageClient.Cohost; + +// These are needed to we can get auto-insert trigger character collection +// during registration of CohostOnAutoInsertProvider without using a remote service + +[Export(typeof(IOnAutoInsertTriggerCharacterProvider))] +internal sealed class CohostAutoClosingTagOnAutoInsertTriggerCharacterProvider : AutoClosingTagOnAutoInsertProvider; + +[Export(typeof(IOnAutoInsertTriggerCharacterProvider))] +internal sealed class CohostCloseTextTagOnAutoInsertTriggerCharacterProvider : CloseTextTagOnAutoInsertProvider; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/AutoClosingTagOnAutoInsertProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/AutoClosingTagOnAutoInsertProviderTest.cs index 0e806675535..3eba6dff3fd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/AutoClosingTagOnAutoInsertProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/AutoClosingTagOnAutoInsertProviderTest.cs @@ -3,8 +3,8 @@ using System; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.AutoInsert; using Xunit; using Xunit.Abstractions; using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; @@ -13,8 +13,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.AutoInsert; public class AutoClosingTagOnAutoInsertProviderTest(ITestOutputHelper testOutput) : RazorOnAutoInsertProviderTestBase(testOutput) { - private RazorLSPOptions Options { get; set; } = RazorLSPOptions.Default; - private static TagHelperDescriptor CatchAllTagHelper { get @@ -104,19 +102,18 @@ private static TagHelperDescriptor WithoutEndTagTagHelper public void OnTypeCloseAngle_ConflictingAutoClosingBehaviorsChoosesMostSpecific() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly - -$$ -", -expected: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly - -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { WithoutEndTagTagHelper, CatchAllTagHelper }); + $$ + """, + expected: """ + @addTagHelper *, TestAssembly + + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { WithoutEndTagTagHelper, CatchAllTagHelper }); } [Fact] @@ -124,18 +121,18 @@ public void OnTypeCloseAngle_ConflictingAutoClosingBehaviorsChoosesMostSpecific( public void OnTypeCloseAngle_TagHelperAlreadyHasEndTag() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly - -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -143,18 +140,18 @@ public void OnTypeCloseAngle_TagHelperAlreadyHasEndTag() public void OnTypeCloseAngle_VoidTagHelperHasEndTag_ShouldStillAutoClose() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly - -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { UnspecifiedInputTagHelper }); + + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { UnspecifiedInputTagHelper }); } [Fact] @@ -162,12 +159,12 @@ public void OnTypeCloseAngle_VoidTagHelperHasEndTag_ShouldStillAutoClose() public void OnTypeCloseAngle_TagAlreadyHasEndTag() { RunAutoInsertTest( -input: @" -
$$
-", -expected: @" -
-"); + input: """ +
$$
+ """, + expected: """ +
+ """); } [Fact] @@ -175,33 +172,33 @@ public void OnTypeCloseAngle_TagAlreadyHasEndTag() public void OnTypeCloseAngle_TagDoesNotAutoCloseOutOfScope() { RunAutoInsertTest( -input: @" -
- @if (true) - { -
$$
- } -", -expected: @" -
- @if (true) - { -
- } -"); + input: """ +
+ @if (true) + { +
$$
+ } + """, + expected: """ +
+ @if (true) + { +
+ } + """); } [Fact] [WorkItem("https://github.com/dotnet/aspnetcore/issues/36125")] - public void OnTypeCloseAngle_VoidTagHasEndTag_ShouldStillAutoClose() + public void OnTypeCloseAngle_VoidTagHasEndTag_ShouldStillClose() { RunAutoInsertTest( -input: @" -$$ -", -expected: @" - -"); + input: """ + $$ + """, + expected: """ + + """); } [Fact] @@ -209,18 +206,18 @@ public void OnTypeCloseAngle_VoidTagHasEndTag_ShouldStillAutoClose() public void OnTypeCloseAngle_VoidElementMirroringTagHelper() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly -$0 -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { UnspecifiedInputMirroringTagHelper }); + $0 + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { UnspecifiedInputMirroringTagHelper }); } [Fact] @@ -228,82 +225,82 @@ public void OnTypeCloseAngle_VoidElementMirroringTagHelper() public void OnTypeCloseAngle_VoidHtmlElementCapitalized_SelfCloses() { RunAutoInsertTest( -input: "$$", -expected: "", -fileKind: FileKinds.Legacy, -tagHelpers: Array.Empty()); + input: "$$", + expected: "", + fileKind: FileKinds.Legacy, + tagHelpers: Array.Empty()); } [Fact] public void OnTypeCloseAngle_NormalOrSelfClosingStructureOverridesVoidTagBehavior() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly -$0 -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfclosingInputTagHelper }); + $0 + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfclosingInputTagHelper }); } [Fact] public void OnTypeCloseAngle_UnspeccifiedStructureInheritsVoidTagBehavior() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly - -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { UnspecifiedInputTagHelper }); + + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { UnspecifiedInputTagHelper }); } [Fact] public void OnTypeCloseAngle_UnspeccifiedTagHelperTagStructure() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly -$0 -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { UnspecifiedTagHelper }); + $0 + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { UnspecifiedTagHelper }); } [Fact] public void OnTypeCloseAngle_NormalOrSelfClosingTagHelperTagStructure() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly -$0 -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + $0 + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -311,24 +308,24 @@ public void OnTypeCloseAngle_NormalOrSelfClosingTagHelperTagStructure() public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$$
-} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { +
$$
+ } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$0
-} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { +
$0
+ } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -336,24 +333,24 @@ public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement() public void OnTypeCloseAngle_HtmlTagInHtml_NestedStatement_WithAttribute() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ - -} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { + + } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ - -} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { + + } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -361,24 +358,24 @@ public void OnTypeCloseAngle_HtmlTagInHtml_NestedStatement_WithAttribute() public void OnTypeCloseAngle_HtmlTagInHtml_NestedStatement_WithAttribute_SpaceBetweenClosingAngleAndAttributeClosingQuote() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ - -} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { + + } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ - -} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { + + } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -386,24 +383,24 @@ public void OnTypeCloseAngle_HtmlTagInHtml_NestedStatement_WithAttribute_SpaceBe public void OnTypeCloseAngle_HtmlTagInHtml_NestedStatement_WithMinimalizedAttribute() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$$
-} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { +
$$
+ } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$0
-} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { +
$0
+ } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -411,24 +408,24 @@ public void OnTypeCloseAngle_HtmlTagInHtml_NestedStatement_WithMinimalizedAttrib public void OnTypeCloseAngle_HtmlTagInHtml_NestedStatement_WithMinimalizedAttribute_SpaceBetweenClosingAngleAndAttributeClosingQuote() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$$
-} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { +
$$
+ } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$0
-} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { +
$0
+ } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -436,24 +433,24 @@ public void OnTypeCloseAngle_HtmlTagInHtml_NestedStatement_WithMinimalizedAttrib public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement_WithAttribute() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$$
-} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { +
$$
+ } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$0
-} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { +
$0
+ } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -461,24 +458,24 @@ public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement_WithAttribute() public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement_WithAttribute_SpaceBetweenClosingAngleAndAttributeClosingQuote() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$$
-} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { +
$$
+ } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$0
-} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { +
$0
+ } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -486,24 +483,24 @@ public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement_WithAttribute_Space public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement_WithMinimalizedAttribute() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$$
-} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { +
$$
+ } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$0
-} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { +
$0
+ } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -511,24 +508,24 @@ public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement_WithMinimalizedAttr public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement_WithMinimalizedAttribute_SpaceBetweenClosingAngleAndAttributeClosingQuote() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$$
-} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { +
$$
+ } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$0
-} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @if (true) + { +
$0
+ } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] @@ -536,24 +533,24 @@ public void OnTypeCloseAngle_TagHelperInHtml_NestedStatement_WithMinimalizedAttr public void OnTypeCloseAngle_TagHelperInTagHelper_NestedStatement() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -$$ -} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { + $$ + } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ - -} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper, UnspecifiedInputTagHelper }); + @if (true) + { + + } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper, UnspecifiedInputTagHelper }); } [Fact] @@ -561,24 +558,24 @@ public void OnTypeCloseAngle_TagHelperInTagHelper_NestedStatement() public void OnTypeCloseAngle_TagHelperNextToVoidTagHelper_NestedStatement() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -$$ -} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { + $$ + } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -$0 -} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper, UnspecifiedInputTagHelper }); + @if (true) + { + $0 + } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper, UnspecifiedInputTagHelper }); } [Fact] @@ -586,64 +583,64 @@ public void OnTypeCloseAngle_TagHelperNextToVoidTagHelper_NestedStatement() public void OnTypeCloseAngle_TagHelperNextToTagHelper_NestedStatement() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -$$ -} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { + $$ + } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -$0 -} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper, NormalOrSelfclosingInputTagHelper }); + @if (true) + { + $0 + } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper, NormalOrSelfclosingInputTagHelper }); } [Fact] public void OnTypeCloseAngle_NormalOrSelfClosingTagHelperTagStructure_CodeBlock() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@{ - $$ -} -", -expected: @" -@addTagHelper *, TestAssembly + @{ + $$ + } + """, + expected: """ + @addTagHelper *, TestAssembly -@{ - $0 -} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + @{ + $0 + } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] public void OnTypeCloseAngle_WithSlash_WithoutEndTagTagHelperTagStructure() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly - -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { WithoutEndTagTagHelper }); + + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { WithoutEndTagTagHelper }); } [Fact] @@ -651,130 +648,130 @@ public void OnTypeCloseAngle_WithSlash_WithoutEndTagTagHelperTagStructure() public void OnTypeCloseAngle_NestedStatement() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
$$
-} -", -expected: @" -@addTagHelper *, TestAssembly + @if (true) + { +
$$
+ } + """, + expected: """ + @addTagHelper *, TestAssembly -@if (true) -{ -
-} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { WithoutEndTagTagHelper }); + @if (true) + { +
+ } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { WithoutEndTagTagHelper }); } [Fact] public void OnTypeCloseAngle_WithSpace_WithoutEndTagTagHelperTagStructure() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly - -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { WithoutEndTagTagHelper }); + + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { WithoutEndTagTagHelper }); } [Fact] public void OnTypeCloseAngle_WithoutEndTagTagHelperTagStructure() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly - -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { WithoutEndTagTagHelper }); + + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { WithoutEndTagTagHelper }); } [Fact] public void OnTypeCloseAngle_WithoutEndTagTagHelperTagStructure_CodeBlock() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -@{ - $$ -} -", -expected: @" -@addTagHelper *, TestAssembly + @{ + $$ + } + """, + expected: """ + @addTagHelper *, TestAssembly -@{ - -} -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { WithoutEndTagTagHelper }); + @{ + + } + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { WithoutEndTagTagHelper }); } [Fact] public void OnTypeCloseAngle_MultipleApplicableTagHelperTagStructures() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly -$0 -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { UnspecifiedTagHelper, NormalOrSelfClosingTagHelper, WithoutEndTagTagHelper }); + $0 + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { UnspecifiedTagHelper, NormalOrSelfClosingTagHelper, WithoutEndTagTagHelper }); } [Fact] public void OnTypeCloseAngle_EscapedTagTagHelperAutoCompletesWithEscape() { RunAutoInsertTest( -input: @" -@addTagHelper *, TestAssembly + input: """ + @addTagHelper *, TestAssembly -$$ -", -expected: @" -@addTagHelper *, TestAssembly + $$ + """, + expected: """ + @addTagHelper *, TestAssembly -$0 -", -fileKind: FileKinds.Legacy, -tagHelpers: new[] { NormalOrSelfClosingTagHelper }); + $0 + """, + fileKind: FileKinds.Legacy, + tagHelpers: new[] { NormalOrSelfClosingTagHelper }); } [Fact] public void OnTypeCloseAngle_AlwaysClosesStandardHTMLTag() { RunAutoInsertTest( -input: @" -
$$
-", -expected: @" -
$0
-"); + input: """ +
$$
+ """, + expected: """ +
$0
+ """); } [Fact] @@ -782,18 +779,18 @@ public void OnTypeCloseAngle_AlwaysClosesStandardHTMLTag() public void OnTypeCloseAngle_ClosesStandardHTMLTag_NestedStatement() { RunAutoInsertTest( -input: @" -@if (true) -{ -

$$

-} -", -expected: @" -@if (true) -{ -

$0

-} -"); + input: """ + @if (true) + { +

$$

+ } + """, + expected: """ + @if (true) + { +

$0

+ } + """); } [Fact] @@ -801,18 +798,18 @@ public void OnTypeCloseAngle_ClosesStandardHTMLTag_NestedStatement() public void OnTypeCloseAngle_TagNextToTag_NestedStatement() { RunAutoInsertTest( -input: @" -@if (true) -{ -

$$

-} -", -expected: @" -@if (true) -{ -

$0

-} -"); + input: """ + @if (true) + { +

$$

+ } + """, + expected: """ + @if (true) + { +

$0

+ } + """); } [Fact] @@ -820,58 +817,58 @@ public void OnTypeCloseAngle_TagNextToTag_NestedStatement() public void OnTypeCloseAngle_TagNextToVoidTag_NestedStatement() { RunAutoInsertTest( -input: @" -@if (true) -{ -

$$ -} -", -expected: @" -@if (true) -{ -

$0

-} -"); + input: """ + @if (true) + { +

$$ + } + """, + expected: """ + @if (true) + { +

$0

+ } + """); } [Fact] public void OnTypeCloseAngle_ClosesStandardHTMLTag() { RunAutoInsertTest( -input: @" -
$$ -", -expected: @" -
$0
-"); + input: """ +
$$ + """, + expected: """ +
$0
+ """); } [Fact] public void OnTypeCloseAngle_ClosesStandardHTMLTag_CodeBlock() { RunAutoInsertTest( -input: @" -@{ -
$$ -} -", -expected: @" -@{ -
$0
-} -"); + input: """ + @{ +
$$ + } + """, + expected: """ + @{ +
$0
+ } + """); } [Fact] public void OnTypeCloseAngle_ClosesVoidHTMLTag() { RunAutoInsertTest( -input: @" - $$ -", -expected: @" - -"); + input: """ + $$ + """, + expected: """ + + """); } [Fact] @@ -879,79 +876,73 @@ public void OnTypeCloseAngle_ClosesVoidHTMLTag() public void OnTypeCloseAngle_ClosesVoidHTMLTag_NestedStatement() { RunAutoInsertTest( -input: @" -@if (true) -{ - $$ -} -", -expected: @" -@if (true) -{ - -} -"); + input: """ + @if (true) + { + $$ + } + """, + expected: """ + @if (true) + { + + } + """); } [Fact] public void OnTypeCloseAngle_ClosesVoidHTMLTag_CodeBlock() { RunAutoInsertTest( -input: @" -@{ - $$ -} -", -expected: @" -@{ - -} -"); + input: """ + @{ + $$ + } + """, + expected: """ + @{ + + } + """); } [Fact] public void OnTypeCloseAngle_WithSlash_ClosesVoidHTMLTag() { RunAutoInsertTest( -input: @" - $$ -", -expected: @" - -"); + input: """ + $$ + """, + expected: """ + + """); } [Fact] public void OnTypeCloseAngle_WithSpace_ClosesVoidHTMLTag() { RunAutoInsertTest( -input: @" - $$ -", -expected: @" - -"); + input: """ + $$ + """, + expected: """ + + """); } [Fact] public void OnTypeCloseAngle_AutoInsertDisabled_Noops() { - Options = RazorLSPOptions.Default with { AutoClosingTags = false }; RunAutoInsertTest( -input: @" -
$$ -", -expected: @" -
-"); + input: """ +
$$ + """, + expected: """ +
+ """, + enableAutoClosingTags: false); } internal override IOnAutoInsertProvider CreateProvider() - { - var configService = StrictMock.Of(); - var optionsMonitor = new RazorLSPOptionsMonitor(configService, Options); - - var provider = new AutoClosingTagOnAutoInsertProvider(optionsMonitor); - return provider; - } + => new AutoClosingTagOnAutoInsertProvider(); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/CloseTextTagOnAutoInsertProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/CloseTextTagOnAutoInsertProviderTest.cs index 18fed07bd85..74b17456c4c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/CloseTextTagOnAutoInsertProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/CloseTextTagOnAutoInsertProviderTest.cs @@ -1,8 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.AutoInsert; using Xunit; using Xunit.Abstractions; @@ -14,36 +13,30 @@ public class CloseTextTagOnAutoInsertProviderTest(ITestOutputHelper testOutput) public void OnTypeCloseAngle_ClosesTextTag() { RunAutoInsertTest( -input: @" -@{ - $$ -} -", -expected: @" -@{ - $0 -} -"); + input: """ + @{ + $$ + } + """, + expected: """ + @{ + $0 + } + """); } [Fact] public void OnTypeCloseAngle_OutsideRazorBlock_DoesNotCloseTextTag() { RunAutoInsertTest( -input: @" - $$ -", -expected: @" - -"); + input: """ + $$ + """, + expected: """ + + """); } - internal override IOnAutoInsertProvider CreateProvider() - { - var configService = StrictMock.Of(); - var optionsMonitor = new RazorLSPOptionsMonitor(configService, RazorLSPOptions.Default); - - var provider = new CloseTextTagOnAutoInsertProvider(optionsMonitor); - return provider; - } + internal override IOnAutoInsertProvider CreateProvider() => + new CloseTextTagOnAutoInsertProvider(); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/OnAutoInsertEndpointTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/OnAutoInsertEndpointTest.NetFx.cs index 26b5acd8561..b5bb61530e2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/OnAutoInsertEndpointTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/OnAutoInsertEndpointTest.NetFx.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; using Microsoft.AspNetCore.Razor.LanguageServer.Test; +using Microsoft.CodeAnalysis.Razor.AutoInsert; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; @@ -27,7 +28,15 @@ public async Task Handle_SingleProvider_InvokesProvider() var optionsMonitor = GetOptionsMonitor(); var insertProvider = new TestOnAutoInsertProvider(">", canResolve: true); var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider], optionsMonitor, TestAdhocWorkspaceFactory.Instance, formattingService, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + formattingService, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -69,7 +78,15 @@ public async Task Handle_MultipleProviderSameTrigger_UsesSuccessful() ResolvedTextEdit = VsLspFactory.CreateTextEdit(position: (0, 0), string.Empty) }; var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider1, insertProvider2], optionsMonitor, TestAdhocWorkspaceFactory.Instance, formattingService, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider1, insertProvider2]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + formattingService, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -114,7 +131,15 @@ public async Task Handle_MultipleProviderSameTrigger_UsesFirstSuccessful() ResolvedTextEdit = VsLspFactory.CreateTextEdit(position: (0, 0), string.Empty) }; var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider1, insertProvider2], optionsMonitor, TestAdhocWorkspaceFactory.Instance, formattingService, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider1, insertProvider2]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + formattingService, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -151,7 +176,15 @@ public async Task Handle_NoApplicableProvider_CallsProviderAndReturnsNull() var optionsMonitor = GetOptionsMonitor(); var insertProvider = new TestOnAutoInsertProvider(">", canResolve: false); var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider], optionsMonitor, TestAdhocWorkspaceFactory.Instance, formattingService, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + formattingService, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -186,7 +219,15 @@ public async Task Handle_OnTypeFormattingOff_Html_CallsLanguageServer() var optionsMonitor = GetOptionsMonitor(formatOnType: false); var insertProvider = new TestOnAutoInsertProvider("<", canResolve: false); var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider], optionsMonitor, TestAdhocWorkspaceFactory.Instance, formattingService, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + formattingService, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -219,7 +260,15 @@ public async Task Handle_AutoInsertAttributeQuotesOff_Html_DoesNotCallLanguageSe var optionsMonitor = GetOptionsMonitor(autoInsertAttributeQuotes: false); var insertProvider = new TestOnAutoInsertProvider("<", canResolve: false); var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider], optionsMonitor, TestAdhocWorkspaceFactory.Instance, formattingService, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + formattingService, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -357,7 +406,15 @@ private async Task VerifyCSharpOnAutoInsertAsync(string input, string expected, var optionsMonitor = GetOptionsMonitor(); var insertProvider = new TestOnAutoInsertProvider("!!!", canResolve: false); var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider], optionsMonitor, TestAdhocWorkspaceFactory.Instance, formattingService, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + formattingService, + LoggerFactory); var text = codeDocument.Source.Text; var @params = new VSInternalDocumentOnAutoInsertParams() diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/OnAutoInsertEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/OnAutoInsertEndpointTest.cs index d5c57f9abff..6811b702468 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/OnAutoInsertEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/OnAutoInsertEndpointTest.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Test; -using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.AutoInsert; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; using Xunit.Abstractions; @@ -27,7 +27,16 @@ public async Task Handle_MultipleProviderUnmatchingTrigger_ReturnsNull() var optionsMonitor = GetOptionsMonitor(); var insertProvider1 = new TestOnAutoInsertProvider(">", canResolve: true); var insertProvider2 = new TestOnAutoInsertProvider("<", canResolve: true); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider1, insertProvider2], optionsMonitor, TestAdhocWorkspaceFactory.Instance, null!, LoggerFactory); + var autoInsertService = new AutoInsertService([insertProvider1, insertProvider2]); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + autoInsertService, + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + null!, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -61,7 +70,15 @@ public async Task Handle_DocumentNotFound_ReturnsNull() var optionsMonitor = GetOptionsMonitor(); var insertProvider = new TestOnAutoInsertProvider(">", canResolve: true); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider], optionsMonitor, TestAdhocWorkspaceFactory.Instance, null!, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + null!, + LoggerFactory); var uri = new Uri("file://path/test.razor"); var @params = new VSInternalDocumentOnAutoInsertParams() { @@ -97,7 +114,15 @@ public async Task Handle_UnsupportedCodeDocument_ReturnsNull() var documentContext = CreateDocumentContext(uri, codeDocument); var optionsMonitor = GetOptionsMonitor(); var insertProvider = new TestOnAutoInsertProvider(">", canResolve: true); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider], optionsMonitor, TestAdhocWorkspaceFactory.Instance, null!, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider]), + optionsMonitor, + TestAdhocWorkspaceFactory.Instance, + razorFormattingService: null!, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -131,7 +156,14 @@ public async Task Handle_OnTypeFormattingOff_CSharp_ReturnsNull() var documentContext = CreateDocumentContext(uri, codeDocument); var optionsMonitor = GetOptionsMonitor(formatOnType: false); var insertProvider = new TestOnAutoInsertProvider(">", canResolve: false); - var endpoint = new OnAutoInsertEndpoint(LanguageServerFeatureOptions, DocumentMappingService, languageServer, [insertProvider], optionsMonitor, TestAdhocWorkspaceFactory.Instance, null!, LoggerFactory); + var endpoint = new OnAutoInsertEndpoint( + LanguageServerFeatureOptions, + DocumentMappingService, + languageServer, + new AutoInsertService([insertProvider]), + optionsMonitor, TestAdhocWorkspaceFactory.Instance, + razorFormattingService: null!, + LoggerFactory); var @params = new VSInternalDocumentOnAutoInsertParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -161,12 +193,15 @@ private class TestOnAutoInsertProvider(string triggerCharacter, bool canResolve) public string TriggerCharacter { get; } = triggerCharacter; - // Disabling because [NotNullWhen] is available in two Assemblies and causes warnings - public bool TryResolveInsertion(Position position, FormattingContext context, [NotNullWhen(true)] out TextEdit? edit, out InsertTextFormat format) + public bool TryResolveInsertion( + Position position, + RazorCodeDocument codeDocument, + bool enableAutoClosingTags, + [NotNullWhen(true)] out VSInternalDocumentOnAutoInsertResponseItem? autoInsertEdit) { Called = true; - edit = ResolvedTextEdit!; - format = default; + autoInsertEdit = canResolve ? new() { TextEdit = ResolvedTextEdit!, TextEditFormat = InsertTextFormat.Plaintext } : null; + return canResolve; } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/RazorOnAutoInsertProviderTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/RazorOnAutoInsertProviderTestBase.cs index 2d77fa619a3..c5e809d705a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/RazorOnAutoInsertProviderTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/RazorOnAutoInsertProviderTestBase.cs @@ -6,14 +6,11 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.CodeAnalysis.Razor.Formatting; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.AutoInsert; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; -using Moq; using Xunit; using Xunit.Abstractions; @@ -28,7 +25,7 @@ protected RazorOnAutoInsertProviderTestBase(ITestOutputHelper testOutput) internal abstract IOnAutoInsertProvider CreateProvider(); - protected void RunAutoInsertTest(string input, string expected, int tabSize = 4, bool insertSpaces = true, string fileKind = default, IReadOnlyList tagHelpers = default) + protected void RunAutoInsertTest(string input, string expected, int tabSize = 4, bool insertSpaces = true, bool enableAutoClosingTags = true, string fileKind = default, IReadOnlyList tagHelpers = default) { // Arrange TestFileMarkupParser.GetPosition(input, out input, out var location); @@ -39,23 +36,14 @@ protected void RunAutoInsertTest(string input, string expected, int tabSize = 4, var path = "file:///path/to/document.razor"; var uri = new Uri(path); var codeDocument = CreateCodeDocument(source, uri.AbsolutePath, tagHelpers, fileKind: fileKind); - var options = new FormattingOptions() - { - TabSize = tabSize, - InsertSpaces = insertSpaces, - }; var provider = CreateProvider(); - using var context = FormattingContext.Create(uri, Mock.Of(MockBehavior.Strict), codeDocument, options, TestAdhocWorkspaceFactory.Instance); // Act - if (!provider.TryResolveInsertion(position, context, out var edit, out _)) - { - edit = null; - } + provider.TryResolveInsertion(position, codeDocument, enableAutoClosingTags: enableAutoClosingTags, out var edit); // Assert - var edited = edit is null ? source : ApplyEdit(source, edit); + var edited = edit is null ? source : ApplyEdit(source, edit.TextEdit); var actual = edited.ToString(); Assert.Equal(expected, actual); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionProviderTest.cs index bd93514e88f..2f668586916 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionProviderTest.cs @@ -333,7 +333,7 @@ private static RazorCodeActionContext CreateRazorCodeActionContext( codeDocument.SetCSharpDocument(csharpDocumentWithDiagnostic); var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync() == Task.FromResult(codeDocument) && + document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text) && document.Project.GetTagHelpersAsync(It.IsAny()) == new ValueTask>(tagHelpers), MockBehavior.Strict); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/TypeAccessibilityCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/TypeAccessibilityCodeActionProviderTest.cs index 610e0462269..86d1b8239e5 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/TypeAccessibilityCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/TypeAccessibilityCodeActionProviderTest.cs @@ -464,7 +464,7 @@ private static RazorCodeActionContext CreateRazorCodeActionContext( codeDocument.SetCSharpDocument(csharpDocumentWithDiagnostic); var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync() == Task.FromResult(codeDocument) && + document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text) && document.Project.GetTagHelpersAsync(It.IsAny()) == new ValueTask>(tagHelpers), MockBehavior.Strict); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs index 297848bba6c..8bcc81013b1 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs @@ -152,7 +152,7 @@ private static RazorCodeActionContext CreateRazorCodeActionContext( var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, FileKinds.Component, importSources: default, tagHelpers); var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync() == Task.FromResult(codeDocument) && + document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text) && document.Project.GetTagHelpersAsync(It.IsAny()) == new ValueTask>(tagHelpers), MockBehavior.Strict); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ComponentAccessibilityCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ComponentAccessibilityCodeActionProviderTest.cs index 56874c402da..2a0e033c4b5 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ComponentAccessibilityCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ComponentAccessibilityCodeActionProviderTest.cs @@ -464,7 +464,7 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP codeDocument.SetCSharpDocument(csharpDocumentWithDiagnostic); var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync() == Task.FromResult(codeDocument) && + document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text) && document.Project.GetTagHelpersAsync(It.IsAny()) == new ValueTask>(tagHelpers), MockBehavior.Strict); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionProviderTest.cs index a0995f885a9..698d462b310 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionProviderTest.cs @@ -396,7 +396,7 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP codeDocument.SetSyntaxTree(syntaxTree); var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync() == Task.FromResult(codeDocument) && + document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text), MockBehavior.Strict); var sourceText = SourceText.From(text); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentPresentation/TextDocumentUriPresentationEndpointTests.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentPresentation/TextDocumentUriPresentationEndpointTests.cs index 52ee2ceb54a..3b814f35d33 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentPresentation/TextDocumentUriPresentationEndpointTests.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentPresentation/TextDocumentUriPresentationEndpointTests.cs @@ -216,7 +216,7 @@ public async Task Handle_NoTypeNameIdentifier_ReturnsNull() var builder = TagHelperDescriptorBuilder.Create("MyTagHelper", "MyAssembly"); var tagHelperDescriptor = builder.Build(); - var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync() == Task.FromResult(componentCodeDocument), MockBehavior.Strict); + var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(componentCodeDocument), MockBehavior.Strict); var uri = new Uri("file://path/test.razor"); var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument); @@ -262,7 +262,7 @@ public async Task Handle_MultipleUris_ReturnsNull() var documentMappingService = Mock.Of( s => s.GetLanguageKind(codeDocument, It.IsAny(), It.IsAny()) == RazorLanguageKind.Html, MockBehavior.Strict); - var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync() == Task.FromResult(codeDocument), MockBehavior.Strict); + var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync(false) == Task.FromResult(codeDocument), MockBehavior.Strict); var uri = new Uri("file://path/test.razor"); var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument); @@ -313,7 +313,7 @@ public async Task Handle_NotComponent_ReturnsNull() var documentMappingService = Mock.Of( s => s.GetLanguageKind(codeDocument, It.IsAny(), It.IsAny()) == RazorLanguageKind.Html, MockBehavior.Strict); - var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync() == Task.FromResult(codeDocument), MockBehavior.Strict); + var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync(false) == Task.FromResult(codeDocument), MockBehavior.Strict); var droppedUri = new Uri("file:///c:/path/MyTagHelper.cshtml"); var uri = new Uri("file://path/test.razor"); @@ -422,7 +422,7 @@ public async Task Handle_CSharp_ReturnsNull() s => s.GetLanguageKind(codeDocument, It.IsAny(), It.IsAny()) == RazorLanguageKind.CSharp && s.TryMapToGeneratedDocumentRange(csharpDocument, It.IsAny(), out projectedRange) == true, MockBehavior.Strict); - var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync() == Task.FromResult(codeDocument), MockBehavior.Strict); + var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync(false) == Task.FromResult(codeDocument), MockBehavior.Strict); var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument); var response = (WorkspaceEdit?)null; @@ -466,7 +466,7 @@ public async Task Handle_DocumentNotFound_ReturnsNull() var documentMappingService = Mock.Of( s => s.GetLanguageKind(codeDocument, It.IsAny(), It.IsAny()) == RazorLanguageKind.Html, MockBehavior.Strict); - var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync() == Task.FromResult(codeDocument), MockBehavior.Strict); + var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync(false) == Task.FromResult(codeDocument), MockBehavior.Strict); var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument); var response = (WorkspaceEdit?)null; @@ -511,7 +511,7 @@ public async Task Handle_UnsupportedCodeDocument_ReturnsNull() var documentMappingService = Mock.Of( s => s.GetLanguageKind(codeDocument, It.IsAny(), It.IsAny()) == RazorLanguageKind.Html, MockBehavior.Strict); - var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync() == Task.FromResult(codeDocument), MockBehavior.Strict); + var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync(false) == Task.FromResult(codeDocument), MockBehavior.Strict); var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument); var response = new WorkspaceEdit(); @@ -555,7 +555,7 @@ public async Task Handle_NoUris_ReturnsNull() var documentMappingService = Mock.Of( s => s.GetLanguageKind(codeDocument, It.IsAny(), It.IsAny()) == RazorLanguageKind.Html, MockBehavior.Strict); - var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync() == Task.FromResult(codeDocument), MockBehavior.Strict); + var documentSnapshot = Mock.Of(s => s.GetGeneratedOutputAsync(false) == Task.FromResult(codeDocument), MockBehavior.Strict); var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument); var response = (WorkspaceEdit?)null; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs index ba5973a36ef..f35c3dffeeb 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs @@ -132,7 +132,13 @@ private static FormattingContext CreateFormattingContext(SourceText source, int InsertSpaces = insertSpaces, }; - var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, TestAdhocWorkspaceFactory.Instance); + var context = FormattingContext.Create( + uri, + documentSnapshot, + codeDocument, + options, + new LspFormattingCodeDocumentProvider(), + TestAdhocWorkspaceFactory.Instance); return context; } @@ -146,7 +152,7 @@ private static (RazorCodeDocument, IDocumentSnapshot) CreateCodeDocumentAndSnaps var documentSnapshot = new Mock(MockBehavior.Strict); documentSnapshot - .Setup(d => d.GetGeneratedOutputAsync()) + .Setup(d => d.GetGeneratedOutputAsync(It.IsAny())) .ReturnsAsync(codeDocument); documentSnapshot .Setup(d => d.TargetPath) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs index 60adc04a1fc..7ca4655ac5e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs @@ -129,7 +129,13 @@ private static FormattingContext CreateFormattingContext(SourceText source, int InsertSpaces = insertSpaces, }; - var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, TestAdhocWorkspaceFactory.Instance); + var context = FormattingContext.Create( + uri, + documentSnapshot, + codeDocument, + options, + new LspFormattingCodeDocumentProvider(), + TestAdhocWorkspaceFactory.Instance); return context; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs index 62221cac30b..dc8c6ddd2be 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs @@ -287,7 +287,7 @@ internal static IDocumentSnapshot CreateDocumentSnapshot(string path, ImmutableA { var documentSnapshot = new Mock(MockBehavior.Strict); documentSnapshot - .Setup(d => d.GetGeneratedOutputAsync()) + .Setup(d => d.GetGeneratedOutputAsync(It.IsAny())) .ReturnsAsync(codeDocument); documentSnapshot .Setup(d => d.FilePath) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs index 151cfc51df9..21b381771c8 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs @@ -57,6 +57,9 @@ public static async Task CreateWithFullSupportAsync( new FormattingContentValidationPass(mappingService, loggerFactory), }; - return new RazorFormattingService(passes, TestAdhocWorkspaceFactory.Instance); + return new RazorFormattingService( + passes, + new LspFormattingCodeDocumentProvider(), + TestAdhocWorkspaceFactory.Instance); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs index 411d6405171..4430868d72e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs @@ -923,7 +923,7 @@ public void Increment(){ var sourceText = SourceText.From(txt); var snapshot = Mock.Of(d => - d.GetGeneratedOutputAsync() == Task.FromResult(codeDocument) && + d.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && d.FilePath == path && d.FileKind == FileKinds.Component && d.GetTextAsync() == Task.FromResult(sourceText) && diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs index 748262ec53a..455da63ae60 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs @@ -937,7 +937,7 @@ private static DocumentContext CreateDocumentContext( .SetupGet(x => x.Project) .Returns(projectSnapshot.Object); documentSnapshotMock - .Setup(x => x.GetGeneratedOutputAsync()) + .Setup(x => x.GetGeneratedOutputAsync(It.IsAny())) .ReturnsAsync(document); documentSnapshotMock .Setup(x => x.GetTextAsync()) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestDocumentSnapshot.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestDocumentSnapshot.cs index 65c0670b26c..645bec36bae 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestDocumentSnapshot.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestDocumentSnapshot.cs @@ -79,7 +79,7 @@ public TestDocumentSnapshot(ProjectSnapshot projectSnapshot, DocumentState docum public HostDocument HostDocument => State.HostDocument; - public override Task GetGeneratedOutputAsync() + public override Task GetGeneratedOutputAsync(bool _) { if (_codeDocument is null) { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RazorComponentDefinitionServiceTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RazorComponentDefinitionServiceTest.cs index 22c43a1a755..041b95c6eee 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RazorComponentDefinitionServiceTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RazorComponentDefinitionServiceTest.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.GoToDefinition; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol;