From 142c3edab08deed8f5aedca01d3f1de79654aa74 Mon Sep 17 00:00:00 2001 From: marcarro Date: Mon, 8 Jul 2024 15:42:42 -0700 Subject: [PATCH 01/22] Arranging selection logic --- .../ExtractToNewComponentCodeActionProvider.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 6c3b3e8ee38..401eb3b64b0 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -56,6 +56,23 @@ public Task> ProvideAsync(RazorCodeAct var componentNode = owner.FirstAncestorOrSelf(); + var selectionStart = context.Request.Range.Start; + var selectionEnd = context.Request.Range.End; + + // If user selects range from end to beginning (i.e., bottom-to-top, right-to-left), simply get the effective start and end. + if (selectionEnd.Line < selectionStart.Line || + (selectionEnd.Line == selectionStart.Line && selectionEnd.Character < selectionStart.Character)) + { + (selectionEnd, selectionStart) = (selectionStart, selectionEnd); + } + + var isSelection = selectionStart != selectionEnd; + + if (isSelection) + { + //var startOwner = syntaxTree.Root.FindInnermostNode(selectionStart.ToSourceSpan(), true); + } + // Make sure we've found tag if (componentNode is null) { From ef995cdb5d0e7347c9b92d17957283697c314c4a Mon Sep 17 00:00:00 2001 From: marcarro Date: Wed, 10 Jul 2024 11:20:25 -0700 Subject: [PATCH 02/22] Basic select range feature: practically not functional --- ...ExtractToNewComponentCodeActionProvider.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 401eb3b64b0..a531a049fca 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Razor.Threading; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; @@ -59,18 +60,40 @@ public Task> ProvideAsync(RazorCodeAct var selectionStart = context.Request.Range.Start; var selectionEnd = context.Request.Range.End; - // If user selects range from end to beginning (i.e., bottom-to-top, right-to-left), simply get the effective start and end. + // If user selects range from end to beginning (i.e., bottom-to-top or right-to-left), get the effective start and end. if (selectionEnd.Line < selectionStart.Line || (selectionEnd.Line == selectionStart.Line && selectionEnd.Character < selectionStart.Character)) { (selectionEnd, selectionStart) = (selectionStart, selectionEnd); } + var selectionEndIndex = new SourceLocation(0, 0, 0); + var endOwner = owner; + var endComponentNode = componentNode; + var isSelection = selectionStart != selectionEnd; if (isSelection) { - //var startOwner = syntaxTree.Root.FindInnermostNode(selectionStart.ToSourceSpan(), true); + if (!selectionEnd.TryGetSourceLocation(context.CodeDocument.GetSourceText(), _logger, out var location)) + { + return SpecializedTasks.Null>(); + } + // Print selectionEndIndex to see if it is correct + if (location is null) + { + return SpecializedTasks.Null>(); + } + + selectionEndIndex = location.Value; + endOwner = syntaxTree.Root.FindInnermostNode(selectionEndIndex.AbsoluteIndex, true); + + if (endOwner is null) + { + return SpecializedTasks.Null>(); + } + + endComponentNode = endOwner.FirstAncestorOrSelf(); } // Make sure we've found tag @@ -99,6 +122,11 @@ public Task> ProvideAsync(RazorCodeAct Namespace = @namespace }; + if (isSelection && endComponentNode is not null) + { + actionParams.ExtractEnd = endComponentNode.Span.End; + } + var resolutionParams = new RazorCodeActionResolutionParams() { Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction, From a81a177aac7f2641759273541be448b1e602fb29 Mon Sep 17 00:00:00 2001 From: marcarro Date: Mon, 15 Jul 2024 10:04:42 -0700 Subject: [PATCH 03/22] Completed and corrected selected range extraction functionality --- ...ExtractToNewComponentCodeActionProvider.cs | 101 ++++++++++-- ...actToNewComponentCodeActionProviderTest.cs | 150 ++++++++++++++++-- 2 files changed, 227 insertions(+), 24 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index a531a049fca..f55c8959a22 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -9,9 +9,11 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using ICSharpCode.Decompiler.CSharp.Syntax; 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; @@ -55,23 +57,22 @@ public Task> ProvideAsync(RazorCodeAct return SpecializedTasks.EmptyImmutableArray(); } - var componentNode = owner.FirstAncestorOrSelf(); + var startComponentNode = owner.FirstAncestorOrSelf(); var selectionStart = context.Request.Range.Start; var selectionEnd = context.Request.Range.End; + var isSelection = selectionStart != selectionEnd; // If user selects range from end to beginning (i.e., bottom-to-top or right-to-left), get the effective start and end. - if (selectionEnd.Line < selectionStart.Line || - (selectionEnd.Line == selectionStart.Line && selectionEnd.Character < selectionStart.Character)) + if (selectionEnd is not null && selectionEnd.Line < selectionStart.Line || + (selectionEnd is not null && selectionEnd.Line == selectionStart.Line && selectionEnd.Character < selectionStart.Character)) { (selectionEnd, selectionStart) = (selectionStart, selectionEnd); } var selectionEndIndex = new SourceLocation(0, 0, 0); var endOwner = owner; - var endComponentNode = componentNode; - - var isSelection = selectionStart != selectionEnd; + var endComponentNode = startComponentNode; if (isSelection) { @@ -79,7 +80,7 @@ public Task> ProvideAsync(RazorCodeAct { return SpecializedTasks.Null>(); } - // Print selectionEndIndex to see if it is correct + if (location is null) { return SpecializedTasks.Null>(); @@ -97,14 +98,14 @@ public Task> ProvideAsync(RazorCodeAct } // Make sure we've found tag - if (componentNode is null) + if (startComponentNode is null) { return SpecializedTasks.EmptyImmutableArray(); } - // 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) + // Do not provide code action if the cursor is inside proper html content (i.e. rendered text) + if (context.Location.AbsoluteIndex > startComponentNode.StartTag.Span.End && + context.Location.AbsoluteIndex < startComponentNode.EndTag.SpanStart) { return SpecializedTasks.EmptyImmutableArray(); } @@ -117,14 +118,28 @@ public Task> ProvideAsync(RazorCodeAct var actionParams = new ExtractToNewComponentCodeActionParams() { Uri = context.Request.TextDocument.Uri, - ExtractStart = componentNode.Span.Start, - ExtractEnd = componentNode.Span.End, + ExtractStart = startComponentNode.Span.Start, + ExtractEnd = startComponentNode.Span.End, Namespace = @namespace }; if (isSelection && endComponentNode is not null) { - actionParams.ExtractEnd = endComponentNode.Span.End; + // If component @ start of the selection includes a parent element of the component @ end of selection, then proceed as usual. + // If not, limit extraction to end of component @ end of selection (in the simplest case) + var selectionStartHasParentElement = endComponentNode.Ancestors().Any(node => node == startComponentNode); + actionParams.ExtractEnd = selectionStartHasParentElement ? actionParams.ExtractEnd : endComponentNode.Span.End; + + // Handle other case: Either start/end of selection is nested within a component + if (!selectionStartHasParentElement) + { + var (extractStart, extractEnd) = FindContainingSiblingPair(startComponentNode, endComponentNode); + if (extractStart != null && extractEnd != null) + { + actionParams.ExtractStart = extractStart.Span.Start; + actionParams.ExtractEnd = extractEnd.Span.End; + } + } } var resolutionParams = new RazorCodeActionResolutionParams() @@ -145,4 +160,62 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen // 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); + + public (SyntaxNode Start, SyntaxNode End) FindContainingSiblingPair(SyntaxNode startNode, SyntaxNode endNode) + { + // Find the lowest common ancestor of both nodes + var lowestCommonAncestor = FindLowestCommonAncestor(startNode, endNode); + if (lowestCommonAncestor == null) + { + return (null, null); + } + + SyntaxNode startContainingNode = null; + SyntaxNode endContainingNode = null; + + // Pre-calculate the spans for comparison + var startSpan = startNode.Span; + var endSpan = endNode.Span; + + + foreach (var child in lowestCommonAncestor.ChildNodes().Where(node => node.Kind == SyntaxKind.MarkupElement)) + { + var childSpan = child.Span; + + if (startContainingNode == null && childSpan.Contains(startSpan)) + { + startContainingNode = child; + if (endContainingNode != null) break; // Exit if we've found both + } + + if (childSpan.Contains(endSpan)) + { + endContainingNode = child; + if (startContainingNode != null) break; // Exit if we've found both + } + } + + return (startContainingNode, endContainingNode); + } + + public SyntaxNode? FindLowestCommonAncestor(SyntaxNode node1, SyntaxNode node2) + { + var current = node1; + + while (current.Kind == SyntaxKind.MarkupElement && current != null) + { + if (current.Span.Contains(node2.Span)) + { + return current; + } + current = current.Parent; + } + + return null; + } + + //private static bool HasUnsupportedChildren(Language.Syntax.SyntaxNode node) + //{ + // return node.DescendantNodes().Any(static n => n is MarkupBlockSyntax or CSharpTransitionSyntax or RazorCommentBlockSyntax); + //} } 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/ExtractToNewComponentCodeActionProviderTest.cs index 120a93192a6..2c9e0d898a7 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -3,13 +3,16 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; +using System.Text.Json; 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.LanguageServer.CodeActions; +using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -32,18 +35,24 @@ public async Task Handle_InvalidFileKind() // Arrange var documentPath = "c:/Test.razor"; var contents = """ - @page "/test" -
-

This is my title!

-

This is my paragraph!

- -
-

This is my other paragraph!

- Alternate Text + @page "/" + + Home + +
+
+

Div a title

+

Div $$a par

+
+
+

Div b title

+

Div b par

- @$$code {} + +

Hello, world!

+ + Welcome to your new app. """; TestFileMarkupParser.GetPosition(contents, out contents, out var cursorPosition); @@ -67,6 +76,127 @@ public async Task Handle_InvalidFileKind() Assert.Empty(commandOrCodeActionContainer); } + [Fact] + public async Task Handle_InProperMarkup_ReturnsNull() + { + // Arrange + var documentPath = "c:/Test.razor"; + var contents = """ + page "/" + + Home + +
+
+

Div a title

+

Div $$a par

+
+
+

Div b title

+

Div b par

+
+
Hello, world! + + 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 ExtractToNewComponentCodeActionProvider(LoggerFactory); + + // Act + var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); + + // Assert + Assert.Null(commandOrCodeActionContainer); + } + + [Theory] + [InlineData(""" +
+ [|
+

Div a title

+

Div a par

+
|] +
+

Div b title

+

Div b par

+
+
+ """)] + [InlineData(""" +
+
+

Div a title

+ [|

Div a par

|] +
+
+

Div b title

+

Div b par

+
+
+ """)] + [InlineData(""" +
+
+

Div a title

+ [|

Div a par

+
+
+

Div b title

+

Div b par

|] +
+
+ """)] + public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupElementSelection) + { + // Arrange + var documentPath = "c:/Test.razor"; + var contents = $$""" + page "/" + + Home + + {{markupElementSelection}} + +

Hello, world!

+ + Welcome to your new app. + """; + + TestFileMarkupParser.GetPositionAndSpans( + contents, out contents, out int cursorPosition, out ImmutableArray spans); + + 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, supportsFileCreation: true); + + var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); + + // Act + var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); + + // Assert + Assert.NotNull(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); From ea0ac5324b28faab9b8a7db0404ebe81fef2532f Mon Sep 17 00:00:00 2001 From: marcarro Date: Fri, 19 Jul 2024 10:56:16 -0700 Subject: [PATCH 04/22] Tried fixing nullability issues --- .../ExtractToNewComponentCodeActionProvider.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index f55c8959a22..fd41ad85fb3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -74,7 +74,7 @@ public Task> ProvideAsync(RazorCodeAct var endOwner = owner; var endComponentNode = startComponentNode; - if (isSelection) + if (isSelection && selectionEnd is not null) { if (!selectionEnd.TryGetSourceLocation(context.CodeDocument.GetSourceText(), _logger, out var location)) { @@ -161,7 +161,7 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen // good namespace to extract to => codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out @namespace); - public (SyntaxNode Start, SyntaxNode End) FindContainingSiblingPair(SyntaxNode startNode, SyntaxNode endNode) + public (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(SyntaxNode startNode, SyntaxNode endNode) { // Find the lowest common ancestor of both nodes var lowestCommonAncestor = FindLowestCommonAncestor(startNode, endNode); @@ -170,14 +170,13 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen return (null, null); } - SyntaxNode startContainingNode = null; - SyntaxNode endContainingNode = null; + SyntaxNode? startContainingNode = null; + SyntaxNode? endContainingNode = null; // Pre-calculate the spans for comparison var startSpan = startNode.Span; var endSpan = endNode.Span; - foreach (var child in lowestCommonAncestor.ChildNodes().Where(node => node.Kind == SyntaxKind.MarkupElement)) { var childSpan = child.Span; @@ -185,13 +184,15 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen if (startContainingNode == null && childSpan.Contains(startSpan)) { startContainingNode = child; - if (endContainingNode != null) break; // Exit if we've found both + if (endContainingNode != null) + break; // Exit if we've found both } if (childSpan.Contains(endSpan)) { endContainingNode = child; - if (startContainingNode != null) break; // Exit if we've found both + if (startContainingNode != null) + break; // Exit if we've found both } } @@ -208,6 +209,7 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen { return current; } + current = current.Parent; } From 0deb9150ed1940871aff63d182cf6e4dd72185ed Mon Sep 17 00:00:00 2001 From: marcarro Date: Fri, 19 Jul 2024 12:05:11 -0700 Subject: [PATCH 05/22] Commented out incomplete test. Must be finished --- ...actToNewComponentCodeActionProviderTest.cs | 149 +++++++++--------- 1 file changed, 75 insertions(+), 74 deletions(-) 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/ExtractToNewComponentCodeActionProviderTest.cs index 2c9e0d898a7..5fca49aebb4 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -122,80 +122,81 @@ public async Task Handle_InProperMarkup_ReturnsNull() Assert.Null(commandOrCodeActionContainer); } - [Theory] - [InlineData(""" -
- [|
-

Div a title

-

Div a par

-
|] -
-

Div b title

-

Div b par

-
-
- """)] - [InlineData(""" -
-
-

Div a title

- [|

Div a par

|] -
-
-

Div b title

-

Div b par

-
-
- """)] - [InlineData(""" -
-
-

Div a title

- [|

Div a par

-
-
-

Div b title

-

Div b par

|] -
-
- """)] - public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupElementSelection) - { - // Arrange - var documentPath = "c:/Test.razor"; - var contents = $$""" - page "/" - - Home - - {{markupElementSelection}} - -

Hello, world!

- - Welcome to your new app. - """; - - TestFileMarkupParser.GetPositionAndSpans( - contents, out contents, out int cursorPosition, out ImmutableArray spans); - - 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, supportsFileCreation: true); - - var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); - - // Act - var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); - - // Assert - Assert.NotNull(commandOrCodeActionContainer); - } + // Holding off on this test until configured correctly (fails on CI) + //[Theory] + //[InlineData(""" + //
+ // [|
+ //

Div a title

+ //

Div a par

+ //
|] + //
+ //

