-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge features/extract-to-component to main (#11019)
New PR because apparently the branch is restricted so I can't handle merge conflicts... Adding work from @marcarro into main 🥳
- Loading branch information
Showing
38 changed files
with
1,539 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
....AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToComponentCodeActionParams.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Text.Json.Serialization; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; | ||
|
||
internal sealed class ExtractToComponentCodeActionParams | ||
{ | ||
[JsonPropertyName("uri")] | ||
public required Uri Uri { get; set; } | ||
|
||
[JsonPropertyName("start")] | ||
public int Start { get; set; } | ||
|
||
[JsonPropertyName("end")] | ||
public int End { get; set; } | ||
|
||
[JsonPropertyName("namespace")] | ||
public required string Namespace { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
...AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using ICSharpCode.Decompiler.CSharp.Syntax; | ||
using Microsoft.AspNetCore.Razor.Language; | ||
using Microsoft.AspNetCore.Razor.Language.Syntax; | ||
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; | ||
using Microsoft.AspNetCore.Razor.PooledObjects; | ||
using Microsoft.AspNetCore.Razor.Threading; | ||
using Microsoft.CodeAnalysis.Razor.Logging; | ||
using Microsoft.CodeAnalysis.Razor.Workspaces; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Microsoft.VisualStudio.LanguageServer.Protocol; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; | ||
|
||
internal sealed class ExtractToComponentCodeActionProvider() : IRazorCodeActionProvider | ||
{ | ||
public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) | ||
{ | ||
if (!context.SupportsFileCreation) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
if (!FileKinds.IsComponent(context.CodeDocument.GetFileKind())) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var syntaxTree = context.CodeDocument.GetSyntaxTree(); | ||
if (syntaxTree?.Root is null) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
if (!TryGetNamespace(context.CodeDocument, out var @namespace)) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var (startNode, endNode) = GetStartAndEndElements(context, syntaxTree); | ||
if (startNode is null || endNode is null) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var actionParams = CreateActionParams(context, startNode, endNode, @namespace); | ||
|
||
var resolutionParams = new RazorCodeActionResolutionParams() | ||
{ | ||
Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction, | ||
Language = LanguageServerConstants.CodeActions.Languages.Razor, | ||
Data = actionParams, | ||
}; | ||
|
||
var codeAction = RazorCodeActionFactory.CreateExtractToComponent(resolutionParams); | ||
return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([codeAction]); | ||
} | ||
|
||
private static (SyntaxNode? Start, SyntaxNode? End) GetStartAndEndElements(RazorCodeActionContext context, RazorSyntaxTree syntaxTree) | ||
{ | ||
var owner = syntaxTree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex, includeWhitespace: true); | ||
if (owner is null) | ||
{ | ||
return (null, null); | ||
} | ||
|
||
var startElementNode = owner.FirstAncestorOrSelf<SyntaxNode>(IsBlockNode); | ||
|
||
if (startElementNode is null || LocationInvalid(context.StartLocation, startElementNode)) | ||
{ | ||
return (null, null); | ||
} | ||
|
||
var endElementNode = context.StartLocation == context.EndLocation | ||
? startElementNode | ||
: GetEndElementNode(context, syntaxTree); | ||
|
||
return (startElementNode, endElementNode); | ||
|
||
static bool LocationInvalid(SourceLocation location, SyntaxNode node) | ||
{ | ||
// Make sure to test for cases where selection | ||
// is inside of a markup tag such as <p>hello$ there</p> | ||
if (node is MarkupElementSyntax markupElement) | ||
{ | ||
return location.AbsoluteIndex > markupElement.StartTag.Span.End && | ||
location.AbsoluteIndex < markupElement.EndTag.SpanStart; | ||
} | ||
|
||
return !node.Span.Contains(location.AbsoluteIndex); | ||
} | ||
} | ||
|
||
private static SyntaxNode? GetEndElementNode(RazorCodeActionContext context, RazorSyntaxTree syntaxTree) | ||
{ | ||
var endOwner = syntaxTree.Root.FindInnermostNode(context.EndLocation.AbsoluteIndex, includeWhitespace: true); | ||
if (endOwner is null) | ||
{ | ||
return null; | ||
} | ||
|
||
// Correct selection to include the current node if the selection ends immediately after a closing tag. | ||
if (endOwner is MarkupTextLiteralSyntax | ||
&& endOwner.ContainsOnlyWhitespace() | ||
&& endOwner.TryGetPreviousSibling(out var previousSibling)) | ||
{ | ||
endOwner = previousSibling; | ||
} | ||
|
||
return endOwner.FirstAncestorOrSelf<SyntaxNode>(IsBlockNode); | ||
} | ||
|
||
private static bool IsBlockNode(SyntaxNode node) | ||
=> node.Kind is | ||
SyntaxKind.MarkupElement or | ||
SyntaxKind.MarkupTagHelperElement or | ||
SyntaxKind.CSharpCodeBlock; | ||
|
||
private static ExtractToComponentCodeActionParams CreateActionParams( | ||
RazorCodeActionContext context, | ||
SyntaxNode startNode, | ||
SyntaxNode endNode, | ||
string @namespace) | ||
{ | ||
var selectionSpan = AreSiblings(startNode, endNode) | ||
? TextSpan.FromBounds(startNode.Span.Start, endNode.Span.End) | ||
: GetEncompassingTextSpan(startNode, endNode); | ||
|
||
return new ExtractToComponentCodeActionParams | ||
{ | ||
Uri = context.Request.TextDocument.Uri, | ||
Start = selectionSpan.Start, | ||
End = selectionSpan.End, | ||
Namespace = @namespace | ||
}; | ||
} | ||
|
||
private static TextSpan GetEncompassingTextSpan(SyntaxNode startNode, SyntaxNode endNode) | ||
{ | ||
// Find a valid node that encompasses both the start and the end to | ||
// become the selection. | ||
var commonAncestor = endNode.Span.Contains(startNode.Span) | ||
? endNode | ||
: startNode; | ||
|
||
while (commonAncestor is MarkupElementSyntax or | ||
MarkupTagHelperAttributeSyntax or | ||
MarkupBlockSyntax) | ||
{ | ||
if (commonAncestor.Span.Contains(startNode.Span) && | ||
commonAncestor.Span.Contains(endNode.Span)) | ||
{ | ||
break; | ||
} | ||
|
||
commonAncestor = commonAncestor.Parent; | ||
} | ||
|
||
// If walking up the tree was required then make sure to reduce | ||
// selection back down to minimal nodes needed. | ||
// For example: | ||
// <div> | ||
// {|result:<span> | ||
// {|selection:<p>Some text</p> | ||
// </span> | ||
// <span> | ||
// <p>More text</p> | ||
// </span> | ||
// <span> | ||
// </span>|}|} | ||
// </div> | ||
if (commonAncestor != startNode && | ||
commonAncestor != endNode) | ||
{ | ||
SyntaxNode? modifiedStart = null, modifiedEnd = null; | ||
foreach (var child in commonAncestor.ChildNodes().Where(static node => node.Kind == SyntaxKind.MarkupElement)) | ||
{ | ||
if (child.Span.Contains(startNode.Span)) | ||
{ | ||
modifiedStart = child; | ||
if (modifiedEnd is not null) | ||
break; // Exit if we've found both | ||
} | ||
|
||
if (child.Span.Contains(endNode.Span)) | ||
{ | ||
modifiedEnd = child; | ||
if (modifiedStart is not null) | ||
break; // Exit if we've found both | ||
} | ||
} | ||
|
||
if (modifiedStart is not null && modifiedEnd is not null) | ||
{ | ||
return TextSpan.FromBounds(modifiedStart.Span.Start, modifiedEnd.Span.End); | ||
} | ||
} | ||
|
||
// Fallback to extracting the nearest common ancestor span | ||
return commonAncestor.Span; | ||
} | ||
|
||
private static bool AreSiblings(SyntaxNode? node1, SyntaxNode? node2) | ||
{ | ||
if (node1 is null) | ||
{ | ||
return false; | ||
} | ||
|
||
if (node2 is null) | ||
{ | ||
return false; | ||
} | ||
|
||
return node1.Parent == node2.Parent; | ||
} | ||
|
||
private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen(returnValue: true)] out string? @namespace) | ||
// If the compiler can't provide a computed namespace it will fallback to "__GeneratedComponent" or | ||
// similar for the NamespaceNode. This would end up with extracting to a wrong namespace | ||
// and causing compiler errors. Avoid offering this refactoring if we can't accurately get a | ||
// good namespace to extract to | ||
=> codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out @namespace); | ||
} |
Oops, something went wrong.