diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractDocumentMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractDocumentMappingService.cs index 9db05857c2d..4d3ad34a23b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractDocumentMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractDocumentMappingService.cs @@ -94,6 +94,35 @@ public IEnumerable GetRazorDocumentEdits(RazorCSharpDocument csharpD } } + // The opposite case of the above: for the last line of a code block, the C# formatter might + // return an edit that starts within our mapping, but ends after. In those cases, when the edit + // spans multiple lines we just take the first line and try to use that. + // + // This can happen with code actions that remove content, where the edit starts inside the mapped + // region but extends beyond it. For example, when removing an unused variable in a single-line + // explicit statement block like `@{ var x = 1; }`, Roslyn may generate edits that span beyond + // the mapped C# region. + if (mappedStart && !mappedEnd && startLine != endLine) + { + // Construct a theoretical edit that is just for the first line of the edit that the C# formatter + // gave us, and see if we can map that. + + // Get the end of the start line + if (!csharpSourceText.TryGetAbsoluteIndex(startLine, csharpSourceText.Lines[startLine].Span.Length, out var endIndex)) + { + break; + } + + if (this.TryMapToRazorDocumentPosition(csharpDocument, endIndex, out _, out hostEndIndex)) + { + // If there's a newline in the new text, only take the part before it + var firstNewLine = change.NewText.AssumeNotNull().IndexOfAny(['\n', '\r']); + var newText = firstNewLine >= 0 ? change.NewText[..firstNewLine] : change.NewText; + yield return new TextChange(TextSpan.FromBounds(hostStartIndex, hostEndIndex), newText); + continue; + } + } + // If we couldn't map either the start or the end then we still might want to do something tricky. // When we have a block like this: // diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CSharpCodeActionTests.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CSharpCodeActionTests.cs index c8b120513dd..6638595248c 100644 --- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CSharpCodeActionTests.cs +++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CSharpCodeActionTests.cs @@ -372,4 +372,78 @@ private string GetDebuggerDisplay() await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.AddDebuggerDisplay); } + + [Fact] + public async Task RemoveUnusedVariable_Local() + { + var input = """ + @code + { + void M() + { + int {|IDE0059:[||]x|} = 5; + } + } + """; + + var expected = """ + @code + { + void M() + { + } + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.RemoveUnusedVariable); + } + + [Fact] + public async Task RemoveUnusedVariable_ExplicitStatement_SingleLine() + { + var input = """ + @{ var {|IDE0059:[||]x|} = 1; var y = 2; } + """; + + var expected = """ + @{ + var y = 2; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.RemoveUnusedVariable); + } + + [Fact] + public async Task RemoveUnusedVariable_ExplicitStatement_SingleLine_OnlyVariable() + { + var input = """ + @{ var {|IDE0059:[||]message|} = "Hello World"; } + """; + + var expected = """ + @{} + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.RemoveUnusedVariable); + } + + [Fact] + public async Task RemoveUnusedVariable_ExplicitStatement_MultiLine() + { + var input = """ + @{ + var {|IDE0059:[||]x|} = 1; + var y = 2; + } + """; + + var expected = """ + @{ + var y = 2; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.RemoveUnusedVariable); + } }