Div b title

+ //

Div b par

+ //
+ //
+ //""")] + //[InlineData(""" + //
+ //
+ //

Div a title

+ // [|

Div a par

|] + //
+ //
+ //

Div b title

+ //

Div b par

+ //
+ //
+ //""")] + //[InlineData(""" + //
+ //
+ //

Div a title

+ // [|

Div a par

+ //
+ //
+ //

Div b title

+ //

Div b par

|] + //
+ //
+ //""")] + //public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupElementSelection) + //{ + // // Arrange + // var documentPath = "c:/Test.razor"; + // var contents = $$""" + // page "/" + + // Home + + // {{markupElementSelection}} + + //

Hello, world!

+ + // Welcome to your new app. + // """; + + // TestFileMarkupParser.GetPositionAndSpans( + // contents, out contents, out int cursorPosition, out ImmutableArray spans); + + // 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, supportsFileCreation: true); + + // var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); + + // // Act + // var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); + + // // Assert + // Assert.NotNull(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); From 46932b393c688fd8b821ce4d3311232bb303501a Mon Sep 17 00:00:00 2001 From: marcarro Date: Wed, 24 Jul 2024 14:14:07 -0700 Subject: [PATCH 06/22] Rebased to feature branch and updated according to new API --- .../Razor/ExtractToNewComponentCodeActionProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index fd41ad85fb3..5f368b680f7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -78,12 +78,12 @@ public Task> ProvideAsync(RazorCodeAct { if (!selectionEnd.TryGetSourceLocation(context.CodeDocument.GetSourceText(), _logger, out var location)) { - return SpecializedTasks.Null>(); + return SpecializedTasks.EmptyImmutableArray(); } if (location is null) { - return SpecializedTasks.Null>(); + return SpecializedTasks.EmptyImmutableArray(); } selectionEndIndex = location.Value; @@ -91,7 +91,7 @@ public Task> ProvideAsync(RazorCodeAct if (endOwner is null) { - return SpecializedTasks.Null>(); + return SpecializedTasks.EmptyImmutableArray(); } endComponentNode = endOwner.FirstAncestorOrSelf(); From a93969442130989cdb874dcdb2978f3b997dba56 Mon Sep 17 00:00:00 2001 From: marcarro Date: Wed, 24 Jul 2024 14:26:59 -0700 Subject: [PATCH 07/22] Changed null assert to empty assert according to ci test results --- .../Razor/ExtractToNewComponentCodeActionProviderTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/ExtractToNewComponentCodeActionProviderTest.cs index 5fca49aebb4..a9b99683e0d 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -119,7 +119,7 @@ public async Task Handle_InProperMarkup_ReturnsNull() var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); // Assert - Assert.Null(commandOrCodeActionContainer); + Assert.Empty(commandOrCodeActionContainer); } // Holding off on this test until configured correctly (fails on CI) From a814ca8986fef1d0f3a28cbfda6fa2fa7a428841 Mon Sep 17 00:00:00 2001 From: marcarro Date: Thu, 25 Jul 2024 10:05:59 -0700 Subject: [PATCH 08/22] Refactored element selection logic into methods for easier readability --- ...ExtractToNewComponentCodeActionProvider.cs | 182 ++++++++++-------- ...actToNewComponentCodeActionProviderTest.cs | 151 ++++++++------- .../FileUtilities.cs | 9 +- 3 files changed, 185 insertions(+), 157 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 5f368b680f7..82b303d3cfc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -20,6 +20,7 @@ using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; @@ -50,108 +51,140 @@ public Task> ProvideAsync(RazorCodeAct return SpecializedTasks.EmptyImmutableArray(); } - var owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex, includeWhitespace: true); - if (owner is null) + var (startElementNode, endElementNode) = GetStartAndEndElements(context, syntaxTree, _logger); + + // Make sure we've found tag + if (startElementNode is null) { - _logger.LogWarning($"Owner should never be null."); return SpecializedTasks.EmptyImmutableArray(); } - var startComponentNode = owner.FirstAncestorOrSelf(); - - var selectionStart = context.Request.Range.Start; - var selectionEnd = context.Request.Range.End; - var isSelection = selectionStart != selectionEnd; - - // If user selects range from end to beginning (i.e., bottom-to-top or right-to-left), get the effective start and end. - if (selectionEnd is not null && selectionEnd.Line < selectionStart.Line || - (selectionEnd is not null && selectionEnd.Line == selectionStart.Line && selectionEnd.Character < selectionStart.Character)) + if (!TryGetNamespace(context.CodeDocument, out var @namespace)) { - (selectionEnd, selectionStart) = (selectionStart, selectionEnd); + return SpecializedTasks.EmptyImmutableArray(); } - var selectionEndIndex = new SourceLocation(0, 0, 0); - var endOwner = owner; - var endComponentNode = startComponentNode; + var actionParams = CreateInitialActionParams(context, startElementNode, @namespace); - if (isSelection && selectionEnd is not null) + if (IsMultiPointSelection(context.Request.Range)) { - if (!selectionEnd.TryGetSourceLocation(context.CodeDocument.GetSourceText(), _logger, out var location)) - { - return SpecializedTasks.EmptyImmutableArray(); - } - - if (location is null) - { - return SpecializedTasks.EmptyImmutableArray(); - } + ProcessMultiPointSelection(startElementNode, endElementNode, actionParams); + } - selectionEndIndex = location.Value; - endOwner = syntaxTree.Root.FindInnermostNode(selectionEndIndex.AbsoluteIndex, true); + var resolutionParams = new RazorCodeActionResolutionParams() + { + Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction, + Language = LanguageServerConstants.CodeActions.Languages.Razor, + Data = actionParams, + }; - if (endOwner is null) - { - return SpecializedTasks.EmptyImmutableArray(); - } + var codeAction = RazorCodeActionFactory.CreateExtractToNewComponent(resolutionParams); + return Task.FromResult>([codeAction]); + } - endComponentNode = endOwner.FirstAncestorOrSelf(); + private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAndEndElements(RazorCodeActionContext context, RazorSyntaxTree syntaxTree, ILogger logger) + { + var owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex, includeWhitespace: true); + if (owner is null) + { + logger.LogWarning($"Owner should never be null."); + return (null, null); } - // Make sure we've found tag - if (startComponentNode is null) + var startElementNode = owner.FirstAncestorOrSelf(); + if (startElementNode is null || IsInsideProperHtmlContent(context, startElementNode)) { - return SpecializedTasks.EmptyImmutableArray(); + return (null, null); } - // Do not provide code action if the cursor is inside proper html content (i.e. rendered text) - if (context.Location.AbsoluteIndex > startComponentNode.StartTag.Span.End && - context.Location.AbsoluteIndex < startComponentNode.EndTag.SpanStart) + var endElementNode = GetEndElementNode(context, syntaxTree, logger); + return (startElementNode, endElementNode); + } + + private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, MarkupElementSyntax startElementNode) + { + return context.Location.AbsoluteIndex > startElementNode.StartTag.Span.End && + context.Location.AbsoluteIndex < startElementNode.EndTag.SpanStart; + } + + private static MarkupElementSyntax? GetEndElementNode(RazorCodeActionContext context, RazorSyntaxTree syntaxTree, ILogger logger) + { + var selectionStart = context.Request.Range.Start; + var selectionEnd = context.Request.Range.End; + if (selectionStart == selectionEnd) { - return SpecializedTasks.EmptyImmutableArray(); + return null; } - if (!TryGetNamespace(context.CodeDocument, out var @namespace)) + var endLocation = GetEndLocation(selectionEnd, context.CodeDocument, logger); + if (!endLocation.HasValue) { - return SpecializedTasks.EmptyImmutableArray(); + return null; } - var actionParams = new ExtractToNewComponentCodeActionParams() + var endOwner = syntaxTree.Root.FindInnermostNode(endLocation.Value.AbsoluteIndex, true); + return endOwner?.FirstAncestorOrSelf(); + } + + private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(RazorCodeActionContext context, MarkupElementSyntax startElementNode, string @namespace) + { + return new ExtractToNewComponentCodeActionParams { Uri = context.Request.TextDocument.Uri, - ExtractStart = startComponentNode.Span.Start, - ExtractEnd = startComponentNode.Span.End, + ExtractStart = startElementNode.Span.Start, + ExtractEnd = startElementNode.Span.End, Namespace = @namespace }; + } - if (isSelection && endComponentNode is not null) + /// + /// Processes a multi-point selection to determine the correct range for extraction. + /// + /// The starting element of the selection. + /// The ending element of the selection, if it exists. + /// The parameters for the extraction action, which will be updated. + private static void ProcessMultiPointSelection(MarkupElementSyntax startElementNode, MarkupElementSyntax? endElementNode, ExtractToNewComponentCodeActionParams actionParams) + { + // If there's no end element, we can't process a multi-point selection + if (endElementNode is null) { - // If component @ start of the selection includes a parent element of the component @ end of selection, then proceed as usual. - // If not, limit extraction to end of component @ end of selection (in the simplest case) - var selectionStartHasParentElement = endComponentNode.Ancestors().Any(node => node == startComponentNode); - actionParams.ExtractEnd = selectionStartHasParentElement ? actionParams.ExtractEnd : endComponentNode.Span.End; + return; + } + + // Check if the start element is an ancestor of the end element + var selectionStartHasParentElement = endElementNode.Ancestors().Any(node => node == startElementNode); - // Handle other case: Either start/end of selection is nested within a component - if (!selectionStartHasParentElement) + // If the start element is an ancestor, keep the original end; otherwise, use the end of the end element + actionParams.ExtractEnd = selectionStartHasParentElement ? actionParams.ExtractEnd : endElementNode.Span.End; + + // If the start element is not an ancestor of the end element, we need to find a common parent + if (!selectionStartHasParentElement) + { + // Find the closest containing sibling pair that encompasses both the start and end elements + var (extractStart, extractEnd) = FindContainingSiblingPair(startElementNode, endElementNode); + + // If we found a valid containing pair, update the extraction range + if (extractStart is not null && extractEnd is not null) { - var (extractStart, extractEnd) = FindContainingSiblingPair(startComponentNode, endComponentNode); - if (extractStart != null && extractEnd != null) - { - actionParams.ExtractStart = extractStart.Span.Start; - actionParams.ExtractEnd = extractEnd.Span.End; - } + actionParams.ExtractStart = extractStart.Span.Start; + actionParams.ExtractEnd = extractEnd.Span.End; } + // Note: If we don't find a valid pair, we keep the original extraction range } + } - var resolutionParams = new RazorCodeActionResolutionParams() - { - Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction, - Language = LanguageServerConstants.CodeActions.Languages.Razor, - Data = actionParams, - }; - - var codeAction = RazorCodeActionFactory.CreateExtractToNewComponent(resolutionParams); + private static bool IsMultiPointSelection(Range range) + { + return range.Start != range.End; + } - return Task.FromResult>([codeAction]); + private static SourceLocation? GetEndLocation(Position selectionEnd, RazorCodeDocument codeDocument, ILogger logger) + { + if (!selectionEnd.TryGetSourceLocation(codeDocument.GetSourceText(), logger, out var location)) + { + return null; + } + return location; } private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen(returnValue: true)] out string? @namespace) @@ -161,11 +194,11 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen // good namespace to extract to => codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out @namespace); - public (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(SyntaxNode startNode, SyntaxNode endNode) + private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(SyntaxNode startNode, SyntaxNode endNode) { // Find the lowest common ancestor of both nodes - var lowestCommonAncestor = FindLowestCommonAncestor(startNode, endNode); - if (lowestCommonAncestor == null) + var nearestCommonAncestor = FindNearestCommonAncestor(startNode, endNode); + if (nearestCommonAncestor == null) { return (null, null); } @@ -177,7 +210,7 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen var startSpan = startNode.Span; var endSpan = endNode.Span; - foreach (var child in lowestCommonAncestor.ChildNodes().Where(node => node.Kind == SyntaxKind.MarkupElement)) + foreach (var child in nearestCommonAncestor.ChildNodes().Where(node => node.Kind == SyntaxKind.MarkupElement)) { var childSpan = child.Span; @@ -199,7 +232,7 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen return (startContainingNode, endContainingNode); } - public SyntaxNode? FindLowestCommonAncestor(SyntaxNode node1, SyntaxNode node2) + private static SyntaxNode? FindNearestCommonAncestor(SyntaxNode node1, SyntaxNode node2) { var current = node1; @@ -215,9 +248,4 @@ private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen return null; } - - //private static bool HasUnsupportedChildren(Language.Syntax.SyntaxNode node) - //{ - // return node.DescendantNodes().Any(static n => n is MarkupBlockSyntax or CSharpTransitionSyntax or RazorCommentBlockSyntax); - //} } 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/ExtractToNewComponentCodeActionProviderTest.cs index a9b99683e0d..327357df145 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -76,7 +76,7 @@ public async Task Handle_InvalidFileKind() Assert.Empty(commandOrCodeActionContainer); } - [Fact] + [Fact (Skip = "Incorrectly set up")] public async Task Handle_InProperMarkup_ReturnsNull() { // Arrange @@ -122,81 +122,80 @@ public async Task Handle_InProperMarkup_ReturnsNull() Assert.Empty(commandOrCodeActionContainer); } - // Holding off on this test until configured correctly (fails on CI) - //[Theory] - //[InlineData(""" - //
- // [|
- //

