Skip to content

Commit

Permalink
Rebase. Made some mistakes while rebasing, I'm sorry.
Browse files Browse the repository at this point in the history
Integrated previous PR feedback into this branch.
  • Loading branch information
marcarro committed Sep 3, 2024
1 parent b37e6d8 commit 5b76f23
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ internal sealed class ExtractToCodeBehindCodeActionParams
[JsonPropertyName("extractEnd")]
public int ExtractEnd { get; set; }

[JsonPropertyName("removeStart")]
public int RemoveStart { get; set; }

[JsonPropertyName("removeEnd")]
public int RemoveEnd { get; set; }

[JsonPropertyName("namespace")]
public required string Namespace { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;

Expand All @@ -14,15 +15,15 @@ internal sealed class ExtractToComponentCodeActionParams
[JsonPropertyName("uri")]
public required Uri Uri { get; set; }

[JsonPropertyName("extractStart")]
public int ExtractStart { get; set; }
[JsonPropertyName("selectStart")]
public required Position SelectStart { get; set; }

[JsonPropertyName("extractEnd")]
public int ExtractEnd { get; set; }
[JsonPropertyName("selectEnd")]
public required Position SelectEnd { get; set; }

[JsonPropertyName("absoluteIndex")]
public required int AbsoluteIndex { get; set; }

[JsonPropertyName("namespace")]
public required string Namespace { get; set; }

[JsonPropertyName("usingDirectives")]
public required List<string> usingDirectives { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,15 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct
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));
var actionParams = new ExtractToComponentCodeActionParams
{
Uri = context.Request.TextDocument.Uri,
SelectStart = context.Request.Range.Start,
SelectEnd = context.Request.Range.End,
AbsoluteIndex = context.Location.AbsoluteIndex,
Namespace = @namespace,
};

AddUsingDirectivesInRange(utilityScanRoot,
usingNamespaceStrings,
actionParams.ExtractStart,
actionParams.ExtractEnd,
actionParams);

var resolutionParams = new RazorCodeActionResolutionParams()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
using Microsoft.VisualStudio.Utilities;
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
using Microsoft.AspNetCore.Razor.PooledObjects;

namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;

Expand Down Expand Up @@ -72,6 +73,8 @@ internal sealed class ExtractToComponentCodeActionResolver(
}

var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
var syntaxTree = codeDocument.GetSyntaxTree();

