Large files: bound highlighter cache, scan search once, row ranges as one edit

- HighlighterEngine caches spans only for rows near the requested row
  and trims the cache around it past 16k entries; per-row end states are
  still kept to resume lexing. Jumping to the end of a 78 MB C++ file
  cached every row's spans (854 MB RSS, now 197 MB), and the first edit
  near the top spent 270 ms freeing them.
- Plain and regex search read lines as views into the materialized
  buffer instead of Buffer::Rows(), which built (and kept) a string per
  line, O(lines x pieces) after edits. Plain search scans the whole text
  once and tracks rows by newline.
- transform_rows() applies a per-row rewrite of a row range as one edit
  (replace_rows_text). Indent/unindent region and visual-line insert,
  backspace, delete, newline and yank used one piece-table edit per row,
  each linear in the buffer: 1.1 s per keystroke with 5,000 rows selected
  in a 26 MB file (now 4 ms), and minutes to indent a large region.

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 b9dffd8645
commit bba7d34539
5 changed files with 244 additions and 168 deletions
+82
View File
@@ -1648,3 +1648,85 @@ TEST(Audit_CountedDelete_AcrossJoin_OneUndo)
ASSERT_EQ(h.Text(), std::string("abc\nde"));
}
}
// Plain search scans the materialized text in one pass; it must report the
// same matches as a per-line search, also after scattered edits (many
// pieces) and at line boundaries.
TEST(Audit_Search_WholeBufferScanMatchesPerLine)
{
std::mt19937 rng(7);
const char alphabet[] = {'a', 'b', '\n', 'a', 'b', ' ', '\r'};
for (int iter = 0; iter < 40; ++iter) {
TestHarness h;
std::string text;
const int n = 50 + static_cast<int>(rng() % 400);
for (int i = 0; i < n; ++i)
text.push_back(alphabet[rng() % sizeof(alphabet)]);
h.Buf().insert_text(0, 0, text);
// Scattered edits fragment the piece table.
for (int e = 0; e < 20; ++e) {
const std::size_t y = rng() % h.Buf().Nrows();
h.Buf().insert_text(static_cast<int>(y), 0, (rng() % 2) ? "ab" : "b");
}
const std::string all = h.Text();
const std::string q = (iter % 3 == 0) ? "ab" : (iter % 3 == 1) ? "b a" : "a";
// Per-line reference.
std::vector<std::pair<std::size_t, std::size_t> > want;
std::size_t y = 0, start = 0;
while (true) {
const std::size_t nl = all.find('\n', start);
const std::string line = all.substr(start, nl == std::string::npos ? std::string::npos : nl - start);
for (std::size_t p = line.find(q); p != std::string::npos; p = line.find(q, p + q.size()))
want.emplace_back(y, p);
if (nl == std::string::npos)
break;
start = nl + 1;
++y;
}
h.Buf().SetCursor(0, 0);
ASSERT_TRUE(h.Exec(CommandId::FindStart));
ASSERT_TRUE(h.Exec(CommandId::InsertText, q));
if (want.empty()) {
ASSERT_TRUE(h.EditorRef().Status().find(" 1/") == std::string::npos);
} else {
ASSERT_TRUE(h.EditorRef().Status().find("1/" + std::to_string(want.size())) != std::string::npos);
for (std::size_t i = 0; i < want.size() && i < 10; ++i) {
ASSERT_EQ(h.Buf().Cury(), want[i].first);
ASSERT_EQ(h.Buf().Curx(), want[i].second);
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
}
}
ASSERT_TRUE(h.Exec(CommandId::Refresh));
}
}
// Indent/unindent and visual-line edits over many rows are one edit, not
// one per row (each costing time linear in the buffer): indenting 100k
// rows took minutes. Generous bound; typically well under a second.
TEST(Audit_RowRangeEdits_Scale)
{
TestHarness h;
std::string text;
for (int i = 0; i < 100000; ++i)
text += "line " + std::to_string(i) + "\n";
h.Buf().insert_text(0, 0, text);
h.Buf().SetCursor(0, 0);
const auto t0 = std::chrono::steady_clock::now();
ASSERT_TRUE(h.Exec(CommandId::MarkAllAndJumpEnd));
ASSERT_TRUE(h.Exec(CommandId::IndentRegion));
ASSERT_EQ(h.Buf().GetLineString(99999), std::string("\tline 99999"));
h.Buf().SetCursor(0, 0);
ASSERT_TRUE(h.Exec(CommandId::VisualLineModeToggle));
for (int i = 0; i < 50000; ++i)
(void) Execute(h.EditorRef(), CommandId::MoveDown);
ASSERT_TRUE(h.Exec(CommandId::InsertText, "x"));
ASSERT_EQ(h.Buf().GetLineString(49999), std::string("x\tline 49999"));
ASSERT_EQ(h.Buf().GetLineString(50001), std::string("\tline 50001"));
ASSERT_TRUE(h.Undo());
ASSERT_TRUE(h.Undo());
const auto secs = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
ASSERT_EQ(h.Text(), text);
ASSERT_TRUE(secs < 20.0);
}