Div a title

- //

Div a par

- //
|] - //
- //

Div b title

- //

Div b par

- //
- //
- //""")] - //[InlineData(""" - //
- //
- //

Div a title

- // [|

Div a par

|] - //
- //
- //

Div b title

- //

Div b par

- //
- //
- //""")] - //[InlineData(""" - //
- //
- //

Div a title

- // [|

Div a par

- //
- //
- //

Div b title

- //

Div b par

|] - //
- //
- //""")] - //public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupElementSelection) - //{ - // // Arrange - // var documentPath = "c:/Test.razor"; - // var contents = $$""" - // page "/" - - // Home - - // {{markupElementSelection}} - - //

Hello, world!

- - // Welcome to your new app. - // """; - - // TestFileMarkupParser.GetPositionAndSpans( - // contents, out contents, out int cursorPosition, out ImmutableArray spans); - - // 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, supportsFileCreation: true); - - // var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); - - // // Act - // var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); - - // // Assert - // Assert.NotNull(commandOrCodeActionContainer); - //} + [Theory (Skip = "Incorrectly set up")] + [InlineData(""" +
+ [|
+

Div a title

+

Div a par

+
|] +
+

Div b title

+

Div b par

+
+
+ """)] + [InlineData(""" +
+
+

Div a title

+ [|

Div a par

|] +
+
+

