Skip to content

Commit 4fb2a6b

Browse files
committed
#11793 Simplify tag to self-closing code action
1 parent 4a69788 commit 4fb2a6b

File tree

23 files changed

+631
-0
lines changed

23 files changed

+631
-0
lines changed

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxListOfT.cs

+13
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,19 @@ public bool Any()
318318
return Node != null;
319319
}
320320

321+
public bool Any(Func<TNode, bool> predicate)
322+
{
323+
foreach (var node in this)
324+
{
325+
if (predicate(node))
326+
{
327+
return true;
328+
}
329+
}
330+
331+
return false;
332+
}
333+
321334
public SyntaxList<TNode> Where(Func<TNode, bool> predicate)
322335
{
323336
using var _ = SyntaxListBuilderPool.GetPooledBuilder<TNode>(out var builder);

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ public static void AddCodeActionsServices(this IServiceCollection services)
156156
services.AddSingleton<IRazorCodeActionResolver, PromoteUsingCodeActionResolver>();
157157
services.AddSingleton<IRazorCodeActionProvider, WrapAttributesCodeActionProvider>();
158158
services.AddSingleton<IRazorCodeActionResolver, WrapAttributesCodeActionResolver>();
159+
services.AddSingleton<IRazorCodeActionProvider, SimplifyTagToSelfClosingCodeActionProvider>();
160+
services.AddSingleton<IRazorCodeActionResolver, SimplifyTagToSelfClosingCodeActionResolver>();
159161

160162
// Html Code actions
161163
services.AddSingleton<IHtmlCodeActionProvider, HtmlCodeActionProvider>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Microsoft.CodeAnalysis.Razor.CodeActions.Models;
7+
8+
internal sealed class SimplifyTagToSelfClosingCodeActionParams
9+
{
10+
[JsonPropertyName("start")]
11+
public int Start { get; set; }
12+
13+
[JsonPropertyName("end")]
14+
public int End { get; set; }
15+
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/RazorCodeActionFactory.cs

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal static class RazorCodeActionFactory
1616
private readonly static Guid s_createComponentFromTagTelemetryId = new("a28e0baa-a4d5-4953-a817-1db586035841");
1717
private readonly static Guid s_createExtractToCodeBehindTelemetryId = new("f63167f7-fdc6-450f-8b7b-b240892f4a27");
1818
private readonly static Guid s_createExtractToComponentTelemetryId = new("af67b0a3-f84b-4808-97a7-b53e85b22c64");
19+
private readonly static Guid s_simplifyComponentTelemetryId = new("2207f68c-419e-4baa-8493-2e7769e5c91d");
1920
private readonly static Guid s_generateMethodTelemetryId = new("c14fa003-c752-45fc-bb29-3a123ae5ecef");
2021
private readonly static Guid s_generateAsyncMethodTelemetryId = new("9058ca47-98e2-4f11-bf7c-a16a444dd939");
2122
private readonly static Guid s_promoteUsingDirectiveTelemetryId = new("751f9012-e37b-444a-9211-b4ebce91d96e");
@@ -112,6 +113,19 @@ public static RazorVSInternalCodeAction CreateExtractToComponent(RazorCodeAction
112113
return codeAction;
113114
}
114115

116+
public static RazorVSInternalCodeAction CreateSimplifyTagToSelfClosing(RazorCodeActionResolutionParams resolutionParams)
117+
{
118+
var data = JsonSerializer.SerializeToElement(resolutionParams);
119+
var codeAction = new RazorVSInternalCodeAction()
120+
{
121+
Title = SR.Simplify_Tag_To_SelfClosing_Title,
122+
Data = data,
123+
TelemetryId = s_simplifyComponentTelemetryId,
124+
Name = LanguageServerConstants.CodeActions.SimplifyTagToSelfClosingAction,
125+
};
126+
return codeAction;
127+
}
128+
115129
public static RazorVSInternalCodeAction CreateGenerateMethod(VSTextDocumentIdentifier textDocument, Uri? delegatedDocumentUri, string methodName, string? eventParameterType)
116130
{
117131
var @params = new GenerateMethodCodeActionParams
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Razor.Language;
10+
using Microsoft.AspNetCore.Razor.Language.Components;
11+
using Microsoft.AspNetCore.Razor.Language.Syntax;
12+
using Microsoft.AspNetCore.Razor.Threading;
13+
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
14+
using Microsoft.CodeAnalysis.Razor.GoToDefinition;
15+
using Microsoft.CodeAnalysis.Razor.Logging;
16+
using Microsoft.CodeAnalysis.Razor.Protocol;
17+
using Microsoft.VisualStudio.LanguageServer.Protocol;
18+
19+
namespace Microsoft.CodeAnalysis.Razor.CodeActions.Razor;
20+
21+
internal class SimplifyTagToSelfClosingCodeActionProvider(ILoggerFactory loggerFactory) : IRazorCodeActionProvider
22+
{
23+
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<SimplifyTagToSelfClosingCodeActionProvider>();
24+
25+
private const string RenderFragmentTypeName = "Microsoft.AspNetCore.Components.RenderFragment";
26+
private const string GenericRenderFragmentTypeName = "Microsoft.AspNetCore.Components.RenderFragment<";
27+
28+
public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
29+
{
30+
if (context.HasSelection)
31+
{
32+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
33+
}
34+
35+
// Make sure we're in the right kind and part of file
36+
if (!FileKinds.IsComponent(context.CodeDocument.FileKind))
37+
{
38+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
39+
}
40+
41+
if (context.LanguageKind != RazorLanguageKind.Html)
42+
{
43+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
44+
}
45+
46+
// Caret must be inside a markup element
47+
if (context.ContainsDiagnostic(ComponentDiagnosticFactory.UnexpectedMarkupElement.Id) ||
48+
context.ContainsDiagnostic(ComponentDiagnosticFactory.UnexpectedClosingTag.Id))
49+
{
50+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
51+
}
52+
53+
var syntaxTree = context.CodeDocument.GetSyntaxTree();
54+
if (syntaxTree?.Root is null)
55+
{
56+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
57+
}
58+
59+
var owner = syntaxTree.Root.FindInnermostNode(context.StartAbsoluteIndex, includeWhitespace: !context.HasSelection)?.FirstAncestorOrSelf<MarkupTagHelperElementSyntax>();
60+
if (owner is not MarkupTagHelperElementSyntax markupElementSyntax)
61+
{
62+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
63+
}
64+
65+
// Check whether the code action is applicable to the element
66+
if (!IsApplicableTo(context, markupElementSyntax))
67+
{
68+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
69+
}
70+
71+
// Provide code action to simplify
72+
var actionParams = new SimplifyTagToSelfClosingCodeActionParams
73+
{
74+
Start = markupElementSyntax.StartTag.CloseAngle.SpanStart,
75+
End = markupElementSyntax.EndTag.CloseAngle.EndPosition,
76+
};
77+
78+
var resolutionParams = new RazorCodeActionResolutionParams()
79+
{
80+
TextDocument = context.Request.TextDocument,
81+
Action = LanguageServerConstants.CodeActions.SimplifyTagToSelfClosingAction,
82+
Language = RazorLanguageKind.Razor,
83+
DelegatedDocumentUri = context.DelegatedDocumentUri,
84+
Data = actionParams,
85+
};
86+
87+
var codeAction = RazorCodeActionFactory.CreateSimplifyTagToSelfClosing(resolutionParams);
88+
return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([codeAction]);
89+
}
90+
91+
internal bool IsApplicableTo(RazorCodeActionContext context, MarkupTagHelperElementSyntax markupElementSyntax)
92+
{
93+
// Check whether the element is self-closing
94+
if (markupElementSyntax is not (
95+
{ EndTag.CloseAngle.IsMissing: false } and
96+
{ StartTag.ForwardSlash: null } and
97+
{ StartTag.CloseAngle.IsMissing: false }
98+
))
99+
{
100+
return false;
101+
}
102+
103+
// Check whether the element has any non-whitespace content
104+
if (markupElementSyntax is { Body: { } body } && body.Any(static n => !n.ContainsOnlyWhitespace()))
105+
{
106+
return false;
107+
}
108+
109+
// Get symbols for the markup element
110+
if (!RazorComponentDefinitionHelpers.TryGetBoundTagHelpers(context.CodeDocument, markupElementSyntax.StartTag.Name.SpanStart, true, _logger, out var boundTagHelper, out _))
111+
{
112+
return false;
113+
}
114+
115+
if (!boundTagHelper.IsComponentTagHelper)
116+
{
117+
return false;
118+
}
119+
120+
// Check whether the Component must have children
121+
if (boundTagHelper.BoundAttributes.Any(attribute =>
122+
// Attribute has `EditorRequired` flag
123+
attribute is { TypeName: string typeName, IsEditorRequired: true } &&
124+
125+
// It has type of a `RenderFragment`
126+
(typeName == RenderFragmentTypeName || typeName.StartsWith(GenericRenderFragmentTypeName, StringComparison.Ordinal)) &&
127+
128+
// It is not set or bound as an attribute
129+
!markupElementSyntax.TagHelperInfo!.BindingResult.Attributes.Any(a =>
130+
a.Key == attribute.Name ||
131+
(a.Key.StartsWith("@bind-", StringComparison.Ordinal) && a.Key.AsSpan("@bind-".Length).Equals(attribute.Name, StringComparison.Ordinal))
132+
)
133+
))
134+
{
135+
return false;
136+
}
137+
138+
return true;
139+
}
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Text.Json;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Razor.Language;
8+
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
9+
using Microsoft.CodeAnalysis.Razor.Formatting;
10+
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
11+
using Microsoft.CodeAnalysis.Razor.Protocol;
12+
using Microsoft.VisualStudio.LanguageServer.Protocol;
13+
14+
namespace Microsoft.CodeAnalysis.Razor.CodeActions.Razor;
15+
16+
internal class SimplifyTagToSelfClosingCodeActionResolver() : IRazorCodeActionResolver
17+
{
18+
public string Action => LanguageServerConstants.CodeActions.SimplifyTagToSelfClosingAction;
19+
20+
public async Task<WorkspaceEdit?> ResolveAsync(DocumentContext documentContext, JsonElement data, RazorFormattingOptions options, CancellationToken cancellationToken)
21+
{
22+
if (data.ValueKind == JsonValueKind.Undefined)
23+
{
24+
return null;
25+
}
26+
27+
var actionParams = JsonSerializer.Deserialize<SimplifyTagToSelfClosingCodeActionParams>(data.GetRawText());
28+
if (actionParams is null)
29+
{
30+
return null;
31+
}
32+
33+
var componentDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
34+
if (componentDocument.IsUnsupported())
35+
{
36+
return null;
37+
}
38+
39+
var text = componentDocument.Source.Text;
40+
var removeRange = text.GetRange(actionParams.Start, actionParams.End);
41+
42+
var documentChanges = new TextDocumentEdit[]
43+
{
44+
new TextDocumentEdit
45+
{
46+
TextDocument = new OptionalVersionedTextDocumentIdentifier { Uri = documentContext.Uri },
47+
Edits =
48+
[
49+
new TextEdit
50+
{
51+
NewText = " />",
52+
Range = removeRange,
53+
}
54+
],
55+
}
56+
};
57+
58+
return new WorkspaceEdit
59+
{
60+
DocumentChanges = documentChanges,
61+
};
62+
}
63+
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public static class CodeActions
4343

4444
public const string ExtractToNewComponentAction = "ExtractToNewComponent";
4545

46+
public const string SimplifyTagToSelfClosingAction = "SimplifyTagToSelfClosing";
47+
4648
public const string CreateComponentFromTag = "CreateComponentFromTag";
4749

4850
public const string AddUsing = "AddUsing";

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx

+3
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,7 @@
211211
<data name="Unsupported_razor_project_info_version_encountered" xml:space="preserve">
212212
<value>Unsupported razor project info version encounted.</value>
213213
</data>
214+
<data name="Simplify_Tag_To_SelfClosing_Title" xml:space="preserve">
215+
<value>Simplify tag to self-closing</value>
216+
</data>
214217
</root>

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)