зеркало из https://github.com/dotnet/razor.git
Some Cleanup For ExtractToComponent (#11008)
Clean up some of the extract to component code. Move more work to the resolver, use pooled objects where applicable, and try to make the provider cleaner when creating the ExtractToComponentCodeActionParams
This commit is contained in:
Родитель
17dbcdb70f
Коммит
e060ce0e58
|
@ -67,7 +67,7 @@ internal sealed class DefaultCSharpCodeActionProvider(LanguageServerFeatureOptio
|
|||
}
|
||||
|
||||
var tree = context.CodeDocument.GetSyntaxTree();
|
||||
var node = tree.Root.FindInnermostNode(context.Location.AbsoluteIndex);
|
||||
var node = tree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex);
|
||||
var isInImplicitExpression = node?.AncestorsAndSelf().Any(n => n is CSharpImplicitExpressionSyntax) ?? false;
|
||||
|
||||
var allowList = isInImplicitExpression
|
||||
|
|
|
@ -215,7 +215,7 @@ internal sealed class TypeAccessibilityCodeActionProvider : ICSharpCodeActionPro
|
|||
return false;
|
||||
}
|
||||
|
||||
owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex);
|
||||
owner = syntaxTree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex);
|
||||
if (owner is null)
|
||||
{
|
||||
Debug.Fail("Owner should never be null.");
|
||||
|
|
|
@ -158,16 +158,22 @@ internal sealed class CodeActionEndpoint(
|
|||
request.Range = vsCodeActionContext.SelectionRange;
|
||||
}
|
||||
|
||||
if (!sourceText.TryGetSourceLocation(request.Range.Start, out var location))
|
||||
if (!sourceText.TryGetSourceLocation(request.Range.Start, out var startLocation))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!sourceText.TryGetSourceLocation(request.Range.End, out var endLocation))
|
||||
{
|
||||
endLocation = startLocation;
|
||||
}
|
||||
|
||||
var context = new RazorCodeActionContext(
|
||||
request,
|
||||
documentSnapshot,
|
||||
codeDocument,
|
||||
location,
|
||||
startLocation,
|
||||
endLocation,
|
||||
sourceText,
|
||||
_languageServerFeatureOptions.SupportsFileManipulation,
|
||||
_supportsCodeActionResolve);
|
||||
|
@ -177,7 +183,7 @@ internal sealed class CodeActionEndpoint(
|
|||
|
||||
private async Task<ImmutableArray<RazorVSInternalCodeAction>> GetDelegatedCodeActionsAsync(DocumentContext documentContext, RazorCodeActionContext context, Guid correlationId, CancellationToken cancellationToken)
|
||||
{
|
||||
var languageKind = context.CodeDocument.GetLanguageKind(context.Location.AbsoluteIndex, rightAssociative: false);
|
||||
var languageKind = context.CodeDocument.GetLanguageKind(context.StartLocation.AbsoluteIndex, rightAssociative: false);
|
||||
|
||||
// No point delegating if we're in a Razor context
|
||||
if (languageKind == RazorLanguageKind.Razor)
|
||||
|
|
|
@ -3,26 +3,22 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
|
||||
|
||||
// NOTE: As mentioned before, these have changed in future PRs, where much of the Provider logic was moved to the resolver.
|
||||
// The last three properties are not used in the current implementation.
|
||||
internal sealed class ExtractToComponentCodeActionParams
|
||||
{
|
||||
[JsonPropertyName("uri")]
|
||||
public required Uri Uri { get; set; }
|
||||
|
||||
[JsonPropertyName("extractStart")]
|
||||
public int ExtractStart { get; set; }
|
||||
[JsonPropertyName("start")]
|
||||
public int Start { get; set; }
|
||||
|
||||
[JsonPropertyName("extractEnd")]
|
||||
public int ExtractEnd { get; set; }
|
||||
[JsonPropertyName("end")]
|
||||
public int End { get; set; }
|
||||
|
||||
[JsonPropertyName("namespace")]
|
||||
public required string Namespace { get; set; }
|
||||
|
||||
[JsonPropertyName("usingDirectives")]
|
||||
public required List<string> usingDirectives { get; set; }
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ internal sealed class ComponentAccessibilityCodeActionProvider : IRazorCodeActio
|
|||
public async Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Locate cursor
|
||||
var node = context.CodeDocument.GetSyntaxTree().Root.FindInnermostNode(context.Location.AbsoluteIndex);
|
||||
var node = context.CodeDocument.GetSyntaxTree().Root.FindInnermostNode(context.StartLocation.AbsoluteIndex);
|
||||
if (node is null)
|
||||
{
|
||||
return [];
|
||||
|
@ -44,7 +44,7 @@ internal sealed class ComponentAccessibilityCodeActionProvider : IRazorCodeActio
|
|||
return [];
|
||||
}
|
||||
|
||||
if (context.Location.AbsoluteIndex < startTag.SpanStart)
|
||||
if (context.StartLocation.AbsoluteIndex < startTag.SpanStart)
|
||||
{
|
||||
// Cursor is before the start tag, so we shouldn't show a light bulb. This can happen
|
||||
// in cases where the cursor is in whitespace at the beginning of the document
|
||||
|
|
|
@ -42,7 +42,7 @@ internal sealed class ExtractToCodeBehindCodeActionProvider(ILoggerFactory logge
|
|||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
|
||||
}
|
||||
|
||||
var owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex);
|
||||
var owner = syntaxTree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex);
|
||||
if (owner is null)
|
||||
{
|
||||
_logger.LogWarning($"Owner should never be null.");
|
||||
|
@ -84,7 +84,7 @@ internal sealed class ExtractToCodeBehindCodeActionProvider(ILoggerFactory logge
|
|||
}
|
||||
|
||||
// Do not provide code action if the cursor is inside the code block
|
||||
if (context.Location.AbsoluteIndex > csharpCodeBlockNode.SpanStart)
|
||||
if (context.StartLocation.AbsoluteIndex > csharpCodeBlockNode.SpanStart)
|
||||
{
|
||||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
|
||||
}
|
||||
|
|
|
@ -8,20 +8,21 @@ 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(ILoggerFactory loggerFactory) : IRazorCodeActionProvider
|
||||
internal sealed class ExtractToComponentCodeActionProvider() : IRazorCodeActionProvider
|
||||
{
|
||||
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<ExtractToComponentCodeActionProvider>();
|
||||
|
||||
public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context is null)
|
||||
|
@ -46,39 +47,18 @@ internal sealed class ExtractToComponentCodeActionProvider(ILoggerFactory logger
|
|||
}
|
||||
|
||||
// Make sure the selection starts on an element tag
|
||||
var (startElementNode, endElementNode) = GetStartAndEndElements(context, syntaxTree, _logger);
|
||||
if (startElementNode is null)
|
||||
var (startNode, endNode) = GetStartAndEndElements(context, syntaxTree);
|
||||
if (startNode is null || endNode is null)
|
||||
{
|
||||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
|
||||
}
|
||||
|
||||
if (endElementNode is null)
|
||||
{
|
||||
endElementNode = startElementNode;
|
||||
}
|
||||
|
||||
if (!TryGetNamespace(context.CodeDocument, out var @namespace))
|
||||
{
|
||||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
|
||||
}
|
||||
|
||||
var actionParams = CreateInitialActionParams(context, startElementNode, @namespace);
|
||||
|
||||
ProcessSelection(startElementNode, endElementNode, actionParams);
|
||||
|
||||
var utilityScanRoot = FindNearestCommonAncestor(startElementNode, endElementNode) ?? startElementNode;
|
||||
|
||||
// The new component usings are going to be a subset of the usings in the source razor file.
|
||||
var usingStrings = syntaxTree.Root.DescendantNodes().Where(node => node.IsUsingDirective(out var _)).Select(node => node.ToFullString().TrimEnd());
|
||||
|
||||
// Get only the namespace after the "using" keyword.
|
||||
var usingNamespaceStrings = usingStrings.Select(usingString => usingString.Substring("using ".Length));
|
||||
|
||||
AddUsingDirectivesInRange(utilityScanRoot,
|
||||
usingNamespaceStrings,
|
||||
actionParams.ExtractStart,
|
||||
actionParams.ExtractEnd,
|
||||
actionParams);
|
||||
var actionParams = CreateActionParams(context, startNode, endNode, @namespace);
|
||||
|
||||
var resolutionParams = new RazorCodeActionResolutionParams()
|
||||
{
|
||||
|
@ -91,49 +71,36 @@ internal sealed class ExtractToComponentCodeActionProvider(ILoggerFactory logger
|
|||
return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([codeAction]);
|
||||
}
|
||||
|
||||
private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAndEndElements(RazorCodeActionContext context, RazorSyntaxTree syntaxTree, ILogger logger)
|
||||
private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAndEndElements(RazorCodeActionContext context, RazorSyntaxTree syntaxTree)
|
||||
{
|
||||
var owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex, includeWhitespace: true);
|
||||
var owner = syntaxTree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex, includeWhitespace: true);
|
||||
if (owner is null)
|
||||
{
|
||||
logger.LogWarning($"Owner should never be null.");
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var startElementNode = owner.FirstAncestorOrSelf<MarkupElementSyntax>();
|
||||
if (startElementNode is null || IsInsideProperHtmlContent(context, startElementNode))
|
||||
if (startElementNode is not { EndTag: not null } || LocationOutsideNode(context.StartLocation, startElementNode))
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var endElementNode = GetEndElementNode(context, syntaxTree);
|
||||
var endElementNode = context.StartLocation == context.EndLocation
|
||||
? startElementNode
|
||||
: GetEndElementNode(context, syntaxTree);
|
||||
|
||||
return (startElementNode, endElementNode);
|
||||
}
|
||||
|
||||
private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, MarkupElementSyntax startElementNode)
|
||||
private static bool LocationOutsideNode(SourceLocation location, MarkupElementSyntax node)
|
||||
{
|
||||
// If the provider executes before the user/completion inserts an end tag, the below return fails
|
||||
if (startElementNode.EndTag.IsMissing)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return context.Location.AbsoluteIndex > startElementNode.StartTag.Span.End &&
|
||||
context.Location.AbsoluteIndex < startElementNode.EndTag.SpanStart;
|
||||
return location.AbsoluteIndex > node.StartTag.Span.End &&
|
||||
location.AbsoluteIndex < node.EndTag.SpanStart;
|
||||
}
|
||||
|
||||
private static MarkupElementSyntax? GetEndElementNode(RazorCodeActionContext context, RazorSyntaxTree syntaxTree)
|
||||
{
|
||||
var selectionStart = context.Request.Range.Start;
|
||||
var selectionEnd = context.Request.Range.End;
|
||||
if (selectionStart == selectionEnd)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var endAbsoluteIndex = context.SourceText.GetRequiredAbsoluteIndex(selectionEnd);
|
||||
var endOwner = syntaxTree.Root.FindInnermostNode(endAbsoluteIndex, true);
|
||||
var endOwner = syntaxTree.Root.FindInnermostNode(context.EndLocation.AbsoluteIndex, includeWhitespace: true);
|
||||
if (endOwner is null)
|
||||
{
|
||||
return null;
|
||||
|
@ -148,43 +115,48 @@ internal sealed class ExtractToComponentCodeActionProvider(ILoggerFactory logger
|
|||
return endOwner.FirstAncestorOrSelf<MarkupElementSyntax>();
|
||||
}
|
||||
|
||||
private static ExtractToComponentCodeActionParams CreateInitialActionParams(RazorCodeActionContext context, MarkupElementSyntax startElementNode, string @namespace)
|
||||
private static ExtractToComponentCodeActionParams CreateActionParams(
|
||||
RazorCodeActionContext context,
|
||||
MarkupElementSyntax startNode,
|
||||
MarkupElementSyntax endNode,
|
||||
string @namespace)
|
||||
{
|
||||
var selectionSpan = AreSiblings(startNode, endNode)
|
||||
? TextSpanFromNodes(startNode, endNode)
|
||||
: GetEncompassingTextSpan(startNode, endNode);
|
||||
|
||||
return new ExtractToComponentCodeActionParams
|
||||
{
|
||||
Uri = context.Request.TextDocument.Uri,
|
||||
ExtractStart = startElementNode.Span.Start,
|
||||
ExtractEnd = startElementNode.Span.End,
|
||||
Namespace = @namespace,
|
||||
usingDirectives = []
|
||||
Start = selectionSpan.Start,
|
||||
End = selectionSpan.End,
|
||||
Namespace = @namespace
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a multi-point selection to determine the correct range for extraction.
|
||||
/// </summary>
|
||||
/// <param name="startElementNode">The starting element of the selection.</param>
|
||||
/// <param name="endElementNode">The ending element of the selection, if it exists.</param>
|
||||
/// <param name="actionParams">The parameters for the extraction action, which will be updated.</param>
|
||||
private static void ProcessSelection(MarkupElementSyntax startElementNode, MarkupElementSyntax? endElementNode, ExtractToComponentCodeActionParams actionParams)
|
||||
private static TextSpan GetEncompassingTextSpan(MarkupElementSyntax startNode, MarkupElementSyntax endNode)
|
||||
{
|
||||
// If there's no end element, we can't process a multi-point selection
|
||||
if (endElementNode is null)
|
||||
// Find a valid node that encompasses both the start and the end to
|
||||
// become the selection.
|
||||
SyntaxNode commonAncestor = endNode.Span.Contains(startNode.Span)
|
||||
? endNode
|
||||
: startNode;
|
||||
|
||||
while (commonAncestor is MarkupElementSyntax or
|
||||
MarkupTagHelperAttributeSyntax or
|
||||
MarkupBlockSyntax)
|
||||
{
|
||||
return;
|
||||
if (commonAncestor.Span.Contains(startNode.Span) &&
|
||||
commonAncestor.Span.Contains(endNode.Span))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var startNodeContainsEndNode = endElementNode.Ancestors().Any(node => node == startElementNode);
|
||||
|
||||
// If the start element is an ancestor, keep the original end; otherwise, use the end of the end element
|
||||
if (startNodeContainsEndNode)
|
||||
{
|
||||
actionParams.ExtractEnd = startElementNode.Span.End;
|
||||
return;
|
||||
commonAncestor = commonAncestor.Parent;
|
||||
}
|
||||
|
||||
// If the start element is not an ancestor of the end element, we need to find a common parent
|
||||
// This conditional handles cases where the user's selection spans across different levels of the DOM.
|
||||
// If walking up the tree was required then make sure to reduce
|
||||
// selection back down to minimal nodes needed.
|
||||
// For example:
|
||||
// <div>
|
||||
// {|result:<span>
|
||||
|
@ -196,19 +168,42 @@ internal sealed class ExtractToComponentCodeActionProvider(ILoggerFactory logger
|
|||
// <span>
|
||||
// </span>|}|}
|
||||
// </div>
|
||||
// In this case, we need to find the smallest set of complete elements that covers the entire selection.
|
||||
|
||||
// Find the closest containing sibling pair that encompasses both the start and end elements
|
||||
var (extractStart, extractEnd) = FindContainingSiblingPair(startElementNode, endElementNode);
|
||||
|
||||
// If we found a valid containing pair, update the extraction range
|
||||
if (extractStart is not null && extractEnd is not null)
|
||||
if (commonAncestor != startNode &&
|
||||
commonAncestor != endNode)
|
||||
{
|
||||
actionParams.ExtractStart = extractStart.Span.Start;
|
||||
actionParams.ExtractEnd = extractEnd.Span.End;
|
||||
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
|
||||
}
|
||||
// Note: If we don't find a valid pair, we keep the original extraction range
|
||||
|
||||
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 TextSpanFromNodes(modifiedStart, modifiedEnd);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to extracting the nearest common ancestor span
|
||||
return commonAncestor.Span;
|
||||
}
|
||||
|
||||
private static TextSpan TextSpanFromNodes(SyntaxNode start, SyntaxNode end)
|
||||
=> new(start.Span.Start, end.Span.End - start.Span.Start);
|
||||
|
||||
private static bool AreSiblings(SyntaxNode node1, SyntaxNode node2)
|
||||
=> 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
|
||||
|
@ -216,114 +211,4 @@ internal sealed class ExtractToComponentCodeActionProvider(ILoggerFactory logger
|
|||
// 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);
|
||||
|
||||
private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(SyntaxNode startNode, SyntaxNode endNode)
|
||||
{
|
||||
// Find the lowest common ancestor of both nodes
|
||||
var nearestCommonAncestor = FindNearestCommonAncestor(startNode, endNode);
|
||||
if (nearestCommonAncestor is null)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
SyntaxNode? startContainingNode = null;
|
||||
SyntaxNode? endContainingNode = null;
|
||||
|
||||
// Pre-calculate the spans for comparison
|
||||
var startSpan = startNode.Span;
|
||||
var endSpan = endNode.Span;
|
||||
|
||||
foreach (var child in nearestCommonAncestor.ChildNodes().Where(static node => node.Kind == SyntaxKind.MarkupElement))
|
||||
{
|
||||
var childSpan = child.Span;
|
||||
|
||||
if (startContainingNode is null && childSpan.Contains(startSpan))
|
||||
{
|
||||
startContainingNode = child;
|
||||
if (endContainingNode is not null)
|
||||
break; // Exit if we've found both
|
||||
}
|
||||
|
||||
if (childSpan.Contains(endSpan))
|
||||
{
|
||||
endContainingNode = child;
|
||||
if (startContainingNode is not null)
|
||||
break; // Exit if we've found both
|
||||
}
|
||||
}
|
||||
|
||||
return (startContainingNode, endContainingNode);
|
||||
}
|
||||
|
||||
private static SyntaxNode? FindNearestCommonAncestor(SyntaxNode node1, SyntaxNode node2)
|
||||
{
|
||||
var current = node1;
|
||||
|
||||
while (current is MarkupElementSyntax or
|
||||
MarkupTagHelperAttributeSyntax or
|
||||
MarkupBlockSyntax &&
|
||||
current is not null)
|
||||
{
|
||||
if (current.Span.Contains(node2.Span))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void AddUsingDirectivesInRange(SyntaxNode root, IEnumerable<string> usingsInSourceRazor, int extractStart, int extractEnd, ExtractToComponentCodeActionParams actionParams)
|
||||
{
|
||||
var components = new HashSet<string>();
|
||||
var extractSpan = new TextSpan(extractStart, extractEnd - extractStart);
|
||||
|
||||
foreach (var node in root.DescendantNodes().Where(node => extractSpan.Contains(node.Span)))
|
||||
{
|
||||
if (node is MarkupTagHelperElementSyntax { TagHelperInfo: { } tagHelperInfo })
|
||||
{
|
||||
AddUsingFromTagHelperInfo(tagHelperInfo, components, usingsInSourceRazor, actionParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashSet<string> components, IEnumerable<string> usingsInSourceRazor, ExtractToComponentCodeActionParams actionParams)
|
||||
{
|
||||
foreach (var descriptor in tagHelperInfo.BindingResult.Descriptors)
|
||||
{
|
||||
if (descriptor is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var typeNamespace = descriptor.GetTypeNamespace();
|
||||
|
||||
// Since the using directive at the top of the file may be relative and not absolute,
|
||||
// we need to generate all possible partial namespaces from `typeNamespace`.
|
||||
|
||||
// Potentially, the alternative could be to ask if the using namespace at the top is a substring of `typeNamespace`.
|
||||
// The only potential edge case is if there are very similar namespaces where one
|
||||
// is a substring of the other, but they're actually different (e.g., "My.App" and "My.Apple").
|
||||
|
||||
// Generate all possible partial namespaces from `typeNamespace`, from least to most specific
|
||||
// (assuming that the user writes absolute `using` namespaces most of the time)
|
||||
|
||||
// This is a bit inefficient because at most 'k' string operations are performed (k = parts in the namespace),
|
||||
// for each potential using directive.
|
||||
|
||||
var parts = typeNamespace.Split('.');
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var partialNamespace = string.Join(".", parts.Skip(i));
|
||||
|
||||
if (components.Add(partialNamespace) && usingsInSourceRazor.Contains(partialNamespace))
|
||||
{
|
||||
actionParams.usingDirectives.Add($"@using {partialNamespace}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,36 +3,30 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Syntax;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
|
||||
using Microsoft.AspNetCore.Razor.PooledObjects;
|
||||
using Microsoft.AspNetCore.Razor.Utilities;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
|
||||
using Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor;
|
||||
|
||||
internal sealed class ExtractToComponentCodeActionResolver
|
||||
(
|
||||
internal sealed class ExtractToComponentCodeActionResolver(
|
||||
IDocumentContextFactory documentContextFactory,
|
||||
LanguageServerFeatureOptions languageServerFeatureOptions) : IRazorCodeActionResolver
|
||||
{
|
||||
|
||||
private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory;
|
||||
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;
|
||||
|
||||
|
@ -62,82 +56,75 @@ internal sealed class ExtractToComponentCodeActionResolver
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!FileKinds.IsComponent(componentDocument.GetFileKind()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = FilePathNormalizer.Normalize(actionParams.Uri.GetAbsoluteOrUNCPath());
|
||||
var directoryName = Path.GetDirectoryName(path).AssumeNotNull();
|
||||
var templatePath = Path.Combine(directoryName, "Component");
|
||||
var componentPath = FileUtilities.GenerateUniquePath(templatePath, ".razor");
|
||||
|
||||
// VS Code in Windows expects path to start with '/'
|
||||
var updatedComponentPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !componentPath.StartsWith('/')
|
||||
? '/' + componentPath
|
||||
: componentPath;
|
||||
|
||||
var newComponentUri = new UriBuilder
|
||||
{
|
||||
Scheme = Uri.UriSchemeFile,
|
||||
Path = updatedComponentPath,
|
||||
Host = string.Empty,
|
||||
}.Uri;
|
||||
|
||||
var text = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (text is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = FilePathNormalizer.Normalize(actionParams.Uri.GetAbsoluteOrUNCPath());
|
||||
var directoryName = Path.GetDirectoryName(path).AssumeNotNull();
|
||||
var templatePath = Path.Combine(directoryName, "Component.razor");
|
||||
var componentPath = FileUtilities.GenerateUniquePath(templatePath, ".razor");
|
||||
var componentName = Path.GetFileNameWithoutExtension(componentPath);
|
||||
var newComponentContent = string.Empty;
|
||||
|
||||
newComponentContent += string.Join(Environment.NewLine, actionParams.usingDirectives);
|
||||
if (actionParams.usingDirectives.Count > 0)
|
||||
// VS Code in Windows expects path to start with '/'
|
||||
componentPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !componentPath.StartsWith('/')
|
||||
? '/' + componentPath
|
||||
: componentPath;
|
||||
|
||||
var newComponentUri = new UriBuilder
|
||||
{
|
||||
newComponentContent += Environment.NewLine + Environment.NewLine; // Ensure there's a newline after the dependencies if any exist.
|
||||
Scheme = Uri.UriSchemeFile,
|
||||
Path = componentPath,
|
||||
Host = string.Empty,
|
||||
}.Uri;
|
||||
|
||||
using var _ = StringBuilderPool.GetPooledObject(out var builder);
|
||||
|
||||
var syntaxTree = componentDocument.GetSyntaxTree();
|
||||
foreach (var usingDirective in GetUsingsInDocument(syntaxTree))
|
||||
{
|
||||
builder.AppendLine(usingDirective);
|
||||
}
|
||||
|
||||
newComponentContent += text.GetSubTextString(new CodeAnalysis.Text.TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart)).Trim();
|
||||
var extractedText = text.GetSubTextString(new TextSpan(actionParams.Start, actionParams.End - actionParams.Start)).Trim();
|
||||
builder.Append(extractedText);
|
||||
|
||||
var start = componentDocument.Source.Text.Lines.GetLinePosition(actionParams.ExtractStart);
|
||||
var end = componentDocument.Source.Text.Lines.GetLinePosition(actionParams.ExtractEnd);
|
||||
var start = componentDocument.Source.Text.Lines.GetLinePosition(actionParams.Start);
|
||||
var end = componentDocument.Source.Text.Lines.GetLinePosition(actionParams.End);
|
||||
var removeRange = new Range
|
||||
{
|
||||
Start = new Position(start.Line, start.Character),
|
||||
End = new Position(end.Line, end.Character)
|
||||
};
|
||||
|
||||
var componentDocumentIdentifier = new OptionalVersionedTextDocumentIdentifier { Uri = actionParams.Uri };
|
||||
var newComponentDocumentIdentifier = new OptionalVersionedTextDocumentIdentifier { Uri = newComponentUri };
|
||||
|
||||
var documentChanges = new SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[]
|
||||
{
|
||||
new CreateFile { Uri = newComponentUri },
|
||||
new TextDocumentEdit
|
||||
{
|
||||
TextDocument = componentDocumentIdentifier,
|
||||
Edits = new[]
|
||||
{
|
||||
TextDocument = new OptionalVersionedTextDocumentIdentifier { Uri = actionParams.Uri },
|
||||
Edits =
|
||||
[
|
||||
new TextEdit
|
||||
{
|
||||
NewText = $"<{componentName} />",
|
||||
Range = removeRange,
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
new TextDocumentEdit
|
||||
{
|
||||
TextDocument = newComponentDocumentIdentifier,
|
||||
Edits = new[]
|
||||
{
|
||||
TextDocument = new OptionalVersionedTextDocumentIdentifier { Uri = newComponentUri },
|
||||
Edits =
|
||||
[
|
||||
new TextEdit
|
||||
{
|
||||
NewText = newComponentContent,
|
||||
NewText = builder.ToString(),
|
||||
Range = new Range { Start = new Position(0, 0), End = new Position(0, 0) },
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -146,4 +133,19 @@ internal sealed class ExtractToComponentCodeActionResolver
|
|||
DocumentChanges = documentChanges,
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetUsingsInDocument(RazorSyntaxTree syntaxTree)
|
||||
=> syntaxTree
|
||||
.Root
|
||||
.ChildNodes()
|
||||
.Select(node =>
|
||||
{
|
||||
if (node.IsUsingDirective(out var _))
|
||||
{
|
||||
return node.ToFullString().Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.WhereNotNull();
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ internal sealed class GenerateMethodCodeActionProvider : IRazorCodeActionProvide
|
|||
}
|
||||
|
||||
var syntaxTree = context.CodeDocument.GetSyntaxTree();
|
||||
var owner = syntaxTree.Root.FindToken(context.Location.AbsoluteIndex).Parent.AssumeNotNull();
|
||||
var owner = syntaxTree.Root.FindToken(context.StartLocation.AbsoluteIndex).Parent.AssumeNotNull();
|
||||
|
||||
if (IsGenerateEventHandlerValid(owner, out var methodName, out var eventName))
|
||||
{
|
||||
|
|
|
@ -13,7 +13,8 @@ internal sealed record class RazorCodeActionContext(
|
|||
VSCodeActionParams Request,
|
||||
IDocumentSnapshot DocumentSnapshot,
|
||||
RazorCodeDocument CodeDocument,
|
||||
SourceLocation Location,
|
||||
SourceLocation StartLocation,
|
||||
SourceLocation EndLocation,
|
||||
SourceText SourceText,
|
||||
bool SupportsFileCreation,
|
||||
bool SupportsCodeActionResolve);
|
||||
|
|
|
@ -339,7 +339,15 @@ $$Path;
|
|||
|
||||
var sourceText = SourceText.From(text);
|
||||
|
||||
var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, supportsCodeActionResolve);
|
||||
var context = new RazorCodeActionContext(
|
||||
request,
|
||||
documentSnapshot,
|
||||
codeDocument,
|
||||
location,
|
||||
location,
|
||||
sourceText,
|
||||
supportsFileCreation,
|
||||
supportsCodeActionResolve);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
|
@ -470,7 +470,15 @@ public class TypeAccessibilityCodeActionProviderTest(ITestOutputHelper testOutpu
|
|||
|
||||
var sourceText = SourceText.From(text);
|
||||
|
||||
var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, supportsCodeActionResolve);
|
||||
var context = new RazorCodeActionContext(
|
||||
request,
|
||||
documentSnapshot,
|
||||
codeDocument,
|
||||
location,
|
||||
location,
|
||||
sourceText,
|
||||
supportsFileCreation,
|
||||
supportsCodeActionResolve);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
|
@ -1043,7 +1043,7 @@ public class CodeActionEndToEndTest(ITestOutputHelper testOutput) : SingleServer
|
|||
input,
|
||||
expectedRazorComponent,
|
||||
ExtractToComponentTitle,
|
||||
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
|
||||
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider()],
|
||||
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
|
||||
}
|
||||
|
||||
|
@ -1076,7 +1076,7 @@ public class CodeActionEndToEndTest(ITestOutputHelper testOutput) : SingleServer
|
|||
input,
|
||||
expectedRazorComponent,
|
||||
ExtractToComponentTitle,
|
||||
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
|
||||
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider()],
|
||||
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
|
||||
}
|
||||
|
||||
|
@ -1111,7 +1111,7 @@ public class CodeActionEndToEndTest(ITestOutputHelper testOutput) : SingleServer
|
|||
input,
|
||||
expectedRazorComponent,
|
||||
ExtractToComponentTitle,
|
||||
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
|
||||
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider()],
|
||||
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
|
||||
}
|
||||
|
||||
|
|
|
@ -158,7 +158,15 @@ public class DefaultHtmlCodeActionProviderTest(ITestOutputHelper testOutput) : L
|
|||
|
||||
var sourceText = SourceText.From(text);
|
||||
|
||||
var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, supportsCodeActionResolve);
|
||||
var context = new RazorCodeActionContext(
|
||||
request,
|
||||
documentSnapshot,
|
||||
codeDocument,
|
||||
location,
|
||||
location,
|
||||
sourceText,
|
||||
supportsFileCreation,
|
||||
supportsCodeActionResolve);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
|
@ -470,7 +470,15 @@ public class ComponentAccessibilityCodeActionProviderTest(ITestOutputHelper test
|
|||
|
||||
var sourceText = SourceText.From(text);
|
||||
|
||||
var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, SupportsCodeActionResolve: true);
|
||||
var context = new RazorCodeActionContext(
|
||||
request,
|
||||
documentSnapshot,
|
||||
codeDocument,
|
||||
location,
|
||||
location,
|
||||
sourceText,
|
||||
supportsFileCreation,
|
||||
SupportsCodeActionResolve: true);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
|
@ -401,7 +401,15 @@ public class ExtractToCodeBehindCodeActionProviderTest(ITestOutputHelper testOut
|
|||
|
||||
var sourceText = SourceText.From(text);
|
||||
|
||||
var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, SupportsCodeActionResolve: true);
|
||||
var context = new RazorCodeActionContext(
|
||||
request,
|
||||
documentSnapshot,
|
||||
codeDocument,
|
||||
location,
|
||||
location,
|
||||
sourceText,
|
||||
supportsFileCreation,
|
||||
SupportsCodeActionResolve: true);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
<div id="parent">
|
||||
<div>
|
||||
<h1>Div a title</h1>
|
||||
<p>Div $$a par</p>
|
||||
<p>Div [||]a par</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Div b title</h1>
|
||||
|
@ -50,7 +50,7 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
|
||||
Welcome to your new app.
|
||||
""";
|
||||
TestFileMarkupParser.GetPosition(contents, out contents, out var cursorPosition);
|
||||
TestFileMarkupParser.GetSpan(contents, out contents, out var selectionSpan);
|
||||
|
||||
var request = new VSCodeActionParams()
|
||||
{
|
||||
|
@ -59,11 +59,10 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
Context = new VSInternalCodeActionContext()
|
||||
};
|
||||
|
||||
var location = new SourceLocation(cursorPosition, -1, -1);
|
||||
var context = CreateRazorCodeActionContext(request, location, documentPath, contents);
|
||||
var context = CreateRazorCodeActionContext(request, selectionSpan, documentPath, contents);
|
||||
context.CodeDocument.SetFileKind(FileKinds.Legacy);
|
||||
|
||||
var provider = new ExtractToComponentCodeActionProvider(LoggerFactory);
|
||||
var provider = new ExtractToComponentCodeActionProvider();
|
||||
|
||||
// Act
|
||||
var commandOrCodeActionContainer = await provider.ProvideAsync(context, default);
|
||||
|
@ -83,7 +82,7 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<div id="parent">
|
||||
<$$div>
|
||||
<[||]div>
|
||||
<h1>Div a title</h1>
|
||||
<p>Div a par</p>
|
||||
</div>
|
||||
|
@ -97,7 +96,7 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
|
||||
Welcome to your new app.
|
||||
""";
|
||||
TestFileMarkupParser.GetPosition(contents, out contents, out var cursorPosition);
|
||||
TestFileMarkupParser.GetSpan(contents, out contents, out var selectionSpan);
|
||||
|
||||
var request = new VSCodeActionParams()
|
||||
{
|
||||
|
@ -106,10 +105,9 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
Context = new VSInternalCodeActionContext()
|
||||
};
|
||||
|
||||
var location = new SourceLocation(cursorPosition, -1, -1);
|
||||
var context = CreateRazorCodeActionContext(request, location, documentPath, contents, supportsFileCreation: true);
|
||||
var context = CreateRazorCodeActionContext(request, selectionSpan, documentPath, contents, supportsFileCreation: true);
|
||||
|
||||
var provider = new ExtractToComponentCodeActionProvider(LoggerFactory);
|
||||
var provider = new ExtractToComponentCodeActionProvider();
|
||||
|
||||
// Act
|
||||
var commandOrCodeActionContainer = await provider.ProvideAsync(context, default);
|
||||
|
@ -118,119 +116,6 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
Assert.NotEmpty(commandOrCodeActionContainer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultiPointSelection_ReturnsNotEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var documentPath = "c:/Test.razor";
|
||||
var contents = """
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<div id="parent">
|
||||
[|<div>
|
||||
$$<h1>Div a title</h1>
|
||||
<p>Div a par</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Div b title</h1>
|
||||
<p>Div b par</p>
|
||||
</div|]>
|
||||
</div>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
""";
|
||||
TestFileMarkupParser.GetPositionAndSpan(contents, out contents, out var cursorPosition, out var selectionSpan);
|
||||
|
||||
var request = new VSCodeActionParams()
|
||||
{
|
||||
TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) },
|
||||
Range = VsLspFactory.DefaultRange,
|
||||
Context = new VSInternalCodeActionContext()
|
||||
};
|
||||
|
||||
var location = new SourceLocation(cursorPosition, -1, -1);
|
||||
var context = CreateRazorCodeActionContext(request, location, documentPath, contents);
|
||||
|
||||
var lineSpan = context.SourceText.GetLinePositionSpan(selectionSpan);
|
||||
request.Range = VsLspFactory.CreateRange(lineSpan);
|
||||
|
||||
var provider = new ExtractToComponentCodeActionProvider(LoggerFactory);
|
||||
|
||||
// Act
|
||||
var commandOrCodeActionContainer = await provider.ProvideAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(commandOrCodeActionContainer);
|
||||
var codeAction = Assert.Single(commandOrCodeActionContainer);
|
||||
var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize<RazorCodeActionResolutionParams>();
|
||||
Assert.NotNull(razorCodeActionResolutionParams);
|
||||
var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize<ExtractToComponentCodeActionParams>();
|
||||
Assert.NotNull(actionParams);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultiPointSelection_WithEndAfterElement_ReturnsCurrentElement()
|
||||
{
|
||||
// Arrange
|
||||
var documentPath = "c:/Test.razor";
|
||||
var contents = """
|
||||
@page "/"
|
||||
@namespace MarketApp.Pages.Product.Home
|
||||
|
||||
namespace MarketApp.Pages.Product.Home
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<div id="parent">
|
||||
[|$$<div>
|
||||
<h1>Div a title</h1>
|
||||
<p>Div a par</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Div b title</h1>
|
||||
<p>Div b par</p>
|
||||
</div>|]
|
||||
</div>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
""";
|
||||
TestFileMarkupParser.GetPositionAndSpan(contents, out contents, out var cursorPosition, out var selectionSpan);
|
||||
|
||||
var request = new VSCodeActionParams()
|
||||
{
|
||||
TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) },
|
||||
Range = VsLspFactory.DefaultRange,
|
||||
Context = new VSInternalCodeActionContext()
|
||||
};
|
||||
|
||||
var location = new SourceLocation(cursorPosition, -1, -1);
|
||||
var context = CreateRazorCodeActionContext(request, location, documentPath, contents);
|
||||
|
||||
var lineSpan = context.SourceText.GetLinePositionSpan(selectionSpan);
|
||||
request.Range = VsLspFactory.CreateRange(lineSpan);
|
||||
|
||||
var provider = new ExtractToComponentCodeActionProvider(LoggerFactory);
|
||||
|
||||
// Act
|
||||
var commandOrCodeActionContainer = await provider.ProvideAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(commandOrCodeActionContainer);
|
||||
var codeAction = Assert.Single(commandOrCodeActionContainer);
|
||||
var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize<RazorCodeActionResolutionParams>();
|
||||
Assert.NotNull(razorCodeActionResolutionParams);
|
||||
var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize<ExtractToComponentCodeActionParams>();
|
||||
Assert.NotNull(actionParams);
|
||||
Assert.Equal(selectionSpan.Start, actionParams.ExtractStart);
|
||||
Assert.Equal(selectionSpan.End, actionParams.ExtractEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_InProperMarkup_ReturnsEmpty()
|
||||
{
|
||||
|
@ -244,7 +129,7 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
<div id="parent">
|
||||
<div>
|
||||
<h1>Div a title</h1>
|
||||
<p>Div $$a par</p>
|
||||
<p>Div [||]a par</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Div b title</h1>
|
||||
|
@ -256,7 +141,7 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
|
||||
Welcome to your new app.
|
||||
""";
|
||||
TestFileMarkupParser.GetPosition(contents, out contents, out var cursorPosition);
|
||||
TestFileMarkupParser.GetSpan(contents, out contents, out var selectionSpan);
|
||||
|
||||
var request = new VSCodeActionParams()
|
||||
{
|
||||
|
@ -265,10 +150,13 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
Context = new VSInternalCodeActionContext()
|
||||
};
|
||||
|
||||
var location = new SourceLocation(cursorPosition, -1, -1);
|
||||
var context = CreateRazorCodeActionContext(request, location, documentPath, contents);
|
||||
var context = CreateRazorCodeActionContext(
|
||||
request,
|
||||
selectionSpan,
|
||||
documentPath,
|
||||
contents);
|
||||
|
||||
var provider = new ExtractToComponentCodeActionProvider(LoggerFactory);
|
||||
var provider = new ExtractToComponentCodeActionProvider();
|
||||
|
||||
// Act
|
||||
var commandOrCodeActionContainer = await provider.ProvideAsync(context, default);
|
||||
|
@ -277,11 +165,117 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
Assert.Empty(commandOrCodeActionContainer);
|
||||
}
|
||||
|
||||
private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionParams request, SourceLocation location, string filePath, string text, bool supportsFileCreation = true)
|
||||
=> CreateRazorCodeActionContext(request, location, filePath, text, relativePath: filePath, supportsFileCreation: supportsFileCreation);
|
||||
[Fact]
|
||||
public Task Handle_MultiPointSelection_ReturnsNotEmpty()
|
||||
=> TestSelectionStartAndCursorAsync("""
|
||||
@page "/"
|
||||
|
||||
private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionParams request, SourceLocation location, string filePath, string text, string? relativePath, bool supportsFileCreation = true)
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<div id="parent">
|
||||
[|<div>
|
||||
|]<h1>Div a title</h1>
|
||||
<p>Div a par</p>
|
||||
</div>$$
|
||||
<div>
|
||||
<h1>Div b title</h1>
|
||||
<p>Div b par</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
""");
|
||||
|
||||
[Fact]
|
||||
public Task Handle_MultiPointSelection_WithEndAfterElement()
|
||||
=> TestSelectionStartAndCursorAsync("""
|
||||
@page "/"
|
||||
@namespace MarketApp.Pages.Product.Home
|
||||
|
||||
namespace MarketApp.Pages.Product.Home
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<div id="parent">
|
||||
[|<div>
|
||||
<h1>Div a title</h1>
|
||||
<p>Div a par</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Div b title</h1>
|
||||
<p>Div b par</p>
|
||||
</div>$$|]
|
||||
</div>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
""");
|
||||
|
||||
[Fact]
|
||||
public Task Handle_MultiPointSelection_WithEndInsideSiblingElement()
|
||||
=> TestSelectionStartAndCursorAsync("""
|
||||
@page "/"
|
||||
@namespace MarketApp.Pages.Product.Home
|
||||
|
||||
namespace MarketApp.Pages.Product.Home
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<div id="parent">
|
||||
[|<div>
|
||||
<h1>Div a title</h1>
|
||||
<p>Div a par</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Div b title</h1>|]
|
||||
<p>Div b par</p>
|
||||
</div>$$
|
||||
</div>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
""");
|
||||
|
||||
[Fact]
|
||||
public Task Handle_MultiPointSelection_WithEndInsideElement()
|
||||
=> TestSelectionStartAndCursorAsync("""
|
||||
@page "/"
|
||||
@namespace MarketApp.Pages.Product.Home
|
||||
|
||||
namespace MarketApp.Pages.Product.Home
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<div id="parent">
|
||||
[|<div>
|
||||
<h1>Div a title</h1>
|
||||
<p>Div a par</p>|]
|
||||
</div>$$
|
||||
<div>
|
||||
<h1>Div b title</h1>
|
||||
<p>Div b par</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
""");
|
||||
|
||||
private static RazorCodeActionContext CreateRazorCodeActionContext(
|
||||
VSCodeActionParams request,
|
||||
TextSpan selectionSpan,
|
||||
string filePath,
|
||||
string text,
|
||||
string? relativePath = null,
|
||||
bool supportsFileCreation = true)
|
||||
{
|
||||
relativePath ??= filePath;
|
||||
|
||||
var sourceDocument = RazorSourceDocument.Create(text, RazorSourceDocumentProperties.Create(filePath, relativePath));
|
||||
var options = RazorParserOptions.Create(o =>
|
||||
{
|
||||
|
@ -303,8 +297,53 @@ public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutp
|
|||
|
||||
var sourceText = SourceText.From(text);
|
||||
|
||||
var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, SupportsCodeActionResolve: true);
|
||||
var context = new RazorCodeActionContext(
|
||||
request,
|
||||
documentSnapshot,
|
||||
codeDocument,
|
||||
new SourceLocation(selectionSpan.Start, -1, -1),
|
||||
new SourceLocation(selectionSpan.End, -1, -1),
|
||||
sourceText,
|
||||
supportsFileCreation,
|
||||
SupportsCodeActionResolve: true);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the contents where the expected start/end are marked by '[|' and '$$'
|
||||
/// </summary>
|
||||
private async Task TestSelectionStartAndCursorAsync(string contents)
|
||||
{
|
||||
// Arrange
|
||||
var documentPath = "c:/Test.razor";
|
||||
TestFileMarkupParser.GetPositionAndSpan(contents, out contents, out var cursorPosition, out var selectionSpan);
|
||||
|
||||
var request = new VSCodeActionParams()
|
||||
{
|
||||
TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) },
|
||||
Range = VsLspFactory.DefaultRange,
|
||||
Context = new VSInternalCodeActionContext()
|
||||
};
|
||||
|
||||
var context = CreateRazorCodeActionContext(request, selectionSpan, documentPath, contents);
|
||||
|
||||
var lineSpan = context.SourceText.GetLinePositionSpan(selectionSpan);
|
||||
request.Range = VsLspFactory.CreateRange(lineSpan);
|
||||
|
||||
var provider = new ExtractToComponentCodeActionProvider();
|
||||
|
||||
// Act
|
||||
var commandOrCodeActionContainer = await provider.ProvideAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(commandOrCodeActionContainer);
|
||||
var codeAction = Assert.Single(commandOrCodeActionContainer);
|
||||
var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize<RazorCodeActionResolutionParams>();
|
||||
Assert.NotNull(razorCodeActionResolutionParams);
|
||||
var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize<ExtractToComponentCodeActionParams>();
|
||||
Assert.NotNull(actionParams);
|
||||
Assert.Equal(selectionSpan.Start, actionParams.Start);
|
||||
Assert.Equal(cursorPosition, actionParams.End);
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче