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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent 790fbc90e1
commit b9dffd8645
2 changed files with 54 additions and 6 deletions
+23 -6
View File
@@ -5172,15 +5172,19 @@ cmd_reflow_paragraph(CommandContext &ctx)
if (para_start > para_end) if (para_start > para_end)
return false; return false;
// CRLF text: reflow the lines without their CR (it would otherwise be // 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. // joined into the middle of lines). If any line had one, every new line
bool crlf = true; // but the last gets one; the last keeps whatever the paragraph's last
for (std::size_t i = para_start; i <= para_end && crlf; ++i) { // 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]); const std::string_view lv = line_view(rows[i]);
crlf = !lv.empty() && lv.back() == '\r'; 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) { auto row_text = [&](std::size_t i) {
std::string t = static_cast<std::string>(rows[i]); std::string t = static_cast<std::string>(rows[i]);
if (crlf) if (!t.empty() && t.back() == '\r')
t.pop_back(); t.pop_back();
return t; return t;
}; };
@@ -5266,6 +5270,15 @@ cmd_reflow_paragraph(CommandContext &ctx)
return out.substr(start, end - start); 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<unsigned char>(wrd[i])))
++i;
return i > 0 && i + 1 == wrd.size() && (wrd[i] == '.' || wrd[i] == ')');
};
auto wrap_with_prefixes = [&](const std::string &content, auto wrap_with_prefixes = [&](const std::string &content,
const std::string &first_prefix, const std::string &first_prefix,
const std::string &cont_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 // first word on a line: that emitted a line holding only the
// prefix (a blank line, or a bullet marker split from its text) // prefix (a blank line, or a bullet marker split from its text)
// ahead of a word longer than the width, again on every reflow. // ahead of a word longer than the width, again on every reflow.
if (!first_word_on_line && static_cast<int>(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<int>(cur_len + needed) > w &&
!looks_like_marker(wrd)) {
flush_line(); flush_line();
} }
if (!first_word_on_line) { if (!first_word_on_line) {
@@ -5474,7 +5491,7 @@ cmd_reflow_paragraph(CommandContext &ctx)
if (i > 0) if (i > 0)
new_text.push_back('\n'); new_text.push_back('\n');
new_text += new_lines[i]; new_text += new_lines[i];
if (crlf) if (crlf && (i + 1 < new_lines.size() || last_cr))
new_text.push_back('\r'); new_text.push_back('\r');
} }
if (new_text != old_text) if (new_text != old_text)
+31
View File
@@ -1592,6 +1592,37 @@ TEST(Audit_Reflow_BlankishSeparatorAndCrlf)
ASSERT_TRUE(h.Undo()); ASSERT_TRUE(h.Undo());
ASSERT_EQ(h.Text(), std::string("one two\r\nthree four\r\nfive\r\n\r\nnext\r\n")); 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);
} }