if (codeDocument.IsUnsupported())
{
return null;
Expand Down Expand Up @@ -199,7 +202,10 @@ internal sealed record SelectionAnalysisResult

private static SelectionAnalysisResult TryAnalyzeSelection(RazorCodeDocument codeDocument, ExtractToComponentCodeActionParams actionParams)
{
var (startElementNode, endElementNode) = GetStartAndEndElements(codeDocument, actionParams);
var syntaxTree = codeDocument.GetSyntaxTree();
var sourceText = codeDocument.Source.Text;

var (startElementNode, endElementNode) = GetStartAndEndElements(sourceText, syntaxTree, actionParams);
if (startElementNode is null)
{
return new SelectionAnalysisResult { Success = false };
Expand All @@ -221,7 +227,7 @@ private static SelectionAnalysisResult TryAnalyzeSelection(RazorCodeDocument cod
}

var dependencyScanRoot = FindNearestCommonAncestor(startElementNode, endElementNode) ?? startElementNode;
var usingDirectives = GetUsingDirectivesInRange(dependencyScanRoot, extractStart, extractEnd);
var usingDirectives = GetUsingDirectivesInRange(syntaxTree, dependencyScanRoot, extractStart, extractEnd);
var hasOtherIdentifiers = CheckHasOtherIdentifiers(dependencyScanRoot, extractStart, extractEnd);

return new SelectionAnalysisResult
Expand All @@ -235,9 +241,8 @@ private static SelectionAnalysisResult TryAnalyzeSelection(RazorCodeDocument cod
};
}

private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndElements(RazorCodeDocument codeDocument, ExtractToComponentCodeActionParams actionParams)
private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndElements(SourceText sourceText, RazorSyntaxTree syntaxTree, ExtractToComponentCodeActionParams actionParams)
{
var syntaxTree = codeDocument.GetSyntaxTree();
if (syntaxTree is null)
{
return (null, null);
Expand All @@ -255,32 +260,23 @@ private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndEl
return (null, null);
}

var sourceText = codeDocument.GetSourceText();
if (sourceText is null)
{
return (null, null);
}

var endElementNode = TryGetEndElementNode(actionParams.SelectStart, actionParams.SelectEnd, syntaxTree, sourceText);
var endElementNode = GetEndElementNode(sourceText, syntaxTree, actionParams);

return (startElementNode, endElementNode);
}

private static MarkupSyntaxNode? TryGetEndElementNode(Position selectionStart, Position selectionEnd, RazorSyntaxTree syntaxTree, SourceText sourceText)
private static MarkupSyntaxNode? GetEndElementNode(SourceText sourceText, RazorSyntaxTree syntaxTree, ExtractToComponentCodeActionParams actionParams)
{
if (selectionStart == selectionEnd)
{
return null;
}
var selectionStart = actionParams.SelectStart;
var selectionEnd = actionParams.SelectEnd;

var endLocation = GetEndLocation(selectionEnd, sourceText);
if (!endLocation.HasValue)
if (selectionStart == selectionEnd)
{
return null;
}

var endOwner = syntaxTree.Root.FindInnermostNode(endLocation.Value.AbsoluteIndex, true);

var endAbsoluteIndex = sourceText.GetRequiredAbsoluteIndex(selectionEnd);
var endOwner = syntaxTree.Root.FindInnermostNode(endAbsoluteIndex, true);
if (endOwner is null)
{
return null;
Expand All @@ -295,16 +291,6 @@ private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndEl
return endOwner.FirstAncestorOrSelf<MarkupSyntaxNode>(node => node is MarkupTagHelperElementSyntax or MarkupElementSyntax);
}

private static SourceLocation? GetEndLocation(Position selectionEnd, SourceText sourceText)
{
if (!selectionEnd.TryGetSourceLocation(sourceText, logger: default, out var location))
{
return null;
}

return location;
}

/// <summary>
/// Processes a selection, providing the start and end of the extraction range if successful.
/// </summary>
Expand Down Expand Up @@ -377,13 +363,9 @@ private static bool TryProcessSelection(
return true;
}

var endLocation = GetEndLocation(actionParams.SelectEnd, codeDocument.GetSourceText());
if (!endLocation.HasValue)
{
return false;
}
var endLocation = codeDocument.Source.Text.GetRequiredAbsoluteIndex(actionParams.SelectEnd);

var endOwner = codeDocument.GetSyntaxTree().Root.FindInnermostNode(endLocation.Value.AbsoluteIndex, true);
var endOwner = codeDocument.GetSyntaxTree().Root.FindInnermostNode(endLocation, true);
var endCodeBlock = endOwner?.FirstAncestorOrSelf<CSharpCodeBlockSyntax>();
if (endOwner is not null && endOwner.TryGetPreviousSibling(out var previousSibling))
{
Expand Down Expand Up @@ -475,8 +457,48 @@ private static bool IsValidNode(SyntaxNode node, bool isCodeBlock)
return node is MarkupElementSyntax or MarkupTagHelperElementSyntax || (isCodeBlock && node is CSharpCodeBlockSyntax);
}

private static HashSet<string> GetUsingDirectivesInRange(SyntaxNode root, int extractStart, int extractEnd)
private static HashSet<string> GetUsingDirectivesInRange(RazorSyntaxTree syntaxTree, SyntaxNode root, int extractStart, int extractEnd)
{
// The new component usings are going to be a subset of the usings in the source razor file.
using var pooledStringArray = new PooledArrayBuilder<string>();
foreach (var node in syntaxTree.Root.DescendantNodes())
{
if (node.IsUsingDirective(out var children))
{
var sb = new StringBuilder();
var identifierFound = false;
var lastIdentifierIndex = -1;

// First pass: find the last identifier
for (var i = 0; i < children.Count; i++)
{
if (children[i] is Language.Syntax.SyntaxToken token && token.Kind == Language.SyntaxKind.Identifier)
{
lastIdentifierIndex = i;
}
}

// Second pass: build the string
for (var i = 0; i <= lastIdentifierIndex; i++)
{
var child = children[i];
if (child is Language.Syntax.SyntaxToken tkn && tkn.Kind == Language.SyntaxKind.Identifier)
{
identifierFound = true;
}
if (identifierFound)
{
var token = child as Language.Syntax.SyntaxToken;
sb.Append(token?.Content);
}
}

pooledStringArray.Add(sb.ToString());
}
}

var usingsInSourceRazor = pooledStringArray.ToArray();

var usings = new HashSet<string>();
var extractSpan = new TextSpan(extractStart, extractEnd - extractStart);

Expand All @@ -490,7 +512,7 @@ private static HashSet<string> GetUsingDirectivesInRange(SyntaxNode root, int ex

if (node is MarkupTagHelperElementSyntax { TagHelperInfo: { } tagHelperInfo })
{
AddUsingFromTagHelperInfo(tagHelperInfo, usings);
AddUsingFromTagHelperInfo(tagHelperInfo, usings, usingsInSourceRazor);
}
}

Expand Down Expand Up @@ -550,7 +572,7 @@ private static bool CheckHasOtherIdentifiers(SyntaxNode root, int extractStart,
return false;
}

private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashSet<string> dependencies)
private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashSet<string> usings, string[] usingsInSourceRazor)
{
foreach (var descriptor in tagHelperInfo.BindingResult.Descriptors)
{
Expand All @@ -560,7 +582,31 @@ private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashS
}

var typeNamespace = descriptor.GetTypeNamespace();
dependencies.Add(typeNamespace);

// 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 (usingsInSourceRazor.Contains(partialNamespace))
{
usings.Add(partialNamespace);
break;
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,24 +331,10 @@ private async Task ValidateExtractComponentCodeActionAsync(
var razorFilePath = "C:/path/Test.razor";
var componentFilePath = "C:/path/Component.razor";
var codeDocument = CreateCodeDocument(input, filePath: razorFilePath);
var sourceText = codeDocument.GetSourceText();
var sourceText = codeDocument.Source.Text;
var uri = new Uri(razorFilePath);
var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath, additionalRazorDocuments);

//var projectManager = CreateProjectSnapshotManager();

//await projectManager.UpdateAsync(updater =>
//{
// updater.ProjectAdded(new(
// projectFilePath: "C:/path/to/project.csproj",
// intermediateOutputPath: "C:/path/to/obj",
// razorConfiguration: RazorConfiguration.Default,
// rootNamespace: "project"));
//});

//var componentSearchEngine = new DefaultRazorComponentSearchEngine(projectManager, LoggerFactory);
//var componentDefinitionService = new RazorComponentDe

var documentContext = CreateDocumentContext(uri, codeDocument);
var requestContext = new RazorRequestContext(documentContext, null!, "lsp/method", uri: null);

Expand Down

0 comments on commit 5b76f23

Please sign in to comment.