Skip to content
16 changes: 16 additions & 0 deletions src/Compilers/Test/Core/SourceGeneration/TestGenerators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,19 @@ public IncrementalAndSourceCallbackGenerator(Action<GeneratorInitializationConte
public void Initialize(IncrementalGeneratorInitializationContext context) => _onInit(context);
}
}

namespace Microsoft.NET.Sdk.Razor.SourceGenerators
{
/// <summary>
/// We check for the presence of the razor SG by full name
/// so we have to make sure this is the right name in the right namespace.
/// </summary>
internal sealed class RazorSourceGenerator(Action<GeneratorExecutionContext> execute) : ISourceGenerator
Copy link
Member Author

Choose a reason for hiding this comment

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

Copied this from #79510

{
private readonly Action<GeneratorExecutionContext> _execute = execute;

public void Initialize(GeneratorInitializationContext context) { }

public void Execute(GeneratorExecutionContext context) => _execute(context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,50 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;

internal abstract partial class AbstractEditorInlineRenameService
{
private sealed class InlineRenameLocationSet : IInlineRenameLocationSet
{
public static async Task<InlineRenameLocationSet> CreateAsync(
SymbolInlineRenameInfo renameInfo,
LightweightRenameLocations renameLocationSet,
CancellationToken cancellationToken)
{
var solution = renameLocationSet.Solution;
var validLocations = renameLocationSet.Locations.Where(RenameLocation.ShouldRename);
var locations = await validLocations.SelectAsArrayAsync(static (loc, solution, ct) => ConvertLocationAsync(solution, loc, ct), solution, cancellationToken).ConfigureAwait(false);

return new InlineRenameLocationSet(renameInfo, renameLocationSet, locations);
}

private readonly LightweightRenameLocations _renameLocationSet;
private readonly SymbolInlineRenameInfo _renameInfo;

public IList<InlineRenameLocation> Locations { get; }

public InlineRenameLocationSet(
private InlineRenameLocationSet(
SymbolInlineRenameInfo renameInfo,
LightweightRenameLocations renameLocationSet)
LightweightRenameLocations renameLocationSet,
ImmutableArray<InlineRenameLocation> locations)
{
_renameInfo = renameInfo;
_renameLocationSet = renameLocationSet;
this.Locations = renameLocationSet.Locations.Where(RenameLocation.ShouldRename)
.Select(ConvertLocation)
.ToImmutableArray();
this.Locations = locations;
}

private InlineRenameLocation ConvertLocation(RenameLocation location)
private static async ValueTask<InlineRenameLocation> ConvertLocationAsync(Solution solution, RenameLocation location, CancellationToken cancellationToken)
{
return new InlineRenameLocation(
_renameLocationSet.Solution.GetRequiredDocument(location.DocumentId), location.Location.SourceSpan);
var document = await solution.GetRequiredDocumentAsync(location.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);

Contract.ThrowIfTrue(location.DocumentId.IsSourceGenerated && !document.IsRazorSourceGeneratedDocument());
Copy link
Member Author

Choose a reason for hiding this comment

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

It's a little odd that these contracts come after the possibly-expensive method, but sadly thats the way it has to be. They do prove that at least if the GetRequiredDocumentAsync had to go async, then it was for Razor which is what we want anyway.

return new InlineRenameLocation(document, location.Location.SourceSpan);
}

public async Task<IInlineRenameReplacementInfo> GetReplacementsAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;

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

return new InlineRenameLocationSet(this, locations);
return await InlineRenameLocationSet.CreateAsync(this, locations, cancellationToken).ConfigureAwait(false);
}

public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable<DocumentId> changedDocumentIDs, string replacementText)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ protected AbstractEditorInlineRenameService(IEnumerable<IRefactorNotifyService>

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

Expand Down
48 changes: 37 additions & 11 deletions src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, b
async Task<ImmutableArray<(DocumentId documentId, string newName, SyntaxNode newRoot, SourceText newText)>> CalculateFinalDocumentChangesAsync(
Solution newSolution, CancellationToken cancellationToken)
{
var changes = _baseSolution.GetChanges(newSolution);
var changes = newSolution.GetChanges(_baseSolution);
Copy link
Member Author

Choose a reason for hiding this comment

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

These parameters were backwards, and it was a huge pain to find! For regular documents it turns out not to matter.

var changedDocumentIDs = changes.GetProjectChanges().SelectManyAsArray(c => c.GetChangedDocuments());

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

foreach (var documentId in changes.GetExplicitlyChangedSourceGeneratedDocuments())
{
var newDocument = newSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(documentId);
result.Add((documentId, newDocument.Name, newRoot: null, await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false)));
}

return result.ToImmutableAndClear();
}
}
Expand All @@ -892,16 +898,7 @@ private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, b
if (!RenameInfo.TryOnBeforeGlobalSymbolRenamed(Workspace, documentChanges.SelectAsArray(t => t.documentId), this.ReplacementText))
return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid);

// Grab the workspace's current solution, and make the document changes to it we computed.
var finalSolution = Workspace.CurrentSolution;
foreach (var (documentId, newName, newRoot, newText) in documentChanges)
{
finalSolution = newRoot != null
? finalSolution.WithDocumentSyntaxRoot(documentId, newRoot)
: finalSolution.WithDocumentText(documentId, newText);

finalSolution = finalSolution.WithDocumentName(documentId, newName);
}
var finalSolution = _threadingContext.JoinableTaskFactory.Run(() => GetFinalSolutionAsync(documentChanges));

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

private async Task<Solution> GetFinalSolutionAsync(ImmutableArray<(DocumentId, string, SyntaxNode, SourceText)> documentChanges)
{
// Grab the workspace's current solution, and make the document changes to it we computed.
var finalSolution = Workspace.CurrentSolution;
foreach (var (documentId, newName, newRoot, newText) in documentChanges)
{
if (documentId.IsSourceGenerated)
{
var document = await finalSolution.GetDocumentAsync(documentId, includeSourceGenerated: true, CancellationToken.None).ConfigureAwait(true);
Contract.ThrowIfFalse(document.IsRazorSourceGeneratedDocument());
}

finalSolution = newRoot != null
? finalSolution.WithDocumentSyntaxRoot(documentId, newRoot)
: finalSolution.WithDocumentText(documentId, newText);

// WithDocumentName doesn't support source generated documents, and there should be no circumstance were they'd
// be renamed anyway. Given rename only supports Razor generated documents, and those are always named for the Razor
// file they come from, and nothing in Roslyn knows to rename those documents, we can safely skip this step. Razor
// may process the results of this rename and rename the document if it needs to later.
if (!documentId.IsSourceGenerated)
{
finalSolution = finalSolution.WithDocumentName(documentId, newName);
}
Copy link
Member

Choose a reason for hiding this comment

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

could you even have a case where you had an SG doc, and it gota new name? is this just being paranoid (which i'm fine with). might be good to doc.

Copy link
Member Author

Choose a reason for hiding this comment

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

Source generated documents don't support modifying document attributes, which this call does. Just like WithDocumentText and WithSyntaxRoot weren't supported until I added it a few months ago.

It's a bit of an annoying chicken and egg - it would be nice if this call could no-op if the attributes haven't actually changed, regardless of document types, but it means adding support for source generated documents in order to get far enough to know whether that is what is happening.

Copy link
Member Author

Choose a reason for hiding this comment

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

Docced. I doubt there is a scenario were a SG doc would want to get renamed here, and restricting it to Razor makes it entirely impossible - Roslyn would have to know to rename a seemingly arbitrary additional document to cause a change of Razor SG doc name.

}

return finalSolution;
}

internal bool TryGetContainingEditableSpan(SnapshotPoint point, out SnapshotSpan editableSpan)
{
editableSpan = default;
Expand Down
38 changes: 38 additions & 0 deletions src/EditorFeatures/Test2/Rename/CSharp/SourceGeneratorTests.vb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
' The .NET Foundation licenses this file to you under the MIT license.
' See the LICENSE file in the project root for more information.

Imports System.Threading
Imports Microsoft.CodeAnalysis.Rename.ConflictEngine
Imports Microsoft.CodeAnalysis.Testing
Imports Microsoft.CodeAnalysis.Text

Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename.CSharp
<[UseExportProvider]>
<Trait(Traits.Feature, Traits.Features.Rename)>
Expand Down Expand Up @@ -56,6 +61,39 @@ public class GeneratedClass
End Using
End Sub

<Theory, CombinatorialData>
Public Async Function RenameWithReferenceInRazorGeneratedFile(host As RenameTestHost) As Task
Dim generatedMarkup = "
public class GeneratedClass
{
public void M([|RegularClass|] c) { }
}
"
Dim generatedCode As String = ""
Dim span As TextSpan
TestFileMarkupParser.GetSpan(generatedMarkup, generatedCode, span)
Dim sourceGenerator = New Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator(Sub(c) c.AddSource("generated_file.cs", generatedCode))
Using result = RenameEngineResult.Create(_outputHelper,
<Workspace>
<Project Language="C#" AssemblyName="ClassLibrary1" CommonReferences="true">
<Document>
public class [|$$RegularClass|]
{
}
</Document>
</Project>
</Workspace>, host:=host, sourceGenerator:=sourceGenerator, renameTo:="A")

Dim project = result.ConflictResolution.OldSolution.Projects.Single()
Dim generatedDocuments = Await project.GetSourceGeneratedDocumentsAsync(CancellationToken.None)
Dim generatedTree = Await generatedDocuments.Single().GetSyntaxTreeAsync(CancellationToken.None)
Dim location = CodeAnalysis.Location.Create(generatedTree, span)
'Manually assert the generated location, because the test workspace doesn't know about it
result.AssertLocationReferencedAs(location, RelatedLocationType.NoConflict)

End Using
End Function

<Theory, CombinatorialData>
<WorkItem("https://github.com/dotnet/roslyn/issues/51537")>
Public Sub RenameWithCascadeIntoGeneratedFile(host As RenameTestHost)
Expand Down
47 changes: 47 additions & 0 deletions src/EditorFeatures/Test2/Rename/InlineRenameTests.vb
Original file line number Diff line number Diff line change
Expand Up @@ -2379,6 +2379,53 @@ class [|C|]
Await session.CommitAsync(previewChanges:=False, editorOperationContext:=Nothing)

Await VerifyTagsAreCorrect(workspace)

Await VerifyChangedSourceGeneratedDocumentFilenames(workspace)
End Using
End Function

<WpfTheory>
<CombinatorialData, Trait(Traits.Feature, Traits.Features.Rename)>
Public Async Function RenameWithRazorGeneratedFile(host As RenameTestHost) As Task
Dim generatedCode = "
public class GeneratedClass
{
public void M(MyClass c) { }
}
"
Dim razorGenerator = New Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator(Sub(c) c.AddSource("generated_file.cs", generatedCode))

Using workspace = CreateWorkspaceWithWaiter(
<Workspace>
<Project Language="C#" CommonReferences="true" LanguageVersion="preview">
<Document>
partial class [|$$MyClass|]
{
public void M1()
{
}
}
</Document>
</Project>
</Workspace>, host)

Dim project = workspace.CurrentSolution.Projects.First().AddAnalyzerReference(New TestGeneratorReference(razorGenerator))
workspace.TryApplyChanges(project.Solution)

Dim session = StartSession(workspace)

' Type a bit in the file
Dim cursorDocument = workspace.Documents.Single(Function(d) d.CursorPosition.HasValue)
Dim caretPosition = cursorDocument.CursorPosition.Value
Dim textBuffer = cursorDocument.GetTextBuffer()

textBuffer.Insert(caretPosition, "Example")

Await session.CommitAsync(previewChanges:=False, editorOperationContext:=Nothing)

Await VerifyTagsAreCorrect(workspace)

Await VerifyChangedSourceGeneratedDocumentFilenames(workspace, "generated_file.cs")
End Using
End Function

Expand Down
4 changes: 2 additions & 2 deletions src/EditorFeatures/Test2/Rename/RenameEngineResult.vb
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename
Return locations
End Function

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

Private Sub AssertLocationReferencedAs(location As Location, type As RelatedLocationType)
Public Sub AssertLocationReferencedAs(location As Location, type As RelatedLocationType)
Try
Dim documentId = ConflictResolution.OldSolution.GetDocumentId(location.SourceTree)
Dim reference = _unassertedRelatedLocations.SingleOrDefault(
Expand Down
6 changes: 6 additions & 0 deletions src/EditorFeatures/Test2/Rename/RenameTestHelpers.vb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename
Next
End Function

Public Async Function VerifyChangedSourceGeneratedDocumentFileNames(workspace As EditorTestWorkspace, ParamArray mappedFiles As String()) As Task
Await WaitForRename(workspace)

Assert.Equal(workspace.ChangedSourceGeneratedDocumentFileNames, mappedFiles)
End Function

Public Sub VerifyFileName(document As Document, newIdentifierName As String)
Dim expectedName = Path.ChangeExtension(newIdentifierName, Path.GetExtension(document.Name))
Assert.Equal(expectedName, document.Name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.CSharp.DecompiledSource;
Expand All @@ -15,6 +16,7 @@
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Composition;
Expand All @@ -25,13 +27,16 @@
using Roslyn.Test.EditorUtilities;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Test.Utilities;

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

public List<string> ChangedSourceGeneratedDocumentFileNames { get; } = [];

private readonly Dictionary<string, ITextBuffer2> _createdTextBuffers = [];

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

return (reference, image);
}

internal override void ApplyMappedFileChanges(SolutionChanges solutionChanges)
{
foreach (var (docId, _) in solutionChanges.NewSolution.CompilationState.FrozenSourceGeneratedDocumentStates.States)
{
var document = solutionChanges.NewSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(docId);
Assert.NotNull(document.FilePath);
ChangedSourceGeneratedDocumentFileNames.Add(Path.GetFileName(document.FilePath));
}
}
}
Loading