Div b title

+

Div b par

+
+
+ """)] + [InlineData(""" +
+
+

Div a title

+ [|

Div a par

+
+
+

Div b title

+

Div b par

|] +
+
+ """)] + public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupElementSelection) + { + // Arrange + var documentPath = "c:/Test.razor"; + var contents = $$""" + page "/" + + Home + + {{markupElementSelection}} + +

Hello, world!

+ + Welcome to your new app. + """; + + TestFileMarkupParser.GetPositionAndSpans( + contents, out contents, out int cursorPosition, out ImmutableArray spans); + + 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, supportsFileCreation: true); + + var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); + + // Act + var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); + + // Assert + Assert.NotNull(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); diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs index c93a6e35fd2..b7e8afe1068 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs @@ -19,10 +19,11 @@ internal static class FileUtilities /// A non-existent file path with a name in the specified format and a corresponding extension. public static string GenerateUniquePath(string path, string extension) { - if (!Path.IsPathRooted(path)) - { - throw new ArgumentException("The path is not rooted.", nameof(path)); - } + // Add check for rooted path in the future, currently having issues in platforms other than Windows. + //if (!Path.IsPathRooted(path)) + //{ + // throw new ArgumentException("The path is not rooted.", nameof(path)); + //} var directoryName = Path.GetDirectoryName(path).AssumeNotNull(); var baseFileName = Path.GetFileNameWithoutExtension(path); From 8ba59f24b55e86cbc1e30ffb13220e9001129806 Mon Sep 17 00:00:00 2001 From: marcarro Date: Thu, 25 Jul 2024 12:25:42 -0700 Subject: [PATCH 09/22] Removed unnecessary using --- .../ExtractToNewComponentCodeActionProvider.cs | 14 +++++++++++++- .../FileUtilities.cs | 1 - 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 82b303d3cfc..0c462e0849b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -53,7 +53,7 @@ public Task> ProvideAsync(RazorCodeAct var (startElementNode, endElementNode) = GetStartAndEndElements(context, syntaxTree, _logger); - // Make sure we've found tag + // Make sure the selection starts on an element tag if (startElementNode is null) { return SpecializedTasks.EmptyImmutableArray(); @@ -158,6 +158,18 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN actionParams.ExtractEnd = selectionStartHasParentElement ? actionParams.ExtractEnd : endElementNode.Span.End; // If the start element is not an ancestor of the end element, we need to find a common parent + // This conditional handles cases where the user's selection spans across different levels of the DOM. + // For example: + //
+ // + // Selected text starts here

Some text

+ //
+ // + //

More text

+ //
+ // Selected text ends here + //
+ // In this case, we need to find the smallest set of complete elements that covers the entire selection. if (!selectionStartHasParentElement) { // Find the closest containing sibling pair that encompasses both the start and end elements diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs index b7e8afe1068..fd73c5bccb4 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs @@ -1,7 +1,6 @@ // 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.Globalization; using System.IO; From cd6b24a6708a7fec39c22a7874158b2c798dc7f3 Mon Sep 17 00:00:00 2001 From: marcarro Date: Thu, 25 Jul 2024 13:17:30 -0700 Subject: [PATCH 10/22] Added check for IsInsideProperHtmlContent --- .../Razor/ExtractToNewComponentCodeActionProvider.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 0c462e0849b..c989d1ab4e8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -103,6 +103,11 @@ private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAn private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, MarkupElementSyntax startElementNode) { + // If the provider executes before the user/completion inserts an end tag, the below return fails + if (startElementNode.EndTag.IsMissing) + { + return true; + } return context.Location.AbsoluteIndex > startElementNode.StartTag.Span.End && context.Location.AbsoluteIndex < startElementNode.EndTag.SpanStart; } @@ -222,8 +227,11 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy var startSpan = startNode.Span; var endSpan = endNode.Span; + int i = 0; + foreach (var child in nearestCommonAncestor.ChildNodes().Where(node => node.Kind == SyntaxKind.MarkupElement)) { + i++; var childSpan = child.Span; if (startContainingNode == null && childSpan.Contains(startSpan)) From 55669a7933540d848b277ba5da0f83456effcd6e Mon Sep 17 00:00:00 2001 From: marcarro Date: Thu, 25 Jul 2024 13:18:56 -0700 Subject: [PATCH 11/22] Removed unnecessary iteration variable in FindContainingSiblignPair --- .../Razor/ExtractToNewComponentCodeActionProvider.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index c989d1ab4e8..66dcfc022e8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -227,11 +227,8 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy var startSpan = startNode.Span; var endSpan = endNode.Span; - int i = 0; - foreach (var child in nearestCommonAncestor.ChildNodes().Where(node => node.Kind == SyntaxKind.MarkupElement)) { - i++; var childSpan = child.Span; if (startContainingNode == null && childSpan.Contains(startSpan)) From 8ee84decd11a7905b55dcd943f04f05abd5b7cf0 Mon Sep 17 00:00:00 2001 From: marcarro Date: Fri, 26 Jul 2024 08:57:32 -0700 Subject: [PATCH 12/22] nits and test fixes --- .../Razor/ExtractToNewComponentCodeActionProvider.cs | 11 ++++------- .../ExtractToNewComponentCodeActionProviderTest.cs | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 66dcfc022e8..2b9118a994d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -190,10 +190,7 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN } } - private static bool IsMultiPointSelection(Range range) - { - return range.Start != range.End; - } + private static bool IsMultiPointSelection(Range range) => range.Start != range.End; private static SourceLocation? GetEndLocation(Position selectionEnd, RazorCodeDocument codeDocument, ILogger logger) { @@ -234,14 +231,14 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy if (startContainingNode == null && childSpan.Contains(startSpan)) { startContainingNode = child; - if (endContainingNode != null) + if (endContainingNode is not null) break; // Exit if we've found both } if (childSpan.Contains(endSpan)) { endContainingNode = child; - if (startContainingNode != null) + if (startContainingNode is not null) break; // Exit if we've found both } } @@ -253,7 +250,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy { var current = node1; - while (current.Kind == SyntaxKind.MarkupElement && current != null) + while (current.Kind == SyntaxKind.MarkupElement && current is not null) { if (current.Span.Contains(node2.Span)) { 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/ExtractToNewComponentCodeActionProviderTest.cs index 327357df145..d5cf54200ef 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -194,7 +194,7 @@ public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupEleme var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); // Assert - Assert.NotNull(commandOrCodeActionContainer); + Assert.NotEmpty(commandOrCodeActionContainer); } private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionParams request, SourceLocation location, string filePath, string text, bool supportsFileCreation = true) From eba1b015e7c81331806f6dd67adbf75b7c354307 Mon Sep 17 00:00:00 2001 From: marcarro Date: Fri, 26 Jul 2024 11:55:56 -0700 Subject: [PATCH 13/22] Functioning tests and fixed selection issue --- ...ExtractToNewComponentCodeActionProvider.cs | 7 + ...actToNewComponentCodeActionProviderTest.cs | 138 ++++++++++-------- 2 files changed, 87 insertions(+), 58 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 2b9118a994d..52539dc62d6 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -128,6 +128,13 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma } var endOwner = syntaxTree.Root.FindInnermostNode(endLocation.Value.AbsoluteIndex, true); + + // Correct selection to include the current node if the selection ends immediately after a closing tag. + if (string.IsNullOrWhiteSpace(endOwner.ToFullString()) && endOwner.TryGetPreviousSibling(out var previousSibling)) + { + endOwner = previousSibling; + } + return endOwner?.FirstAncestorOrSelf(); } 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/ExtractToNewComponentCodeActionProviderTest.cs index d5cf54200ef..13c21f0a05c 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.CodeActions.Razor; public class ExtractToNewComponentCodeActionProviderTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { - [Fact (Skip = "Not fully set up yet")] + [Fact] public async Task Handle_InvalidFileKind() { // Arrange @@ -76,29 +76,29 @@ public async Task Handle_InvalidFileKind() Assert.Empty(commandOrCodeActionContainer); } - [Fact (Skip = "Incorrectly set up")] - public async Task Handle_InProperMarkup_ReturnsNull() + [Fact] + public async Task Handle_SinglePointSelection_ReturnsNotEmpty() { // Arrange - var documentPath = "c:/Test.razor"; + var documentPath = "c:/Test.cs"; var contents = """ - page "/" - + @page "/" + Home - +
-
+ <$$div>

