Skip to content

Commit d62ba62

Browse files
authored
More rename (#12480)
Part of #8541 Started poking at rename issues, and decided this was a big enough PR to put out as is. I think this is everything up until we need some Roslyn changes to help with more. Reviewing commit at a time is probably easiest, as this does a few things: * Adds a bunch of tests for missed scenarios * Improves test infra and validation * Fixes a bug when renaming a component to a name that clashes with another component * Adds support for renaming .razor.cs and .razor.css files when we rename a .razor file
2 parents a394895 + f0c66e7 commit d62ba62

File tree

12 files changed

+988
-161
lines changed

12 files changed

+988
-161
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,16 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V
4848

4949
protected override string CustomMessageTarget => CustomMessageNames.RazorRenameEndpointName;
5050

51-
protected override Task<WorkspaceEdit?> TryHandleAsync(RenameParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
51+
protected override async Task<WorkspaceEdit?> TryHandleAsync(RenameParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
5252
{
5353
var documentContext = requestContext.DocumentContext;
5454
if (documentContext is null)
5555
{
56-
return SpecializedTasks.Null<WorkspaceEdit>();
56+
return null;
5757
}
5858

59-
return _renameService.TryGetRazorRenameEditsAsync(documentContext, positionInfo, request.NewName, _projectManager.GetQueryOperations(), cancellationToken);
59+
var result = await _renameService.TryGetRazorRenameEditsAsync(documentContext, positionInfo, request.NewName, _projectManager.GetQueryOperations(), cancellationToken).ConfigureAwait(false);
60+
return result.Edit;
6061
}
6162

6263
protected override bool IsSupported()

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ namespace Microsoft.CodeAnalysis.Razor.Rename;
1010

1111
internal interface IRenameService
1212
{
13-
Task<WorkspaceEdit?> TryGetRazorRenameEditsAsync(
13+
Task<RenameResult> TryGetRazorRenameEditsAsync(
1414
DocumentContext documentContext,
1515
DocumentPositionInfo positionInfo,
1616
string newName,
1717
ISolutionQueryOperations solutionQueryOperations,
1818
CancellationToken cancellationToken);
1919
}
20+
21+
internal readonly record struct RenameResult(WorkspaceEdit? Edit, bool FallbackToCSharp = true);

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ namespace Microsoft.CodeAnalysis.Razor.Rename;
2424

2525
internal class RenameService(
2626
IRazorComponentSearchEngine componentSearchEngine,
27+
IFileSystem fileSystem,
2728
LanguageServerFeatureOptions languageServerFeatureOptions) : IRenameService
2829
{
2930
private readonly IRazorComponentSearchEngine _componentSearchEngine = componentSearchEngine;
31+
private readonly IFileSystem _fileSystem = fileSystem;
3032
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;
3133

32-
public async Task<WorkspaceEdit?> TryGetRazorRenameEditsAsync(
34+
public async Task<RenameResult> TryGetRazorRenameEditsAsync(
3335
DocumentContext documentContext,
3436
DocumentPositionInfo positionInfo,
3537
string newName,
@@ -39,36 +41,39 @@ internal class RenameService(
3941
// We only support renaming of .razor components, not .cshtml tag helpers
4042
if (!documentContext.FileKind.IsComponent())
4143
{
42-
return null;
44+
return new(Edit: null);
4345
}
4446

4547
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
4648

4749
var originTagHelpers = await GetOriginTagHelpersAsync(documentContext, positionInfo.HostDocumentIndex, cancellationToken).ConfigureAwait(false);
4850
if (originTagHelpers.IsDefaultOrEmpty)
4951
{
50-
return null;
52+
return new(Edit: null);
5153
}
5254

5355
var originComponentDocumentSnapshot = await _componentSearchEngine
5456
.TryLocateComponentAsync(originTagHelpers.First(), solutionQueryOperations, cancellationToken)
5557
.ConfigureAwait(false);
5658
if (originComponentDocumentSnapshot is null)
5759
{
58-
return null;
60+
return new(Edit: null);
5961
}
6062

6163
var originComponentDocumentFilePath = originComponentDocumentSnapshot.FilePath;
6264
var newPath = MakeNewPath(originComponentDocumentFilePath, newName);
63-
if (File.Exists(newPath))
65+
if (_fileSystem.FileExists(newPath))
6466
{
65-
return null;
67+
// We found a tag, but the new name would cause a conflict, so we can't proceed with the rename,
68+
// even if C# might have worked.
69+
return new(Edit: null, FallbackToCSharp: false);
6670
}
6771

6872
using var _ = ListPool<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>.GetPooledObject(out var documentChanges);
69-
var fileRename = GetFileRenameForComponent(originComponentDocumentSnapshot, newPath);
73+
var fileRename = GetRenameFileEdit(originComponentDocumentFilePath, newPath);
7074
documentChanges.Add(fileRename);
7175
AddEditsForCodeDocument(documentChanges, originTagHelpers, newName, new(documentContext.Uri), codeDocument);
76+
AddAdditionalFileRenames(documentChanges, originComponentDocumentFilePath, newPath);
7277

7378
var documentSnapshots = GetAllDocumentSnapshots(documentContext.FilePath, solutionQueryOperations);
7479

@@ -86,10 +91,10 @@ internal class RenameService(
8691
}
8792
}
8893

89-
return new WorkspaceEdit
94+
return new(new WorkspaceEdit
9095
{
9196
DocumentChanges = documentChanges.ToArray(),
92-
};
97+
});
9398
}
9499

95100
private static ImmutableArray<IDocumentSnapshot> GetAllDocumentSnapshots(string filePath, ISolutionQueryOperations solutionQueryOperations)
@@ -126,11 +131,26 @@ private static ImmutableArray<IDocumentSnapshot> GetAllDocumentSnapshots(string
126131
return documentSnapshots.ToImmutableAndClear();
127132
}
128133

129-
private RenameFile GetFileRenameForComponent(IDocumentSnapshot documentSnapshot, string newPath)
134+
private void AddAdditionalFileRenames(List<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>> documentChanges, string oldFilePath, string newFilePath)
135+
{
136+
TryAdd(".cs");
137+
TryAdd(".css");
138+
139+
void TryAdd(string extension)
140+
{
141+
var changedPath = oldFilePath + extension;
142+
if (_fileSystem.FileExists(changedPath))
143+
{
144+
documentChanges.Add(GetRenameFileEdit(changedPath, newFilePath + extension));
145+
}
146+
}
147+
}
148+
149+
private RenameFile GetRenameFileEdit(string oldFilePath, string newFilePath)
130150
=> new RenameFile
131151
{
132-
OldDocumentUri = new(LspFactory.CreateFilePathUri(documentSnapshot.FilePath, _languageServerFeatureOptions)),
133-
NewDocumentUri = new(LspFactory.CreateFilePathUri(newPath, _languageServerFeatureOptions)),
152+
OldDocumentUri = new(LspFactory.CreateFilePathUri(oldFilePath, _languageServerFeatureOptions)),
153+
NewDocumentUri = new(LspFactory.CreateFilePathUri(newFilePath, _languageServerFeatureOptions)),
134154
};
135155

136156
private static string MakeNewPath(string originalPath, string newName)

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/OOPRenameService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.Rename;
1111
[method: ImportingConstructor]
1212
internal sealed class OOPRenameService(
1313
IRazorComponentSearchEngine componentSearchEngine,
14+
IFileSystem fileSystem,
1415
LanguageServerFeatureOptions languageServerFeatureOptions)
15-
: RenameService(componentSearchEngine, languageServerFeatureOptions)
16+
: RenameService(componentSearchEngine, fileSystem, languageServerFeatureOptions)
1617
{
1718
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,21 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args)
5858
.TryGetRazorRenameEditsAsync(context, positionInfo, newName, context.GetSolutionQueryOperations(), cancellationToken)
5959
.ConfigureAwait(false);
6060

61-
if (razorEdit is not null)
61+
if (razorEdit.Edit is { } edit)
6262
{
63-
return Results(razorEdit);
63+
return Results(edit);
6464
}
6565

6666
if (positionInfo.LanguageKind != CodeAnalysis.Razor.Protocol.RazorLanguageKind.CSharp)
6767
{
6868
return CallHtml;
6969
}
7070

71+
if (!razorEdit.FallbackToCSharp)
72+
{
73+
return NoFurtherHandling;
74+
}
75+
7176
var csharpEdit = await ExternalHandlers.Rename
7277
.GetRenameEditAsync(generatedDocument, positionInfo.Position.ToLinePosition(), newName, cancellationToken)
7378
.ConfigureAwait(false);

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ await projectManager.UpdateAsync(updater =>
6464

6565
var searchEngine = new RazorComponentSearchEngine(LoggerFactory);
6666

67-
var renameService = new RenameService(searchEngine, LanguageServerFeatureOptions);
67+
var renameService = new RenameService(searchEngine, new FileSystem(), LanguageServerFeatureOptions);
6868

6969
var endpoint = new RenameEndpoint(
7070
renameService,

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ await projectManager.UpdateAsync(updater =>
710710

711711
clientConnection ??= StrictMock.Of<IClientConnection>();
712712

713-
var renameService = new RenameService(searchEngine, options);
713+
var renameService = new RenameService(searchEngine, new FileSystem(), options);
714714
var endpoint = new RenameEndpoint(
715715
renameService,
716716
options,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Text;
9+
using Microsoft.AspNetCore.Razor;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.Razor.Utilities;
12+
using Microsoft.CodeAnalysis.Razor.Workspaces;
13+
14+
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
15+
16+
internal sealed class TestFileSystem((string filePath, string contents)[]? files) : IFileSystem
17+
{
18+
public bool FileExists(string filePath)
19+
=> files?.Any(f => FilePathNormalizingComparer.Instance.Equals(f.filePath, filePath)) ?? false;
20+
21+
public string ReadFile(string filePath)
22+
=> files.AssumeNotNull().Single(f => FilePathNormalizingComparer.Instance.Equals(f.filePath, filePath)).contents;
23+
24+
public Stream OpenReadStream(string filePath)
25+
=> new MemoryStream(Encoding.UTF8.GetBytes(ReadFile(filePath)));
26+
27+
public IEnumerable<string> GetDirectories(string workspaceDirectory)
28+
=> throw new NotImplementedException();
29+
30+
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
31+
=> throw new NotImplementedException();
32+
}

src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/AssertExtensions.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
12+
using Microsoft.CodeAnalysis.Razor;
13+
using Roslyn.Test.Utilities;
414
using Roslyn.Text.Adornments;
515
using Xunit;
616

@@ -18,4 +28,83 @@ internal static void AssertExpectedClassification(
1828
Assert.Equal(expectedClassificationType, run.ClassificationTypeName);
1929
Assert.Equal(expectedClassificationStyle, run.Style);
2030
}
31+
32+
public static async Task AssertWorkspaceEditAsync(this WorkspaceEdit workspaceEdit, Solution solution, IEnumerable<(Uri fileUri, string contents)> expectedChanges, CancellationToken cancellationToken)
33+
{
34+
var changes = Assert.NotNull(workspaceEdit.DocumentChanges);
35+
36+
foreach (var change in Flatten(changes))
37+
{
38+
if (change.TryGetFirst(out var textDocumentEdit))
39+
{
40+
var uri = textDocumentEdit.TextDocument.DocumentUri.GetRequiredParsedUri();
41+
var documentId = solution.GetDocumentIdsWithFilePath(RazorUri.GetDocumentFilePathFromUri(uri)).Single();
42+
var document = solution.GetDocument(documentId) ?? solution.GetAdditionalDocument(documentId);
43+
Assert.NotNull(document);
44+
var text = await document.GetTextAsync(cancellationToken);
45+
46+
text = text.WithChanges(textDocumentEdit.Edits.Select(e => text.GetTextChange((TextEdit)e)));
47+
48+
solution = document is Document
49+
? solution.WithDocumentText(document.Id, text)
50+
: solution.WithAdditionalDocumentText(document.Id, text);
51+
}
52+
else if (change.TryGetSecond(out var createFile))
53+
{
54+
var uri = createFile.DocumentUri.GetRequiredParsedUri();
55+
var documentId = DocumentId.CreateNewId(solution.ProjectIds.Single());
56+
var filePath = createFile.DocumentUri.GetRequiredParsedUri().GetDocumentFilePath();
57+
var documentInfo = DocumentInfo.Create(documentId, Path.GetFileName(filePath), filePath: filePath);
58+
solution = solution.AddDocument(documentInfo);
59+
}
60+
else if (change.TryGetThird(out var renameFile))
61+
{
62+
var (oldUri, newUri) = (renameFile.OldDocumentUri.GetRequiredParsedUri(), renameFile.NewDocumentUri.GetRequiredParsedUri());
63+
var documentId = solution.GetDocumentIdsWithFilePath(RazorUri.GetDocumentFilePathFromUri(oldUri)).Single();
64+
var document = solution.GetDocument(documentId) ?? solution.GetAdditionalDocument(documentId);
65+
Assert.NotNull(document);
66+
if (document is Document)
67+
{
68+
solution = solution.WithDocumentFilePath(document.Id, newUri.GetDocumentFilePath());
69+
}
70+
else
71+
{
72+
var filePath = newUri.GetDocumentFilePath();
73+
var text = await document.GetTextAsync(cancellationToken);
74+
solution = document.Project
75+
.RemoveAdditionalDocument(document.Id)
76+
.AddAdditionalDocument(Path.GetFileName(filePath), text, filePath: filePath).Project.Solution;
77+
}
78+
}
79+
else
80+
{
81+
Assert.Fail($"Don't know how to process a {change.Value?.GetType().Name}.");
82+
}
83+
}
84+
85+
foreach (var (uri, contents) in expectedChanges)
86+
{
87+
var document = solution.GetTextDocuments(uri).First();
88+
var text = await document.GetTextAsync(cancellationToken);
89+
AssertEx.EqualOrDiff(contents, text.ToString());
90+
}
91+
92+
static IEnumerable<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>> Flatten(SumType<TextDocumentEdit[], SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[]> documentChanges)
93+
{
94+
if (documentChanges.TryGetFirst(out var textDocumentEdits))
95+
{
96+
foreach (var edit in textDocumentEdits)
97+
{
98+
yield return edit;
99+
}
100+
}
101+
else if (documentChanges.TryGetSecond(out var changes))
102+
{
103+
foreach (var change in changes)
104+
{
105+
yield return change;
106+
}
107+
}
108+
}
109+
}
21110
}

0 commit comments

Comments
 (0)