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 664ce6c61bf..c9da210be94 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,17 @@ private GenerateMethodCodeActionResolver[] CreateRazorCodeActionResolvers(
razorFormattingService)
];
+ // TODO: Make this func
+ private ExtractToComponentCodeActionResolver[] CreateExtractComponentCodeActionResolvers(string filePath, RazorCodeDocument codeDocument)
+ {
+ var emptyDocumentContextFactory = new TestDocumentContextFactory();
+ return [
+ new ExtractToComponentCodeActionResolver(
+ new GenerateMethodResolverDocumentContextFactory(filePath, codeDocument),
+ TestLanguageServerFeatureOptions.Instance)
+ ];
+ }
+
#region CSharp CodeAction Tests
[Fact]
@@ -1005,6 +1018,36 @@ 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()
+ {
+ var input = """
+ <[||]div id="a">
+
Div a title
+
+ Div a par
+
+
+
+
+ """;
+
+ var expectedRazorComponent = """
+
+
Div a title
+
+
Div a par
+
+ """;
+
+ await ValidateExtractComponentCodeActionAsync(
+ input,
+ expectedRazorComponent,
+ ExtractToComponentTitle,
+ razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
+ codeActionResolversCreator: CreateExtractComponentCodeActionResolvers);
+ }
+
#endregion
private async Task ValidateCodeBehindFileAsync(
@@ -1148,6 +1191,66 @@ private async Task ValidateCodeActionAsync(
AssertEx.EqualOrDiff(expected, actual);
}
+ private async Task ValidateExtractComponentCodeActionAsync(
+ string input,
+ string? expected,
+ string codeAction,
+ int childActionIndex = 0,
+ IRazorCodeActionProvider[]? razorCodeActionProviders = null,
+ Func? codeActionResolversCreator = null,
+ RazorLSPOptionsMonitor? optionsMonitor = null,
+ Diagnostic[]? diagnostics = null)
+ {
+ TestFileMarkupParser.GetSpan(input, out input, out var textSpan);
+
+ var razorFilePath = "C:/path/test.razor";
+ var componentFilePath = "C:/path/Component.razor";
+ var codeDocument = CreateCodeDocument(input, filePath: razorFilePath);
+ var sourceText = codeDocument.GetSourceText();
+ var uri = new Uri(razorFilePath);
+ var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath);
+ 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 = new List();
+
+ // Only get changes made in the new component file
+ foreach (var change in changes.Where(e => e.TextDocument.Uri.AbsolutePath == componentFilePath))
+ {
+ edits.AddRange(change.Edits.Select(e => e.ToTextChange(sourceText)));
+ }
+
+ var actual = sourceText.WithChanges(edits).ToString();
+ AssertEx.EqualOrDiff(expected, actual);
+ }
+
private static VSInternalCodeAction? GetCodeActionToRun(string codeAction, int childActionIndex, SumType[] result)
{
var codeActionToRun = (VSInternalCodeAction?)result.SingleOrDefault(e => ((RazorVSInternalCodeAction)e.Value!).Name == codeAction || ((RazorVSInternalCodeAction)e.Value!).Title == codeAction).Value;
@@ -1306,4 +1409,78 @@ static IEnumerable BuildTagHelpers()
}
}
}
+
+ private class ExtractToComponentResolverDocumentContextFactory : TestDocumentContextFactory
+ {
+ private readonly List _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.GetSourceText().ToString(), CodeAnalysis.VersionStamp.Default, projectWorkspaceState);
+ testDocumentSnapshot.With(CodeDocument);
+
+ context = CreateDocumentContext(new Uri(FilePath), testDocumentSnapshot);
+ return true;
+ }
+
+ private static List CreateTagHelperDescriptors()
+ {
+ return BuildTagHelpers().ToList();
+
+ static IEnumerable BuildTagHelpers()
+ {
+ var builder = TagHelperDescriptorBuilder.Create("oncontextmenu", "Microsoft.AspNetCore.Components");
+ builder.SetMetadata(
+ new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"),
+ new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind));
+ yield return builder.Build();
+
+ builder = TagHelperDescriptorBuilder.Create("onclick", "Microsoft.AspNetCore.Components");
+ builder.SetMetadata(
+ new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"),
+ new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind));
+
+ yield return builder.Build();
+
+ builder = TagHelperDescriptorBuilder.Create("oncopy", "Microsoft.AspNetCore.Components");
+ builder.SetMetadata(
+ new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.ClipboardEventArgs"),
+ new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind));
+
+ yield return builder.Build();
+
+ builder = TagHelperDescriptorBuilder.Create("ref", "Microsoft.AspNetCore.Components");
+ builder.SetMetadata(
+ new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.Ref.TagHelperKind),
+ new KeyValuePair(ComponentMetadata.Common.DirectiveAttribute, bool.TrueString));
+
+ yield return builder.Build();
+ }
+ }
+ }
}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs
index caaf2f20eb3..9e9539ead9c 100644
--- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs
@@ -365,34 +365,6 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP
return context;
}
- //private static IDocumentSnapshot CreateSupplementaryRazorFile(string filePath, string text, bool supportsFileCreation = true)
- // => CreateSupplementaryRazorFile(filePath, text, relativePath: filePath, supportsFileCreation: supportsFileCreation);
-
- //private static IDocumentSnapshot CreateSupplementaryRazorFile(string filePath, string text, string? relativePath, bool supportsFileCreation = true)
- //{
- // var sourceDocument = RazorSourceDocument.Create(text, RazorSourceDocumentProperties.Create(filePath, relativePath));
- // var options = RazorParserOptions.Create(o =>
- // {
- // o.Directives.Add(ComponentCodeDirective.Directive);
- // o.Directives.Add(FunctionsDirective.Directive);
- // });
- // var syntaxTree = RazorSyntaxTree.Parse(sourceDocument, options);
-
- // var codeDocument = TestRazorCodeDocument.Create(sourceDocument, imports: default);
- // codeDocument.SetFileKind(FileKinds.Component);
- // codeDocument.SetCodeGenerationOptions(RazorCodeGenerationOptions.Create(o =>
- // {
- // o.RootNamespace = "ExtractToNewComponentTest";
- // }));
- // codeDocument.SetSyntaxTree(syntaxTree);
-
- // var documentSnapshot = Mock.Of(document =>
- // document.GetGeneratedOutputAsync() == Task.FromResult(codeDocument) &&
- // document.GetTextAsync() == Task.FromResult(codeDocument.GetSourceText()), MockBehavior.Strict);
-
- // return documentSnapshot;
- //}
-
private static void AddMultiPointSelectionToContext(ref RazorCodeActionContext context, TextSpan selectionSpan)
{
var sourceText = context.CodeDocument.GetSourceText();