From 0075c7aa8678e167633e385d6d798adf06dc60c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 04:06:55 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB --- PieceTable.cc | 7 +++++-- tests/test_audit_regressions.cc | 36 +++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/PieceTable.cc b/PieceTable.cc index a8583a8..1e923cf 100644 --- a/PieceTable.cc +++ b/PieceTable.cc @@ -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(end_idx)); pieces_.insert(pieces_.begin() + static_cast(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_++; diff --git a/tests/test_audit_regressions.cc b/tests/test_audit_regressions.cc index c9a25a8..dc94142 100644 --- a/tests/test_audit_regressions.cc +++ b/tests/test_audit_regressions.cc @@ -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); +}