From ee8002eeb87e4ac3b89a1854b87a50c6ed4bdb91 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 30 Jul 2026 11:37:23 +1000 Subject: [PATCH 1/3] Add InvertIf explicit statement repro Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 43fe0e8f-fe50-416d-8f56-bcbfe4420e4e --- .../CodeActions/CSharpCodeActionTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs index 3b6555beeefc5..9339de2d3dd7c 100644 --- a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs +++ b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs @@ -45,6 +45,55 @@ void M(bool value) var invertIfAction = Assert.Single(shownActions, action => ((RazorVSInternalCodeAction)action.Value!).Name == PredefinedCodeRefactoringProviderNames.InvertIf); } + [Fact] + public async Task InvertIf_ExplicitStatement() + { + var input = """ + @(booleanValue ?@
: @
) + + @{ + [||]if (true) + { + // true + } + else + { + // false + } + } + + @code + { + private bool booleanValue = true; + } + """; + + var expected = """ + @(booleanValue ?@
: @
) + + @{ + if (false) + { + // false + } + else + { + // true + } + } + + @code + { + private bool booleanValue = true; + } + """; + + var advancedSettings = ClientSettingsManager.GetClientSettings().AdvancedSettings; + ClientSettingsManager.Update(advancedSettings with { ShowAllCSharpCodeActions = true }); + + await VerifyCodeActionAsync(input, expected, PredefinedCodeRefactoringProviderNames.InvertIf); + } + [Fact] public async Task GenerateConstructor() { From e7eb82328f5fd3c3dc02c5e57a5516e1a28c671c Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 30 Jul 2026 12:35:23 +1000 Subject: [PATCH 2/3] Preserve explicit Razor block indentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 43fe0e8f-fe50-416d-8f56-bcbfe4420e4e --- .../Passes/CSharpOnTypeFormattingPass.cs | 15 +++++++++++---- .../CodeActions/CSharpCodeActionTests.cs | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Razor/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs index 6c8e37b2791db..67710800deaaf 100644 --- a/src/Razor/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs @@ -756,12 +756,20 @@ owner.Parent is RazorDirectiveSyntax containingDirective && var line = context.SourceText.Lines[i]; var lineStart = line.GetFirstNonWhitespacePosition() ?? line.Start; var lineStartSpan = new TextSpan(lineStart, 0); - if (!ShouldFormatLine(context, lineStartSpan, allowImplicitStatements: true)) + if (!ShouldFormatLine(context, lineStartSpan, allowImplicitStatements: true, out var owner)) { // We don't care about this line as it lies in an area we don't want to format. continue; } + var razorDesiredIndentation = context.GetIndentationOffsetForLevel(indentations[i].IndentationLevel); + if (owner is CSharpTransitionSyntax { Parent: CSharpStatementSyntax }) + { + // An explicit statement delimiter has no C# source mapping, so use only its Razor/HTML indentation. + newIndentations[i] = razorDesiredIndentation; + continue; + } + if (!lineStartIndentations.TryGetValue(lineStart, out var csharpDesiredIndentation)) { // Couldn't remap. This is probably a non-C# location. @@ -820,7 +828,6 @@ owner.Parent is RazorDirectiveSyntax containingDirective && } var effectiveCSharpDesiredIndentation = csharpDesiredIndentation - minCSharpIndentation; - var razorDesiredIndentation = context.GetIndentationOffsetForLevel(indentations[i].IndentationLevel); if (indentations[i].StartsInHtmlContext) { // This is a non-C# line. @@ -857,8 +864,8 @@ private static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan private static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements, out RazorSyntaxNode? foundOwner) => ShouldFormat(context, mappingSpan, new ShouldFormatOptions(allowImplicitStatements, isLineRequest: false), out foundOwner); - private static bool ShouldFormatLine(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements) - => ShouldFormat(context, mappingSpan, new ShouldFormatOptions(allowImplicitStatements, isLineRequest: true), out _); + private static bool ShouldFormatLine(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements, out RazorSyntaxNode? foundOwner) + => ShouldFormat(context, mappingSpan, new ShouldFormatOptions(allowImplicitStatements, isLineRequest: true), out foundOwner); private static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, ShouldFormatOptions options, out RazorSyntaxNode? foundOwner) { diff --git a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs index 9339de2d3dd7c..3c6a92289edd3 100644 --- a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs +++ b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs @@ -71,7 +71,7 @@ public async Task InvertIf_ExplicitStatement() var expected = """ @(booleanValue ?@
: @
) - @{ + @{ if (false) { // false From b734cfdf3f2348ac4606f22b2e8c297df22b6361 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 30 Jul 2026 12:36:04 +1000 Subject: [PATCH 3/3] Add explicit Razor block formatting coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 43fe0e8f-fe50-416d-8f56-bcbfe4420e4e --- .../Formatting/DocumentFormattingTest.cs | 130 ++++++++++++++++++ .../Cohost/Formatting/OnTypeFormattingTest.cs | 45 ++++++ 2 files changed, 175 insertions(+) diff --git a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Formatting/DocumentFormattingTest.cs b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Formatting/DocumentFormattingTest.cs index 2fc60220a105b..3915f80c9b7a2 100644 --- a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Formatting/DocumentFormattingTest.cs +++ b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Formatting/DocumentFormattingTest.cs @@ -7182,6 +7182,136 @@ void Method() """); } + [Fact] + public async Task ExplicitStatement_ShiftedOpeningBrace() + { + await RunFormattingTestAsync( + input: """ + @(booleanValue ?@
: @
) + + @{ + if (false) + { + // false + } + else + { + // true + } + } + + @code + { + private bool booleanValue = true; + } + """, + htmlFormatted: """ + @(booleanValue ?@ +
: @ +
) + + @{ + if (false) + { + // false + } + else + { + // true + } + } + + @code + { + private bool booleanValue = true; + } + """, + expected: """ + @(booleanValue ?@
: @
) + + @{ + if (false) + { + // false + } + else + { + // true + } + } + + @code + { + private bool booleanValue = true; + } + """); + } + + [Fact] + public async Task ExplicitStatement_AlreadyFormatted() + { + await RunFormattingTestAsync( + input: """ + @(booleanValue ?@
: @
) + + @{ + if (false) + { + // false + } + else + { + // true + } + } + + @code + { + private bool booleanValue = true; + } + """, + htmlFormatted: """ + @(booleanValue ?@ +
: @ +
) + + @{ + if (false) + { + // false + } + else + { + // true + } + } + + @code + { + private bool booleanValue = true; + } + """, + expected: """ + @(booleanValue ?@
: @
) + + @{ + if (false) + { + // false + } + else + { + // true + } + } + + @code + { + private bool booleanValue = true; + } + """); + } + [Fact] public async Task Formats_NonCodeBlockDirectives() { diff --git a/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/Formatting/OnTypeFormattingTest.cs b/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/Formatting/OnTypeFormattingTest.cs index 7580e336fcd83..ff3c4dbdba86e 100644 --- a/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/Formatting/OnTypeFormattingTest.cs +++ b/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/Formatting/OnTypeFormattingTest.cs @@ -52,6 +52,51 @@ await RunOnTypeFormattingTestAsync( triggerCharacter: '}'); } + [FormattingTestFact] + public async Task CloseCurly_ExplicitStatement_PreservesOpeningBraceAsync() + { + await RunOnTypeFormattingTestAsync( + input: """ + @(booleanValue ?@
: @
) + + @{ + if (false) + { + // false + } + else + { + // true + }$$ + } + + @code + { + private bool booleanValue = true; + } + """, + expected: """ + @(booleanValue ?@
: @
) + + @{ + if (false) + { + // false + } + else + { + // true + } + } + + @code + { + private bool booleanValue = true; + } + """, + triggerCharacter: '}'); + } + [FormattingTestFact] public async Task FormatsIfStatementInComponent() {