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 e7583d893..11ad81ade 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 4b04786b9..0ce667322 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 b57d77c27..6be949ece 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 3537ab5b6..b8373d8e5 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 fee7302f2..68506a46e 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)