Fix formatting of tables where headers span multiple rows (#6504)

* In cases where the header spans multiple rows, need to correctly calculate whitespace and trim appropriately

* Use System.Span<int> and C# 7.2 language in SMA

* Added new ref assemblies to Files.wxs
refactor tests to remove similar xml content
added single column test case
This commit is contained in:
Steve Lee 2018-04-03 12:24:23 +09:00 committed by Ilya
parent d1cf82ec20
commit 5d3456d11b
4 changed files with 210 additions and 13 deletions

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<?define ProductDirectoryName = "$(env.ProductDirectoryName)" ?>
<?define FileArchitecture = "$(env.FileArchitecture)" ?>
@ -7,6 +7,12 @@
<Component Id="cmp3CC027D3F160412C9F0044EBED3115DD" Guid="{8D7CAA67-8F28-422C-85FB-BDE04902E64F}">
<File Id="filFD2EF6BC74AF459D1BB52CA1E8C6E33B" KeyPath="yes" Source="$(env.ProductSourcePath)\NJsonSchema.dll" />
</Component>
<Component Id="cmp85032A09199C30E63F9F2CF55B6FA9B9" Guid="{D3414CD0-7366-4008-A406-DC9E69224F42}">
<File Id="fil85032A09199C30E63F9F2CF55B6FA9B9" KeyPath="yes" Source="$(env.ProductSourcePath)\System.Memory.dll" />
</Component>
<Component Id="cmp12FFA2A71C4F95630792652F192DAAB3" Guid="{9F6FF303-33C5-4C51-9092-B03BC1593AEF}">
<File Id="fil12FFA2A71C4F95630792652F192DAAB3" KeyPath="yes" Source="$(env.ProductSourcePath)\System.Runtime.CompilerServices.Unsafe.dll" />
</Component>
<Component Id="cmp3B130879A26D2E954251BB81E8948069" Guid="{CD268615-4603-4A5F-B126-340ADF2EDDD5}">
<File Id="filAACDEEE28FEA076C73D082A0AD21B8E0" KeyPath="yes" Source="$(env.ProductSourcePath)\Microsoft.CSharp.dll" />
</Component>
@ -1822,6 +1828,8 @@
<Fragment>
<ComponentGroup Id="$(var.ProductDirectoryName)">
<ComponentRef Id="cmp3CC027D3F160412C9F0044EBED3115DD" />
<ComponentRef Id="cmp85032A09199C30E63F9F2CF55B6FA9B9" />
<ComponentRef Id="cmp12FFA2A71C4F95630792652F192DAAB3" />
<ComponentRef Id="cmp3B130879A26D2E954251BB81E8948069" />
<ComponentRef Id="cmp02ABBE4A3EDBEBFD05DC17A009A2B79D" />
<ComponentRef Id="cmpAA10498DF244C013CB5043C62E3AA83A" />

View file

@ -261,7 +261,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
// pad with a blank (or any character that ALWAYS maps to a single screen cell
if (k > 0)
{
// skipping the first ones, add a separator for catenation
// skipping the first ones, add a separator for concatenation
for (int j = 0; j < scArray[k].Count; j++)
{
scArray[k][j] = StringUtil.Padding(ScreenInfo.separatorCharacterCount) + scArray[k][j];
@ -289,22 +289,64 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
screenRows = scArray[k].Count;
}
// add padding for the columns that are shorter
for (int col = 0; col < scArray.Length-1; col++)
// column headers can span multiple rows if the width of the column is shorter than the header text like:
//
// Long Header2 Head
// Head er3
// er
// ---- ------- ----
// 1 2 3
//
// To ensure we don't add whitespace to the end, we need to determine the last column in each row with content
System.Span<int> lastColWithContent = stackalloc int[screenRows];
for (int row = 0; row < screenRows; row++)
{
int paddingBlanks = _si.columnInfo[validColumnArray[col]].width;
if (col > 0)
paddingBlanks += ScreenInfo.separatorCharacterCount;
else
for (int col = scArray.Length - 1; col > 0; col--)
{
paddingBlanks += _startColumn;
int colWidth = _si.columnInfo[validColumnArray[col]].width;
int headerLength = values[col].Length;
if (headerLength / colWidth >= row && headerLength % colWidth > 0)
{
lastColWithContent[row] = col;
break;
}
}
}
// add padding for the columns that are shorter
for (int col = 0; col < scArray.Length; col++)
{
int paddingBlanks = 0;
// don't pad if last column
if (col < scArray.Length - 1)
{
paddingBlanks = _si.columnInfo[validColumnArray[col]].width;
if (col > 0)
{
paddingBlanks += ScreenInfo.separatorCharacterCount;
}
else
{
paddingBlanks += _startColumn;
}
}
int paddingEntries = screenRows - scArray[col].Count;
if (paddingEntries > 0)
{
for (int j = 0; j < paddingEntries; j++)
for (int row = screenRows - paddingEntries; row < screenRows; row++)
{
scArray[col].Add(StringUtil.Padding(paddingBlanks));
// if the column is beyond the last column with content, just use empty string
if (col > lastColWithContent[row])
{
scArray[col].Add("");
}
else
{
scArray[col].Add(StringUtil.Padding(paddingBlanks));
}
}
}
}
@ -314,10 +356,18 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
for (int row = 0; row < rows.Length; row++)
{
StringBuilder sb = new StringBuilder();
// for a give row, walk the columns
// for a given row, walk the columns
for (int col = 0; col < scArray.Length; col++)
{
sb.Append(scArray[col][row]);
// if the column is the last column with content, we need to trim trailing whitespace
if (col == lastColWithContent[row])
{
sb.Append(scArray[col][row].TrimEnd());
}
else
{
sb.Append(scArray[col][row]);
}
}
rows[row] = sb.ToString();
}

View file

@ -4,6 +4,7 @@
<Description>PowerShell Core's System.Management.Automation project</Description>
<NoWarn>$(NoWarn);CS1570;CS1734</NoWarn>
<AssemblyName>System.Management.Automation</AssemblyName>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
@ -13,6 +14,7 @@
<!-- the following package(s) are from https://github.com/dotnet/corefx -->
<PackageReference Include="Microsoft.Win32.Registry.AccessControl" Version="4.4.0" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.4.0" />
<PackageReference Include="System.Memory" Version="4.5.0-*"/>
<PackageReference Include="System.Security.AccessControl" Version="4.4.1" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="4.4.0" />
<PackageReference Include="System.Security.Permissions" Version="4.4.1" />

View file

@ -339,4 +339,141 @@ Left Center Right
$output = $ps.Invoke()
$output.Replace("`n","").Replace("`r","") | Should -BeExactly $expected
}
It "Format-Table should correctly render headers that span multiple rows: <variation>" -TestCases @(
@{ view = "Default"; widths = 4,7,4; variation = "4 row, 1 row, 2 row"; expectedTable = @"
Long Header2 Head
Long er3
Head
er
---- ------- ----
1 2 3
"@ },
@{ view = "Default"; widths = 4,4,7; variation = "4 row, 2 row, 1 row"; expectedTable = @"
Long Head Header3
Long er2
Head
er
---- ---- -------
1 2 3
"@ },
@{ view = "Default"; widths = 14,7,3; variation = "1 row, 1 row, 3 row"; expectedTable = @"
LongLongHeader Header2 Hea
der
3
-------------- ------- ---
1 2 3
"@ },
@{ view = "One"; widths = 4,1,1; variation = "1 column"; expectedTable = @"
Long
Long
Head
er
----
1
"@ }
) {
param($view, $widths, $expectedTable)
$ps1xml = @"
<Configuration>
<ViewDefinitions>
<View>
<Name>Default</Name>
<ViewSelectedBy>
<TypeName>Test.Format</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>LongLongHeader</Label>
<Width>{0}</Width>
</TableColumnHeader>
<TableColumnHeader>
<Label>Header2</Label>
<Width>{1}</Width>
</TableColumnHeader>
<TableColumnHeader>
<Label>Header3</Label>
<Width>{2}</Width>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>First</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Second</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Third</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>One</Name>
<ViewSelectedBy>
<TypeName>Test.Format</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>LongLongHeader</Label>
<Width>{0}</Width>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>First</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
"@
$ps1xml = $ps1xml.Replace("{0}", $widths[0]).Replace("{1}", $widths[1]).Replace("{2}", $widths[2])
$ps1xmlPath = Join-Path -Path $TestDrive -ChildPath "test.format.ps1xml"
Set-Content -Path $ps1xmlPath -Value $ps1xml
# run in own runspace so not affect global sessionstate
$ps = [powershell]::Create()
$ps.AddScript( {
param($ps1xmlPath, $view)
Update-FormatData -AppendPath $ps1xmlPath
$a = [PSCustomObject]@{First=1;Second=2;Third=3}
$a.PSObject.TypeNames.Insert(0,"Test.Format")
$a | Format-Table -View $view | Out-String
} ).AddArgument($ps1xmlPath).AddArgument($view) | Out-Null
$output = $ps.Invoke()
foreach ($e in $ps.Streams.Error)
{
Write-Verbose $e.ToString() -Verbose
}
$ps.HadErrors | Should -BeFalse
$output.Replace("`r","").Replace(" ",".").Replace("`n","``") | Should -BeExactly $expectedTable.Replace("`r","").Replace(" ",".").Replace("`n","``")
}
}