diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToNewComponentCodeActionParams.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToComponentCodeActionParams.cs similarity index 60% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToNewComponentCodeActionParams.cs rename to src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToComponentCodeActionParams.cs index 9c30b64aeed..3834355e6c5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToNewComponentCodeActionParams.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToComponentCodeActionParams.cs @@ -2,18 +2,27 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; -internal sealed class ExtractToNewComponentCodeActionParams +// 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("extractEnd")] public int ExtractEnd { get; set; } + [JsonPropertyName("namespace")] public required string Namespace { get; set; } + + [JsonPropertyName("usingDirectives")] + public required List<string> usingDirectives { get; set; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs similarity index 68% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs rename to src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs index 6ae53437ba7..d6d8967308a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs @@ -6,27 +6,21 @@ 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.Intermediate; 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; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; -using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; -internal sealed class ExtractToNewComponentCodeActionProvider(ILoggerFactory loggerFactory) : IRazorCodeActionProvider +internal sealed class ExtractToComponentCodeActionProvider(ILoggerFactory loggerFactory) : IRazorCodeActionProvider { - private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<ExtractToNewComponentCodeActionProvider>(); + private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<ExtractToComponentCodeActionProvider>(); public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) { @@ -51,14 +45,18 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); } - var (startElementNode, endElementNode) = GetStartAndEndElements(context, syntaxTree, _logger); - // Make sure the selection starts on an element tag + var (startElementNode, endElementNode) = GetStartAndEndElements(context, syntaxTree, _logger); if (startElementNode is null) { return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); } + if (endElementNode is null) + { + endElementNode = startElementNode; + } + if (!TryGetNamespace(context.CodeDocument, out var @namespace)) { return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); @@ -68,6 +66,20 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct 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 resolutionParams = new RazorCodeActionResolutionParams() { Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction, @@ -75,7 +87,7 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct Data = actionParams, }; - var codeAction = RazorCodeActionFactory.CreateExtractToNewComponent(resolutionParams); + var codeAction = RazorCodeActionFactory.CreateExtractToComponent(resolutionParams); return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([codeAction]); } @@ -95,6 +107,7 @@ private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAn } var endElementNode = GetEndElementNode(context, syntaxTree); + return (startElementNode, endElementNode); } @@ -135,14 +148,15 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma return endOwner.FirstAncestorOrSelf<MarkupElementSyntax>(); } - private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(RazorCodeActionContext context, MarkupElementSyntax startElementNode, string @namespace) + private static ExtractToComponentCodeActionParams CreateInitialActionParams(RazorCodeActionContext context, MarkupElementSyntax startElementNode, string @namespace) { - return new ExtractToNewComponentCodeActionParams + return new ExtractToComponentCodeActionParams { Uri = context.Request.TextDocument.Uri, ExtractStart = startElementNode.Span.Start, ExtractEnd = startElementNode.Span.End, - Namespace = @namespace + Namespace = @namespace, + usingDirectives = [] }; } @@ -152,7 +166,7 @@ private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(R /// <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, ExtractToNewComponentCodeActionParams actionParams) + private static void ProcessSelection(MarkupElementSyntax startElementNode, MarkupElementSyntax? endElementNode, ExtractToComponentCodeActionParams actionParams) { // If there's no end element, we can't process a multi-point selection if (endElementNode is null) @@ -183,7 +197,7 @@ private static void ProcessSelection(MarkupElementSyntax startElementNode, Marku // </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); @@ -207,7 +221,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy { // Find the lowest common ancestor of both nodes var nearestCommonAncestor = FindNearestCommonAncestor(startNode, endNode); - if (nearestCommonAncestor == null) + if (nearestCommonAncestor is null) { return (null, null); } @@ -223,7 +237,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy { var childSpan = child.Span; - if (startContainingNode == null && childSpan.Contains(startSpan)) + if (startContainingNode is null && childSpan.Contains(startSpan)) { startContainingNode = child; if (endContainingNode is not null) @@ -245,7 +259,10 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy { var current = node1; - while (current.Kind == SyntaxKind.MarkupElement && current is not null) + while (current is MarkupElementSyntax or + MarkupTagHelperAttributeSyntax or + MarkupBlockSyntax && + current is not null) { if (current.Span.Contains(node2.Span)) { @@ -257,4 +274,56 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy 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; + } + } + } + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs similarity index 87% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs rename to src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs index 7cc6dfce792..4cff9dc9a9d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs @@ -27,7 +27,8 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; -internal sealed class ExtractToNewComponentCodeActionResolver( +internal sealed class ExtractToComponentCodeActionResolver + ( IDocumentContextFactory documentContextFactory, LanguageServerFeatureOptions languageServerFeatureOptions) : IRazorCodeActionResolver { @@ -44,7 +45,7 @@ internal sealed class ExtractToNewComponentCodeActionResolver( return null; } - var actionParams = JsonSerializer.Deserialize<ExtractToNewComponentCodeActionParams>(data.GetRawText()); + var actionParams = JsonSerializer.Deserialize<ExtractToComponentCodeActionParams>(data.GetRawText()); if (actionParams is null) { return null; @@ -90,7 +91,15 @@ internal sealed class ExtractToNewComponentCodeActionResolver( } var componentName = Path.GetFileNameWithoutExtension(componentPath); - var newComponentContent = text.GetSubTextString(new TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart)).Trim(); + var newComponentContent = string.Empty; + + newComponentContent += string.Join(Environment.NewLine, actionParams.usingDirectives); + if (actionParams.usingDirectives.Count > 0) + { + newComponentContent += Environment.NewLine + Environment.NewLine; // Ensure there's a newline after the dependencies if any exist. + } + + newComponentContent += text.GetSubTextString(new CodeAnalysis.Text.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); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs index 6a7d697c1ca..f9f4a94cd27 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs @@ -14,7 +14,7 @@ internal static class RazorCodeActionFactory private readonly static Guid s_fullyQualifyComponentTelemetryId = new("3d9abe36-7d10-4e08-8c18-ad88baa9a923"); private readonly static Guid s_createComponentFromTagTelemetryId = new("a28e0baa-a4d5-4953-a817-1db586035841"); private readonly static Guid s_createExtractToCodeBehindTelemetryId = new("f63167f7-fdc6-450f-8b7b-b240892f4a27"); - private readonly static Guid s_createExtractToNewComponentTelemetryId = new("af67b0a3-f84b-4808-97a7-b53e85b22c64"); + private readonly static Guid s_createExtractToComponentTelemetryId = new("af67b0a3-f84b-4808-97a7-b53e85b22c64"); private readonly static Guid s_generateMethodTelemetryId = new("c14fa003-c752-45fc-bb29-3a123ae5ecef"); private readonly static Guid s_generateAsyncMethodTelemetryId = new("9058ca47-98e2-4f11-bf7c-a16a444dd939"); @@ -68,15 +68,15 @@ public static RazorVSInternalCodeAction CreateExtractToCodeBehind(RazorCodeActio return codeAction; } - public static RazorVSInternalCodeAction CreateExtractToNewComponent(RazorCodeActionResolutionParams resolutionParams) + public static RazorVSInternalCodeAction CreateExtractToComponent(RazorCodeActionResolutionParams resolutionParams) { - var title = SR.ExtractTo_NewComponent_Title; + var title = SR.ExtractTo_Component_Title; var data = JsonSerializer.SerializeToElement(resolutionParams); var codeAction = new RazorVSInternalCodeAction() { Title = title, Data = data, - TelemetryId = s_createExtractToNewComponentTelemetryId, + TelemetryId = s_createExtractToComponentTelemetryId, }; return codeAction; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index a4e0cc1bc80..dad0994085a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -146,8 +146,8 @@ public static void AddCodeActionsServices(this IServiceCollection services) // Razor Code actions services.AddSingleton<IRazorCodeActionProvider, ExtractToCodeBehindCodeActionProvider>(); services.AddSingleton<IRazorCodeActionResolver, ExtractToCodeBehindCodeActionResolver>(); - services.AddSingleton<IRazorCodeActionProvider, ExtractToNewComponentCodeActionProvider>(); - services.AddSingleton<IRazorCodeActionResolver ,ExtractToNewComponentCodeActionResolver>(); + services.AddSingleton<IRazorCodeActionProvider, ExtractToComponentCodeActionProvider>(); + services.AddSingleton<IRazorCodeActionResolver ,ExtractToComponentCodeActionResolver>(); services.AddSingleton<IRazorCodeActionProvider, ComponentAccessibilityCodeActionProvider>(); services.AddSingleton<IRazorCodeActionResolver, CreateComponentCodeActionResolver>(); services.AddSingleton<IRazorCodeActionResolver, AddUsingsCodeActionResolver>(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx index b94c06607a4..cd69afc13d3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx @@ -183,7 +183,7 @@ <data name="Statement" xml:space="preserve"> <value>statement</value> </data> - <data name="ExtractTo_NewComponent_Title" xml:space="preserve"> + <data name="ExtractTo_Component_Title" xml:space="preserve"> <value>Extract element to new component</value> </data> </root> \ No newline at end of file diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf index e9bd2e979f3..de1af9b86ea 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf @@ -47,7 +47,7 @@ <target state="translated">Extrahovat blok do kódu na pozadí</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf index 94402ecb4de..7da645a36d2 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf @@ -47,7 +47,7 @@ <target state="translated">Block auf CodeBehind extrahieren</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf index 3e5b8b11c1e..72e2980b035 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf @@ -47,7 +47,7 @@ <target state="translated">Extraer el bloque al código subyacente</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf index 4808f1c26a4..0c3ee61e8de 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf @@ -47,7 +47,7 @@ <target state="translated">Extraire le bloc vers le code-behind</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf index 33470714554..20333e772ac 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf @@ -47,7 +47,7 @@ <target state="translated">Estrai il blocco in code-behind</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf index fd8f4765763..6016bbea305 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf @@ -47,7 +47,7 @@ <target state="translated">ブロック抽出から分離コード</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf index 62d14e8d091..4579e5803b2 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf @@ -47,7 +47,7 @@ <target state="translated">코드 숨김에 블록 추출</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf index b5ca44e0fb3..765d2f018bc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf @@ -47,7 +47,7 @@ <target state="translated">Wyodrębnij blok do kodu znajdującego się poza</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pt-BR.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pt-BR.xlf index 023270c89c0..62ad6fd0364 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pt-BR.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pt-BR.xlf @@ -47,7 +47,7 @@ <target state="translated">Extrair o bloco para codificar atrás</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ru.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ru.xlf index 9b4b541b350..df1d6913ba1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ru.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ru.xlf @@ -47,7 +47,7 @@ <target state="translated">Извлечь блок в код программной части</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.tr.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.tr.xlf index 211840b046c..002635c7ab8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.tr.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.tr.xlf @@ -47,7 +47,7 @@ <target state="translated">Bloğu arkadaki koda ayıkla</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hans.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hans.xlf index c746d3ecc08..c75b40f673c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hans.xlf @@ -47,7 +47,7 @@ <target state="translated">将块提取到代码隐藏中</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hant.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hant.xlf index ddd96b48565..588a1945b6c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hant.xlf @@ -47,7 +47,7 @@ <target state="translated">擷取區塊以在後方編碼</target> <note /> </trans-unit> - <trans-unit id="ExtractTo_NewComponent_Title"> + <trans-unit id="ExtractTo_Component_Title"> <source>Extract element to new component</source> <target state="new">Extract element to new component</target> <note /> diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs index 33b35e3df98..992ff410c0e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Castle.Core.Logging; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; @@ -38,6 +39,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; public class CodeActionEndToEndTest(ITestOutputHelper testOutput) : SingleServerDelegatingEndpointTestBase(testOutput) { private const string GenerateEventHandlerTitle = "Generate Event Handler 'DoesNotExist'"; + private const string ExtractToComponentTitle = "Extract element to new component"; private const string GenerateAsyncEventHandlerTitle = "Generate Async Event Handler 'DoesNotExist'"; private const string GenerateEventHandlerReturnType = "void"; private const string GenerateAsyncEventHandlerReturnType = "global::System.Threading.Tasks.Task"; @@ -59,6 +61,15 @@ private GenerateMethodCodeActionResolver[] CreateRazorCodeActionResolvers( razorFormattingService) ]; + private ExtractToComponentCodeActionResolver[] CreateExtractComponentCodeActionResolver(string filePath, RazorCodeDocument codeDocument) + { + return [ + new ExtractToComponentCodeActionResolver( + new GenerateMethodResolverDocumentContextFactory(filePath, codeDocument), + TestLanguageServerFeatureOptions.Instance) + ]; + } + #region CSharp CodeAction Tests [Fact] @@ -1005,6 +1016,104 @@ await ValidateCodeActionAsync(input, diagnostics: [new Diagnostic() { Code = "CS0103", Message = "The name 'DoesNotExist' does not exist in the current context" }]); } + [Fact] + public async Task Handle_ExtractComponent_SingleElement_ReturnsResult() + { + var input = """ + <[||]div id="a"> + <h1>Div a title</h1> + <Book Title="To Kill a Mockingbird" Author="Harper Lee" Year="Long ago" /> + <p>Div a par</p> + </div> + <div id="shouldSkip"> + <Movie Title="Aftersun" Director="Charlotte Wells" Year="2022" /> + </div> + """; + + var expectedRazorComponent = """ + <div id="a"> + <h1>Div a title</h1> + <Book Title="To Kill a Mockingbird" Author="Harper Lee" Year="Long ago" /> + <p>Div a par</p> + </div> + """; + + await ValidateExtractComponentCodeActionAsync( + input, + expectedRazorComponent, + ExtractToComponentTitle, + razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)], + codeActionResolversCreator: CreateExtractComponentCodeActionResolver); + } + + [Fact] + public async Task Handle_ExtractComponent_SiblingElement_ReturnsResult() + { + var input = """ + <[|div id="a"> + <h1>Div a title</h1> + <Book Title="To Kill a Mockingbird" Author="Harper Lee" Year="Long ago" /> + <p>Div a par</p> + </div> + <div id="b"> + <Movie Title="Aftersun" Director="Charlotte Wells" Year="2022" /> + </div|]> + """; + + var expectedRazorComponent = """ + <div id="a"> + <h1>Div a title</h1> + <Book Title="To Kill a Mockingbird" Author="Harper Lee" Year="Long ago" /> + <p>Div a par</p> + </div> + <div id="b"> + <Movie Title="Aftersun" Director="Charlotte Wells" Year="2022" /> + </div> + """; + + await ValidateExtractComponentCodeActionAsync( + input, + expectedRazorComponent, + ExtractToComponentTitle, + razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)], + codeActionResolversCreator: CreateExtractComponentCodeActionResolver); + } + + [Fact] + public async Task Handle_ExtractComponent_StartNodeContainsEndNode_ReturnsResult() + { + var input = """ + <[|div id="parent"> + <div> + <div> + <div> + <p>Deeply nested par</p|]> + </div> + </div> + </div> + </div> + """; + + var expectedRazorComponent = """ + <div id="parent"> + <div> + <div> + <div> + <p>Deeply nested par</p> + </div> + </div> + </div> + </div> + """; + + await ValidateExtractComponentCodeActionAsync( + input, + expectedRazorComponent, + ExtractToComponentTitle, + razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)], + codeActionResolversCreator: CreateExtractComponentCodeActionResolver); + } + #endregion private async Task ValidateCodeBehindFileAsync( @@ -1148,6 +1257,61 @@ private async Task ValidateCodeActionAsync( AssertEx.EqualOrDiff(expected, actual); } + private async Task ValidateExtractComponentCodeActionAsync( + string input, + string? expected, + string codeAction, + int childActionIndex = 0, + IEnumerable<(string filePath, string contents)>? additionalRazorDocuments = null, + IRazorCodeActionProvider[]? razorCodeActionProviders = null, + Func<string, RazorCodeDocument, IRazorCodeActionResolver[]>? codeActionResolversCreator = null, + RazorLSPOptionsMonitor? optionsMonitor = null, + Diagnostic[]? diagnostics = null) + { + TestFileMarkupParser.GetSpan(input, out input, out var textSpan); + + var razorFilePath = "C:/path/to/test.razor"; + var componentFilePath = "C:/path/to/Component.razor"; + var codeDocument = CreateCodeDocument(input, filePath: razorFilePath); + var sourceText = codeDocument.Source.Text; + var uri = new Uri(razorFilePath); + var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath, additionalRazorDocuments); + var documentContext = CreateDocumentContext(uri, codeDocument); + var requestContext = new RazorRequestContext(documentContext, null!, "lsp/method", uri: null); + + var result = await GetCodeActionsAsync( + uri, + textSpan, + sourceText, + requestContext, + languageServer, + razorCodeActionProviders, + diagnostics); + + Assert.NotEmpty(result); + var codeActionToRun = GetCodeActionToRun(codeAction, childActionIndex, result); + + if (expected is null) + { + Assert.Null(codeActionToRun); + return; + } + + Assert.NotNull(codeActionToRun); + + var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, documentContext.Snapshot, optionsMonitor?.CurrentValue); + var changes = await GetEditsAsync( + codeActionToRun, + requestContext, + languageServer, + codeActionResolversCreator?.Invoke(razorFilePath, codeDocument) ?? []); + + var edits = changes.Where(change => change.TextDocument.Uri.AbsolutePath == componentFilePath).Single(); + var actual = edits.Edits.Select(edit => edit.NewText).Single(); + + AssertEx.EqualOrDiff(expected, actual); + } + private static VSInternalCodeAction? GetCodeActionToRun(string codeAction, int childActionIndex, SumType<Command, CodeAction>[] result) { var codeActionToRun = (VSInternalCodeAction?)result.SingleOrDefault(e => ((RazorVSInternalCodeAction)e.Value!).Name == codeAction || ((RazorVSInternalCodeAction)e.Value!).Title == codeAction).Value; @@ -1306,4 +1470,78 @@ static IEnumerable<TagHelperDescriptor> BuildTagHelpers() } } } + + private class ExtractToComponentResolverDocumentContextFactory : TestDocumentContextFactory + { + private readonly List<TagHelperDescriptor> _tagHelperDescriptors; + + public ExtractToComponentResolverDocumentContextFactory + (string filePath, + RazorCodeDocument codeDocument, + TagHelperDescriptor[]? tagHelpers = null, + int? version = null) + : base(filePath, codeDocument, version) + { + _tagHelperDescriptors = CreateTagHelperDescriptors(); + if (tagHelpers is not null) + { + _tagHelperDescriptors.AddRange(tagHelpers); + } + } + + public override bool TryCreate( + Uri documentUri, + VSProjectContext? projectContext, + bool versioned, + [NotNullWhen(true)] out DocumentContext? context) + { + if (FilePath is null || CodeDocument is null) + { + context = null; + return false; + } + + var projectWorkspaceState = ProjectWorkspaceState.Create(_tagHelperDescriptors.ToImmutableArray()); + var testDocumentSnapshot = TestDocumentSnapshot.Create(FilePath, CodeDocument.Source.Text.ToString(), CodeAnalysis.VersionStamp.Default, projectWorkspaceState); + testDocumentSnapshot.With(CodeDocument); + + context = CreateDocumentContext(new Uri(FilePath), testDocumentSnapshot); + return true; + } + + private static List<TagHelperDescriptor> CreateTagHelperDescriptors() + { + return BuildTagHelpers().ToList(); + + static IEnumerable<TagHelperDescriptor> BuildTagHelpers() + { + var builder = TagHelperDescriptorBuilder.Create("oncontextmenu", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair<string, string>(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"), + new KeyValuePair<string, string>(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("onclick", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair<string, string>(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"), + new KeyValuePair<string, string>(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("oncopy", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair<string, string>(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.ClipboardEventArgs"), + new KeyValuePair<string, string>(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("ref", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair<string, string>(ComponentMetadata.SpecialKindKey, ComponentMetadata.Ref.TagHelperKind), + new KeyValuePair<string, string>(ComponentMetadata.Common.DirectiveAttribute, bool.TrueString)); + + yield return builder.Build(); + } + } + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToNewComponentCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs similarity index 81% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToNewComponentCodeActionProviderTest.cs rename to src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs index 185f34b0b70..d884dc9c0b9 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToNewComponentCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.CodeActions.Razor; -public class ExtractToNewComponentCodeActionProviderTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) +public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { [Fact] public async Task Handle_InvalidFileKind() @@ -63,7 +63,7 @@ public async Task Handle_InvalidFileKind() var context = CreateRazorCodeActionContext(request, location, documentPath, contents); context.CodeDocument.SetFileKind(FileKinds.Legacy); - var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); + var provider = new ExtractToComponentCodeActionProvider(LoggerFactory); // Act var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); @@ -109,7 +109,7 @@ public async Task Handle_SinglePointSelection_ReturnsNotEmpty() var location = new SourceLocation(cursorPosition, -1, -1); var context = CreateRazorCodeActionContext(request, location, documentPath, contents, supportsFileCreation: true); - var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); + var provider = new ExtractToComponentCodeActionProvider(LoggerFactory); // Act var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); @@ -158,7 +158,7 @@ public async Task Handle_MultiPointSelection_ReturnsNotEmpty() var lineSpan = context.SourceText.GetLinePositionSpan(selectionSpan); request.Range = VsLspFactory.CreateRange(lineSpan); - var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); + var provider = new ExtractToComponentCodeActionProvider(LoggerFactory); // Act var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); @@ -168,7 +168,7 @@ public async Task Handle_MultiPointSelection_ReturnsNotEmpty() var codeAction = Assert.Single(commandOrCodeActionContainer); var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize<RazorCodeActionResolutionParams>(); Assert.NotNull(razorCodeActionResolutionParams); - var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize<ExtractToNewComponentCodeActionParams>(); + var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize<ExtractToComponentCodeActionParams>(); Assert.NotNull(actionParams); } @@ -179,6 +179,9 @@ public async Task Handle_MultiPointSelection_WithEndAfterElement_ReturnsCurrentE var documentPath = "c:/Test.razor"; var contents = """ @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home <PageTitle>Home</PageTitle> @@ -212,7 +215,7 @@ public async Task Handle_MultiPointSelection_WithEndAfterElement_ReturnsCurrentE var lineSpan = context.SourceText.GetLinePositionSpan(selectionSpan); request.Range = VsLspFactory.CreateRange(lineSpan); - var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); + var provider = new ExtractToComponentCodeActionProvider(LoggerFactory); // Act var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); @@ -222,12 +225,58 @@ public async Task Handle_MultiPointSelection_WithEndAfterElement_ReturnsCurrentE var codeAction = Assert.Single(commandOrCodeActionContainer); var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize<RazorCodeActionResolutionParams>(); Assert.NotNull(razorCodeActionResolutionParams); - var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize<ExtractToNewComponentCodeActionParams>(); + 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() + { + // 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.GetPosition(contents, out contents, out var cursorPosition); + + var request = new VSCodeActionParams() + { + TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) }, + Range = new Range(), + Context = new VSInternalCodeActionContext() + }; + + var location = new SourceLocation(cursorPosition, -1, -1); + var context = CreateRazorCodeActionContext(request, location, documentPath, contents); + + var provider = new ExtractToComponentCodeActionProvider(LoggerFactory); + + // Act + var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); + + // Assert + 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);