Skip to content

Commit a461c84

Browse files
authored
Rename Razor source generated documents in all scenarios, and map edits (#79604)
Part of dotnet/razor#9519 Half of the fix for dotnet/razor#12054, along with the Razor side at dotnet/razor#12055 Previously I allowed rename to work on any source generated document, but made it only opt in for when the rename request came from Razor. Now that we have a little more time for development, it's time to finish the feature off properly. This PR: * Rename will only consider source generated documents if they're from Razor * Any rename operation will automatically do so, without opt in * Before applying changes to the workspace, we call in to Razor to map edits * This matches the behaviour of non-cohosting, but that does edit mapping via a document service. Since with cohosting Razor doesn't control the document creation, there is no way to set up a document service, so there is just a workspace service that does the same thing. There will still need to be more PRs to make this work in VS Code, and to hook up the span mapping aspect of the service, but I didn't want this PR to get too big.
2 parents 3753829 + 33fde18 commit a461c84

30 files changed

+491
-131
lines changed

src/Compilers/Test/Core/SourceGeneration/TestGenerators.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,19 @@ public IncrementalAndSourceCallbackGenerator(Action<GeneratorInitializationConte
199199
public void Initialize(IncrementalGeneratorInitializationContext context) => _onInit(context);
200200
}
201201
}
202+
203+
namespace Microsoft.NET.Sdk.Razor.SourceGenerators
204+
{
205+
/// <summary>
206+
/// We check for the presence of the razor SG by full name
207+
/// so we have to make sure this is the right name in the right namespace.
208+
/// </summary>
209+
internal sealed class RazorSourceGenerator(Action<GeneratorExecutionContext> execute) : ISourceGenerator
210+
{
211+
private readonly Action<GeneratorExecutionContext> _execute = execute;
212+
213+
public void Initialize(GeneratorInitializationContext context) { }
214+
215+
public void Execute(GeneratorExecutionContext context) => _execute(context);
216+
}
217+
}

src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameLocationSet.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,50 @@
77
using System.Linq;
88
using System.Threading;
99
using System.Threading.Tasks;
10+
using Microsoft.CodeAnalysis.Host;
1011
using Microsoft.CodeAnalysis.Rename;
1112
using Microsoft.CodeAnalysis.Shared.Extensions;
13+
using Roslyn.Utilities;
1214

1315
namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
1416

