From fb00d7be6ceb0503fecfb8a7058db4dfcec2973f Mon Sep 17 00:00:00 2001 From: KiGamji Date: Sun, 7 Sep 2025 22:36:45 +0500 Subject: [PATCH] Fixed Text.wrap trimming lines in no_wrap mode Previously, `rstrip_end` and `truncate` were being called even when soft wrapping was enabled, which led to visual bugs where trailing spaces and their backgrounds were incorrectly trimmed. This change ensures that line trimming logic is now correctly applied only during hard wrapping. This also updates the expected output in a `Syntax` test that was dependent on the old buggy behavior. Fixes #3841 --- CHANGELOG.md | 5 +++++ CONTRIBUTORS.md | 1 + rich/text.py | 7 +++---- tests/test_syntax.py | 2 +- tests/test_text.py | 16 ++++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7583d8936..11ad81ade0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed +- Fixed Text.wrap trimming lines in no_wrap mode https://github.com/Textualize/rich/issues/3841 + ## [14.1.0] - 2025-06-25 ### Changed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b04786b9c..0ce667322e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -47,6 +47,7 @@ The following people have contributed to the development of Rich: - [Antony Milne](https://github.com/AntonyMilneQB) - [Michael Milton](https://github.com/multimeric) - [Martina Oefelein](https://github.com/oefe) +- [Igor Oleynik](https://github.com/KiGamji) - [Nathan Page](https://github.com/nathanrpage97) - [Dave Pearson](https://github.com/davep/) - [Avi Perl](https://github.com/avi-perl) diff --git a/rich/text.py b/rich/text.py index b57d77c276..6be949ecea 100644 --- a/rich/text.py +++ b/rich/text.py @@ -1236,14 +1236,13 @@ def wrap( else: offsets = divide_line(str(line), width, fold=wrap_overflow == "fold") new_lines = line.divide(offsets) - for line in new_lines: - line.rstrip_end(width) + for line in new_lines: + line.rstrip_end(width) + line.truncate(width, overflow=wrap_overflow) if wrap_justify: new_lines.justify( console, width, justify=wrap_justify, overflow=wrap_overflow ) - for line in new_lines: - line.truncate(width, overflow=wrap_overflow) lines.extend(new_lines) return lines diff --git a/tests/test_syntax.py b/tests/test_syntax.py index 3537ab5b6b..b8373d8e5f 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -115,7 +115,7 @@ def test_python_render_simple_indent_guides() -> None: ) rendered_syntax = render(syntax) print(repr(rendered_syntax)) - expected = '\x1b[34mdef\x1b[0m\x1b[37m \x1b[0m\x1b[32mloop_first_last\x1b[0m(values: Iterable[T]) -> Iterable[Tuple[\x1b[36mb\x1b[0m\n\x1b[2;37m│ \x1b[0m\x1b[33m"""Iterate and generate a tuple with a flag for first an\x1b[0m\n\x1b[2m│ \x1b[0miter_values = \x1b[36miter\x1b[0m(values)\n\x1b[2m│ \x1b[0m\x1b[34mtry\x1b[0m:\n\x1b[2m│ │ \x1b[0mprevious_value = \x1b[36mnext\x1b[0m(iter_values)\n\x1b[2m│ \x1b[0m\x1b[34mexcept\x1b[0m \x1b[36mStopIteration\x1b[0m:\n\x1b[2m│ │ \x1b[0m\x1b[34mreturn\x1b[0m\n\x1b[2m│ \x1b[0mfirst = \x1b[34mTrue\x1b[0m\n\x1b[2m│ \x1b[0m\x1b[34mfor\x1b[0m value \x1b[35min\x1b[0m iter_values:\n\x1b[2m│ │ \x1b[0m\x1b[34myield\x1b[0m first, \x1b[34mFalse\x1b[0m, previous_value\n\x1b[2m│ │ \x1b[0mfirst = \x1b[34mFalse\x1b[0m\n\x1b[2m│ │ \x1b[0mprevious_value = value\n\x1b[2m│ \x1b[0m\x1b[34myield\x1b[0m first, \x1b[34mTrue\x1b[0m, previous_value\n' + expected = '\x1b[34mdef\x1b[0m\x1b[37m \x1b[0m\x1b[32mloop_first_last\x1b[0m(values: Iterable[T]) -> Iterable[Tuple[\x1b[36mbool\x1b[0m, \x1b[36mbool\x1b[0m, T]]:\n\x1b[2;37m│ \x1b[0m\x1b[33m"""Iterate and generate a tuple with a flag for first and last value."""\x1b[0m\n\x1b[2m│ \x1b[0miter_values = \x1b[36miter\x1b[0m(values)\n\x1b[2m│ \x1b[0m\x1b[34mtry\x1b[0m:\n\x1b[2m│ │ \x1b[0mprevious_value = \x1b[36mnext\x1b[0m(iter_values)\n\x1b[2m│ \x1b[0m\x1b[34mexcept\x1b[0m \x1b[36mStopIteration\x1b[0m:\n\x1b[2m│ │ \x1b[0m\x1b[34mreturn\x1b[0m\n\x1b[2m│ \x1b[0mfirst = \x1b[34mTrue\x1b[0m\n\x1b[2m│ \x1b[0m\x1b[34mfor\x1b[0m value \x1b[35min\x1b[0m iter_values:\n\x1b[2m│ │ \x1b[0m\x1b[34myield\x1b[0m first, \x1b[34mFalse\x1b[0m, previous_value\n\x1b[2m│ │ \x1b[0mfirst = \x1b[34mFalse\x1b[0m\n\x1b[2m│ │ \x1b[0mprevious_value = value\n\x1b[2m│ \x1b[0m\x1b[34myield\x1b[0m first, \x1b[34mTrue\x1b[0m, previous_value\n' assert rendered_syntax == expected diff --git a/tests/test_text.py b/tests/test_text.py index fee7302f2b..68506a46e6 100644 --- a/tests/test_text.py +++ b/tests/test_text.py @@ -641,6 +641,22 @@ def test_no_wrap_no_crop(): ) +def test_no_wrap_no_strip_trailing_space(): + """Test that Text.wrap doesn't strip trailing spaces from styled segments in no wrap mode.""" + console = Console(width=40) + + text = Text() + text.append("x" * 35) + text.append(" test ", style="white on blue") + + lines = text.wrap(console, width=40, no_wrap=True) + + assert len(lines) == 1 + result_text = lines[0] + + assert result_text.plain == "x" * 35 + " test " + + def test_fit(): text = Text("Hello\nWorld") lines = text.fit(3)