From 9d5d3b750b2711e0485ad8fba107aa1828fb35f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:29:59 +0000 Subject: [PATCH 1/4] Initial plan From e021fce253d074376331710de108bc2231c708b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:35:06 +0000 Subject: [PATCH 2/4] Fix formatter range tracking through iterative edits Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Engine/ScriptAnalyzer.cs | 30 +++++++++-------- Tests/Engine/InvokeFormatter.tests.ps1 | 45 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 91bf188f2..dbc06a3e1 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -1556,7 +1556,6 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra } range = isRangeNull ? null : SnapToEdges(text, range); - var previousLineCount = text.LineCount; var previousUnusedCorrections = 0; do { @@ -1578,7 +1577,7 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra this.outputWriter.WriteVerbose($"Found {corrections.Count} violations."); int unusedCorrections; - Fix(text, corrections, out unusedCorrections); + Fix(text, corrections, out unusedCorrections, ref range); var numberOfFixedViolatons = corrections.Count - unusedCorrections; fixesWereApplied = numberOfFixedViolatons > 0; this.outputWriter.WriteVerbose($"Fixed {numberOfFixedViolatons} violations."); @@ -1595,16 +1594,6 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra } previousUnusedCorrections = unusedCorrections; - var lineCount = text.LineCount; - if (!isRangeNull && lineCount != previousLineCount) - { - range = new Range( - range.Start, - range.End.Shift(lineCount - previousLineCount, 0)); - range = SnapToEdges(text, range); - } - - previousLineCount = lineCount; } while (previousUnusedCorrections > 0); updatedRange = range; @@ -1728,7 +1717,8 @@ private static IEnumerable GetCorrectionExtentsForFix( private static EditableText Fix( EditableText text, List correctionExtents, - out int unusedCorrections) + out int unusedCorrections, + ref Range range) { var correctionsToUse = GetCorrectionExtentsForFix(correctionExtents); var count = 0; @@ -1736,6 +1726,20 @@ private static EditableText Fix( { count++; text.ApplyEdit(correction); + if (range != null) + { + var lineDelta = correction.Lines.Length - 1 + - (correction.EndLineNumber - correction.StartLineNumber); + var columnDelta = 0; + if (correction.EndLineNumber == range.End.Line) + { + var replacementEndColumn = correction.Lines.Last().TrimEnd('\r').Length + + (correction.Lines.Length == 1 ? correction.StartColumnNumber : 1); + columnDelta = replacementEndColumn - correction.EndColumnNumber; + } + + range = new Range(range.Start, range.End.Shift(lineDelta, columnDelta)); + } } unusedCorrections = correctionExtents.Count - count; diff --git a/Tests/Engine/InvokeFormatter.tests.ps1 b/Tests/Engine/InvokeFormatter.tests.ps1 index dcc0a6174..ccaaf4c02 100644 --- a/Tests/Engine/InvokeFormatter.tests.ps1 +++ b/Tests/Engine/InvokeFormatter.tests.ps1 @@ -81,6 +81,51 @@ function foo { Invoke-Formatter -ScriptDefinition $def -Range @(3, 1, 4, 1) | Should -Be $expected } + + It "Should apply all corrections when earlier fixes expand the selected line" { + $def = "if(`$a){'x'}`nif(`$b){'y'}" + $expected = "if(`$a){'x'}`nif (`$b) { 'y' }" + + Invoke-Formatter -ScriptDefinition $def -Range @(2, 1, 2, 12) | Should -Be $expected + } + + It "Should track the selected lines when fixes " -TestCases @( + @{ + Name = 'expand the last line' + Selected = "if(`$b){'y'}" + Expected = "if (`$b) { 'y' }" + } + @{ + Name = 'shorten the last line' + Selected = "if (`$b) { 'y' }" + Expected = "if (`$b) { 'y' }" + } + @{ + Name = 'expand multiple lines' + Selected = "if(`$b){'y'}`nif(`$c){'z'}" + Expected = "if (`$b) { 'y' }`nif (`$c) { 'z' }" + } + @{ + Name = 'remove a line' + Selected = "if(`$b)`n{`n'y'`n}" + Expected = "if (`$b) {`n 'y'`n}" + } + @{ + Name = 'add a line' + Selected = "if(`$b) {`n'y'`n}" + Expected = "if (`$b)`n{`n 'y'`n}" + Settings = 'CodeFormattingAllman' + } + ) { + param($Selected, $Expected, $Settings = 'CodeFormatting') + + $def = "if(`$a){'x'}`n$Selected`nif(`$d){'w'}" + $lines = $Selected -split "`n" + $range = @(2, 1, (1 + $lines.Count), (1 + $lines[-1].Length)) + + Invoke-Formatter -ScriptDefinition $def -Range $range -Settings $Settings | + Should -Be "if(`$a){'x'}`n$Expected`nif(`$d){'w'}" + } } Context "When no settings are given" { From e45a7720f8e67193fc03a225d3aff20e4cb37067 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:31:16 +0000 Subject: [PATCH 3/4] Handle empty formatter replacements in ranges Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Engine/ScriptAnalyzer.cs | 28 ++++++++++++-- Tests/Engine/InvokeFormatter.tests.ps1 | 51 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index dbc06a3e1..4f1499420 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -1733,12 +1733,34 @@ private static EditableText Fix( var columnDelta = 0; if (correction.EndLineNumber == range.End.Line) { - var replacementEndColumn = correction.Lines.Last().TrimEnd('\r').Length - + (correction.Lines.Length == 1 ? correction.StartColumnNumber : 1); + var replacementEndColumn = correction.Lines.Length == 0 + ? correction.StartColumnNumber + : correction.Lines.Last().TrimEnd('\r').Length + + (correction.Lines.Length == 1 ? correction.StartColumnNumber : 1); columnDelta = replacementEndColumn - correction.EndColumnNumber; } - range = new Range(range.Start, range.End.Shift(lineDelta, columnDelta)); + var endLine = range.End.Line + lineDelta; + if (correction.Lines.Length == 0 && endLine < range.Start.Line) + { + if (text.LineCount == 0) + { + range = null; + } + else + { + endLine = Math.Min(Math.Max(endLine, 1), text.LineCount); + var endColumn = Math.Min( + Math.Max(range.End.Column + columnDelta, 1), + text.Lines[endLine - 1].Length + 1); + var end = new Position(endLine, endColumn); + range = new Range(end, end); + } + } + else + { + range = new Range(range.Start, range.End.Shift(lineDelta, columnDelta)); + } } } diff --git a/Tests/Engine/InvokeFormatter.tests.ps1 b/Tests/Engine/InvokeFormatter.tests.ps1 index ccaaf4c02..b15cd25ef 100644 --- a/Tests/Engine/InvokeFormatter.tests.ps1 +++ b/Tests/Engine/InvokeFormatter.tests.ps1 @@ -82,6 +82,57 @@ function foo { Invoke-Formatter -ScriptDefinition $def -Range @(3, 1, 4, 1) | Should -Be $expected } + It "Should track the range when an edit deletes its last line" { + $text = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.EditableText" -ArgumentList "first`n`nlast" + $correctionType = "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent" -as [type] + $correction = [Activator]::CreateInstance( + $correctionType, + [object[]]@(2, 2, 1, 1, [string[]]@(), $null, $null)) + $correctionsType = [System.Collections.Generic.List``1].MakeGenericType($correctionType) + $corrections = [Activator]::CreateInstance($correctionsType) + $corrections.Add($correction) + $range = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.Range" -ArgumentList 1, 1, 2, 1 + $arguments = [object[]]@( + $text.PSObject.BaseObject, + $corrections.PSObject.BaseObject, + 0, + $range.PSObject.BaseObject) + $fixMethod = ("Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer" -as [type]).GetMethod( + 'Fix', + [Reflection.BindingFlags]'NonPublic, Static') + + $fixMethod.Invoke($null, $arguments) | Out-Null + + $text.ToString() | Should -Be "first`nlast" + $arguments[3].End.Line | Should -Be 1 + } + + It "Should collapse the range when an edit deletes all selected lines" { + $text = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.EditableText" -ArgumentList "first`nlast" + $correctionType = "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent" -as [type] + $correction = [Activator]::CreateInstance( + $correctionType, + [object[]]@(2, 2, 1, 5, [string[]]@(), $null, $null)) + $correctionsType = [System.Collections.Generic.List``1].MakeGenericType($correctionType) + $corrections = [Activator]::CreateInstance($correctionsType) + $corrections.Add($correction) + $range = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.Range" -ArgumentList 2, 1, 2, 5 + $arguments = [object[]]@( + $text.PSObject.BaseObject, + $corrections.PSObject.BaseObject, + 0, + $range.PSObject.BaseObject) + $fixMethod = ("Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer" -as [type]).GetMethod( + 'Fix', + [Reflection.BindingFlags]'NonPublic, Static') + + $fixMethod.Invoke($null, $arguments) | Out-Null + + $text.ToString() | Should -Be "first" + $arguments[3].Start.Line | Should -Be 1 + $arguments[3].End.Line | Should -Be 1 + } + It "Should apply all corrections when earlier fixes expand the selected line" { $def = "if(`$a){'x'}`nif(`$b){'y'}" $expected = "if(`$a){'x'}`nif (`$b) { 'y' }" From d039aa281767046c67c12d2d17f89cf3d4bb7e40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:53:27 +0000 Subject: [PATCH 4/4] Preserve exhausted formatter selections Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Engine/Formatter.cs | 5 +++++ Engine/ScriptAnalyzer.cs | 24 ++++++++++-------------- Tests/Engine/InvokeFormatter.tests.ps1 | 7 +++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Engine/Formatter.cs b/Engine/Formatter.cs index a6a25f0fb..a60943719 100644 --- a/Engine/Formatter.cs +++ b/Engine/Formatter.cs @@ -54,6 +54,7 @@ public static string Format( ScriptBlockAst scriptAst = null; Token[] scriptTokens = null; bool skipParsing = false; + bool isRangeSpecified = range != null; foreach (var rule in ruleOrder) { if (!settings.RuleArguments.ContainsKey(rule)) @@ -68,6 +69,10 @@ public static string Format( text = ScriptAnalyzer.Instance.Fix(text, range, skipParsing, out Range updatedRange, out bool fixesWereApplied, ref scriptAst, ref scriptTokens, skipVariableAnalysis: true); skipParsing = !fixesWereApplied; range = updatedRange; + if (isRangeSpecified && range == null) + { + break; + } } return text.ToString(); diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 4f1499420..19659a78b 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -1594,7 +1594,7 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra } previousUnusedCorrections = unusedCorrections; - } while (previousUnusedCorrections > 0); + } while (previousUnusedCorrections > 0 && (isRangeNull || range != null)); updatedRange = range; return text; @@ -1743,19 +1743,15 @@ private static EditableText Fix( var endLine = range.End.Line + lineDelta; if (correction.Lines.Length == 0 && endLine < range.Start.Line) { - if (text.LineCount == 0) - { - range = null; - } - else - { - endLine = Math.Min(Math.Max(endLine, 1), text.LineCount); - var endColumn = Math.Min( - Math.Max(range.End.Column + columnDelta, 1), - text.Lines[endLine - 1].Length + 1); - var end = new Position(endLine, endColumn); - range = new Range(end, end); - } + range = null; + } + else if (correction.Lines.Length == 0 + && lineDelta < 0 + && correction.EndLineNumber == range.End.Line) + { + range = new Range( + range.Start, + new Position(endLine, text.Lines[endLine - 1].Length + 1)); } else { diff --git a/Tests/Engine/InvokeFormatter.tests.ps1 b/Tests/Engine/InvokeFormatter.tests.ps1 index b15cd25ef..e49827a6a 100644 --- a/Tests/Engine/InvokeFormatter.tests.ps1 +++ b/Tests/Engine/InvokeFormatter.tests.ps1 @@ -105,9 +105,10 @@ function foo { $text.ToString() | Should -Be "first`nlast" $arguments[3].End.Line | Should -Be 1 + $arguments[3].End.Column | Should -Be 6 } - It "Should collapse the range when an edit deletes all selected lines" { + It "Should exhaust the range when an edit deletes all selected lines" { $text = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.EditableText" -ArgumentList "first`nlast" $correctionType = "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent" -as [type] $correction = [Activator]::CreateInstance( @@ -127,10 +128,8 @@ function foo { [Reflection.BindingFlags]'NonPublic, Static') $fixMethod.Invoke($null, $arguments) | Out-Null - $text.ToString() | Should -Be "first" - $arguments[3].Start.Line | Should -Be 1 - $arguments[3].End.Line | Should -Be 1 + $arguments[3] | Should -BeNullOrEmpty } It "Should apply all corrections when earlier fixes expand the selected line" {