Div a title

-

Div $$a par

+

Div a par

Div b title

Div b par

-
+

Hello, world!

- + Welcome to your new app. """; TestFileMarkupParser.GetPosition(contents, out contents, out var cursorPosition); @@ -119,64 +119,81 @@ public async Task Handle_InProperMarkup_ReturnsNull() var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); // Assert - Assert.Empty(commandOrCodeActionContainer); + Assert.NotEmpty(commandOrCodeActionContainer); } - [Theory (Skip = "Incorrectly set up")] - [InlineData(""" -
- [|
-

Div a title

-

Div a par

-
|] -
-

Div b title

-

Div b par

-
-
- """)] - [InlineData(""" -
-
-

Div a title

- [|

Div a par

|] -
-
-

Div b title

-

Div b par

-
-
- """)] - [InlineData(""" -
-
-

Div a title

- [|

Div a par

-
-
-

Div b title

-

Div b par

|] -
-
- """)] - public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupElementSelection) + [Fact] + public async Task Handle_MultiPointSelection_ReturnsNotEmpty() { // Arrange - var documentPath = "c:/Test.razor"; - var contents = $$""" - page "/" + var documentPath = "c:/Test.cs"; + var contents = """ + @page "/" Home - {{markupElementSelection}} +
+ [|
+ $$

Div a title

+

Div a par

+
+
+

Div b title

+

Div b par

+ +

Hello, world!

Welcome to your new app. """; + TestFileMarkupParser.GetPositionAndSpan(contents, out contents, out var cursorPosition, out var selectionSpan); + + 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 ExtractToNewComponentCodeActionProvider(LoggerFactory); - TestFileMarkupParser.GetPositionAndSpans( - contents, out contents, out int cursorPosition, out ImmutableArray spans); + // Act + var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); + + // Assert + Assert.NotEmpty(commandOrCodeActionContainer); + } + + [Fact] + public async Task Handle_MultiPointSelectionWithEndAfterElement_ReturnsCurrentElement() + { + // Arrange + var documentPath = "c:/Test.cs"; + var contents = """ + @page "/" + + Home + +
+ [|
+ $$

Div a title

+

Div a par

+
+
+

Div b title

+

Div b par

+
|] +
+ +

