PieceTable: keep the line index across consolidation; reset moved-from

Consolidation rewrites pieces without changing content or offsets but
invalidated the line index, so once a buffer passed the piece limit
every edit paid a full rescan again. A moved-from table kept the index
of the text it gave away, and Insert now updates that index in place,
producing ranges past the end of its buffer.

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 7b8ccbb8db
commit 0075c7aa86
2 changed files with 39 additions and 4 deletions
+5 -2
View File
@@ -74,6 +74,7 @@ PieceTable::PieceTable(PieceTable &&other) noexcept
{
other.dirty_ = true;
other.total_size_ = 0;
other.InvalidateLineIndex(); // its index described the moved-away text
version_ = other.version_;
range_cache_ = {};
find_cache_ = {};
@@ -93,6 +94,7 @@ PieceTable::operator=(PieceTable &&other) noexcept
total_size_ = other.total_size_;
other.dirty_ = true;
other.total_size_ = 0;
other.InvalidateLineIndex(); // its index described the moved-away text
version_ = other.version_;
range_cache_ = {};
find_cache_ = {};
@@ -603,9 +605,10 @@ PieceTable::consolidateRange(std::size_t start_idx, std::size_t end_idx)
pieces_.begin() + static_cast<std::ptrdiff_t>(end_idx));
pieces_.insert(pieces_.begin() + static_cast<std::ptrdiff_t>(start_idx), consolidated);
// total_size_ unchanged
// total_size_ unchanged. Content and byte offsets are unchanged too, so
// the line index stays valid (invalidating it here forced a full rescan
// on every edit once a buffer had many pieces).
dirty_ = true;
InvalidateLineIndex();
coalesceNeighbors(start_idx);
// Layout changed; invalidate caches/version
version_++;
+34 -2
View File
@@ -511,10 +511,10 @@ TEST(Audit_B6_HorizontalScroll_CountsCells)
// P2: the line index is updated in place by Insert/Delete. Query it after
// every edit (so it is never dirty) and compare with a plain string model.
TEST(Audit_P2_IncrementalLineIndex_MatchesModel)
static void
check_incremental_line_index(PieceTable &t)
{
std::mt19937 rng(20260923u);
PieceTable t;
std::string model;
const char alphabet[] = "ab\nc\n\nd";
for (int step = 0; step < 3000; ++step) {
@@ -546,3 +546,35 @@ TEST(Audit_P2_IncrementalLineIndex_MatchesModel)
}
}
}
TEST(Audit_P2_IncrementalLineIndex_MatchesModel)
{
PieceTable t;
check_incremental_line_index(t);
}
// Consolidation (forced here by a tiny piece limit) rewrites pieces without
// changing content; the incrementally maintained index must stay correct.
TEST(Audit_P2_IncrementalLineIndex_WithConsolidation)
{
PieceTable t(0, 8, 64, 1 << 20);
check_incremental_line_index(t);
}
// A moved-from table must not keep (and incrementally update) the index of
// the text it gave away.
TEST(Audit_PieceTable_MovedFrom_LineIndexReset)
{
const std::string text = "l1\nl2\nl3\n";
PieceTable a;
a.Append(text.data(), text.size());
ASSERT_EQ(a.LineCount(), (std::size_t) 4);
PieceTable b(std::move(a));
a.Insert(0, "x", 1);
ASSERT_EQ(a.Size(), (std::size_t) 1);
ASSERT_EQ(a.LineCount(), (std::size_t) 1);
ASSERT_EQ(b.LineCount(), (std::size_t) 4);
}