1517
internal abstract partial class AbstractEditorInlineRenameService
1618
{
1719
private sealed class InlineRenameLocationSet : IInlineRenameLocationSet
1820
{
21+
public static async Task<InlineRenameLocationSet> CreateAsync(
22+
SymbolInlineRenameInfo renameInfo,
23+
LightweightRenameLocations renameLocationSet,
24+
CancellationToken cancellationToken)
25+
{
26+
var solution = renameLocationSet.Solution;
27+
var validLocations = renameLocationSet.Locations.Where(RenameLocation.ShouldRename);
28+
var locations = await validLocations.SelectAsArrayAsync(static (loc, solution, ct) => ConvertLocationAsync(solution, loc, ct), solution, cancellationToken).ConfigureAwait(false);
29+
30+
return new InlineRenameLocationSet(renameInfo, renameLocationSet, locations);
31+
}
32+
1933
private readonly LightweightRenameLocations _renameLocationSet;
2034
private readonly SymbolInlineRenameInfo _renameInfo;
2135

2236
public IList<InlineRenameLocation> Locations { get; }
2337

24-
public InlineRenameLocationSet(
38+
private InlineRenameLocationSet(
2539
SymbolInlineRenameInfo renameInfo,
26-
LightweightRenameLocations renameLocationSet)
40+
LightweightRenameLocations renameLocationSet,
41+
ImmutableArray<InlineRenameLocation> locations)
2742
{
2843
_renameInfo = renameInfo;
2944
_renameLocationSet = renameLocationSet;
30-
this.Locations = renameLocationSet.Locations.Where(RenameLocation.ShouldRename)
31-
.Select(ConvertLocation)
32-
.ToImmutableArray();
45+
this.Locations = locations;
3346
}
3447

35-
private InlineRenameLocation ConvertLocation(RenameLocation location)
48+
private static async ValueTask<InlineRenameLocation> ConvertLocationAsync(Solution solution, RenameLocation location, CancellationToken cancellationToken)
3649
{
37-
return new InlineRenameLocation(
38-
_renameLocationSet.Solution.GetRequiredDocument(location.DocumentId), location.Location.SourceSpan);
50+
var document = await solution.GetRequiredDocumentAsync(location.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
51+
52+
Contract.ThrowIfTrue(location.DocumentId.IsSourceGenerated && !document.IsRazorSourceGeneratedDocument());
53+
return new InlineRenameLocation(document, location.Location.SourceSpan);
3954
}
4055

4156
public async Task<IInlineRenameReplacementInfo> GetReplacementsAsync(

src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.SymbolRenameInfo.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
using Microsoft.CodeAnalysis.Shared.Extensions;
1515
using Microsoft.CodeAnalysis.Text;
1616
using Microsoft.CodeAnalysis.Utilities;
17-
using Roslyn.Utilities;
1817

1918
namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
2019

@@ -135,7 +134,7 @@ public async Task<IInlineRenameLocationSet> FindRenameLocationsAsync(SymbolRenam
135134
var locations = await Renamer.FindRenameLocationsAsync(
136135
solution, this.RenameSymbol, options, cancellationToken).ConfigureAwait(false);
137136

138-
return new InlineRenameLocationSet(this, locations);
137+
return await InlineRenameLocationSet.CreateAsync(this, locations, cancellationToken).ConfigureAwait(false);
139138
}
140139

141140
public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable<DocumentId> changedDocumentIDs, string replacementText)

src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ protected AbstractEditorInlineRenameService(IEnumerable<IRefactorNotifyService>
2424

2525
public async Task<IInlineRenameInfo> GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken)
2626
{
27-
var symbolicInfo = await SymbolicRenameInfo.GetRenameInfoAsync(document, position, includeSourceGenerated: false, cancellationToken).ConfigureAwait(false);
27+
var symbolicInfo = await SymbolicRenameInfo.GetRenameInfoAsync(document, position, cancellationToken).ConfigureAwait(false);
2828
if (symbolicInfo.LocalizedErrorMessage != null)
2929
return new FailureInlineRenameInfo(symbolicInfo.LocalizedErrorMessage);
3030

src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,7 @@ private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, b
859859
async Task<ImmutableArray<(DocumentId documentId, string newName, SyntaxNode newRoot, SourceText newText)>> CalculateFinalDocumentChangesAsync(
860860
Solution newSolution, CancellationToken cancellationToken)
861861
{
862-
var changes = _baseSolution.GetChanges(newSolution);
862+
var changes = newSolution.GetChanges(_baseSolution);
863863
var changedDocumentIDs = changes.GetProjectChanges().SelectManyAsArray(c => c.GetChangedDocuments());
864864

865865
using var _ = PooledObjects.ArrayBuilder<(DocumentId documentId, string newName, SyntaxNode newRoot, SourceText newText)>.GetInstance(out var result);
@@ -877,6 +877,12 @@ private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, b
877877
: (documentId, newDocument.Name, newRoot: null, await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false)));
878878
}
879879

880+
foreach (var documentId in changes.GetExplicitlyChangedSourceGeneratedDocuments())
881+
{
882+
var newDocument = newSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(documentId);
883+
result.Add((documentId, newDocument.Name, newRoot: null, await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false)));
884+
}
885+
880886
return result.ToImmutableAndClear();
881887
}
882888
}
@@ -892,16 +898,7 @@ private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, b
892898
if (!RenameInfo.TryOnBeforeGlobalSymbolRenamed(Workspace, documentChanges.SelectAsArray(t => t.documentId), this.ReplacementText))
893899
return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid);
894900

