Skip to content

Commit 148eb97

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

File tree

10 files changed

+593
-27
lines changed

10 files changed

+593
-27
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

+30-27
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -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.Remote.Razor/CodeActions/RemoteServices.cs

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ internal sealed class OOPExtractToCodeBehindCodeActionProvider(ILoggerFactory lo
4242
[Export(typeof(IRazorCodeActionProvider)), Shared]
4343
internal sealed class OOPExtractToComponentCodeActionProvider : ExtractToComponentCodeActionProvider;
4444

45+
[Export(typeof(IRazorCodeActionProvider)), Shared]
46+
[method: ImportingConstructor]
47+
internal sealed class OOPSimplifyTagToSelfClosingCodeActionProvider(ILoggerFactory loggerFactory) : SimplifyTagToSelfClosingCodeActionProvider(loggerFactory);
48+
4549
[Export(typeof(IRazorCodeActionProvider)), Shared]
4650
[method: ImportingConstructor]
4751
internal sealed class OOPComponentAccessibilityCodeActionProvider(IFileSystem fileSystem) : ComponentAccessibilityCodeActionProvider(fileSystem);
@@ -79,6 +83,10 @@ internal sealed class OOPExtractToCodeBehindCodeActionResolver(
7983
[method: ImportingConstructor]
8084
internal sealed class OOPExtractToComponentCodeActionResolver(LanguageServerFeatureOptions languageServerFeatureOptions) : ExtractToComponentCodeActionResolver(languageServerFeatureOptions);
8185

86+
[Export(typeof(IRazorCodeActionResolver)), Shared]
87+
[method: ImportingConstructor]
88+
internal sealed class OOPSimplifyTagToSelfClosingCodeActionResolver() : SimplifyTagToSelfClosingCodeActionResolver();
89+
8290
[Export(typeof(IRazorCodeActionResolver)), Shared]
8391
[method: ImportingConstructor]
8492
internal sealed class OOPCreateComponentCodeActionResolver(LanguageServerFeatureOptions languageServerFeatureOptions) : CreateComponentCodeActionResolver(languageServerFeatureOptions);

0 commit comments

Comments
 (0)