Perf: update the line index in place; renderers read only visible rows

- PieceTable::Insert/Delete adjust a valid line index directly (shift
  later line starts, add or drop starts for inserted or deleted
  newlines) instead of invalidating it, which forced a byte-by-byte
  rescan of the whole buffer on the next line query. On a 38 MB,
  1M-line buffer, insert + line query drops from 14.7 ms to 0.28 ms.
- The terminal and ImGui renderers fetch only the rows they draw
  (Nrows + GetLineString) instead of Buffer::Rows(), which rebuilt a
  string for every line of the file on the first frame after each edit.
  Qt's line-count uses switch to Nrows().

ImGuiRenderer.cc was syntax-checked with g++ and clang++ against the
vendored ImGui headers; the GUI targets could not be built here (no
SDL2 or Qt6).

Audit findings P1, P2.

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 2a1c49a0f0
commit 4c42761fc0
6 changed files with 112 additions and 24 deletions
+12 -10
View File
@@ -66,7 +66,9 @@ ImGuiRenderer::Draw(Editor &ed)
if (!buf) {
ImGui::TextUnformatted("[no buffer]");
} else {
const auto &lines = buf->Rows();
// Per-row access only: Rows() rebuilds a string for every line of the
// file after each edit, which made every frame O(file size).
const std::size_t nlines = buf->Nrows();
std::size_t cy = buf->Cury();
std::size_t cx = buf->Curx();
const float line_h = ImGui::GetTextLineHeight();
@@ -175,7 +177,7 @@ ImGuiRenderer::Draw(Editor &ed)
// Compute the visible row range and skip rendering work for off-screen
// lines. ImGui clips drawing, but string allocation, tab expansion,
// syntax-highlight lookups, and width measurement are not free.
const std::size_t total_rows = lines.size();
const std::size_t total_rows = nlines;
std::size_t first_vis = 0;
std::size_t last_vis = total_rows; // exclusive
if (row_h > 0.0f && total_rows > 0) {
@@ -221,14 +223,14 @@ ImGuiRenderer::Draw(Editor &ed)
if (by_l < 0)
by_l = 0;
std::size_t by = static_cast<std::size_t>(by_l);
if (by >= lines.size())
by = lines.empty() ? 0 : (lines.size() - 1);
if (by >= nlines)
by = (nlines == 0) ? 0 : (nlines - 1);
if (lines.empty())
if (nlines == 0)
return {0, 0};
// Expand tabs for the clicked line
std::string line_clicked = static_cast<std::string>(lines[by]);
std::string line_clicked = buf->GetLineString(by);
const std::size_t tabw = 8;
std::string click_expanded;
click_expanded.reserve(line_clicked.size() + 16);
@@ -322,7 +324,7 @@ ImGuiRenderer::Draw(Editor &ed)
for (std::size_t i = first_vis; i < last_vis; ++i) {
// Capture the screen position before drawing the line
ImVec2 line_pos = ImGui::GetCursorScreenPos();
std::string line = static_cast<std::string>(lines[i]);
std::string line = buf->GetLineString(i);
// Expand tabs to spaces with width=8
const std::size_t tabw = 8;
@@ -607,8 +609,8 @@ ImGuiRenderer::Draw(Editor &ed)
// Horizontal scroll: ensure cursor is visible (pixel-based for proportional fonts)
float cursor_px_abs = 0.0f;
if (cy < lines.size()) {
std::string cur_line = static_cast<std::string>(lines[cy]);
if (cy < nlines) {
std::string cur_line = buf->GetLineString(cy);
const std::size_t tabw = 8;
// Expand tabs for cursor line to measure pixel position
std::string cur_expanded;
@@ -791,7 +793,7 @@ ImGuiRenderer::Draw(Editor &ed)
left += " *";
// Append total line count as "<n>L"
{
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
unsigned long lcount = static_cast<unsigned long>(buf->Nrows());
left += " ";
left += std::to_string(lcount);
left += "L";