895-
// Grab the workspace's current solution, and make the document changes to it we computed.
896-
var finalSolution = Workspace.CurrentSolution;
897-
foreach (var (documentId, newName, newRoot, newText) in documentChanges)
898-
{
899-
finalSolution = newRoot != null
900-
? finalSolution.WithDocumentSyntaxRoot(documentId, newRoot)
901-
: finalSolution.WithDocumentText(documentId, newText);
902-
903-
finalSolution = finalSolution.WithDocumentName(documentId, newName);
904-
}
901+
var finalSolution = _threadingContext.JoinableTaskFactory.Run(() => GetFinalSolutionAsync(documentChanges));
905902

906903
// Now actually go and apply the changes to the workspace. We expect this to succeed as we're on the UI
907904
// thread, and nothing else should have been able to make a change to workspace since we we grabbed its
@@ -934,6 +931,35 @@ private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, b
934931
}
935932
}
936933

934+
private async Task<Solution> GetFinalSolutionAsync(ImmutableArray<(DocumentId, string, SyntaxNode, SourceText)> documentChanges)
935+
{
936+
// Grab the workspace's current solution, and make the document changes to it we computed.
937+
var finalSolution = Workspace.CurrentSolution;
938+
foreach (var (documentId, newName, newRoot, newText) in documentChanges)
939+
{
940+
if (documentId.IsSourceGenerated)
941+
{
942+
var document = await finalSolution.GetDocumentAsync(documentId, includeSourceGenerated: true, CancellationToken.None).ConfigureAwait(true);
943+
Contract.ThrowIfFalse(document.IsRazorSourceGeneratedDocument());
944+
}
945+
946+
finalSolution = newRoot != null
947+
? finalSolution.WithDocumentSyntaxRoot(documentId, newRoot)
948+
: finalSolution.WithDocumentText(documentId, newText);
949+
950+
// WithDocumentName doesn't support source generated documents, and there should be no circumstance were they'd
951+
// be renamed anyway. Given rename only supports Razor generated documents, and those are always named for the Razor
952+
// file they come from, and nothing in Roslyn knows to rename those documents, we can safely skip this step. Razor
953+
// may process the results of this rename and rename the document if it needs to later.
954+
if (!documentId.IsSourceGenerated)
955+
{
956+
finalSolution = finalSolution.WithDocumentName(documentId, newName);
957+
}
958+
}
959+
960+
return finalSolution;
961+
}
962+
937963
internal bool TryGetContainingEditableSpan(SnapshotPoint point, out SnapshotSpan editableSpan)
938964
{
939965
editableSpan = default;

src/EditorFeatures/Test2/Rename/CSharp/SourceGeneratorTests.vb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
' The .NET Foundation licenses this file to you under the MIT license.
33
' See the LICENSE file in the project root for more information.
44

5+
Imports System.Threading
6+
Imports Microsoft.CodeAnalysis.Rename.ConflictEngine
7+
Imports Microsoft.CodeAnalysis.Testing
8+
Imports Microsoft.CodeAnalysis.Text
9+
510
Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename.CSharp
611
<[UseExportProvider]>
712
<Trait(Traits.Feature, Traits.Features.Rename)>
@@ -56,6 +61,39 @@ public class GeneratedClass
5661
End Using
5762
End Sub
5863

64+
<Theory, CombinatorialData>
65+
Public Async Function RenameWithReferenceInRazorGeneratedFile(host As RenameTestHost) As Task
66+
Dim generatedMarkup = "
67+
public class GeneratedClass
68+
{
69+
public void M([|RegularClass|] c) { }
70+
}
71+
"
72+
Dim generatedCode As String = ""
73+
Dim span As TextSpan
74+
TestFileMarkupParser.GetSpan(generatedMarkup, generatedCode, span)
75+
Dim sourceGenerator = New Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator(Sub(c) c.AddSource("generated_file.cs", generatedCode))
76+
Using result = RenameEngineResult.Create(_outputHelper,
77+
<Workspace>
78+
<Project Language="C#" AssemblyName="ClassLibrary1" CommonReferences="true">
79+
<Document>
80+
public class [|$$RegularClass|]
81+
{
82+
}
83+
</Document>
84+
</Project>
85+
</Workspace>, host:=host, sourceGenerator:=sourceGenerator, renameTo:="A")
86+
87+
Dim project = result.ConflictResolution.OldSolution.Projects.Single()
88+
Dim generatedDocuments = Await project.GetSourceGeneratedDocumentsAsync(CancellationToken.None)
89+
Dim generatedTree = Await generatedDocuments.Single().GetSyntaxTreeAsync(CancellationToken.None)
90+
Dim location = CodeAnalysis.Location.Create(generatedTree, span)
91+
'Manually assert the generated location, because the test workspace doesn't know about it
92+
result.AssertLocationReferencedAs(location, RelatedLocationType.NoConflict)
93+
94+
End Using
95+
End Function
96+
5997
<Theory, CombinatorialData>
6098
<WorkItem("https://github.com/dotnet/roslyn/issues/51537")>
6199
Public Sub RenameWithCascadeIntoGeneratedFile(host As RenameTestHost)

src/EditorFeatures/Test2/Rename/InlineRenameTests.vb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2379,6 +2379,53 @@ class [|C|]
23792379
Await session.CommitAsync(previewChanges:=False, editorOperationContext:=Nothing)
23802380

23812381
Await VerifyTagsAreCorrect(workspace)
2382+
2383+
Await VerifyChangedSourceGeneratedDocumentFilenames(workspace)
2384+
End Using
2385+
End Function
2386+
2387+
<WpfTheory>
2388+
<CombinatorialData, Trait(Traits.Feature, Traits.Features.Rename)>
2389+
Public Async Function RenameWithRazorGeneratedFile(host As RenameTestHost) As Task
2390+
Dim generatedCode = "
2391+
public class GeneratedClass
2392+
{
2393+
public void M(MyClass c) { }
2394+
}
2395+
"
2396+
Dim razorGenerator = New Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator(Sub(c) c.AddSource("generated_file.cs", generatedCode))
2397+
2398+
Using workspace = CreateWorkspaceWithWaiter(
2399+
<Workspace>
2400+
<Project Language="C#" CommonReferences="true" LanguageVersion="preview">
2401+
<Document>
2402+
partial class [|$$MyClass|]
2403+
{
2404+
public void M1()
2405+
{
2406+
}
2407+
}
2408+
</Document>
2409+
</Project>
2410+
</Workspace>, host)
2411+
2412+
Dim project = workspace.CurrentSolution.Projects.First().AddAnalyzerReference(New TestGeneratorReference(razorGenerator))
2413+
workspace.TryApplyChanges(project.Solution)
2414+
2415+
Dim session = StartSession(workspace)
2416+
2417+
' Type a bit in the file
2418+
Dim cursorDocument = workspace.Documents.Single(Function(d) d.CursorPosition.HasValue)
2419+
Dim caretPosition = cursorDocument.CursorPosition.Value
2420+
Dim textBuffer = cursorDocument.GetTextBuffer()
2421+
2422+
textBuffer.Insert(caretPosition, "Example")
2423+
2424+
Await session.CommitAsync(previewChanges:=False, editorOperationContext:=Nothing)
2425+
2426+
Await VerifyTagsAreCorrect(workspace)
2427+
2428+
Await VerifyChangedSourceGeneratedDocumentFilenames(workspace, "generated_file.cs")
23822429
End Using
23832430
End Function
23842431

src/EditorFeatures/Test2/Rename/RenameEngineResult.vb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename
219219
Return locations
220220
End Function
221221

222-
Private Sub AssertLocationReplacedWith(location As Location, replacementText As String, Optional isRenameWithinStringOrComment As Boolean = False)
222+
Public Sub AssertLocationReplacedWith(location As Location, replacementText As String, Optional isRenameWithinStringOrComment As Boolean = False)
223223
Try
224224
Dim documentId = ConflictResolution.OldSolution.GetDocumentId(location.SourceTree)
225225
Dim newLocation = ConflictResolution.GetResolutionTextSpan(location.SourceSpan, documentId)
@@ -242,7 +242,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename
242242
End Try
243243
End Sub
244244

245-
Private Sub AssertLocationReferencedAs(location As Location, type As RelatedLocationType)
245+
Public Sub AssertLocationReferencedAs(location As Location, type As RelatedLocationType)
246246
Try
247247
Dim documentId = ConflictResolution.OldSolution.GetDocumentId(location.SourceTree)
248248
Dim reference = _unassertedRelatedLocations.SingleOrDefault(

src/EditorFeatures/Test2/Rename/RenameTestHelpers.vb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename
8181
Next
8282
End Function
8383

84+
Public Async Function VerifyChangedSourceGeneratedDocumentFileNames(workspace As EditorTestWorkspace, ParamArray mappedFiles As String()) As Task
85+
Await WaitForRename(workspace)
86+
87+
Assert.Equal(workspace.ChangedSourceGeneratedDocumentFileNames, mappedFiles)
88+
End Function
89+
8490
Public Sub VerifyFileName(document As Document, newIdentifierName As String)
8591
Dim expectedName = Path.ChangeExtension(newIdentifierName, Path.GetExtension(document.Name))
8692
Assert.Equal(expectedName, document.Name)

src/EditorFeatures/TestUtilities/Workspaces/EditorTestWorkspace.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
8+
using System.IO;
89
using System.Linq;
910
using System.Xml.Linq;
1011
using Microsoft.CodeAnalysis.CSharp.DecompiledSource;
@@ -15,6 +16,7 @@
1516
using Microsoft.CodeAnalysis.Editor.UnitTests;
1617
using Microsoft.CodeAnalysis.Formatting;
1718
using Microsoft.CodeAnalysis.Host;
19+
using Microsoft.CodeAnalysis.Shared.Extensions;
1820
using Microsoft.CodeAnalysis.Text;
1921
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
2022
using Microsoft.VisualStudio.Composition;
@@ -25,13 +27,16 @@
2527
using Roslyn.Test.EditorUtilities;
2628
using Roslyn.Test.Utilities;
2729
using Roslyn.Utilities;
30+
using Xunit;
2831

2932
namespace Microsoft.CodeAnalysis.Test.Utilities;
3033

3134
public sealed partial class EditorTestWorkspace : TestWorkspace<EditorTestHostDocument, EditorTestHostProject, EditorTestHostSolution>
3235
{
3336
private const string ReferencesOnDiskAttributeName = "ReferencesOnDisk";
3437

38+
public List<string> ChangedSourceGeneratedDocumentFileNames { get; } = [];
39+
3540
private readonly Dictionary<string, ITextBuffer2> _createdTextBuffers = [];
3641

3742
internal EditorTestWorkspace(
@@ -521,4 +526,14 @@ protected override (MetadataReference reference, ImmutableArray<byte> peImage) C
521526

522527
return (reference, image);
523528
}
529+
530+
internal override void ApplyMappedFileChanges(SolutionChanges solutionChanges)
531+
{
532+
foreach (var (docId, _) in solutionChanges.NewSolution.CompilationState.FrozenSourceGeneratedDocumentStates.States)
533+
{
534+
var document = solutionChanges.NewSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(docId);
535+
Assert.NotNull(document.FilePath);
536+
ChangedSourceGeneratedDocumentFileNames.Add(Path.GetFileName(document.FilePath));
537+
}
538+
}
524539
}

0 commit comments

Comments
 (0)