Support numbered lists in reflow-paragraph.

Add `reflow-paragraph` tests for numbered lists with hanging indents and extend support for numbered list parsing and wrapping logic.
This commit is contained in:
2026-02-10 21:23:20 -08:00
parent d2d155f211
commit 2551388420
3 changed files with 200 additions and 26 deletions

View File

@@ -301,10 +301,15 @@ if (BUILD_TESTS)
tests/test_buffer_io.cc
tests/test_piece_table.cc
tests/test_search.cc
tests/test_reflow_paragraph.cc
# minimal engine sources required by Buffer
PieceTable.cc
Buffer.cc
Editor.cc
Command.cc
HelpText.cc
Swap.cc
OptimizedSearch.cc
UndoNode.cc
UndoTree.cc

View File

@@ -4026,6 +4026,29 @@ cmd_reflow_paragraph(CommandContext &ctx)
return false;
};
auto is_numbered_line = [&](const std::string &s,
std::string &indent_out,
std::string &marker_out,
std::size_t &after_prefix_idx) -> bool {
indent_out = leading_ws(s);
std::size_t i = indent_out.size();
if (i >= s.size() || !std::isdigit(static_cast<unsigned char>(s[i])))
return false;
std::size_t j = i;
while (j < s.size() && std::isdigit(static_cast<unsigned char>(s[j])))
++j;
if (j >= s.size())
return false;
char delim = s[j];
if (!(delim == '.' || delim == ')'))
return false;
if (j + 1 >= s.size() || s[j + 1] != ' ')
return false;
marker_out = s.substr(i, (j - i) + 1); // e.g. "1." or "10)"
after_prefix_idx = j + 2; // after delimiter + space
return true;
};
auto normalize_spaces = [](const std::string &in) {
std::string out;
out.reserve(in.size());
@@ -4107,20 +4130,25 @@ cmd_reflow_paragraph(CommandContext &ctx)
std::vector<std::string> new_lines;
// Determine if this region looks like a list: any line starting with bullet
bool region_has_bullet = false;
// Determine if this region looks like a list: any line starting with bullet or number
bool region_has_list = false;
for (std::size_t i = para_start; i <= para_end; ++i) {
std::string s = static_cast<std::string>(rows[i]);
std::string indent;
char marker;
std::size_t idx;
if (is_bullet_line(s, indent, marker, idx)) {
region_has_bullet = true;
region_has_list = true;
break;
}
std::string nmarker;
if (is_numbered_line(s, indent, nmarker, idx)) {
region_has_list = true;
break;
}
}
if (region_has_bullet) {
if (region_has_list) {
// Parse as list items; support hanging indent continuations
for (std::size_t i = para_start; i <= para_end; ++i) {
std::string s = static_cast<std::string>(rows[i]);
@@ -4148,12 +4176,46 @@ cmd_reflow_paragraph(CommandContext &ctx)
if (is_bullet_line(ns, nindent, nmarker, nidx)) {
break; // next item
}
std::string nnmarker;
if (is_numbered_line(ns, nindent, nnmarker, nidx)) {
break; // next item
}
// Not a continuation and not a bullet: stop (treat as separate paragraph chunk)
break;
}
content = normalize_spaces(content);
wrap_with_prefixes(content, first_prefix, cont_prefix, width, new_lines);
i = j - 1; // advance
} else {
std::string nmarker;
if (is_numbered_line(s, indent, nmarker, after_idx)) {
std::string first_prefix = indent + nmarker + " ";
std::string cont_prefix = indent + std::string(nmarker.size() + 1, ' ');
std::string content = s.substr(after_idx);
// consume continuation lines that are part of this numbered item
std::size_t j = i + 1;
while (j <= para_end) {
std::string ns = static_cast<std::string>(rows[j]);
if (starts_with(ns, cont_prefix)) {
content += ' ';
content += ns.substr(cont_prefix.size());
++j;
continue;
}
// stop if next item
std::string nindent2;
char bmarker;
std::size_t nidx;
if (is_bullet_line(ns, nindent2, bmarker, nidx))
break;
std::string nnmarker;
if (is_numbered_line(ns, nindent2, nnmarker, nidx))
break;
break;
}
content = normalize_spaces(content);
wrap_with_prefixes(content, first_prefix, cont_prefix, width, new_lines);
i = j - 1;
} else {
// A non-bullet line within a list region; treat as its own wrapped paragraph preserving its indent
std::string base_indent = leading_ws(s);
@@ -4168,6 +4230,10 @@ cmd_reflow_paragraph(CommandContext &ctx)
if (is_bullet_line(ns, tmp_indent, tmp_marker, tmp_idx)) {
break; // next bullet starts
}
std::string tmp_nmarker;
if (is_numbered_line(ns, tmp_indent, tmp_nmarker, tmp_idx)) {
break; // next numbered starts
}
if (nindent.size() >= base_indent.size()) {
content += ' ';
content += ns.substr(base_indent.size());
@@ -4181,6 +4247,7 @@ cmd_reflow_paragraph(CommandContext &ctx)
i = j - 1;
}
}
}
} else {
// Normal paragraph: preserve indentation of first line
std::string s0 = static_cast<std::string>(rows[para_start]);

View File

@@ -0,0 +1,102 @@
#include "Test.h"
#include "Buffer.h"
#include "Command.h"
#include "Editor.h"
#include <iostream>
#include <string>
static std::string
to_string_rows(const Buffer &buf)
{
std::string out;
for (const auto &r: buf.Rows()) {
out += static_cast<std::string>(r);
out.push_back('\n');
}
return out;
}
TEST (ReflowParagraph_NumberedList_HangingIndent)
{
InstallDefaultCommands();
Editor ed;
ed.SetDimensions(24, 80);
Buffer b;
// Two list items in one paragraph (no blank lines).
// Second line of each item already uses a hanging indent.
const std::string initial =
"1. one two three four five six seven eight nine ten eleven\n"
" twelve thirteen fourteen\n"
"10. alpha beta gamma delta epsilon zeta eta theta iota kappa lambda\n"
" mu nu xi omicron\n";
b.insert_text(0, 0, initial);
// Put cursor on first item
b.SetCursor(0, 0);
ed.AddBuffer(std::move(b));
Buffer *buf = ed.CurrentBuffer();
ASSERT_TRUE(buf != nullptr);
const int width = 25;
ASSERT_TRUE(Execute(ed, std::string("reflow-paragraph"), std::string(), width));
const auto &rows = buf->Rows();
ASSERT_TRUE(!rows.empty());
const std::string dump = to_string_rows(*buf);
// Find the start of the second item.
bool any_too_long = false;
std::size_t idx_10 = rows.size();
for (std::size_t i = 0; i < rows.size(); ++i) {
const std::string line = static_cast<std::string>(rows[i]);
if (static_cast<int>(line.size()) > width)
any_too_long = true;
if (line.rfind("10. ", 0) == 0) {
idx_10 = i;
break;
}
}
ASSERT_TRUE(idx_10 < rows.size());
if (any_too_long) {
std::cerr << "Reflow produced a line longer than width=" << width << "\n";
std::cerr << to_string_rows(*buf) << "\n";
}
EXPECT_TRUE(!any_too_long);
// Item 1: first line has "1. ", continuation lines have 3 spaces.
for (std::size_t i = 0; i < idx_10; ++i) {
const std::string line = static_cast<std::string>(rows[i]);
if (i == 0) {
ASSERT_TRUE(line.rfind("1. ", 0) == 0);
} else {
ASSERT_TRUE(line.rfind(" ", 0) == 0);
ASSERT_TRUE(line.rfind("1. ", 0) != 0);
}
}
// Item 10: first line has "10. ", continuation lines have 4 spaces.
ASSERT_TRUE(static_cast<std::string>(rows[idx_10]).rfind("10. ", 0) == 0);
bool bad_10 = false;
for (std::size_t i = idx_10 + 1; i < rows.size(); ++i) {
const std::string line = static_cast<std::string>(rows[i]);
if (line.empty())
break; // paragraph terminator / trailing empty line
if (line.rfind(" ", 0) != 0)
bad_10 = true;
if (line.rfind("10. ", 0) == 0)
bad_10 = true;
}
if (bad_10) {
std::cerr << "Unexpected prefix in reflow output:\n" << dump << "\n";
}
ASSERT_TRUE(!bad_10);
// Debug helper if something goes wrong (kept as a string for easy inspection).
EXPECT_TRUE(!to_string_rows(*buf).empty());
}