Skip to content

Commit 598c0c4

Browse files
authored
Generate EventHandler CodeAction: Add Async Version and Fully Qualified Type Params (#8962)
* Add fully qualified parameter type to generate event handler * Add generate async event handler code action * other clean up * naming violation fix * PR feedback * Guard against setting event parameter * simplify guard against setting event parameter * bit more clean up
1 parent a419a6f commit 598c0c4

File tree

22 files changed

+504
-111
lines changed

22 files changed

+504
-111
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/GenerateMethodCodeActionParams.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ internal class GenerateMethodCodeActionParams
99
{
1010
public required Uri Uri { get; set; }
1111
public required string MethodName { get; set; }
12+
public required string EventName { get; set;}
13+
public required bool IsAsync { get; set; }
1214
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/CodeBlockService.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ internal static class CodeBlockService
2020
/// <param name="code">
2121
/// The <see cref="RazorCodeDocument"/> of the file where the generated method will be placed.
2222
/// </param>
23-
/// <param name="templateWithMethodName">
23+
/// <param name="templateWithMethodSignature">
2424
/// The skeleton of the generated method where a <see cref="FormattingUtilities.Indent"/> should be placed
2525
/// anywhere that needs to have some indenting, <see cref="FormattingUtilities.InitialIndent"/> anywhere that
26-
/// needs some initial indenting, and a '$$MethodName$$' for where the method name should be placed.
26+
/// needs some initial indenting.
2727
/// It should look something like:
28-
/// <see cref="FormattingUtilities.InitialIndent"/><see cref="FormattingUtilities.Indent"/>public void $$MethodName$$()
28+
/// <see cref="FormattingUtilities.InitialIndent"/><see cref="FormattingUtilities.Indent"/>public void MethodName()
2929
/// <see cref="FormattingUtilities.InitialIndent"/><see cref="FormattingUtilities.Indent"/>{
3030
/// <see cref="FormattingUtilities.InitialIndent"/><see cref="FormattingUtilities.Indent"/><see cref="FormattingUtilities.Indent"/>throw new NotImplementedException();
3131
/// <see cref="FormattingUtilities.InitialIndent"/><see cref="FormattingUtilities.Indent"/>}
@@ -36,7 +36,7 @@ internal static class CodeBlockService
3636
/// <returns>
3737
/// A <see cref="TextEdit"/> that will place the formatted generated method within a @code block in the file.
3838
/// </returns>
39-
public static TextEdit CreateFormattedTextEdit(RazorCodeDocument code, string templateWithMethodName, RazorLSPOptions options)
39+
public static TextEdit CreateFormattedTextEdit(RazorCodeDocument code, string templateWithMethodSignature, RazorLSPOptions options)
4040
{
4141
var csharpCodeBlock = code.GetSyntaxTree().Root.DescendantNodes()
4242
.Select(RazorSyntaxFacts.TryGetCSharpCodeFromCodeBlock)
@@ -46,7 +46,7 @@ public static TextEdit CreateFormattedTextEdit(RazorCodeDocument code, string te
4646
|| !csharpCodeBlock.Children.TryGetCloseBraceNode(out var closeBrace))
4747
{
4848
// No well-formed @code block exists. Generate the method within an @code block at the end of the file.
49-
var indentedMethod = FormattingUtilities.AddIndentationToMethod(templateWithMethodName, options, startingIndent: 0);
49+
var indentedMethod = FormattingUtilities.AddIndentationToMethod(templateWithMethodSignature, options, startingIndent: 0);
5050
var textWithCodeBlock = "@code {" + Environment.NewLine + indentedMethod + Environment.NewLine + "}";
5151
var lastCharacterLocation = code.Source.Lines.GetLocation(code.Source.Length - 1);
5252
var insertCharacterIndex = 0;
@@ -82,7 +82,7 @@ public static TextEdit CreateFormattedTextEdit(RazorCodeDocument code, string te
8282
closeBraceLocation.LineIndex,
8383
insertLineLocation,
8484
options,
85-
templateWithMethodName);
85+
templateWithMethodSignature);
8686

8787
var insertCharacter = openBraceLocation.LineIndex == closeBraceLocation.LineIndex
8888
? closeBraceLocation.CharacterIndex
Lines changed: 29 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

4-
using System;
54
using System.Collections.Generic;
65
using System.Diagnostics.CodeAnalysis;
76
using System.Linq;
@@ -12,7 +11,6 @@
1211
using Microsoft.AspNetCore.Razor.Language.Legacy;
1312
using Microsoft.AspNetCore.Razor.Language.Syntax;
1413
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
15-
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
1614
using Microsoft.CodeAnalysis;
1715
using SyntaxFacts = Microsoft.CodeAnalysis.CSharp.SyntaxFacts;
1816
using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
@@ -39,33 +37,27 @@ internal class GenerateMethodCodeActionProvider : IRazorCodeActionProvider
3937
return s_emptyResult;
4038
}
4139

42-
if (IsGenerateEventHandlerValid(owner, context, out var @params))
40+
if (IsGenerateEventHandlerValid(owner, out var methodName, out var eventName))
4341
{
44-
return Task.FromResult<IReadOnlyList<RazorVSInternalCodeAction>?>(CreateCodeAction(@params));
42+
var uri = context.Request.TextDocument.Uri;
43+
var codeActions = new List<RazorVSInternalCodeAction>()
44+
{
45+
RazorCodeActionFactory.CreateGenerateMethod(uri, methodName, eventName),
46+
RazorCodeActionFactory.CreateAsyncGenerateMethod(uri, methodName, eventName)
47+
};
48+
return Task.FromResult<IReadOnlyList<RazorVSInternalCodeAction>?>(codeActions);
4549
}
4650

4751
return s_emptyResult;
4852
}
4953

50-
private static List<RazorVSInternalCodeAction> CreateCodeAction(GenerateMethodCodeActionParams @params)
51-
{
52-
var resolutionParams = new RazorCodeActionResolutionParams()
53-
{
54-
Action = LanguageServerConstants.CodeActions.GenerateEventHandler,
55-
Language = LanguageServerConstants.CodeActions.Languages.Razor,
56-
Data = @params,
57-
};
58-
59-
var codeAction = RazorCodeActionFactory.CreateGenerateMethod(resolutionParams);
60-
return new List<RazorVSInternalCodeAction> { codeAction };
61-
}
62-
6354
private static bool IsGenerateEventHandlerValid(
6455
SyntaxNode owner,
65-
RazorCodeActionContext context,
66-
[NotNullWhen(true)] out GenerateMethodCodeActionParams? @params)
56+
[NotNullWhen(true)] out string? methodName,
57+
[NotNullWhen(true)] out string? eventName)
6758
{
68-
@params = null;
59+
methodName = null;
60+
eventName = null;
6961

7062
// The owner should have a SyntaxKind of CSharpExpressionLiteral or MarkupTextLiteral.
7163
// MarkupTextLiteral if the cursor is directly before the first letter of the method name.
@@ -75,51 +67,34 @@ private static bool IsGenerateEventHandlerValid(
7567
return false;
7668
}
7769

78-
var parent = owner.Kind == SyntaxKind.CSharpExpressionLiteral ? owner.Parent.Parent : owner.Parent;
79-
if (parent.Kind != SyntaxKind.MarkupTagHelperDirectiveAttribute)
70+
// We want to get MarkupTagHelperDirectiveAttribute since this has information about the event name.
71+
// Hierarchy:
72+
// MarkupTagHelperDirectiveAttribute > MarkupTextLiteral
73+
// or
74+
// MarkupTagHelperDirectiveAttribute > MarkupTagHelperAttributeValue > CSharpExpressionLiteral
75+
var commonParent = owner.Kind == SyntaxKind.CSharpExpressionLiteral ? owner.Parent.Parent : owner.Parent;
76+
if (commonParent is not MarkupTagHelperDirectiveAttributeSyntax markupTagHelperDirectiveAttribute)
8077
{
8178
return false;
8279
}
8380

84-
var methodName = string.Empty;
85-
if (owner.Kind == SyntaxKind.CSharpExpressionLiteral)
81+
if (markupTagHelperDirectiveAttribute.TagHelperAttributeInfo.ParameterName is not null)
8682
{
87-
var content = owner.GetContent();
88-
if (!SyntaxFacts.IsValidIdentifier(content))
89-
{
90-
return false;
91-
}
92-
93-
methodName = content;
94-
}
95-
else
96-
{
97-
var children = parent.ChildNodes();
98-
foreach (var child in children)
99-
{
100-
if (child.Kind == SyntaxKind.MarkupTagHelperAttributeValue)
101-
{
102-
var content = child.GetContent();
103-
if (SyntaxFacts.IsValidIdentifier(content))
104-
{
105-
methodName = content;
106-
break;
107-
}
108-
}
109-
}
83+
// An event parameter is being set instead of the event handler e.g.
84+
// <button @onclick:preventDefault=SomeValue/>, this is not a generate event handler scenario.
85+
return false;
11086
}
11187

112-
if (methodName.IsNullOrEmpty())
88+
// The TagHelperAttributeInfo Name property includes the '@' in the beginning so exclude it.
89+
eventName = markupTagHelperDirectiveAttribute.TagHelperAttributeInfo.Name[1..];
90+
91+
var content = markupTagHelperDirectiveAttribute.Value.GetContent();
92+
if (!SyntaxFacts.IsValidIdentifier(content))
11393
{
11494
return false;
11595
}
11696

117-
@params = new GenerateMethodCodeActionParams()
118-
{
119-
Uri = context.Request.TextDocument.Uri,
120-
MethodName = methodName,
121-
};
122-
97+
methodName = content;
12398
return true;
12499
}
125100
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Razor.Language;
10+
using Microsoft.AspNetCore.Razor.Language.Components;
1011
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
1112
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
1213
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
@@ -26,8 +27,11 @@ internal class GenerateMethodCodeActionResolver : IRazorCodeActionResolver
2627
private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor;
2728

