Fix progress bar rendering with double-width unicode characters#26185
Conversation
40387ce to
b837291
Compare
|
@microsoft-github-policy-service agree |
|
It would be great to have tests. Perhaps we could use our TestHost from test\tools\Modules\HelpersHostCS |
818382e to
b837291
Compare
Added Tests and Fixed Padding BugThank you for the feedback! I've added comprehensive tests using Tests AddedAdded test file
The tests use
Bug Found and FixedWhile running the tests, I discovered the original PR code still had a bug: Problem: Lines 410 and 494 had incorrect padding logic // Line 410 - character count based, not cell width based
int padding = maxWidth + PSStyle.Instance.Progress.Style.Length
+ PSStyle.Instance.Reverse.Length
+ PSStyle.Instance.ReverseOff.Length;
// Line 494 - PadRight() adds extra spaces for double-width characters
.PadRight(padding)Why it was wrong:
Fix: Removed both the // Line 410 - deleted
// Line 492 - changed from:
PSStyle.Instance.Reset)
.PadRight(padding));
// to:
PSStyle.Instance.Reset));Why this is correct:
Test Results
All tests now pass, confirming Issue #21293 is completely resolved. Changes Summary
Status: Tests passing (9/9). Found an additional truncation edge case during manual testing - fixing |
|
@yotsuda Thank you for your efforts! I'm afraid that these tests are too low-level and they won't work if the implementation changes. It would be great if you used the module that I specified to create tests on PowerShell. |
a5227d3 to
382d0e4
Compare
Update: Replaced C# xUnit tests with PowerShell testsThank you for the feedback. I have replaced the low-level C# xUnit tests with PowerShell-based tests as requested. Changes in this update:
About the tests:The new tests verify that
Technical limitation:I wanted to clarify a technical constraint: It is not possible to verify the actual rendering width (that the progress bar does not exceed maxWidth) from PowerShell tests. Here's why:
What the current tests verify:
What cannot be verified at PowerShell level:
Manual verification:I have manually verified the fix works correctly by:
I believe this approach aligns with your guidance to use PowerShell-level tests rather than implementation-specific C# tests. However, if there is a way to verify the rendering width from PowerShell that I'm not aware of, I'd be happy to add those tests. Let me know if you have any suggestions or if this approach is acceptable. |
|
@yotsuda Please don't rebase until maintainers ask because this kills commit history and force them to re-review. |
|
@iSazonov Thanks for letting me know. |
|
/azp run PowerShell-Windows-Packaging-CI, PowerShell-CI-linux-packaging |
|
Azure Pipelines could not run because the pipeline triggers exclude this branch/path. |
There was a problem hiding this comment.
Pull Request Overview
This PR fixes a rendering corruption issue (#21293) where progress bars display incorrectly when StatusDescription contains double-width Unicode characters (Japanese, Chinese, Korean, emojis). The root cause was using string.Length for width calculations, which counts characters rather than buffer cells—double-width characters occupy 2 buffer cells but count as 1 character.
Key changes:
- Modified
RenderAnsito acceptrawUIparameter for accurate buffer cell width calculations - Replaced
string.LengthwithLengthInBufferCells()andTruncateToBufferCellWidth()for proper display width handling - Added character-by-character VT sequence insertion position calculation to respect character boundaries
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| test/powershell/Modules/Microsoft.PowerShell.Utility/Write-Progress.DoubleWidth.Tests.ps1 | Adds comprehensive test coverage for double-width characters in progress bars across multiple languages and scenarios |
| src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs | Fixes progress bar rendering by using buffer cell width calculations instead of character counts for Unicode text |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
|
@yotsuda Please look Copilot comments and rebase to get latest build workflows. |
Fixes PowerShell#21293 The progress bar was corrupted when StatusDescription contained Japanese or other double-width unicode characters. The closing bracket would appear on the next line and remain after completion. Root cause: The code used string.Length to calculate display widths, which counts characters rather than buffer cells. Double-width characters (CJK, emojis) occupy 2 buffer cells but count as 1 character. Changes: - Add rawUI parameter to RenderAnsi method for buffer cell calculations - Calculate display widths before adding VT sequences - Use LengthInBufferCells() for Activity and StatusDescription - Use TruncateToBufferCellWidth() for proper truncation - Calculate VT insertion position character-by-character to respect character boundaries - Fix char to string conversion for LengthInBufferCells calls Tested with: - Japanese, Chinese (Simplified/Traditional), Korean - Emojis - Thai (complex combining characters) - Arabic (RTL text) - Mixed languages - Long strings (truncation) All tests passed successfully. The progress bar now renders correctly with any unicode characters.
Fixes PowerShell#21293 This commit addresses an issue where progress bars containing double-width Unicode characters (such as Japanese, Chinese, Korean, and emoji) would exceed the terminal width, causing rendering issues. Changes: - Remove incorrect padding calculation that used character count - Properly calculate ellipsis width using LengthInBufferCells() - Remove unnecessary PadRight() call that caused width overflow - All width calculations now consistently use buffer cell width The fix ensures that the progress bar never exceeds maxWidth by: 1. Calculating the actual display width of the ellipsis 2. Removing the character-count-based padding variable 3. Relying on the correct emptyPadLength calculation that already uses LengthInBufferCells() Tests: - Add PowerShell-based test suite (Write-Progress.DoubleWidth.Tests.ps1) - 11 test cases covering various scenarios: - Japanese, Chinese, Korean characters - Emoji and mixed-width text - Very long text requiring truncation - Parent-child progress relationships - All tests verify the cmdlet handles double-width characters without crashing Note: The tests verify functional correctness but cannot verify the actual rendering width at PowerShell test level, as Write-Progress renders directly to the console and does not output to stdout/stderr.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…gress.DoubleWidth.Tests.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
8c9c630 to
281a5cb
Compare
SummaryReviewed and incorporated Copilot's suggestions. Rebase completed. ChangesApplied both fixes (accurate width calculation and performance optimization). Details in Copilot's comments above. VerificationBuilt and tested with Japanese, Chinese, Korean text and emojis. Issue #21293 remains fixed. StatusReady for re-review. Apologies for the oversight. |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
|
@iSazonov Thank you for the feedback! I believe the test_ProgressNode.cs that I removed based on your feedback was actually doing what you're suggesting - testing the rendering output (ANSI sequences), not just the Progress stream data. That test:
Regarding alternative testing approaches:
My question: |
|
@yotsuda PowerShell uses many overloaded layers for outputting to host terminal. Unit tests skip the layers and does not guaranty that user see on host terminal. That's why we need tests on PowerShell itself, not on C#. To enable VT sequences in the test host there is SupportsVirtualTerminal property, I think we need to set it to true. |
|
@iSazonov I investigated the test framework and found that the C# code in public override void WriteProgress(long sourceId, ProgressRecord record)
{
Streams.Progress.Add(record); // Only stores the record, no rendering
}Therefore, it seems impossible to write an appropriate test for this PR using .ps1 tests. I believe we need either:
However, rewriting the test framework appears technically challenging. Even if possible, I lack the knowledge to assess the impact on other tests, so this work would need to be done by someone from the PowerShell team. What approach would you recommend? |
|
I looked at commit history and there really aren't any rendering tests. Writing reliable tests requires improving the test module, which is beyond the scope of this change. (Although you can do it if you want.) So let's ditch script tests in favor of xUnit ones. |
…Shell#21293) Replace Pester tests with comprehensive xUnit tests covering: - 10 test scenarios with 144 total test cases - Multiple window widths including odd (25, 73, 93, 155) and irregular sizes - Double-width characters (Japanese, Chinese, Korean, emoji) - Edge cases: truncation, surrogate pairs, mixed-width text
| /// - Double-width characters (CJK, emoji) which occupy 2 cells | ||
| /// - Regular characters which occupy 1 cell | ||
| /// </summary> | ||
| public override int LengthInBufferCells(string str) |
There was a problem hiding this comment.
Tests have access to internal methods so no need to implement the method.
Update: Code Review Feedback AddressedThank you @iSazonov for the review feedback. I've made the following improvements: Main Changes (commits 463cd77..60f69c1)1. Simplified Test Implementation (35d2336)
2. Code Quality Improvements
3. Refactoring (5a77b9a)
4. Documentation (0d65578, 2c6230a)
Summary
The PR is now ready for another review. Please let me know if there are any other concerns. |
iSazonov
left a comment
There was a problem hiding this comment.
Only style comments at the time.
| [InlineData(20)] | ||
| [InlineData(25)] | ||
| [InlineData(37)] | ||
| [InlineData(40)] | ||
| [InlineData(60)] | ||
| [InlineData(73)] | ||
| [InlineData(80)] | ||
| [InlineData(93)] | ||
| [InlineData(100)] | ||
| [InlineData(120)] | ||
| [InlineData(155)] | ||
| [InlineData(200)] |
There was a problem hiding this comment.
Do we really need so many tests?
I'd check edge cases and and scenarios when the size of the screen width is -1, 0, +1 of the required?
| public override ConsoleColor ForegroundColor { get; set; } | ||
|
|
||
| public override ConsoleColor BackgroundColor { get; set; } | ||
|
|
||
| public override Coordinates CursorPosition { get; set; } |
There was a problem hiding this comment.
I'd remove blank all lines to compress the helper.
| public override void SetBufferContents(Rectangle rectangle, BufferCell fill) | ||
| { | ||
| throw new NotImplementedException(); | ||
| } |
There was a problem hiding this comment.
To compress the helper I suggest follow style:
| public override void SetBufferContents(Rectangle rectangle, BufferCell fill) | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| public override void SetBufferContents(Rectangle rectangle, BufferCell fill) => throw new NotImplementedException(); |
| // if the activity is really long, only use up to half the width | ||
| string activity; | ||
| if (Activity.Length > maxWidth / 2) | ||
| int activityDisplayWidth = rawUI.LengthInBufferCells(Activity); |
There was a problem hiding this comment.
Maybe rename to activityDisplayCellsWidth
| int emptyPadLength = barWidth - statusPartDisplayWidth - secRemainLength; | ||
| if (emptyPadLength > 0) | ||
| { | ||
| sb.Append(string.Empty.PadRight(emptyPadLength)); |
Changes in response to maintainer feedback on PR PowerShell#26185: ProgressNode.cs: - Rename activityDisplayWidth to activityDisplayCellsWidth for clarity - Use StringBuilder.Append(char, int) for better performance test_ProgressNode.cs: - Reduce test cases to edge cases around standard terminal width (79, 80, 81) - Simplify TestProgressRawUI with expression-bodied members Test results: 36/36 pass Before fix: 33/36 failed (91.7% failure rate) After fix: 36/36 pass (100% success rate)
Code Review Feedback AddressedThank you @iSazonov for the detailed review feedback. I've addressed your style comments in commit e0d89df. Changes MadeProgressNode.cs:
test_ProgressNode.cs:
Test ResultsBefore fix:
After fix:
VerificationBuilt and tested locally:
Ready for re-review. Please let me know if any other changes are needed. |
|
Friendly ping — this PR has been waiting for review for over 5 months. All feedback has been addressed (final commits on 2025-11-02, commit e0d89df), and only minor style comments were outstanding at the last review round, which have all been resolved. @SteveL-MSFT @iSazonov would you have time to take another look? The fix resolves #21293 (progress bar corruption with double-width characters) and is covered by the xUnit tests added in this PR. I'd really appreciate getting this merged. Thank you! |
|
@yotsuda Thanks for taking this on! Could you address the CodeFactor issues and the test failures? |
The xUnit build was failing with CA1852 (type can be sealed). Seal the TestProgressRawUI helper and move it into its own file, which also resolves CodeFactor's 'single type per file' on test_ProgressNode.cs. Remove the now-unused usings from test_ProgressNode.cs.
|
Thanks @jshigetomi! Pushed a fix.
Since this is a fork PR, the workflow runs are waiting on approval — could you approve them so CI can verify? |
|
@yotsuda Tests seem to be passing. Just a couple more CodeFactor issues left. |
Reduce the (pre-existing) cyclomatic complexity of RenderAnsi flagged by CodeFactor by extracting the status-truncation and ReverseOff-insertion logic into helpers. Behavior is unchanged and covered by the existing ProgressNode tests.
|
Pushed the The one remaining CodeFactor item is "file name should match first type name" on |
|
LGTM |
Summary
Fixes #21293 - Progress bar rendering corruption when
StatusDescriptioncontains Japanese or other double-width unicode characters.Problem
The progress bar's closing bracket
]would appear on the next line and remain visible after completion when using double-wide characters (Japanese, Chinese, Korean, emojis, etc.).Root cause: The code used
string.Lengthto calculate display widths, which counts characters rather than buffer cells. Double-width characters occupy 2 buffer cells but count as 1 character, causing incorrect width calculations.Solution
rawUIparameter toRenderAnsimethod for buffer cell calculationsLengthInBufferCells()before adding VT sequencesTruncateToBufferCellWidth()for proper text truncationLengthInBufferCellscallsKey Technical Details
Why PR #21303 Failed
PR #21303 attempted to fix this issue but was closed due to unresolved technical problems identified in code review:
LengthInBufferCells()on strings that already contained VT escape sequences, leading to incorrect calculationsLengthInBufferCellsdoesn't strip VT sequences when counting cellsThis PR's Approach
This PR solves the issue by separating concerns:
Before (incorrect):
After (correct):
Character Boundary Protection
The VT insertion position calculation respects character boundaries:
Performance Impact
Minimal. The change adds:
LengthInBufferCellscall for ActivityBackward Compatibility
✅ No breaking changes. The fix only affects internal calculations and produces correct output for all character types.
Testing
Comprehensive testing performed with:
All tests passed successfully. The progress bar now renders correctly with any unicode characters.
Related
This PR solves all technical issues that were left unresolved in PR #21303, which was closed due to unaddressed review comments about VT sequence handling.
Test Script
Click to view the comprehensive test script used (VerifyProgressFix.ps1)