diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs index 6563e5bb05..5c9ed1cc24 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; @@ -250,23 +251,7 @@ internal class RenameService( return default; } - var node = owner.FirstAncestorOrSelf(n => n.Kind == RazorSyntaxKind.MarkupTagHelperStartTag); - if (node is not MarkupTagHelperStartTagSyntax tagHelperStartTag) - { - return default; - } - - // Ensure the rename action was invoked on the component name - // instead of a component parameter. This serves as an issue - // mitigation till `textDocument/prepareRename` is supported - // and we can ensure renames aren't triggered in unsupported - // contexts. (https://github.com/dotnet/aspnetcore/issues/26407) - if (!tagHelperStartTag.Name.FullSpan.IntersectsWith(absoluteIndex)) - { - return default; - } - - if (tagHelperStartTag.Parent is not MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding }) + if (!TryGetTagHelperBinding(owner, absoluteIndex, out var binding)) { return default; } @@ -288,6 +273,43 @@ internal class RenameService( return [primaryTagHelper, associatedTagHelper]; } + private static bool TryGetTagHelperBinding(RazorSyntaxNode owner, int absoluteIndex, [NotNullWhen(true)] out TagHelperBinding? binding) + { + // End tags are easy, because there is only one possible binding result + if (owner is MarkupTagHelperEndTagSyntax { Parent: MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var endTagBindingResult } }) + { + binding = endTagBindingResult; + return true; + } + + // A rename of a start tag could have an "owner" of one of its attributes, so we do a bit more checking + // to support this case + var node = owner.FirstAncestorOrSelf(n => n.Kind == RazorSyntaxKind.MarkupTagHelperStartTag); + if (node is not MarkupTagHelperStartTagSyntax tagHelperStartTag) + { + binding = null; + return false; + } + + // Ensure the rename action was invoked on the component name instead of a component parameter. This serves as an issue + // mitigation till `textDocument/prepareRename` is supported and we can ensure renames aren't triggered in unsupported + // contexts. (https://github.com/dotnet/razor/issues/4285) + if (!tagHelperStartTag.Name.FullSpan.IntersectsWith(absoluteIndex)) + { + binding = null; + return false; + } + + if (tagHelperStartTag is { Parent: MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var startTagBindingResult } }) + { + binding = startTagBindingResult; + return true; + } + + binding = null; + return false; + } + private static TagHelperDescriptor? FindAssociatedTagHelper(TagHelperDescriptor tagHelper, ImmutableArray tagHelpers) { var typeName = tagHelper.GetTypeName(); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs index 3aa916de91..5526787422 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs @@ -265,6 +265,29 @@ public class RenameEndpointTest(ITestOutputHelper testOutput) : LanguageServerTe Assert.NotNull(result); } + [Fact] + public async Task Handle_Rename_OnComponentEndTag_ReturnsResult() + { + // Arrange + var (endpoint, documentContextFactory) = await CreateEndpointAndDocumentContextFactoryAsync(); + var uri = PathUtilities.GetUri(s_componentWithParamFilePath); + var request = new RenameParams + { + TextDocument = new() { Uri = uri }, + Position = VsLspFactory.CreatePosition(1, 36), + NewName = "Test2" + }; + + Assert.True(documentContextFactory.TryCreateForOpenDocument(uri, out var documentContext)); + var requestContext = CreateRazorRequestContext(documentContext); + + // Act + var result = await endpoint.HandleRequestAsync(request, requestContext, DisposalToken); + + // Assert + Assert.NotNull(result); + } + [Fact] public async Task Handle_Rename_OnComponentNameTrailingEdge_ReturnsResult() { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs index c273c9d63a..9328744f2a 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs @@ -112,62 +112,62 @@ public class CohostRenameEndpointTest(ITestOutputHelper testOutputHelper) : Coho """, renames: [("Component.razor", "DifferentName.razor")]); - [Theory(Skip = "https://github.com/dotnet/razor/issues/10717")] + [Theory] [InlineData("$$Component")] [InlineData("Com$$ponent")] [InlineData("Component$$")] public Task Component_EndTag(string endTag) - => VerifyRenamesAsync( - input: $""" - This is a Razor document. + => VerifyRenamesAsync( + input: $""" + This is a Razor document. - - -
- - +
- + +
+ + + +
-
- The end. - """, - additionalFiles: [ - // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file - (File("Component.cs"), """ - namespace SomeProject; + The end. + """, + additionalFiles: [ + // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file + (File("Component.cs"), """ + namespace SomeProject; - public class Component : Microsoft.AspNetCore.Components.ComponentBase - { - } - """), - // The above will make the component exist, but the .razor file needs to exist too for Uri presentation - (File("Component.razor"), "") - ], - newName: "DifferentName", - expected: """ - This is a Razor document. + public class Component : Microsoft.AspNetCore.Components.ComponentBase + { + } + """), + // The above will make the component exist, but the .razor file needs to exist too for Uri presentation + (File("Component.razor"), "") + ], + newName: "DifferentName", + expected: """ + This is a Razor document. - - -
- - +
+
+ + + +
-
- The end. - """, - renames: [("Component.razor", "DifferentName.razor")]); + The end. + """, + renames: [("Component.razor", "DifferentName.razor")]); [Fact] public Task Mvc()