Undo: hold large texts as storage spans; byte budget with pruning

- Undo texts of 4 KiB or more (Insert/Paste from the buffer, Delete from
  the spans the piece table remembers for its last large deletion) are
  held as TextSpan references into the piece table's append-only
  storage, after checking they hold exactly the recorded bytes. Undo and
  redo insert them with Buffer::insert_spans, which adds pieces over the
  stored bytes instead of copying them into storage again (the journal
  still gets the bytes, as one record). A replace-all session on a 26 MB
  file peaks at 181 MB instead of 332 MB; undo/redo cycles of a 6.6 MB
  kill no longer grow memory by 6.6 MB each.
- UndoSystem keeps a byte budget (default max(64 MiB, twice the buffer
  size); SetByteBudget). Over it, history is pruned to 75% of the budget:
  branches off the current path, then the oldest edits (an undo group
  goes whole, and never the current state's group), then the redo line.
  When the saved state is dropped the buffer stays modified until saved.
- replace_all_bytes clears undo history, which was recorded against (and
  may reference the storage of) the previous content.

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 7165bb286b
commit 404b983009
11 changed files with 687 additions and 29 deletions
+40
View File
@@ -43,6 +43,9 @@
#include <vector>
#include <limits>
#include <mutex>
#include <string_view>
#include "TextSpan.h"
class PieceTable {
@@ -139,6 +142,36 @@ public:
// Stream out content without materializing the entire buffer
void WriteToStream(std::ostream &out) const;
// ===== Span access (see TextSpan.h) =====
// The storage spans backing [byte_offset, byte_offset + len).
[[nodiscard]] std::vector<TextSpan> SpansInRange(std::size_t byte_offset, std::size_t len) const;
// Call fn(const char *data, std::size_t len) for each span's bytes, in order.
template<typename Fn>
void VisitSpans(const std::vector<TextSpan> &spans, Fn &&fn) const
{
for (const TextSpan &sp: spans) {
const std::string &src = sp.add ? add_ : original_;
if (sp.len > 0 && sp.start + sp.len <= src.size())
fn(src.data() + sp.start, sp.len);
}
}
// Whether the spans hold exactly `text`.
[[nodiscard]] bool SpansEqual(const std::vector<TextSpan> &spans, std::string_view text) const;
// Insert the spans' bytes at byte_offset without copying them into
// storage again (the new pieces reference the existing bytes).
void InsertSpans(std::size_t byte_offset, const std::vector<TextSpan> &spans);
// Deletions of at least kCaptureDeletedMin bytes remember the spans they
// removed; this returns them if the most recent deletion was exactly
// [byte_offset, byte_offset + len), else nothing. Consumes the capture.
static constexpr std::size_t kCaptureDeletedMin = 4096;
[[nodiscard]] std::vector<TextSpan> TakeDeletedSpans(std::size_t byte_offset, std::size_t len);
// Heuristic configuration
void SetConsolidationParams(std::size_t piece_limit,
std::size_t small_piece_threshold,
@@ -220,6 +253,13 @@ private:
std::size_t result = std::numeric_limits<std::size_t>::max();
};
struct DeletedCapture {
bool valid = false;
std::size_t offset = 0;
std::size_t len = 0;
std::vector<TextSpan> spans;
} last_deleted_;
mutable RangeCache range_cache_;
mutable FindCache find_cache_;