diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRoslynRenameTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRoslynRenameTest.cs index 48cbf70b466..2f61c34b934 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRoslynRenameTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRoslynRenameTest.cs @@ -17,9 +17,12 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.CohostingShared; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Rename; using Microsoft.CodeAnalysis.Text; +using Microsoft.NET.Sdk.Razor.SourceGenerators; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -44,7 +47,7 @@ public string MyMethod() razorFile: """ This is a Razor document. -

@_myClass.MyMethod()

+

@_myClass.MyM$$ethod()

@code { @@ -58,7 +61,6 @@ public string M() The end. """, - symbolToRename: "MyMethod", newName: "CallThisFunction", expectedCSharpFile: """ public class MyClass @@ -88,6 +90,65 @@ The end. """, useLsp); + [Theory] + [CombinatorialData] + public Task CSharp_Local(bool useLsp) + => VerifyRenamesAsync( + csharpFile: """ + public class MyClass + { + public string MyMethod() + { + return $"Hi from {nameof(MyMethod)}"; + } + } + """, + razorFile: """ + This is a Razor document. + + @{var someV$$ariable = true;} @someVariable + + @code + { + private MyClass _myClass = new MyClass(); + + public string M() + { + return _myClass.MyMethod(); + } + } + + The end. + """, + newName: "fishPants", + expectedCSharpFile: """ + public class MyClass + { + public string MyMethod() + { + return $"Hi from {nameof(MyMethod)}"; + } + } + """, + expectedRazorFile: """ + This is a Razor document. + + @{var fishPants = true;} @fishPants + + @code + { + private MyClass _myClass = new MyClass(); + + public string M() + { + return _myClass.MyMethod(); + } + } + + The end. + """, + useLsp); + private protected override TestComposition ConfigureRoslynDevenvComposition(TestComposition composition) { return composition @@ -97,30 +158,64 @@ private protected override TestComposition ConfigureRoslynDevenvComposition(Test private async Task VerifyRenamesAsync( string csharpFile, - string razorFile, - string symbolToRename, + TestCode razorFile, string newName, string expectedCSharpFile, string expectedRazorFile, bool useLsp) { - var razorDocument = CreateProjectAndRazorDocument(razorFile, additionalFiles: [(Path.Combine(TestProjectData.SomeProjectPath, "File.cs"), csharpFile)], createSeparateRemoteAndLocalWorkspaces: true); + var razorDocument = CreateProjectAndRazorDocument(razorFile.Text, additionalFiles: [(Path.Combine(TestProjectData.SomeProjectPath, "File.cs"), csharpFile)], createSeparateRemoteAndLocalWorkspaces: true); var project = razorDocument.Project; var csharpDocument = project.Documents.First(); var compilation = await project.GetCompilationAsync(DisposalToken); - var symbol = compilation.AssumeNotNull().GetSymbolsWithName(symbolToRename).First(); + Assert.True(razorDocument.TryComputeHintNameFromRazorDocument(out var hintName)); + var generatedDocument = await project.TryGetSourceGeneratedDocumentFromHintNameAsync(hintName, DisposalToken); + + var node = await GetSyntaxNodeAsync(generatedDocument.AssumeNotNull(), razorFile.Position, razorDocument); + if (useLsp) { - await VerifyLspRenameAsync(newName, expectedCSharpFile, expectedRazorFile, razorDocument, csharpDocument, symbol); + await VerifyLspRenameAsync(newName, expectedCSharpFile, expectedRazorFile, razorDocument, csharpDocument, node); } else { + var symbol = FindSymbolToRename(compilation.AssumeNotNull(), node); await VerifyVSRenameAsync(newName, expectedCSharpFile, expectedRazorFile, razorDocument, project, csharpDocument, symbol); } } + private ISymbol FindSymbolToRename(Compilation compilation, SyntaxNode node) + { + var semanticModel = compilation.GetSemanticModel(node.SyntaxTree); + var symbol = semanticModel.GetDeclaredSymbol(node, DisposalToken); + if (symbol is null) + { + symbol = semanticModel.GetSymbolInfo(node, DisposalToken).Symbol; + } + + Assert.NotNull(symbol); + return symbol; + } + + private async Task GetSyntaxNodeAsync(Document document, int position, TextDocument razorDocument) + { + var snapshotManager = OOPExportProvider.GetExportedValue(); + var documentMappingService = OOPExportProvider.GetExportedValue(); + var snapshot = snapshotManager.GetSnapshot(razorDocument); + var codeDocument = await snapshot.GetGeneratedOutputAsync(DisposalToken); + + Assert.True(documentMappingService.TryMapToCSharpDocumentPosition(codeDocument.GetCSharpDocument().AssumeNotNull(), position, out var csharpPosition, out _)); + + var sourceText = await document.GetTextAsync(DisposalToken); + var span = sourceText.GetTextSpan(csharpPosition, csharpPosition); + var tree = await document.AssumeNotNull().GetSyntaxTreeAsync(DisposalToken); + var root = await tree.AssumeNotNull().GetRootAsync(DisposalToken); + + return root.FindNode(span); + } + private async Task VerifyVSRenameAsync(string newName, string expectedCSharpFile, string expectedRazorFile, TextDocument razorDocument, Project project, Document csharpDocument, ISymbol symbol) { var solution = await Renamer.RenameSymbolAsync(project.Solution, symbol, new SymbolRenameOptions(), newName, DisposalToken); @@ -155,13 +250,17 @@ private async Task VerifyVSRenameAsync(string newName, string expectedCSharpFile AssertEx.EqualOrDiff(expectedRazorFile, razorText.ToString()); } - private async Task VerifyLspRenameAsync(string newName, string expectedCSharpFile, string expectedRazorFile, TextDocument razorDocument, Document csharpDocument, ISymbol symbol) + private async Task VerifyLspRenameAsync(string newName, string expectedCSharpFile, string expectedRazorFile, TextDocument razorDocument, Document csharpDocument, SyntaxNode node) { // Normally in cohosting tests we directly construct and invoke the endpoints, but in this scenario Roslyn is going to do it // using a service in their MEF composition, so we have to jump through an extra hook to hook up our test invoker. var invoker = RoslynDevenvExportProvider.AssumeNotNull().GetExportedValue(); invoker.SetInvoker(RemoteServiceInvoker); - var workspaceEdit = await Rename.GetRenameEditAsync(csharpDocument, symbol.Locations.First().GetLineSpan().StartLinePosition, newName, DisposalToken); + + var tree = node.SyntaxTree; + var documentToRename = razorDocument.Project.Solution.GetDocument(tree).AssumeNotNull(); + var linePosition = node.SyntaxTree.GetText().GetLinePosition(node.SpanStart); + var workspaceEdit = await Rename.GetRenameEditAsync(documentToRename, linePosition, newName, DisposalToken); Assert.NotNull(workspaceEdit); var csharpSourceText = await csharpDocument.GetTextAsync(DisposalToken);