2829
private static readonly string s_beginningIndents = $"{FormattingUtilities.InitialIndent}{FormattingUtilities.Indent}";
30+
private static readonly string s_returnType = "$$ReturnType$$";
31+
private static readonly string s_methodName = "$$MethodName$$";
32+
private static readonly string s_eventArgs = "$$EventArgs$$";
2933
private static readonly string s_generateMethodTemplate =
30-
$"{s_beginningIndents}private void $$MethodName$$(){Environment.NewLine}" +
34+
$"{s_beginningIndents}private {s_returnType} {s_methodName}({s_eventArgs}){Environment.NewLine}" +
3135
s_beginningIndents + "{" + Environment.NewLine +
3236
$"{s_beginningIndents}{FormattingUtilities.Indent}throw new System.NotImplementedException();{Environment.NewLine}" +
3337
s_beginningIndents + "}";
@@ -59,7 +63,8 @@ public GenerateMethodCodeActionResolver(DocumentContextFactory documentContextFa
5963
return null;
6064
}
6165

62-
var templateWithMethodName = s_generateMethodTemplate.Replace("$$MethodName$$", actionParams.MethodName);
66+
var templateWithMethodSignature = PopulateMethodSignature(documentContext, actionParams);
67+
6368
var code = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
6469
var uriPath = FilePathNormalizer.Normalize(actionParams.Uri.GetAbsoluteOrUNCPath());
6570
var razorClassName = Path.GetFileNameWithoutExtension(uriPath);
@@ -69,7 +74,7 @@ public GenerateMethodCodeActionResolver(DocumentContextFactory documentContextFa
6974
|| razorClassName is null
7075
|| !code.TryComputeNamespace(fallbackToRootNamespace: true, out var razorNamespace))
7176
{
72-
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodName);
77+
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodSignature);
7378
}
7479

