-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added basic extract to component functionality on cursor over html tag (
#10578) ### Summary of the changes Part of the implementation of the _Extract To Component_ code action. Functional in one of the two cases, when the user is not selecting a certain range of a Razor component, but rather when the cursor is on either the opening or closing tag.
- Loading branch information
Showing
23 changed files
with
492 additions
and
28 deletions.
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
...pNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToNewComponentCodeActionParams.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
...NetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using System.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(ILoggerFactory loggerFactory) : IRazorCodeActionProvider | ||
{ | ||
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<ExtractToNewComponentCodeActionProvider>(); | ||
|
||
public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) | ||
{ | ||
if (context is null) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
if (!context.SupportsFileCreation) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
if (!FileKinds.IsComponent(context.CodeDocument.GetFileKind())) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var syntaxTree = context.CodeDocument.GetSyntaxTree(); | ||
if (syntaxTree?.Root is null) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex, includeWhitespace: true); | ||
if (owner is null) | ||
{ | ||
_logger.LogWarning($"Owner should never be null."); | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var componentNode = owner.FirstAncestorOrSelf<MarkupElementSyntax>(); | ||
|
||
// Make sure we've found tag | ||
if (componentNode is null) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<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.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
if (!TryGetNamespace(context.CodeDocument, out var @namespace)) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<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); | ||
|
||
return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([codeAction]); | ||
} | ||
|
||
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); | ||
} |
139 changes: 139 additions & 0 deletions
139
...NetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
// 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.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.LanguageServer.CodeActions.Models; | ||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; | ||
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.VisualStudio.LanguageServer.Protocol; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; | ||
|
||
internal sealed class ExtractToNewComponentCodeActionResolver( | ||
IDocumentContextFactory documentContextFactory, | ||
LanguageServerFeatureOptions languageServerFeatureOptions) : IRazorCodeActionResolver | ||
{ | ||
|
||
private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; | ||
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; | ||
|
||
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 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 componentName = Path.GetFileNameWithoutExtension(componentPath); | ||
var newComponentContent = text.GetSubTextString(new TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart)).Trim(); | ||
|
||
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, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
5 changes: 5 additions & 0 deletions
5
src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
5 changes: 5 additions & 0 deletions
5
src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.