Hello, world!

+ + Welcome to your new app. + """; + TestFileMarkupParser.GetPositionAndSpan(contents, out contents, out var cursorPosition, out var selectionSpan); var request = new VSCodeActionParams() { @@ -186,7 +203,7 @@ public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupEleme }; var location = new SourceLocation(cursorPosition, -1, -1); - var context = CreateRazorCodeActionContext(request, location, documentPath, contents, supportsFileCreation: true); + var context = CreateRazorCodeActionContext(request, location, documentPath, contents); var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); @@ -195,6 +212,11 @@ public async Task Handle_ValidElementSelection_ReturnsNotNull(string markupEleme // Assert Assert.NotEmpty(commandOrCodeActionContainer); + var codeAction = Assert.Single(commandOrCodeActionContainer); + var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize(); + Assert.NotNull(razorCodeActionResolutionParams); + var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize(); + Assert.NotNull(actionParams); } private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionParams request, SourceLocation location, string filePath, string text, bool supportsFileCreation = true) From 6656f7a2a2040abb64355e0523cbb87ea0526bf7 Mon Sep 17 00:00:00 2001 From: marcarro Date: Fri, 26 Jul 2024 12:12:42 -0700 Subject: [PATCH 14/22] Added null check --- .../Razor/ExtractToNewComponentCodeActionProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 52539dc62d6..369d125b888 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -128,6 +128,11 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma } var endOwner = syntaxTree.Root.FindInnermostNode(endLocation.Value.AbsoluteIndex, true); + + if (endOwner is null) + { + return null; + } // Correct selection to include the current node if the selection ends immediately after a closing tag. if (string.IsNullOrWhiteSpace(endOwner.ToFullString()) && endOwner.TryGetPreviousSibling(out var previousSibling)) From 31282c8b45552a5522ce09263fb63ad19a09524a Mon Sep 17 00:00:00 2001 From: marcarro Date: Tue, 30 Jul 2024 08:18:00 -0700 Subject: [PATCH 15/22] Test updates --- ...actToNewComponentCodeActionProviderTest.cs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) 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/ExtractToNewComponentCodeActionProviderTest.cs index 13c21f0a05c..c1f443df593 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -2,10 +2,6 @@ // 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.Linq; -using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; @@ -179,8 +175,8 @@ public async Task Handle_MultiPointSelectionWithEndAfterElement_ReturnsCurrentEl Home
- [|
- $$

Div a title

+ [|$$
+

Div a title

Div a par

@@ -205,6 +201,8 @@ public async Task Handle_MultiPointSelectionWithEndAfterElement_ReturnsCurrentEl var location = new SourceLocation(cursorPosition, -1, -1); var context = CreateRazorCodeActionContext(request, location, documentPath, contents); + AddMultiPointSelectionToContext(ref context, selectionSpan); + var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); // Act @@ -250,4 +248,21 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP return context; } + + + private static void AddMultiPointSelectionToContext(ref RazorCodeActionContext context, TextSpan selectionSpan) + { + var sourceText = context.CodeDocument.GetSourceText(); + var startLinePosition = sourceText.Lines.GetLinePosition(selectionSpan.Start); + var startPosition = new Position(startLinePosition.Line, startLinePosition.Character); + + var endLinePosition = sourceText.Lines.GetLinePosition(selectionSpan.End); + var endPosition = new Position(endLinePosition.Line, endLinePosition.Character); + + context.Request.Range = new Range + { + Start = startPosition, + End = endPosition + }; + } } From 04aa3499c9fcaf57f4d71172a949b1f9962366bf Mon Sep 17 00:00:00 2001 From: marcarro Date: Tue, 30 Jul 2024 09:47:28 -0700 Subject: [PATCH 16/22] PR Feedback --- .../ExtractToNewComponentCodeActionProvider.cs | 8 ++++---- .../ExtractToNewComponentCodeActionProviderTest.cs | 14 ++++++++++---- .../FileUtilities.cs | 5 +---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 369d125b888..0c971e2685b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -9,7 +9,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using ICSharpCode.Decompiler.CSharp.Syntax; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Language.Extensions; @@ -108,6 +107,7 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma { return true; } + return context.Location.AbsoluteIndex > startElementNode.StartTag.Span.End && context.Location.AbsoluteIndex < startElementNode.EndTag.SpanStart; } @@ -128,7 +128,6 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma } var endOwner = syntaxTree.Root.FindInnermostNode(endLocation.Value.AbsoluteIndex, true); - if (endOwner is null) { return null; @@ -206,10 +205,11 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN private static SourceLocation? GetEndLocation(Position selectionEnd, RazorCodeDocument codeDocument, ILogger logger) { - if (!selectionEnd.TryGetSourceLocation(codeDocument.GetSourceText(), logger, out var location)) + if (!selectionEnd.TryGetSourceLocation(codeDocument.Source.Text, logger, out var location)) { return null; } + return location; } @@ -236,7 +236,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy var startSpan = startNode.Span; var endSpan = endNode.Span; - foreach (var child in nearestCommonAncestor.ChildNodes().Where(node => node.Kind == SyntaxKind.MarkupElement)) + foreach (var child in nearestCommonAncestor.ChildNodes().Where(static node => node.Kind == SyntaxKind.MarkupElement)) { var childSpan = child.Span; 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/ExtractToNewComponentCodeActionProviderTest.cs index c1f443df593..f4dee81574f 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -155,6 +155,8 @@ public async Task Handle_MultiPointSelection_ReturnsNotEmpty() var location = new SourceLocation(cursorPosition, -1, -1); var context = CreateRazorCodeActionContext(request, location, documentPath, contents); + AddMultiPointSelectionToContext(context, selectionSpan); + var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); // Act @@ -162,6 +164,11 @@ public async Task Handle_MultiPointSelection_ReturnsNotEmpty() // Assert Assert.NotEmpty(commandOrCodeActionContainer); + var codeAction = Assert.Single(commandOrCodeActionContainer); + var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize(); + Assert.NotNull(razorCodeActionResolutionParams); + var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize(); + Assert.NotNull(actionParams); } [Fact] @@ -201,7 +208,7 @@ public async Task Handle_MultiPointSelectionWithEndAfterElement_ReturnsCurrentEl var location = new SourceLocation(cursorPosition, -1, -1); var context = CreateRazorCodeActionContext(request, location, documentPath, contents); - AddMultiPointSelectionToContext(ref context, selectionSpan); + AddMultiPointSelectionToContext(context, selectionSpan); var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); @@ -249,10 +256,9 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP return context; } - - private static void AddMultiPointSelectionToContext(ref RazorCodeActionContext context, TextSpan selectionSpan) + private static void AddMultiPointSelectionToContext(RazorCodeActionContext context, TextSpan selectionSpan) { - var sourceText = context.CodeDocument.GetSourceText(); + var sourceText = context.CodeDocument.Source.Text; var startLinePosition = sourceText.Lines.GetLinePosition(selectionSpan.Start); var startPosition = new Position(startLinePosition.Line, startLinePosition.Character); diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs index fd73c5bccb4..829fb250647 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs @@ -19,10 +19,7 @@ internal static class FileUtilities public static string GenerateUniquePath(string path, string extension) { // Add check for rooted path in the future, currently having issues in platforms other than Windows. - //if (!Path.IsPathRooted(path)) - //{ - // throw new ArgumentException("The path is not rooted.", nameof(path)); - //} + // See: https://github.com/dotnet/razor/issues/10684 var directoryName = Path.GetDirectoryName(path).AssumeNotNull(); var baseFileName = Path.GetFileNameWithoutExtension(path); From 8dbd67249642c88e9c2af233ed00c1433adeb276 Mon Sep 17 00:00:00 2001 From: marcarro Date: Mon, 19 Aug 2024 18:00:30 -0700 Subject: [PATCH 17/22] Adapted to new TryGetSourceLocation and clarified ambiguous Range reference --- .../Razor/ExtractToNewComponentCodeActionProvider.cs | 7 ++++--- .../Razor/ExtractToNewComponentCodeActionResolver.cs | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 0c971e2685b..36595328635 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -20,6 +20,7 @@ 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; @@ -121,7 +122,7 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma return null; } - var endLocation = GetEndLocation(selectionEnd, context.CodeDocument, logger); + var endLocation = GetEndLocation(selectionEnd, context.CodeDocument); if (!endLocation.HasValue) { return null; @@ -203,9 +204,9 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN private static bool IsMultiPointSelection(Range range) => range.Start != range.End; - private static SourceLocation? GetEndLocation(Position selectionEnd, RazorCodeDocument codeDocument, ILogger logger) + private static SourceLocation? GetEndLocation(Position selectionEnd, RazorCodeDocument codeDocument) { - if (!selectionEnd.TryGetSourceLocation(codeDocument.Source.Text, logger, out var location)) + if (!codeDocument.Source.Text.TryGetSourceLocation(selectionEnd, out var location)) { return null; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs index 8b176aeb891..7cc6dfce792 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionResolver.cs @@ -23,6 +23,7 @@ using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; using Newtonsoft.Json.Linq; +using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; From 8a7d0bb56f9580d3e92eb07e6199bed767c4afa8 Mon Sep 17 00:00:00 2001 From: marcarro Date: Mon, 19 Aug 2024 19:01:12 -0700 Subject: [PATCH 18/22] Changed tests to use VsLspFactory utiltiies for multi point selection --- ...ExtractToNewComponentCodeActionProvider.cs | 4 +- ...actToNewComponentCodeActionProviderTest.cs | 46 +++++++------------ 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 36595328635..bf236f92440 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -97,7 +97,7 @@ private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAn return (null, null); } - var endElementNode = GetEndElementNode(context, syntaxTree, logger); + var endElementNode = GetEndElementNode(context, syntaxTree); return (startElementNode, endElementNode); } @@ -113,7 +113,7 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma context.Location.AbsoluteIndex < startElementNode.EndTag.SpanStart; } - private static MarkupElementSyntax? GetEndElementNode(RazorCodeActionContext context, RazorSyntaxTree syntaxTree, ILogger logger) + private static MarkupElementSyntax? GetEndElementNode(RazorCodeActionContext context, RazorSyntaxTree syntaxTree) { var selectionStart = context.Request.Range.Start; var selectionEnd = context.Request.Range.End; 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/ExtractToNewComponentCodeActionProviderTest.cs index f4dee81574f..2ccfee335ba 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -13,13 +13,13 @@ using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; -using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Moq; using Xunit; using Xunit.Abstractions; +using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.CodeActions.Razor; @@ -55,7 +55,7 @@ public async Task Handle_InvalidFileKind() var request = new VSCodeActionParams() { TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) }, - Range = new Range(), + Range = VsLspFactory.DefaultRange, Context = new VSInternalCodeActionContext() }; @@ -76,7 +76,7 @@ public async Task Handle_InvalidFileKind() public async Task Handle_SinglePointSelection_ReturnsNotEmpty() { // Arrange - var documentPath = "c:/Test.cs"; + var documentPath = "c:/Test.razor"; var contents = """ @page "/" @@ -102,12 +102,12 @@ public async Task Handle_SinglePointSelection_ReturnsNotEmpty() var request = new VSCodeActionParams() { TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) }, - Range = new Range(), + Range = VsLspFactory.DefaultRange, Context = new VSInternalCodeActionContext() }; var location = new SourceLocation(cursorPosition, -1, -1); - var context = CreateRazorCodeActionContext(request, location, documentPath, contents); + var context = CreateRazorCodeActionContext(request, location, documentPath, contents, supportsFileCreation: true); var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); @@ -122,7 +122,7 @@ public async Task Handle_SinglePointSelection_ReturnsNotEmpty() public async Task Handle_MultiPointSelection_ReturnsNotEmpty() { // Arrange - var documentPath = "c:/Test.cs"; + var documentPath = "c:/Test.razor"; var contents = """ @page "/" @@ -148,14 +148,15 @@ public async Task Handle_MultiPointSelection_ReturnsNotEmpty() var request = new VSCodeActionParams() { TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) }, - Range = new Range(), + Range = VsLspFactory.DefaultRange, Context = new VSInternalCodeActionContext() }; var location = new SourceLocation(cursorPosition, -1, -1); var context = CreateRazorCodeActionContext(request, location, documentPath, contents); - AddMultiPointSelectionToContext(context, selectionSpan); + var lineSpan = context.SourceText.GetLinePositionSpan(selectionSpan); + request.Range = VsLspFactory.CreateRange(lineSpan); var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); @@ -175,7 +176,7 @@ public async Task Handle_MultiPointSelection_ReturnsNotEmpty() public async Task Handle_MultiPointSelectionWithEndAfterElement_ReturnsCurrentElement() { // Arrange - var documentPath = "c:/Test.cs"; + var documentPath = "c:/Test.razor"; var contents = """ @page "/" @@ -201,14 +202,15 @@ public async Task Handle_MultiPointSelectionWithEndAfterElement_ReturnsCurrentEl var request = new VSCodeActionParams() { TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) }, - Range = new Range(), + Range = VsLspFactory.DefaultRange, Context = new VSInternalCodeActionContext() }; var location = new SourceLocation(cursorPosition, -1, -1); var context = CreateRazorCodeActionContext(request, location, documentPath, contents); - AddMultiPointSelectionToContext(context, selectionSpan); + var lineSpan = context.SourceText.GetLinePositionSpan(selectionSpan); + request.Range = VsLspFactory.CreateRange(lineSpan); var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory); @@ -222,6 +224,8 @@ public async Task Handle_MultiPointSelectionWithEndAfterElement_ReturnsCurrentEl Assert.NotNull(razorCodeActionResolutionParams); var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize(); Assert.NotNull(actionParams); + Assert.Equal(selectionSpan.Start, actionParams.ExtractStart); + Assert.Equal(selectionSpan.End, actionParams.ExtractEnd); } private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionParams request, SourceLocation location, string filePath, string text, bool supportsFileCreation = true) @@ -241,13 +245,13 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP codeDocument.SetFileKind(FileKinds.Component); codeDocument.SetCodeGenerationOptions(RazorCodeGenerationOptions.Create(o => { - o.RootNamespace = "ExtractToCodeBehindTest"; + o.RootNamespace = "ExtractToComponentTest"; })); codeDocument.SetSyntaxTree(syntaxTree); var documentSnapshot = Mock.Of(document => document.GetGeneratedOutputAsync() == Task.FromResult(codeDocument) && - document.GetTextAsync() == Task.FromResult(codeDocument.GetSourceText()), MockBehavior.Strict); + document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text), MockBehavior.Strict); var sourceText = SourceText.From(text); @@ -255,20 +259,4 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP return context; } - - private static void AddMultiPointSelectionToContext(RazorCodeActionContext context, TextSpan selectionSpan) - { - var sourceText = context.CodeDocument.Source.Text; - var startLinePosition = sourceText.Lines.GetLinePosition(selectionSpan.Start); - var startPosition = new Position(startLinePosition.Line, startLinePosition.Character); - - var endLinePosition = sourceText.Lines.GetLinePosition(selectionSpan.End); - var endPosition = new Position(endLinePosition.Line, endLinePosition.Character); - - context.Request.Range = new Range - { - Start = startPosition, - End = endPosition - }; - } } From 7d3c78aa480ae34d47ffabac6257a7f1034f09b2 Mon Sep 17 00:00:00 2001 From: marcarro Date: Mon, 19 Aug 2024 19:08:51 -0700 Subject: [PATCH 19/22] PR Feedback --- .../ExtractToNewComponentCodeActionProvider.cs | 14 ++++++++------ .../ExtractToNewComponentCodeActionProviderTest.cs | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index bf236f92440..da1a62333da 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -135,12 +135,12 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma } // Correct selection to include the current node if the selection ends immediately after a closing tag. - if (string.IsNullOrWhiteSpace(endOwner.ToFullString()) && endOwner.TryGetPreviousSibling(out var previousSibling)) + if (endOwner is MarkupTextLiteralSyntax && string.IsNullOrWhiteSpace(endOwner.ToFullString()) && endOwner.TryGetPreviousSibling(out var previousSibling)) { endOwner = previousSibling; } - return endOwner?.FirstAncestorOrSelf(); + return endOwner.FirstAncestorOrSelf(); } private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(RazorCodeActionContext context, MarkupElementSyntax startElementNode, string @namespace) @@ -168,11 +168,13 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN return; } - // Check if the start element is an ancestor of the end element - var selectionStartHasParentElement = endElementNode.Ancestors().Any(node => node == startElementNode); + var startNodeContainsEndNode = endElementNode.Ancestors().Any(node => node == startElementNode); // If the start element is an ancestor, keep the original end; otherwise, use the end of the end element - actionParams.ExtractEnd = selectionStartHasParentElement ? actionParams.ExtractEnd : endElementNode.Span.End; + if (!startNodeContainsEndNode) + { + actionParams.ExtractEnd = endElementNode.Span.End; + } // If the start element is not an ancestor of the end element, we need to find a common parent // This conditional handles cases where the user's selection spans across different levels of the DOM. @@ -187,7 +189,7 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN // Selected text ends here //
// In this case, we need to find the smallest set of complete elements that covers the entire selection. - if (!selectionStartHasParentElement) + if (!startNodeContainsEndNode) { // Find the closest containing sibling pair that encompasses both the start and end elements var (extractStart, extractEnd) = FindContainingSiblingPair(startElementNode, endElementNode); 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/ExtractToNewComponentCodeActionProviderTest.cs index 2ccfee335ba..185f34b0b70 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/ExtractToNewComponentCodeActionProviderTest.cs @@ -173,7 +173,7 @@ public async Task Handle_MultiPointSelection_ReturnsNotEmpty() } [Fact] - public async Task Handle_MultiPointSelectionWithEndAfterElement_ReturnsCurrentElement() + public async Task Handle_MultiPointSelection_WithEndAfterElement_ReturnsCurrentElement() { // Arrange var documentPath = "c:/Test.razor"; From 842b1628e84b1f6ddddd621651ae9dc92ac8b8b0 Mon Sep 17 00:00:00 2001 From: marcarro Date: Wed, 21 Aug 2024 15:21:13 -0700 Subject: [PATCH 20/22] Added early return in ProcessMultiPointSelection --- ...ExtractToNewComponentCodeActionProvider.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index da1a62333da..bdb194e3bfd 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -171,9 +171,10 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN var startNodeContainsEndNode = endElementNode.Ancestors().Any(node => node == startElementNode); // If the start element is an ancestor, keep the original end; otherwise, use the end of the end element - if (!startNodeContainsEndNode) + if (startNodeContainsEndNode) { - actionParams.ExtractEnd = endElementNode.Span.End; + actionParams.ExtractEnd = startElementNode.Span.End; + return; } // If the start element is not an ancestor of the end element, we need to find a common parent @@ -189,19 +190,17 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN // Selected text ends here //
// In this case, we need to find the smallest set of complete elements that covers the entire selection. - if (!startNodeContainsEndNode) - { - // Find the closest containing sibling pair that encompasses both the start and end elements - var (extractStart, extractEnd) = FindContainingSiblingPair(startElementNode, endElementNode); + + // Find the closest containing sibling pair that encompasses both the start and end elements + var (extractStart, extractEnd) = FindContainingSiblingPair(startElementNode, endElementNode); - // If we found a valid containing pair, update the extraction range - if (extractStart is not null && extractEnd is not null) - { - actionParams.ExtractStart = extractStart.Span.Start; - actionParams.ExtractEnd = extractEnd.Span.End; - } - // Note: If we don't find a valid pair, we keep the original extraction range + // If we found a valid containing pair, update the extraction range + if (extractStart is not null && extractEnd is not null) + { + actionParams.ExtractStart = extractStart.Span.Start; + actionParams.ExtractEnd = extractEnd.Span.End; } + // Note: If we don't find a valid pair, we keep the original extraction range } private static bool IsMultiPointSelection(Range range) => range.Start != range.End; From 4bab4ddae23d8073520ad30957196513be43e967 Mon Sep 17 00:00:00 2001 From: marcarro Date: Wed, 21 Aug 2024 15:57:05 -0700 Subject: [PATCH 21/22] No need to check for multipoint selection --- .../Razor/ExtractToNewComponentCodeActionProvider.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index bdb194e3bfd..72dd5dec289 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -66,10 +66,7 @@ public Task> ProvideAsync(RazorCodeAct var actionParams = CreateInitialActionParams(context, startElementNode, @namespace); - if (IsMultiPointSelection(context.Request.Range)) - { - ProcessMultiPointSelection(startElementNode, endElementNode, actionParams); - } + ProcessSelection(startElementNode, endElementNode, actionParams); var resolutionParams = new RazorCodeActionResolutionParams() { @@ -160,7 +157,7 @@ private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(R /// The starting element of the selection. /// The ending element of the selection, if it exists. /// The parameters for the extraction action, which will be updated. - private static void ProcessMultiPointSelection(MarkupElementSyntax startElementNode, MarkupElementSyntax? endElementNode, ExtractToNewComponentCodeActionParams actionParams) + private static void ProcessSelection(MarkupElementSyntax startElementNode, MarkupElementSyntax? endElementNode, ExtractToNewComponentCodeActionParams actionParams) { // If there's no end element, we can't process a multi-point selection if (endElementNode is null) @@ -203,8 +200,6 @@ private static void ProcessMultiPointSelection(MarkupElementSyntax startElementN // Note: If we don't find a valid pair, we keep the original extraction range } - private static bool IsMultiPointSelection(Range range) => range.Start != range.End; - private static SourceLocation? GetEndLocation(Position selectionEnd, RazorCodeDocument codeDocument) { if (!codeDocument.Source.Text.TryGetSourceLocation(selectionEnd, out var location)) From 4297e6fc8b5b4f5b3b3a6984c8d2bf569a30d97e Mon Sep 17 00:00:00 2001 From: marcarro Date: Wed, 21 Aug 2024 16:26:04 -0700 Subject: [PATCH 22/22] PR Feedback --- ...ExtractToNewComponentCodeActionProvider.cs | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs index 72dd5dec289..6ae53437ba7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToNewComponentCodeActionProvider.cs @@ -119,20 +119,15 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma return null; } - var endLocation = GetEndLocation(selectionEnd, context.CodeDocument); - if (!endLocation.HasValue) - { - return null; - } - - var endOwner = syntaxTree.Root.FindInnermostNode(endLocation.Value.AbsoluteIndex, true); + var endAbsoluteIndex = context.SourceText.GetRequiredAbsoluteIndex(selectionEnd); + var endOwner = syntaxTree.Root.FindInnermostNode(endAbsoluteIndex, true); if (endOwner is null) { return null; } // Correct selection to include the current node if the selection ends immediately after a closing tag. - if (endOwner is MarkupTextLiteralSyntax && string.IsNullOrWhiteSpace(endOwner.ToFullString()) && endOwner.TryGetPreviousSibling(out var previousSibling)) + if (endOwner is MarkupTextLiteralSyntax && endOwner.ContainsOnlyWhitespace() && endOwner.TryGetPreviousSibling(out var previousSibling)) { endOwner = previousSibling; } @@ -178,13 +173,14 @@ private static void ProcessSelection(MarkupElementSyntax startElementNode, Marku // This conditional handles cases where the user's selection spans across different levels of the DOM. // For example: //
- // - // Selected text starts here

Some text

+ // {|result: + // {|selection:

Some text

//
// //

More text

//
- // Selected text ends here + // + // |}|} //
// In this case, we need to find the smallest set of complete elements that covers the entire selection. @@ -200,16 +196,6 @@ private static void ProcessSelection(MarkupElementSyntax startElementNode, Marku // Note: If we don't find a valid pair, we keep the original extraction range } - private static SourceLocation? GetEndLocation(Position selectionEnd, RazorCodeDocument codeDocument) - { - if (!codeDocument.Source.Text.TryGetSourceLocation(selectionEnd, out var location)) - { - return null; - } - - return location; - } - 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