7580
var content = File.ReadAllText(codeBehindPath);
@@ -79,20 +84,20 @@ public GenerateMethodCodeActionResolver(DocumentContextFactory documentContextFa
7984
if (@namespace is null)
8085
{
8186
// The code behind file is malformed, generate the code in the razor file instead.
82-
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodName);
87+
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodSignature);
8388
}
8489

8590
var @class = ((BaseNamespaceDeclarationSyntax)@namespace).Members
8691
.FirstOrDefault(m => m is ClassDeclarationSyntax { } @class && razorClassName == @class.Identifier.Text);
8792
if (@class is null)
8893
{
8994
// The code behind file is malformed, generate the code in the razor file instead.
90-
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodName);
95+
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodSignature);
9196
}
9297

9398
var classLocationLineSpan = @class.GetLocation().GetLineSpan();
9499
var formattedMethod = FormattingUtilities.AddIndentationToMethod(
95-
templateWithMethodName,
100+
templateWithMethodSignature,
96101
_razorLSPOptionsMonitor.CurrentValue,
97102
@class.SpanStart,
98103
classLocationLineSpan.StartLinePosition.Character,
@@ -121,9 +126,9 @@ public GenerateMethodCodeActionResolver(DocumentContextFactory documentContextFa
121126
return new WorkspaceEdit() { DocumentChanges = new[] { codeBehindTextDocEdit } };
122127
}
123128

124-
private WorkspaceEdit GenerateMethodInCodeBlock(RazorCodeDocument code, GenerateMethodCodeActionParams actionParams, string templateWithMethodName)
129+
private WorkspaceEdit GenerateMethodInCodeBlock(RazorCodeDocument code, GenerateMethodCodeActionParams actionParams, string templateWithMethodSignature)
125130
{
126-
var edit = CodeBlockService.CreateFormattedTextEdit(code, templateWithMethodName, _razorLSPOptionsMonitor.CurrentValue);
131+
var edit = CodeBlockService.CreateFormattedTextEdit(code, templateWithMethodSignature, _razorLSPOptionsMonitor.CurrentValue);
127132
var razorTextDocEdit = new TextDocumentEdit()
128133
{
129134
TextDocument = new OptionalVersionedTextDocumentIdentifier() { Uri = actionParams.Uri },
@@ -132,4 +137,20 @@ private WorkspaceEdit GenerateMethodInCodeBlock(RazorCodeDocument code, Generate
132137

133138
return new WorkspaceEdit() { DocumentChanges = new[] { razorTextDocEdit } };
134139
}
140+
141+
private static string PopulateMethodSignature(VersionedDocumentContext documentContext, GenerateMethodCodeActionParams actionParams)
142+
{
143+
var templateWithMethodSignature = s_generateMethodTemplate.Replace(s_methodName, actionParams.MethodName);
144+
145+
var returnType = actionParams.IsAsync ? "async System.Threading.Tasks.Task" : "void";
146+
templateWithMethodSignature = templateWithMethodSignature.Replace(s_returnType, returnType);
147+
148+
var eventTagHelper = documentContext.Project.TagHelpers
149+
.FirstOrDefault(th => th.Name == actionParams.EventName && th.IsEventHandlerTagHelper() && th.GetEventArgsType() is not null);
150+
var eventArgsType = eventTagHelper is null
151+
? string.Empty // Couldn't find the params, generate no params instead.
152+
: $"{eventTagHelper.GetEventArgsType()} e";
153+
154+
return templateWithMethodSignature.Replace(s_eventArgs, eventArgsType);
155+
}
135156
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
6+
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
67
using Microsoft.VisualStudio.LanguageServer.Protocol;
78
using Newtonsoft.Json.Linq;
89

@@ -15,6 +16,7 @@ internal static class RazorCodeActionFactory
1516
private readonly static Guid s_createComponentFromTagTelemetryId = new("a28e0baa-a4d5-4953-a817-1db586035841");
1617
private readonly static Guid s_createExtractToCodeBehindTelemetryId = new("f63167f7-fdc6-450f-8b7b-b240892f4a27");
1718
private readonly static Guid s_generateMethodTelemetryId = new("c14fa003-c752-45fc-bb29-3a123ae5ecef");
19+
private readonly static Guid s_generateAsyncMethodTelemetryId = new("9058ca47-98e2-4f11-bf7c-a16a444dd939");
1820

1921
public static RazorVSInternalCodeAction CreateAddComponentUsing(string @namespace, RazorCodeActionResolutionParams resolutionParams)
2022
{
@@ -66,9 +68,23 @@ public static RazorVSInternalCodeAction CreateExtractToCodeBehind(RazorCodeActio
6668
return codeAction;
6769
}
6870

69-
public static RazorVSInternalCodeAction CreateGenerateMethod(RazorCodeActionResolutionParams resolutionParams)
71+
public static RazorVSInternalCodeAction CreateGenerateMethod(Uri uri, string methodName, string eventName)
7072
{
71-
var title = SR.FormatGenerate_Event_Handler_Title(((GenerateMethodCodeActionParams)resolutionParams.Data).MethodName);
73+
var @params = new GenerateMethodCodeActionParams
74+
{
75+
Uri = uri,
76+
MethodName = methodName,
77+
EventName = eventName,
78+
IsAsync = false
79+
};
80+
var resolutionParams = new RazorCodeActionResolutionParams()
81+
{
82+
Action = LanguageServerConstants.CodeActions.GenerateEventHandler,
83+
Language = LanguageServerConstants.CodeActions.Languages.Razor,
84+
Data = @params,
85+
};
86+
87+
var title = SR.FormatGenerate_Event_Handler_Title(methodName);
7288
var data = JToken.FromObject(resolutionParams);
7389
var codeAction = new RazorVSInternalCodeAction()
7490
{
@@ -78,4 +94,31 @@ public static RazorVSInternalCodeAction CreateGenerateMethod(RazorCodeActionReso
7894
};
7995
return codeAction;
8096
}
97+
98+
public static RazorVSInternalCodeAction CreateAsyncGenerateMethod(Uri uri, string methodName, string eventName)
99+
{
100+
var @params = new GenerateMethodCodeActionParams
101+
{
102+
Uri = uri,
103+
MethodName = methodName,
104+
EventName = eventName,
105+
IsAsync = true
106+
};
107+
var resolutionParams = new RazorCodeActionResolutionParams()
108+
{
109+
Action = LanguageServerConstants.CodeActions.GenerateEventHandler,
110+
Language = LanguageServerConstants.CodeActions.Languages.Razor,
111+
Data = @params,
112+
};
113+
114+
var title = SR.FormatGenerate_Async_Event_Handler_Title(methodName);
115+
var data = JToken.FromObject(resolutionParams);
116+
var codeAction = new RazorVSInternalCodeAction()
117+
{
118+
Title = title,
119+
Data = data,
120+
TelemetryId = s_generateAsyncMethodTelemetryId
121+
};
122+
return codeAction;
123+
}
81124
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@
129129
<data name="ExtractTo_CodeBehind_Title" xml:space="preserve">
130130
<value>Extract block to code behind</value>
131131
</data>
132+
<data name="Generate_Async_Event_Handler_Title" xml:space="preserve">
133+
<value>Generate Async Event Handler '{0}'</value>
134+
</data>
132135
<data name="Generate_Event_Handler_Title" xml:space="preserve">
133136
<value>Generate Event Handler '{0}'</value>
134137
</data>

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)