From b9dffd86454d96b2d175efb7ba65e2d67e6eb44d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 09:07:32 +0000 Subject: [PATCH] Reflow: per-line CR handling; do not wrap before a marker-like word - The CRLF handling applied only when every line of the paragraph ended in CR, so the last paragraph of a CRLF file without a final newline (and mixed CRLF/LF paragraphs) still had CRs joined into the middle of lines. Each line's CR is now stripped; if any line had one, every new line but the last ends in CR, and the last keeps what the paragraph's last line had. - Wrapping never starts a line with a word that reads as a list marker ('-', '*', '+', '1.', '2)'); the next reflow turned such a paragraph into a list. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB --- Command.cc | 29 +++++++++++++++++++++++------ tests/test_audit_regressions.cc | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/Command.cc b/Command.cc index cf80920..b6ba53e 100644 --- a/Command.cc +++ b/Command.cc @@ -5172,15 +5172,19 @@ cmd_reflow_paragraph(CommandContext &ctx) if (para_start > para_end) return false; // CRLF text: reflow the lines without their CR (it would otherwise be - // joined into the middle of lines) and end every new line with one. - bool crlf = true; - for (std::size_t i = para_start; i <= para_end && crlf; ++i) { + // joined into the middle of lines). If any line had one, every new line + // but the last gets one; the last keeps whatever the paragraph's last + // line had (a CRLF file often lacks the final line ending). + bool crlf = false; + for (std::size_t i = para_start; i <= para_end && !crlf; ++i) { const std::string_view lv = line_view(rows[i]); crlf = !lv.empty() && lv.back() == '\r'; } + const std::string_view last_lv = line_view(rows[para_end]); + const bool last_cr = !last_lv.empty() && last_lv.back() == '\r'; auto row_text = [&](std::size_t i) { std::string t = static_cast(rows[i]); - if (crlf) + if (!t.empty() && t.back() == '\r') t.pop_back(); return t; }; @@ -5266,6 +5270,15 @@ cmd_reflow_paragraph(CommandContext &ctx) return out.substr(start, end - start); }; + auto looks_like_marker = [](const std::string &wrd) { + if (wrd == "-" || wrd == "+" || wrd == "*") + return true; + std::size_t i = 0; + while (i < wrd.size() && std::isdigit(static_cast(wrd[i]))) + ++i; + return i > 0 && i + 1 == wrd.size() && (wrd[i] == '.' || wrd[i] == ')'); + }; + auto wrap_with_prefixes = [&](const std::string &content, const std::string &first_prefix, const std::string &cont_prefix, @@ -5305,7 +5318,11 @@ cmd_reflow_paragraph(CommandContext &ctx) // first word on a line: that emitted a line holding only the // prefix (a blank line, or a bullet marker split from its text) // ahead of a word longer than the width, again on every reflow. - if (!first_word_on_line && static_cast(cur_len + needed) > w) { + // Nor before a word that would read as a list marker at the + // start of a line ("-", "*", "1."): the next reflow would turn + // the paragraph into a list. Such a word stays on this line. + if (!first_word_on_line && static_cast(cur_len + needed) > w && + !looks_like_marker(wrd)) { flush_line(); } if (!first_word_on_line) { @@ -5474,7 +5491,7 @@ cmd_reflow_paragraph(CommandContext &ctx) if (i > 0) new_text.push_back('\n'); new_text += new_lines[i]; - if (crlf) + if (crlf && (i + 1 < new_lines.size() || last_cr)) new_text.push_back('\r'); } if (new_text != old_text) diff --git a/tests/test_audit_regressions.cc b/tests/test_audit_regressions.cc index 5934eef..babbf37 100644 --- a/tests/test_audit_regressions.cc +++ b/tests/test_audit_regressions.cc @@ -1592,6 +1592,37 @@ TEST(Audit_Reflow_BlankishSeparatorAndCrlf) ASSERT_TRUE(h.Undo()); ASSERT_EQ(h.Text(), std::string("one two\r\nthree four\r\nfive\r\n\r\nnext\r\n")); } + { + // CRLF file without a final line ending: the last line has no CR. + TestHarness h; + h.Buf().insert_text(0, 0, "aaa bbb\r\nccc ddd\r\neee"); + h.Buf().SetCursor(0, 0); + ASSERT_TRUE(h.Exec(CommandId::ReflowParagraph)); + ASSERT_EQ(h.Text(), std::string("aaa bbb ccc ddd eee")); + } + { + TestHarness h; + h.Buf().insert_text(0, 0, "aaa bbb\r\nccc ddd\r\neee"); + h.Buf().SetCursor(0, 0); + ASSERT_TRUE(h.Exec(CommandId::ReflowParagraph, "", 8)); + ASSERT_EQ(h.Text(), std::string("aaa bbb\r\nccc ddd\r\neee")); + } +} + + +// Reflow does not wrap right before a word that would then read as a list +// marker ("-", "1."), which turned the paragraph into a list next time. +TEST(Audit_Reflow_NoWrapBeforeMarkerWord) +{ + TestHarness h; + h.Buf().insert_text(0, 0, "alpha beta - gamma delta\n"); + h.Buf().SetCursor(0, 0); + ASSERT_TRUE(h.Exec(CommandId::ReflowParagraph, "", 11)); + const std::string once = h.Text(); + ASSERT_EQ(once, std::string("alpha beta -\ngamma delta\n")); + h.Buf().SetCursor(0, 0); + ASSERT_TRUE(h.Exec(CommandId::ReflowParagraph, "", 11)); + ASSERT_EQ(h.Text(), once); }