Skip to content
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

Merged
merged 6 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,8 @@ internal record DelegatedProjectContextsParams(

internal record DelegatedDocumentSymbolParams(
TextDocumentIdentifierAndVersion Identifier);

internal record DelegatedSimplifyMethodParams(
TextDocumentIdentifierAndVersion Identifier,
bool RequiresVirtualDocument,
TextEdit TextEdit);
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal static class CodeBlockService
/// <returns>
/// A <see cref="TextEdit"/> that will place the formatted generated method within a @code block in the file.
/// </returns>
public static TextEdit CreateFormattedTextEdit(RazorCodeDocument code, string templateWithMethodSignature, RazorLSPOptions options)
public static TextEdit[] CreateFormattedTextEdit(RazorCodeDocument code, string templateWithMethodSignature, RazorLSPOptions options)
{
var csharpCodeBlock = code.GetSyntaxTree().Root.DescendantNodes()
.Select(RazorSyntaxFacts.TryGetCSharpCodeFromCodeBlock)
Expand All @@ -45,27 +45,43 @@ public static TextEdit CreateFormattedTextEdit(RazorCodeDocument code, string te
|| !csharpCodeBlock.Children.TryGetOpenBraceNode(out var openBrace)
|| !csharpCodeBlock.Children.TryGetCloseBraceNode(out var closeBrace))
{
// No well-formed @code block exists. Generate the method within an @code block at the end of the file.
// No well-formed @code block exists. Generate the method within an @code block at the end of the file and conduct manual formatting.
var indentedMethod = FormattingUtilities.AddIndentationToMethod(templateWithMethodSignature, options, startingIndent: 0);
var textWithCodeBlock = "@code {" + Environment.NewLine + indentedMethod + Environment.NewLine + "}";
var codeBlockStartText = "@code {" + Environment.NewLine;
var lastCharacterLocation = code.Source.Lines.GetLocation(code.Source.Length - 1);
var insertCharacterIndex = 0;
if (lastCharacterLocation.LineIndex == code.Source.Lines.Count - 1 && !IsLineEmpty(code.Source, lastCharacterLocation))
{
// The last line of the file is not empty so we need to place the code at the end of that line with a new line at the beginning.
insertCharacterIndex = lastCharacterLocation.CharacterIndex + 1;
textWithCodeBlock = $"{Environment.NewLine}{textWithCodeBlock}";
codeBlockStartText = $"{Environment.NewLine}{codeBlockStartText}";
}

var eof = new Position(code.Source.Lines.Count - 1, insertCharacterIndex);
return new TextEdit()
var eofPosition = new Position(code.Source.Lines.Count - 1, insertCharacterIndex);
var eofRange = new Range { Start = eofPosition, End = eofPosition };
var start = new TextEdit()
{
Range = new Range { Start = eof, End = eof },
NewText = textWithCodeBlock
NewText = codeBlockStartText,
Range = eofRange
};

var method = new TextEdit()
{
NewText = indentedMethod,
Range = eofRange
};

var end = new TextEdit()
{
NewText = Environment.NewLine + "}",
Range = eofRange
};

return new TextEdit[] { start, method, end };
}

// A well-formed @code block exists, generate the method within it.

var openBraceLocation = openBrace.GetSourceLocation(code.Source);
var closeBraceLocation = closeBrace.GetSourceLocation(code.Source);
var previousLine = code.Source.Lines.GetLocation(closeBraceLocation.AbsoluteIndex - closeBraceLocation.CharacterIndex - 1);
Expand All @@ -88,11 +104,14 @@ public static TextEdit CreateFormattedTextEdit(RazorCodeDocument code, string te
? closeBraceLocation.CharacterIndex
: 0;
var insertPosition = new Position(insertLineLocation.LineIndex, insertCharacter);
return new TextEdit()

var edit = new TextEdit()
{
Range = new Range { Start = insertPosition, End = insertPosition },
NewText = formattedGeneratedMethod
};

return new TextEdit[] { edit };
}

private static string FormatMethodInCodeBlock(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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$$";
Expand All @@ -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)
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -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
Copy link
Contributor

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 ..."

&& 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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately because of the possibility of result = null, I did not remove a lot of the formatting code in CodeBlockService. If result = null, we should still produce a nicely formatted event handler, but we cannot use the RazorFormattingService at that point since we do not have C# edits, so we still do manual formatting work.

{
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 } };
Expand All @@ -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;
}
}
Loading
Loading