-
Notifications
You must be signed in to change notification settings - Fork 200
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
Added basic extract to component functionality on cursor over html tag #10578
Changes from 5 commits
9ba71db
7e3ef75
74956e1
bce001c
60d5881
8ab2133
c63e51c
09435b4
afc61ea
6d8e44c
2908e6c
9d7a565
371b09a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// 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.Text.Json.Serialization; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; | ||
|
||
internal sealed class ExtractToNewComponentCodeActionParams | ||
{ | ||
[JsonPropertyName("uri")] | ||
public required Uri Uri { get; set; } | ||
[JsonPropertyName("extractStart")] | ||
public int ExtractStart { get; set; } | ||
[JsonPropertyName("extractEnd")] | ||
public int ExtractEnd { get; set; } | ||
[JsonPropertyName("namespace")] | ||
public required string Namespace { get; set; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// 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.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Razor.Language; | ||
using Microsoft.AspNetCore.Razor.Language.Components; | ||
using Microsoft.AspNetCore.Razor.Language.Extensions; | ||
using Microsoft.AspNetCore.Razor.Language.Syntax; | ||
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; | ||
using Microsoft.AspNetCore.Razor.Threading; | ||
using Microsoft.CodeAnalysis.Razor.Logging; | ||
using Microsoft.CodeAnalysis.Razor.Workspaces; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; | ||
internal sealed class ExtractToNewComponentCodeActionProvider : IRazorCodeActionProvider | ||
{ | ||
private readonly ILogger _logger; | ||
|
||
public ExtractToNewComponentCodeActionProvider(ILoggerFactory loggerFactory) | ||
{ | ||
_logger = loggerFactory.GetOrCreateLogger<ExtractToNewComponentCodeActionProvider>(); | ||
} | ||
|
||
public Task<IReadOnlyList<RazorVSInternalCodeAction>?> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that the signature of |
||
{ | ||
if (context is null) | ||
{ | ||
return SpecializedTasks.Null<IReadOnlyList<RazorVSInternalCodeAction>>(); | ||
} | ||
|
||
if (!context.SupportsFileCreation) | ||
{ | ||
return SpecializedTasks.Null<IReadOnlyList<RazorVSInternalCodeAction>>(); | ||
} | ||
|
||
if (!FileKinds.IsComponent(context.CodeDocument.GetFileKind())) | ||
{ | ||
return SpecializedTasks.Null<IReadOnlyList<RazorVSInternalCodeAction>>(); | ||
} | ||
|
||
var syntaxTree = context.CodeDocument.GetSyntaxTree(); | ||
if (syntaxTree?.Root is null) | ||
{ | ||
return SpecializedTasks.Null<IReadOnlyList<RazorVSInternalCodeAction>>(); | ||
} | ||
|
||
var owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex, true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add names for boolean literal arguments for readability. In this case, I guess it would be |
||
if (owner is null) | ||
{ | ||
_logger.LogWarning($"Owner should never be null."); | ||
return SpecializedTasks.Null<IReadOnlyList<RazorVSInternalCodeAction>>(); | ||
} | ||
|
||
var componentNode = owner.FirstAncestorOrSelf<MarkupElementSyntax>(); | ||
|
||
// Make sure we've found tag | ||
if (componentNode is null) | ||
{ | ||
return SpecializedTasks.Null<IReadOnlyList<RazorVSInternalCodeAction>>(); | ||
} | ||
|
||
// Do not provide code action if the cursor is inside proper html content (i.e. page text) | ||
if (context.Location.AbsoluteIndex > componentNode.StartTag.Span.End && | ||
context.Location.AbsoluteIndex < componentNode.EndTag.SpanStart) | ||
{ | ||
return SpecializedTasks.Null<IReadOnlyList<RazorVSInternalCodeAction>>(); | ||
} | ||
|
||
if (!TryGetNamespace(context.CodeDocument, out var @namespace)) | ||
{ | ||
return SpecializedTasks.Null<IReadOnlyList<RazorVSInternalCodeAction>>(); | ||
} | ||
|
||
var actionParams = new ExtractToNewComponentCodeActionParams() | ||
{ | ||
Uri = context.Request.TextDocument.Uri, | ||
ExtractStart = componentNode.Span.Start, | ||
ExtractEnd = componentNode.Span.End, | ||
Namespace = @namespace | ||
}; | ||
|
||
var resolutionParams = new RazorCodeActionResolutionParams() | ||
{ | ||
Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction, | ||
Language = LanguageServerConstants.CodeActions.Languages.Razor, | ||
Data = actionParams, | ||
}; | ||
|
||
var codeAction = RazorCodeActionFactory.CreateExtractToNewComponent(resolutionParams); | ||
var codeActions = new List<RazorVSInternalCodeAction> { codeAction }; | ||
|
||
return Task.FromResult<IReadOnlyList<RazorVSInternalCodeAction>?>(codeActions); | ||
} | ||
|
||
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); | ||
|
||
//private static bool HasUnsupportedChildren(Language.Syntax.SyntaxNode node) | ||
//{ | ||
// return node.DescendantNodes().Any(static n => n is MarkupBlockSyntax or CSharpTransitionSyntax or RazorCommentBlockSyntax); | ||
//} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this commented code be deleted? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was keeping it around in case I was to use it but I think I won't 😄 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
// 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.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Razor.Language; | ||
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; | ||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; | ||
using Microsoft.AspNetCore.Razor.Utilities; | ||
using Microsoft.CodeAnalysis; | ||
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.VisualStudio.LanguageServer.Protocol; | ||
using Newtonsoft.Json.Linq; | ||
using System.Globalization; | ||
using System.Text.Json; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: sort using directives. (System using directives should go first). |
||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; | ||
internal sealed class ExtractToNewComponentCodeActionResolver : IRazorCodeActionResolver | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Please add a blank line between the namespace and the class declaration. |
||
{ | ||
private static readonly Workspace s_workspace = new AdhocWorkspace(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove this static field. It's not used, and it's actually quite expensive. |
||
|
||
private readonly IDocumentContextFactory _documentContextFactory; | ||
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; | ||
private readonly IClientConnection _clientConnection; | ||
public ExtractToNewComponentCodeActionResolver( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Please add a blank line between the fields and the constructor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider changing to a primary constructor (but keep the fields). There's a code action for that in Visual Studio. |
||
IDocumentContextFactory documentContextFactory, | ||
LanguageServerFeatureOptions languageServerFeatureOptions, | ||
IClientConnection clientConnection) | ||
{ | ||
_documentContextFactory = documentContextFactory ?? throw new ArgumentNullException(nameof(documentContextFactory)); | ||
_languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); | ||
_clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These |
||
} | ||
|
||
public string Action => LanguageServerConstants.CodeActions.ExtractToNewComponentAction; | ||
|
||
public async Task<WorkspaceEdit?> ResolveAsync(JsonElement data, CancellationToken cancellationToken) | ||
{ | ||
if (data.ValueKind == JsonValueKind.Undefined) | ||
{ | ||
return null; | ||
} | ||
|
||
var actionParams = JsonSerializer.Deserialize<ExtractToNewComponentCodeActionParams>(data.GetRawText()); | ||
if (actionParams is null) | ||
{ | ||
return null; | ||
} | ||
|
||
if (!_documentContextFactory.TryCreate(actionParams.Uri, out var documentContext)) | ||
{ | ||
return null; | ||
} | ||
|
||
var componentDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); | ||
if (componentDocument.IsUnsupported()) | ||
{ | ||
return null; | ||
} | ||
|
||
if (!FileKinds.IsComponent(componentDocument.GetFileKind())) | ||
{ | ||
return null; | ||
} | ||
|
||
var path = FilePathNormalizer.Normalize(actionParams.Uri.GetAbsoluteOrUNCPath()); | ||
var templatePath = Path.Combine(Path.GetDirectoryName(path), "Component"); | ||
Check failure on line 76 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build macOS debug)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L76
Check failure on line 76 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build macOS release)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L76
Check failure on line 76 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build Linux debug)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L76
Check failure on line 76 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build Linux release)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L76
Check failure on line 76 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-cisrc/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L76
|
||
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; | ||
Comment on lines
+78
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for filing |
||
|
||
var text = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); | ||
if (text is null) | ||
{ | ||
return null; | ||
} | ||
|
||
var componentName = Path.GetFileNameWithoutExtension(componentPath); | ||
var newComponentContent = text.GetSubTextString(new CodeAnalysis.Text.TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart)).Trim(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to add a using directive for |
||
|
||
var start = componentDocument.Source.Text.Lines.GetLinePosition(actionParams.ExtractStart); | ||
var end = componentDocument.Source.Text.Lines.GetLinePosition(actionParams.ExtractEnd); | ||
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[] | ||
{ | ||
new TextEdit | ||
{ | ||
NewText = $"<{componentName} />", | ||
Range = removeRange, | ||
} | ||
}, | ||
}, | ||
new TextDocumentEdit | ||
{ | ||
TextDocument = newComponentDocumentIdentifier, | ||
Edits = new[] | ||
{ | ||
new TextEdit | ||
{ | ||
NewText = newComponentContent, | ||
Range = new Range { Start = new Position(0, 0), End = new Position(0, 0) }, | ||
} | ||
}, | ||
} | ||
}; | ||
|
||
return new WorkspaceEdit | ||
{ | ||
DocumentChanges = documentChanges, | ||
}; | ||
} | ||
|
||
/// <summary> | ||
/// Generate a file path with adjacent to our input path that has the | ||
/// correct code-behind extension, using numbers to differentiate from | ||
/// any collisions. | ||
/// </summary> | ||
/// <param name="path">The origin file path.</param> | ||
/// <returns>A non-existent file path with the same base name and a code-behind extension.</returns> | ||
private static string GenerateComponentBehindPath(string path) | ||
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build macOS debug)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build macOS debug)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build macOS release)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build macOS release)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build Linux debug)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build Linux debug)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build Linux release)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-ci (Build Linux release)src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-cisrc/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-cisrc/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
Check failure on line 153 in src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs Azure Pipelines / razor-tooling-cisrc/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs#L153
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably should move this to a helper class. I'm assuming extract to code behind also does this? |
||
{ | ||
var directoryName = Path.GetDirectoryName(path); | ||
|
||
var n = 0; | ||
string componentBehindPath; | ||
do | ||
{ | ||
var identifier = n > 0 ? n.ToString(CultureInfo.InvariantCulture) : string.Empty; // Make it look nice | ||
Assumes.NotNull(directoryName); | ||
|
||
componentBehindPath = Path.Combine( | ||
directoryName, | ||
$"Component{identifier}.razor"); | ||
n++; | ||
} | ||
while (File.Exists(componentBehindPath)); | ||
|
||
return componentBehindPath; | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider changing to a primary constructor (but keep the fields). There's a code action for that in Visual Studio.