-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate EventHandler CodeAction: Simplify Type Names #9070
Changes from all commits
b2720ad
44c4fa5
6f44a35
216d863
f79de7e
7f38b1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading; | ||
|
@@ -11,6 +12,7 @@ | |
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; | ||
using Microsoft.AspNetCore.Razor.LanguageServer.Common; | ||
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; | ||
using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; | ||
using Microsoft.AspNetCore.Razor.Utilities; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
@@ -25,6 +27,9 @@ internal class GenerateMethodCodeActionResolver : IRazorCodeActionResolver | |
{ | ||
private readonly DocumentContextFactory _documentContextFactory; | ||
private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor; | ||
private readonly ClientNotifierServiceBase _languageServer; | ||
private readonly IRazorDocumentMappingService _documentMappingService; | ||
private readonly IRazorFormattingService _razorFormattingService; | ||
|
||
private static readonly string s_beginningIndents = $"{FormattingUtilities.InitialIndent}{FormattingUtilities.Indent}"; | ||
private static readonly string s_returnType = "$$ReturnType$$"; | ||
|
@@ -33,15 +38,23 @@ internal class GenerateMethodCodeActionResolver : IRazorCodeActionResolver | |
private static readonly string s_generateMethodTemplate = | ||
$"{s_beginningIndents}private {s_returnType} {s_methodName}({s_eventArgs}){Environment.NewLine}" + | ||
s_beginningIndents + "{" + Environment.NewLine + | ||
$"{s_beginningIndents}{FormattingUtilities.Indent}throw new System.NotImplementedException();{Environment.NewLine}" + | ||
$"{s_beginningIndents}{FormattingUtilities.Indent}throw new global::System.NotImplementedException();{Environment.NewLine}" + | ||
s_beginningIndents + "}"; | ||
|
||
public string Action => LanguageServerConstants.CodeActions.GenerateEventHandler; | ||
|
||
public GenerateMethodCodeActionResolver(DocumentContextFactory documentContextFactory, RazorLSPOptionsMonitor razorLSPOptionsMonitor) | ||
public GenerateMethodCodeActionResolver( | ||
DocumentContextFactory documentContextFactory, | ||
RazorLSPOptionsMonitor razorLSPOptionsMonitor, | ||
ClientNotifierServiceBase languageServer, | ||
IRazorDocumentMappingService razorDocumentMappingService, | ||
IRazorFormattingService razorFormattingService) | ||
{ | ||
_documentContextFactory = documentContextFactory; | ||
_razorLSPOptionsMonitor = razorLSPOptionsMonitor; | ||
_languageServer = languageServer; | ||
_documentMappingService = razorDocumentMappingService; | ||
_razorFormattingService = razorFormattingService; | ||
} | ||
|
||
public async Task<WorkspaceEdit?> ResolveAsync(JObject data, CancellationToken cancellationToken) | ||
|
@@ -63,8 +76,6 @@ public GenerateMethodCodeActionResolver(DocumentContextFactory documentContextFa | |
return null; | ||
} | ||
|
||
var templateWithMethodSignature = PopulateMethodSignature(documentContext, actionParams); | ||
|
||
var code = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); | ||
var uriPath = FilePathNormalizer.Normalize(actionParams.Uri.GetAbsoluteOrUNCPath()); | ||
var razorClassName = Path.GetFileNameWithoutExtension(uriPath); | ||
|
@@ -74,27 +85,38 @@ public GenerateMethodCodeActionResolver(DocumentContextFactory documentContextFa | |
|| razorClassName is null | ||
|| !code.TryComputeNamespace(fallbackToRootNamespace: true, out var razorNamespace)) | ||
{ | ||
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodSignature); | ||
return await GenerateMethodInCodeBlockAsync( | ||
code, | ||
actionParams, | ||
documentContext, | ||
razorNamespace: null, | ||
razorClassName, | ||
cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
var content = File.ReadAllText(codeBehindPath); | ||
var mock = CSharpSyntaxFactory.ParseCompilationUnit(content); | ||
var @namespace = mock.Members | ||
.FirstOrDefault(m => m is BaseNamespaceDeclarationSyntax { } @namespace && @namespace.Name.ToString() == razorNamespace); | ||
if (@namespace is null) | ||
if (GetCSharpClassDeclarationSyntax(content, razorNamespace, razorClassName) is not { } @class) | ||
{ | ||
// The code behind file is malformed, generate the code in the razor file instead. | ||
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodSignature); | ||
return await GenerateMethodInCodeBlockAsync( | ||
code, | ||
actionParams, | ||
documentContext, | ||
razorNamespace, | ||
razorClassName, | ||
cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
var @class = ((BaseNamespaceDeclarationSyntax)@namespace).Members | ||
.FirstOrDefault(m => m is ClassDeclarationSyntax { } @class && razorClassName == @class.Identifier.Text); | ||
if (@class is null) | ||
var codeBehindUri = new UriBuilder | ||
{ | ||
// The code behind file is malformed, generate the code in the razor file instead. | ||
return GenerateMethodInCodeBlock(code, actionParams, templateWithMethodSignature); | ||
} | ||
Scheme = Uri.UriSchemeFile, | ||
Path = codeBehindPath, | ||
Host = string.Empty, | ||
}.Uri; | ||
|
||
var codeBehindTextDocumentIdentifier = new OptionalVersionedTextDocumentIdentifier() { Uri = codeBehindUri }; | ||
|
||
var templateWithMethodSignature = PopulateMethodSignature(documentContext, actionParams); | ||
var classLocationLineSpan = @class.GetLocation().GetLineSpan(); | ||
var formattedMethod = FormattingUtilities.AddIndentationToMethod( | ||
templateWithMethodSignature, | ||
|
@@ -103,36 +125,126 @@ public GenerateMethodCodeActionResolver(DocumentContextFactory documentContextFa | |
classLocationLineSpan.StartLinePosition.Character, | ||
content); | ||
|
||
var codeBehindUri = new UriBuilder | ||
{ | ||
Scheme = Uri.UriSchemeFile, | ||
Path = codeBehindPath, | ||
Host = string.Empty, | ||
}.Uri; | ||
|
||
var insertPosition = new Position(classLocationLineSpan.EndLinePosition.Line, 0); | ||
var edit = new TextEdit() | ||
{ | ||
Range = new Range { Start = insertPosition, End = insertPosition }, | ||
NewText = $"{formattedMethod}{Environment.NewLine}" | ||
}; | ||
|
||
var delegatedParams = new DelegatedSimplifyMethodParams( | ||
new TextDocumentIdentifierAndVersion(new TextDocumentIdentifier() { Uri = codeBehindUri}, 1), | ||
RequiresVirtualDocument: false, | ||
edit); | ||
|
||
var result = await _languageServer.SendRequestAsync<DelegatedSimplifyMethodParams, TextEdit[]?>( | ||
CustomMessageNames.RazorSimplifyMethodEndpointName, | ||
delegatedParams, | ||
cancellationToken).ConfigureAwait(false) | ||
?? new TextEdit[] { edit }; | ||
|
||
var codeBehindTextDocEdit = new TextDocumentEdit() | ||
{ | ||
TextDocument = new OptionalVersionedTextDocumentIdentifier() { Uri = codeBehindUri }, | ||
Edits = new TextEdit[] { edit } | ||
TextDocument = codeBehindTextDocumentIdentifier, | ||
Edits = result | ||
}; | ||
|
||
return new WorkspaceEdit() { DocumentChanges = new[] { codeBehindTextDocEdit } }; | ||
} | ||
|
||
private WorkspaceEdit GenerateMethodInCodeBlock(RazorCodeDocument code, GenerateMethodCodeActionParams actionParams, string templateWithMethodSignature) | ||
private async Task<WorkspaceEdit> GenerateMethodInCodeBlockAsync( | ||
RazorCodeDocument code, | ||
GenerateMethodCodeActionParams actionParams, | ||
VersionedDocumentContext documentContext, | ||
string? razorNamespace, | ||
string? razorClassName, | ||
CancellationToken cancellationToken) | ||
{ | ||
var edit = CodeBlockService.CreateFormattedTextEdit(code, templateWithMethodSignature, _razorLSPOptionsMonitor.CurrentValue); | ||
var templateWithMethodSignature = PopulateMethodSignature(documentContext, actionParams); | ||
var edits = CodeBlockService.CreateFormattedTextEdit(code, templateWithMethodSignature, _razorLSPOptionsMonitor.CurrentValue); | ||
|
||
// If there are 3 edits, this means that there is no existing @code block, so we have an edit for '@code {', the method stub, and '}'. | ||
// Otherwise, a singular edit means that an @code block does exist and the only edit is adding the method stub. | ||
var editToSendToRoslyn = edits.Length == 3 ? edits[1] : edits[0]; | ||
if (edits.Length == 3 | ||
&& razorClassName is not null | ||
&& (razorNamespace is not null || code.TryComputeNamespace(fallbackToRootNamespace: true, out razorNamespace)) | ||
&& GetCSharpClassDeclarationSyntax(code.GetCSharpDocument().GeneratedCode, razorNamespace, razorClassName) is { } @class) | ||
{ | ||
// There is no existing @code block. This means that there is no code block source mapping in the generated C# document | ||
// to place the code, so we cannot utilize the document mapping service and the formatting service. | ||
// We are going to arbitrarily place the method at the end of the class in the generated C# file to | ||
// just get the simplified text that comes back from Roslyn. | ||
|
||
var classLocationLineSpan = @class.GetLocation().GetLineSpan(); | ||
var insertPosition = new Position(classLocationLineSpan.EndLinePosition.Line, 0); | ||
var tempTextEdit = new TextEdit() | ||
{ | ||
NewText = editToSendToRoslyn.NewText, | ||
Range = new Range() { Start = insertPosition, End = insertPosition } | ||
}; | ||
|
||
var delegatedParams = new DelegatedSimplifyMethodParams(documentContext.Identifier, RequiresVirtualDocument: true, tempTextEdit); | ||
var result = await _languageServer.SendRequestAsync<DelegatedSimplifyMethodParams, TextEdit[]?>( | ||
CustomMessageNames.RazorSimplifyMethodEndpointName, | ||
delegatedParams, | ||
cancellationToken).ConfigureAwait(false); | ||
|
||
// Roslyn should have passed back 2 edits. One that contains the simplified method stub and the other that contains the new | ||
// location for the class end brace since we had asked to insert the method stub at the original class end brace location. | ||
// We will only use the edit that contains the method stub. | ||
Debug.Assert(result is null || result.Length == 2, $"Unexpected response to {CustomMessageNames.RazorSimplifyMethodEndpointName} from Roslyn"); | ||
var simplificationEdit = result?.FirstOrDefault(edit => edit.NewText.Contains("private")); | ||
if (simplificationEdit is not null) | ||
{ | ||
// Roslyn will have removed the beginning formatting, put it back. | ||
var formatting = editToSendToRoslyn.NewText[0..editToSendToRoslyn.NewText.IndexOf("private")]; | ||
editToSendToRoslyn.NewText = $"{formatting}{simplificationEdit.NewText.TrimEnd()}"; | ||
} | ||
} | ||
else if (_documentMappingService.TryMapToGeneratedDocumentRange(code.GetCSharpDocument(), editToSendToRoslyn.Range, out var remappedRange)) | ||
{ | ||
// If the call to Roslyn is successful, the razor formatting service will format incorrectly if our manual formatting is present, | ||
// strip our manual formatting from the method so we just have a valid method signature. | ||
var unformattedMethodSignature = templateWithMethodSignature | ||
.Replace(FormattingUtilities.InitialIndent, string.Empty) | ||
.Replace(FormattingUtilities.Indent, string.Empty); | ||
|
||
var remappedEdit = new TextEdit() | ||
{ | ||
NewText = unformattedMethodSignature, | ||
Range = remappedRange | ||
}; | ||
|
||
var delegatedParams = new DelegatedSimplifyMethodParams(documentContext.Identifier, RequiresVirtualDocument: true, remappedEdit); | ||
var result = await _languageServer.SendRequestAsync<DelegatedSimplifyMethodParams, TextEdit[]?>( | ||
CustomMessageNames.RazorSimplifyMethodEndpointName, | ||
delegatedParams, | ||
cancellationToken).ConfigureAwait(false); | ||
|
||
if (result is not null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately because of the possibility of |
||
{ | ||
var formattingOptions = new FormattingOptions() | ||
{ | ||
TabSize = _razorLSPOptionsMonitor.CurrentValue.TabSize, | ||
InsertSpaces = _razorLSPOptionsMonitor.CurrentValue.InsertSpaces, | ||
}; | ||
|
||
var formattedEdits = await _razorFormattingService.FormatCodeActionAsync( | ||
documentContext, | ||
RazorLanguageKind.CSharp, | ||
result, | ||
formattingOptions, | ||
CancellationToken.None).ConfigureAwait(false); | ||
|
||
edits = formattedEdits; | ||
} | ||
} | ||
|
||
var razorTextDocEdit = new TextDocumentEdit() | ||
{ | ||
TextDocument = new OptionalVersionedTextDocumentIdentifier() { Uri = actionParams.Uri }, | ||
Edits = new TextEdit[] { edit }, | ||
Edits = edits, | ||
}; | ||
|
||
return new WorkspaceEdit() { DocumentChanges = new[] { razorTextDocEdit } }; | ||
|
@@ -142,15 +254,35 @@ private static string PopulateMethodSignature(VersionedDocumentContext documentC | |
{ | ||
var templateWithMethodSignature = s_generateMethodTemplate.Replace(s_methodName, actionParams.MethodName); | ||
|
||
var returnType = actionParams.IsAsync ? "System.Threading.Tasks.Task" : "void"; | ||
var returnType = actionParams.IsAsync ? "global::System.Threading.Tasks.Task" : "void"; | ||
templateWithMethodSignature = templateWithMethodSignature.Replace(s_returnType, returnType); | ||
|
||
var eventTagHelper = documentContext.Project.TagHelpers | ||
.FirstOrDefault(th => th.Name == actionParams.EventName && th.IsEventHandlerTagHelper() && th.GetEventArgsType() is not null); | ||
var eventArgsType = eventTagHelper is null | ||
? string.Empty // Couldn't find the params, generate no params instead. | ||
: $"{eventTagHelper.GetEventArgsType()} e"; | ||
: $"global::{eventTagHelper.GetEventArgsType()} e"; | ||
|
||
return templateWithMethodSignature.Replace(s_eventArgs, eventArgsType); | ||
} | ||
|
||
private static ClassDeclarationSyntax? GetCSharpClassDeclarationSyntax(string csharpContent, string razorNamespace, string razorClassName) | ||
{ | ||
var mock = CSharpSyntaxFactory.ParseCompilationUnit(csharpContent); | ||
var @namespace = mock.Members | ||
.FirstOrDefault(m => m is BaseNamespaceDeclarationSyntax { } @namespace && @namespace.Name.ToString() == razorNamespace); | ||
if (@namespace is null) | ||
{ | ||
return null; | ||
} | ||
|
||
var @class = ((BaseNamespaceDeclarationSyntax)@namespace).Members | ||
.FirstOrDefault(m => m is ClassDeclarationSyntax { } @class && razorClassName == @class.Identifier.Text); | ||
if (@class is null) | ||
{ | ||
return null; | ||
} | ||
|
||
return (ClassDeclarationSyntax)@class; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to include a comment explaining this. eg "if there are 3 edits then it means .... otherwise ..."