diff --git a/docs/changelog.rst b/docs/changelog.rst index 6cc7e6102..b083c7b18 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -229,6 +229,8 @@ Detailed list of changes - hints kitten: Fix trailing punctuation not being removed from URLs (:pull:`9828`) +- Fix copy/paste dropping spaces at soft-wrap boundaries when ``strip_trailing_spaces`` is set (:iss:`9834`) + 0.46.2 [2026-03-21] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/screen.c b/kitty/screen.c index c0e8864b2..705fc5656 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -3967,7 +3967,10 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st while (x_limit && !line->cpu_cells[x_limit - 1].temp_flag) x_limit--; while (x_start < x_limit && !line->cpu_cells[x_start].temp_flag) x_start++; bool is_only_whitespace_line = false; - if (strip_trailing_whitespace) { + // Don't strip trailing whitespace from soft-wrapped lines as those spaces + // are part of the original text content that continues on the next line + const bool line_is_continued = x_limit == line->xnum && line->cpu_cells[line->xnum-1].next_char_was_wrapped; + if (strip_trailing_whitespace && !line_is_continued) { index_type new_limit = limit_without_trailing_whitespace(line, x_limit); if (new_limit != x_limit) { x_limit = new_limit; @@ -4018,7 +4021,10 @@ ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st while (x_limit && !line->cpu_cells[x_limit - 1].temp_flag) x_limit--; while (x_start < x_limit && !line->cpu_cells[x_start].temp_flag) x_start++; bool is_only_whitespace_line = false; - if (strip_trailing_whitespace) { + // Don't strip trailing whitespace from soft-wrapped lines as those spaces + // are part of the original text content that continues on the next line + const bool line_is_continued = x_limit == line->xnum && line->cpu_cells[line->xnum-1].next_char_was_wrapped; + if (strip_trailing_whitespace && !line_is_continued) { index_type new_limit = limit_without_trailing_whitespace(line, x_limit); if (new_limit != x_limit) { x_limit = new_limit; diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index d0c604862..e444a6318 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -702,13 +702,15 @@ class TestScreen(BaseTest): s.start_selection(0, 0) s.update_selection(1, 3) self.ae(ts(), ''.join(('ab ', 'cd'))) - self.ae(ts(False, True), ''.join(('ab', 'cd'))) + # soft-wrapped lines preserve trailing spaces (issue #9834) + self.ae(ts(False, True), 'ab cd') s.reset() s.draw('ab cd') s.start_selection(0, 0) s.update_selection(3, 4) self.ae(s.text_for_selection(), ('ab ', ' ', 'cd')) - self.ae(s.text_for_selection(False, True), ('ab', '\n', 'cd')) + # soft-wrapped lines preserve trailing spaces (issue #9834) + self.ae(s.text_for_selection(False, True), ('ab ', ' ', 'cd')) s.reset() s.draw('a') s.select_graphic_rendition(32) @@ -719,7 +721,8 @@ class TestScreen(BaseTest): s.update_selection(1, 3) self.ae(s.text_for_selection(), ('abc ', 'xy')) self.ae(s.text_for_selection(True), ('a\x1b[32mb\x1b[39mc ', 'xy', '\x1b[m')) - self.ae(s.text_for_selection(True, True), ('a\x1b[32mb\x1b[39mc', 'xy', '\x1b[m')) + # soft-wrapped lines preserve trailing spaces in ANSI mode too (issue #9834) + self.ae(s.text_for_selection(True, True), ('a\x1b[32mb\x1b[39mc ', 'xy', '\x1b[m')) # ]]]]]]]]]]]]]]]]]]]] s.reset() s.draw('a'), s.carriage_return(), s.linefeed(), s.linefeed(), s.draw('b') @@ -727,6 +730,16 @@ class TestScreen(BaseTest): s.update_selection(4, 4) self.ae(''.join(s.text_for_selection()), 'a\n\nb') self.ae(''.join(s.text_for_selection(True)), 'a\n\nb') + # Trailing spaces on soft-wrapped lines must be preserved when strip_trailing_whitespace + # is set: a space at the line-wrap boundary is word-separator content, not padding + # (regression test for issue #9834) + s.reset() + s.draw('1234 5') # "1234 " soft-wraps at col 4; space must survive stripping + s.start_selection(0, 0) + s.update_selection(4, 4) + self.ae(s.text_for_selection(), ('1234 ', '5')) + self.ae(s.text_for_selection(False, True), ('1234 ', '5')) + self.ae(s.text_for_selection(True, True), ('1234 ', '5', '')) def test_soft_hyphen(self): s = self.create_screen()