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

Extract To Component: Code handling capabilities #10760

Open
wants to merge 29 commits into
base: features/extract-to-component
Choose a base branch
from

Conversation

marcarro
Copy link
Contributor

@marcarro marcarro commented Aug 19, 2024

Summary of the changes

Added base support for code in Razor elements. Specifically, the feature now automatically parametrizes methods when applicable, and extracts any appropriate fields.

Also includes refactoring of previously done work, which shifts complex responsibilities to the code action resolver (i.e. the provider was doing things like scanning through the syntax tree for the dependencies. These and others were moved.)

Note: Added work includes an API call to a roslyn endpoint which has not been pushed to as of writing.

@marcarro marcarro requested review from a team as code owners August 19, 2024 04:05
ryzngard added a commit that referenced this pull request Aug 30, 2024
### Summary of the changes
Addition to feature wherein razor component dependencies are now
identified, and their corresponding `@usings` are now also extracted
into the new document.

## **_Important!_** 
Notes: Some changes in this PR are further rectified in #10760, such as:

`AddComponentDependenciesInRange` does not take actionParams anymore. In
general, most (if not all) of the internal methods that take
`actionParams` as a parameter were moved to the resolver, where each
method returns a collection of appropiate objects.
…ribute promotion functionality, Resolver now handles event handlers and data binding
Integrated previous PR feedback into this branch.
Copy link
Contributor

@ryzngard ryzngard left a comment

Choose a reason for hiding this comment

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

Stopped at ExtractToComponentCodeAction. There seems to be some duplicate work going on to walk the syntax tree that can be reduced.

return context is not null &&
context.SupportsFileCreation &&
FileKinds.IsComponent(context.CodeDocument.GetFileKind()) &&
context.CodeDocument.GetSyntaxTree()?.Root is not null;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: RazorSyntaxTree.Root can't be null, so just need to check if the syntax tree is null

@@ -24,65 +27,35 @@ internal sealed class ExtractToComponentCodeActionProvider(ILoggerFactory logger

public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
{
if (context is null)
if (!IsValidContext(context))
Copy link
Contributor

Choose a reason for hiding this comment

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

much cleaner checking!

{
return null;
}
var tryMakeMarkupElement = owner.FirstAncestorOrSelf<MarkupElementSyntax>();
Copy link
Contributor

Choose a reason for hiding this comment

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

You're walking the tree twice. I would suggest doing

var (startTag, endTag) = GetStartAndEndTag(owner);

if (startTag is null || endTag is null)
{
	return false;
}


return startTag.Span.Contains(context.Location.AbsoluteIndex) || endTag.Span.Contains(context.Location.AbsoluteIndex);

static (MarkupTagHelperStartTagSyntax? startTag, MarkupTagHelperStartTagSyntax? endTag) GetStartAndEndTag(owner)
{
	var potentialElement = owner.FirstAncestor<MarkupSyntaxNode>(e => e is MarkupElementSyntax || MarkupTagHelperElementSyntax);

	return potentialElement switch 
	{
		MarkupElementSyntax markupElement => (markupElement.StartTag, markupElement .EndTag),
		MarkupTagHelperElementSyntax tagHelper => (tagHelper.StartTag, tagHelper.EndTag),
		_ => (null, null);
	}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did try doing this before, but MarkupElementSyntax is inconvertible to MarkupTagHelperElementSyntax, along with their start and end tags. This is why I'm doing two separate variables for it. I think I can walk up the tree once to get them both, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't trying to convert between the two, it's just getting the start/end tags. Although the return type should probably be (MarkupSyntaxNode? startTag, MarkupSyntaxNode? endTag) but that's what I get for coding on GitHub :P

}
// Note: If we don't find a valid pair, we keep the original extraction range
var diagnostics = markupElement.GetDiagnostics();
return !diagnostics.Any(d => d.Severity == RazorDiagnosticSeverity.Error);
}

private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen(returnValue: true)] out string? @namespace)
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this can be removed and the extension method can be used directly

@@ -23,50 +23,93 @@
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Newtonsoft.Json.Linq;
using static System.Net.Mime.MediaTypeNames;
Copy link
Contributor

Choose a reason for hiding this comment

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

order usings

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

if (codeDocument.IsUnsupported())
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems like something that should go into the provider

{
return null;
}

var componentDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
if (componentDocument.IsUnsupported())
if (!FileKinds.IsComponent(codeDocument.GetFileKind()))
Copy link
Contributor

Choose a reason for hiding this comment

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

This should always be true based on the provider check


private static ExtractToComponentCodeActionParams? DeserializeActionParams(JsonElement data)
{
return data.ValueKind == JsonValueKind.Undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this ever possible? Seems like an exception


private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndElements(SourceText sourceText, RazorSyntaxTree syntaxTree, ExtractToComponentCodeActionParams actionParams)
{
if (syntaxTree is null)
Copy link
Contributor

Choose a reason for hiding this comment

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

According to the signature this shouldn't be null ever

return (null, null);
}

var owner = syntaxTree.Root.FindInnermostNode(actionParams.AbsoluteIndex, includeWhitespace: true);
Copy link
Contributor

Choose a reason for hiding this comment

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

We're doing this twice. Why can't we provide the correct way to get the start and end in the provider? We already have to check the nodes there anyways

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it be an improvement if I pass in the spans of the start and element nodes to the resolver and use FindNode() on those spans?

If not, I think the way to go would be to move all of the TryAnalyzeSelection() stuff to the provider.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would say any analysis of validation of selection should always go in the provider, and the action of determining work beyond that is in the resolver. Keep in mind failure in a resolver means that an action was shown and just fails to apply for some reason and isn't as obvious to a user.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. Working on moving selection validation to Provider again

Intended previous commit message: Telemetry was added to provider in previous commit, and cleaned up usings.

This commit: Integrated PR Feedback for IsInsideMarkupTag
@maryamariyan
Copy link
Member

maryamariyan commented Sep 5, 2024

I was able to extract component on

  • html markup
  • extracted code blocks
    and extract component works pretty well :)

overall the feature works fine in many cases I tried
I'm getting some faults in the extract component in some cases
If you can fix

that would be great too

also, I noticed if you try to extract on

@inject WeatherApiClient WeatherApi

the extract component code action does not appear. I wonder what it would take to enable that


NOTE (update)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants