Skip to content

Conversation

@davidwengier
Copy link
Member

Fixes #8541
Fixes #11493

Needs dotnet/roslyn#81549 before this will build, but Roslyn CI seems to be having issues, so putting this up while we wait.

This implements workspace/willRenameFiles support in our LSP server, so that when a .razor file is renamed, we perform a full rename of the component, including updating component tags and getting Roslyn changes for renaming the class name. In VS we also include a poly-fill since willRenameFiles is not supported, and an extra "apply rename" method to fill in the difference between real willRenameFiles support, where edits are made before file renames, and our polyfill which lets CPS handle the file rename, and applies the edits afterwards.

VS:
VSRenameRazorFile

VS Code:
VSCodeRazorFile

@davidwengier davidwengier requested a review from a team as a code owner December 8, 2025 01:15
}

// Make sure that the filename itself is actually changing (ie, we don't care about file moves)
if (newFileName == Path.GetFileNameWithoutExtension(oldDoc.FilePath))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (newFileName == Path.GetFileNameWithoutExtension(oldDoc.FilePath))

Is case-sensitivity a concern here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Case sensitivity is something that we need to follow up on across the board, right now tooling and compiler behave differently (#12202) but based on the comment linked from that issue, I think this is correct. ie, CouNter.razor and Counter.razor are technically different components according to the compiler, so we'd still want to rename if only case was changing.

""")
],
expectedFiles: [
(FileUri("OtherComponent.razor"), """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(FileUri("OtherComponent.razor"), """

I must be reading this wrong. I would have thought there would be a DifferentName.razor entry in the expected files.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectedFiles is just for validating the edits that we'll send to the IDE. The original component rename is not part of them, because it's already happened, we're just responding to it.

return;
}

ApplyWorkspaceEditAsync(request).Forget();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplyWorkspaceEditAsync(request).Forget();

Seems a little odd for nothing to be tracking this work. Seems like at least tests would want to be able to determine when it completes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where this gets called by CPS, if we await this task we end up with a JTF hang. Sadly there are no tests for this file, only for the things it calls that do the actual work.

{
if (edit.TryGetFirst(out var textDocumentEdit) &&
textDocumentEdit.TextDocument.DocumentUri is { UriString: { } uriString } documentUri &&
!fileSystem.FileExists(documentUri.GetRequiredParsedUri().GetDocumentFilePath()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these two conditions be inside the if block? I'm just curious if the file doesn't exist whether we still want the edit added to documentChanges.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. These edits are separate from the rename, and the only thing that produces them is us, so we're not dropping anyone elses data on the floor here. This is more for the "what if the file gets deleted/renamed again while we were in the request queue" case.

""")
],
expectedFiles: [
(FileUri("Component.razor"), """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Component

Feeling dumb, why isn't this DifferentName.razor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't feel dumb, the timeline of these things is all very confusing :)

This is testing willRename, which happens before the rename. So VS Code will rename Component.razor for us, but before it does we get to edit its contents.

var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(uriString);
var fileNamePartLength = fileNameWithoutExtension.Length + extension.Length;

Debug.Assert(uriString.Length >= fileNamePartLength);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug.Assert(uriString.Length >= fileNamePartLength);

I don't think this assert really adds much value.

var fromComponentName = Path.GetFileNameWithoutExtension(request.OldFilePath);
var toComponentName = Path.GetFileNameWithoutExtension(request.NewFilePath);

var dialogFactory = (IVsThreadedWaitDialogFactory)_serviceProvider.GetService(typeof(SVsThreadedWaitDialogFactory));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetService

Could this code use IAsyncServiceProvider.GetServiceAsync instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, I'm far from an expert at this. The comments on the service indicate its safe to get from any thread, so my assumption is that it wouldn't make a difference how the service is retrieved because it wouldn't need to thread switch. Am I misunderstanding?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what that service initialization looks like. I just thought that since the service was being obtained in an async context that it might as well use the async version to get it.

Copy link
Contributor

@ToddGrun ToddGrun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

var position = text.GetLinePosition(declaration.Identifier.SpanStart);

string newComponentName;
using (var _ = StringBuilderPool.GetPooledObject(out var builder))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using (var _ = StringBuilderPool.GetPooledObject(out var builder))

nit: removing the explicit scope will allow newComponentName to be declared with a var.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but then the string builder doesnt get returned to the pool until after the OOP call. I felt bad holding it for so long :)

@davidwengier davidwengier merged commit 60f53f1 into dotnet:main Jan 7, 2026
10 checks passed
@davidwengier davidwengier deleted the RazorFileRename branch January 7, 2026 05:58
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Jan 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto rename partial class in code-behind on rename Allow developers to properly rename razor components in the editor

3 participants