diff --git a/ImGuiRenderer.cc b/ImGuiRenderer.cc index d29d7b6..ed406ab 100644 --- a/ImGuiRenderer.cc +++ b/ImGuiRenderer.cc @@ -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(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(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(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(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 "L" { - unsigned long lcount = static_cast(buf->Rows().size()); + unsigned long lcount = static_cast(buf->Nrows()); left += " "; left += std::to_string(lcount); left += "L"; diff --git a/PieceTable.cc b/PieceTable.cc index c116d3a..a8583a8 100644 --- a/PieceTable.cc +++ b/PieceTable.cc @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -358,6 +359,42 @@ PieceTable::InvalidateLineIndex() const } +void +PieceTable::lineIndexOnInsert(std::size_t offset, const char *text, std::size_t len) const +{ + std::lock_guard lock(mutex_); + if (line_index_dirty_) + return; + // Line starts after the insertion point move right; a start exactly at + // the insertion point stays (the text joins that line). + auto first_after = std::upper_bound(line_index_.begin(), line_index_.end(), offset); + for (auto it = first_after; it != line_index_.end(); ++it) + *it += len; + std::vector added; + const char *end = text + len; + for (const char *p = text; (p = static_cast(std::memchr(p, '\n', end - p))) != nullptr; ++p) + added.push_back(offset + static_cast(p - text) + 1); + if (!added.empty()) + line_index_.insert(first_after, added.begin(), added.end()); +} + + +void +PieceTable::lineIndexOnDelete(std::size_t offset, std::size_t len) const +{ + std::lock_guard lock(mutex_); + if (line_index_dirty_) + return; + // Starts in (offset, offset+len] followed a deleted newline: drop them. + // Later starts move left. + auto lo = std::upper_bound(line_index_.begin(), line_index_.end(), offset); + auto hi = std::upper_bound(lo, line_index_.end(), offset + len); + for (auto it = hi; it != line_index_.end(); ++it) + *it -= len; + line_index_.erase(lo, hi); +} + + void PieceTable::RebuildLineIndex() const { @@ -405,7 +442,7 @@ PieceTable::Insert(std::size_t byte_offset, const char *text, std::size_t len) pieces_.push_back(Piece{Source::Add, add_start, len}); total_size_ += len; dirty_ = true; - InvalidateLineIndex(); + lineIndexOnInsert(byte_offset, add_.data() + add_start, len); maybeConsolidate(); version_++; range_cache_ = {}; @@ -419,7 +456,7 @@ PieceTable::Insert(std::size_t byte_offset, const char *text, std::size_t len) pieces_.push_back(Piece{Source::Add, add_start, len}); total_size_ += len; dirty_ = true; - InvalidateLineIndex(); + lineIndexOnInsert(byte_offset, add_.data() + add_start, len); coalesceNeighbors(pieces_.size() - 1); maybeConsolidate(); version_++; @@ -447,7 +484,7 @@ PieceTable::Insert(std::size_t byte_offset, const char *text, std::size_t len) total_size_ += len; dirty_ = true; - InvalidateLineIndex(); + lineIndexOnInsert(byte_offset, add_.data() + add_start, len); // Try coalescing around the inserted position (the inserted piece is at idx + (inner>0 ? 1 : 0)) std::size_t ins_index = idx + (inner > 0 ? 1 : 0); coalesceNeighbors(ins_index); @@ -517,7 +554,7 @@ PieceTable::Delete(std::size_t byte_offset, std::size_t len) total_size_ -= len; dirty_ = true; - InvalidateLineIndex(); + lineIndexOnDelete(byte_offset, len); if (idx < pieces_.size()) coalesceNeighbors(idx); if (idx > 0) diff --git a/PieceTable.h b/PieceTable.h index c417f8f..a77b688 100644 --- a/PieceTable.h +++ b/PieceTable.h @@ -175,6 +175,12 @@ private: // Line index support (rebuilt lazily on demand) void InvalidateLineIndex() const; + // Keep a valid line index in step with an edit instead of rescanning the + // whole buffer on the next query. No-ops if the index is already dirty. + void lineIndexOnInsert(std::size_t offset, const char *text, std::size_t len) const; + + void lineIndexOnDelete(std::size_t offset, std::size_t len) const; + void RebuildLineIndex() const; // Underlying storages diff --git a/QtFrontend.cc b/QtFrontend.cc index 2d29dee..b0b77ee 100644 --- a/QtFrontend.cc +++ b/QtFrontend.cc @@ -474,7 +474,7 @@ protected: left += QStringLiteral(" *"); // total lines suffix " L" - unsigned long lcount = static_cast(buf->Rows().size()); + unsigned long lcount = static_cast(buf->Nrows()); left += QStringLiteral(" "); left += QString::number(static_cast(lcount)); left += QStringLiteral("L"); @@ -617,7 +617,7 @@ protected: long nr = static_cast(new_rowoffs) + d_rows; if (nr < 0) nr = 0; - const auto nrows = static_cast(buf->Rows().size()); + const auto nrows = static_cast(buf->Nrows()); if (nr > std::max(0L, nrows - 1)) nr = std::max(0L, nrows - 1); new_rowoffs = static_cast(nr); diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc index 7c38d45..465c806 100644 --- a/TerminalRenderer.cc +++ b/TerminalRenderer.cc @@ -42,7 +42,9 @@ TerminalRenderer::Draw(Editor &ed) int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area if (buf) { - 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 rowoffs = buf->Rowoffs(); std::size_t coloffs = buf->Coloffs(); @@ -62,8 +64,8 @@ TerminalRenderer::Draw(Editor &ed) // Compute matches for this line if search highlighting is active bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty(); std::vector > ranges; // [start, end) - if (search_mode && li < lines.size()) { - std::string sline = static_cast(lines[li]); + if (search_mode && li < nlines) { + std::string sline = buf->GetLineString(li); // If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges if (ed.PromptActive() && ( ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed. @@ -140,8 +142,8 @@ TerminalRenderer::Draw(Editor &ed) return true; }; int written = 0; - if (li < lines.size()) { - std::string line = static_cast(lines[li]); + if (li < nlines) { + std::string line = buf->GetLineString(li); const bool vsel_on_line = vsel_active && li >= vsel_sy && li <= vsel_ey; const std::size_t vsel_spot_src = vsel_on_line ? std::min(buf->Curx(), line.size()) @@ -389,8 +391,8 @@ TerminalRenderer::Draw(Editor &ed) std::size_t cx = buf->Curx(); int cur_y = static_cast(cy) - static_cast(buf->Rowoffs()); std::size_t rx_recomputed = 0; - if (cy < lines.size()) { - const std::string line_for_cursor = static_cast(lines[cy]); + if (cy < nlines) { + const std::string line_for_cursor = buf->GetLineString(cy); std::size_t src_i_cur = 0; std::size_t render_col_cur = 0; while (src_i_cur < line_for_cursor.size() && src_i_cur < cx) { @@ -541,7 +543,7 @@ TerminalRenderer::Draw(Editor &ed) left += " [RO]"; // Append total line count as "L" if (b) { - unsigned long lcount = static_cast(b->Rows().size()); + unsigned long lcount = static_cast(b->Nrows()); left += " "; left += std::to_string(lcount); left += "L"; diff --git a/tests/test_audit_regressions.cc b/tests/test_audit_regressions.cc index 1d63f72..4de17cf 100644 --- a/tests/test_audit_regressions.cc +++ b/tests/test_audit_regressions.cc @@ -11,6 +11,8 @@ #include "UndoTree.h" #include +#include +#include #include #include #include @@ -440,3 +442,42 @@ TEST(Audit_B6_HorizontalScroll_CountsCells) // 200 cells wide on an 80-column screen: the cursor cell is 200. ASSERT_EQ(coloffs, (std::size_t) (200 - 80 + 1)); } + + +// 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) +{ + std::mt19937 rng(20260923u); + PieceTable t; + std::string model; + const char alphabet[] = "ab\nc\n\nd"; + for (int step = 0; step < 3000; ++step) { + const bool do_insert = model.empty() || (rng() % 3 != 0); + if (do_insert) { + const std::size_t off = model.empty() ? 0 : rng() % (model.size() + 1); + std::string text; + const std::size_t n = 1 + rng() % 6; + for (std::size_t i = 0; i < n; ++i) + text.push_back(alphabet[rng() % (sizeof(alphabet) - 1)]); + t.Insert(off, text.data(), text.size()); + model.insert(off, text); + } else { + const std::size_t off = rng() % model.size(); + const std::size_t n = 1 + rng() % 5; + t.Delete(off, n); + model.erase(off, n); + } + // Reference line starts. + std::vector starts{0}; + for (std::size_t i = 0; i < model.size(); ++i) + if (model[i] == '\n') + starts.push_back(i + 1); + ASSERT_EQ(t.LineCount(), starts.size()); + for (std::size_t l = 0; l < starts.size(); ++l) { + const auto [s, e] = t.GetLineRange(l); + ASSERT_EQ(s, starts[l]); + ASSERT_EQ(e, (l + 1 < starts.size()) ? starts[l + 1] : model.size()); + } + } +}