A loaded file's bytes are adopted as the piece table's original storage instead of being appended to the add buffer: the first keystroke no longer reallocates and copies the whole file (140 ms at 200 MB), and the read buffer is not copied again. Save writes the pieces in order instead of materializing the buffer (a second full copy that then stayed resident). Buffer::Bytes() assembles a copy from the pieces and replaces four hand-rolled copies (swap snapshots, recovery comparison, Command.cc's per-row join, the tests' accessor). A single-piece table (an unedited file) is viewed in place; GetLineView serves a line from its piece when it lies within one, so it no longer copies the whole buffer after every edit. Code that needed the whole text contiguous says so with ContentView(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
981 lines
25 KiB
C++
981 lines
25 KiB
C++
#include <algorithm>
|
|
#include <cstring>
|
|
#include <utility>
|
|
#include <limits>
|
|
#include <ostream>
|
|
|
|
#include "PieceTable.h"
|
|
|
|
|
|
PieceTable::PieceTable() = default;
|
|
|
|
|
|
PieceTable::PieceTable(const std::size_t initialCapacity,
|
|
const std::size_t piece_limit,
|
|
const std::size_t small_piece_threshold,
|
|
const std::size_t max_consolidation_bytes)
|
|
{
|
|
add_.reserve(initialCapacity);
|
|
materialized_.reserve(initialCapacity);
|
|
piece_limit_ = piece_limit;
|
|
small_piece_threshold_ = small_piece_threshold;
|
|
max_consolidation_bytes_ = max_consolidation_bytes;
|
|
}
|
|
|
|
|
|
PieceTable::PieceTable(const PieceTable &other)
|
|
: original_(other.original_),
|
|
add_(other.add_),
|
|
pieces_(other.pieces_),
|
|
materialized_(other.materialized_),
|
|
dirty_(other.dirty_),
|
|
total_size_(other.total_size_)
|
|
{
|
|
version_ = other.version_;
|
|
// caches are per-instance, mark invalid
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
}
|
|
|
|
|
|
PieceTable &
|
|
PieceTable::operator=(const PieceTable &other)
|
|
{
|
|
if (this == &other)
|
|
return *this;
|
|
original_ = other.original_;
|
|
add_ = other.add_;
|
|
pieces_ = other.pieces_;
|
|
materialized_ = other.materialized_;
|
|
dirty_ = other.dirty_;
|
|
total_size_ = other.total_size_;
|
|
version_ = other.version_;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
last_deleted_ = {};
|
|
InvalidateLineIndex();
|
|
return *this;
|
|
}
|
|
|
|
|
|
PieceTable::PieceTable(PieceTable &&other) noexcept
|
|
: original_(std::move(other.original_)),
|
|
add_(std::move(other.add_)),
|
|
pieces_(std::move(other.pieces_)),
|
|
materialized_(std::move(other.materialized_)),
|
|
dirty_(other.dirty_),
|
|
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_ = {};
|
|
}
|
|
|
|
|
|
PieceTable &
|
|
PieceTable::operator=(PieceTable &&other) noexcept
|
|
{
|
|
if (this == &other)
|
|
return *this;
|
|
original_ = std::move(other.original_);
|
|
add_ = std::move(other.add_);
|
|
pieces_ = std::move(other.pieces_);
|
|
materialized_ = std::move(other.materialized_);
|
|
dirty_ = other.dirty_;
|
|
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_ = {};
|
|
last_deleted_ = {};
|
|
other.last_deleted_ = {};
|
|
InvalidateLineIndex();
|
|
return *this;
|
|
}
|
|
|
|
|
|
PieceTable::~PieceTable() = default;
|
|
|
|
|
|
// Setter to allow tuning consolidation heuristics
|
|
void
|
|
PieceTable::SetConsolidationParams(const std::size_t piece_limit,
|
|
const std::size_t small_piece_threshold,
|
|
const std::size_t max_consolidation_bytes)
|
|
{
|
|
piece_limit_ = piece_limit;
|
|
small_piece_threshold_ = small_piece_threshold;
|
|
max_consolidation_bytes_ = max_consolidation_bytes;
|
|
}
|
|
|
|
|
|
// (removed helper) — we'll invalidate caches inline inside mutating methods
|
|
|
|
|
|
void
|
|
PieceTable::AppendChar(char c)
|
|
{
|
|
const std::size_t start = add_.size();
|
|
|
|
add_.push_back(c);
|
|
addPieceBack(Source::Add, start, 1);
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::Append(const char *s, const std::size_t len)
|
|
{
|
|
if (len == 0) {
|
|
return;
|
|
}
|
|
|
|
const std::size_t start = add_.size();
|
|
add_.append(s, len);
|
|
addPieceBack(Source::Add, start, len);
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::Append(const PieceTable &other)
|
|
{
|
|
// Simpler and safe: materialize "other" and append bytes
|
|
const char *d = other.Data();
|
|
Append(d, other.Size());
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::PrependChar(const char c)
|
|
{
|
|
const std::size_t start = add_.size();
|
|
|
|
add_.push_back(c);
|
|
addPieceFront(Source::Add, start, 1);
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::Prepend(const char *s, const std::size_t len)
|
|
{
|
|
if (len == 0) {
|
|
return;
|
|
}
|
|
|
|
const std::size_t start = add_.size();
|
|
|
|
add_.append(s, len);
|
|
addPieceFront(Source::Add, start, len);
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::Prepend(const PieceTable &other)
|
|
{
|
|
const char *d = other.Data();
|
|
Prepend(d, other.Size());
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::Clear()
|
|
{
|
|
pieces_.clear();
|
|
std::string().swap(original_);
|
|
add_.clear();
|
|
materialized_.clear();
|
|
total_size_ = 0;
|
|
dirty_ = true;
|
|
line_index_.clear();
|
|
line_index_dirty_ = true;
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
last_deleted_ = {};
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::AdoptOriginal(std::string &&bytes)
|
|
{
|
|
Clear();
|
|
std::string().swap(add_); // release a previous content's capacity
|
|
std::string().swap(materialized_);
|
|
original_ = std::move(bytes);
|
|
total_size_ = original_.size();
|
|
if (total_size_ > 0)
|
|
pieces_.push_back(Piece{Source::Original, 0, total_size_});
|
|
}
|
|
|
|
|
|
std::string_view
|
|
PieceTable::ContentView() const
|
|
{
|
|
if (total_size_ == 0)
|
|
return {};
|
|
if (pieces_.size() == 1) {
|
|
const Piece &p = pieces_.front();
|
|
return {(p.src == Source::Original ? original_ : add_).data() + p.start, p.len};
|
|
}
|
|
materialize();
|
|
return {materialized_.data(), materialized_.size()};
|
|
}
|
|
|
|
|
|
std::string_view
|
|
PieceTable::View(std::size_t byte_offset, std::size_t len) const
|
|
{
|
|
if (byte_offset >= total_size_ || len == 0)
|
|
return {};
|
|
len = std::min(len, total_size_ - byte_offset);
|
|
if (dirty_) {
|
|
// Materialized text is stale: serve the range from its piece when it
|
|
// fits in one, rather than copying the whole buffer.
|
|
auto [idx, inner] = locate(byte_offset);
|
|
if (idx < pieces_.size() && inner + len <= pieces_[idx].len) {
|
|
const Piece &p = pieces_[idx];
|
|
return {(p.src == Source::Original ? original_ : add_).data() + p.start + inner, len};
|
|
}
|
|
}
|
|
return ContentView().substr(byte_offset, len);
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::addPieceBack(const Source src, const std::size_t start, const std::size_t len)
|
|
{
|
|
if (len == 0) {
|
|
return;
|
|
}
|
|
|
|
// Attempt to coalesce with last piece if contiguous and same source
|
|
if (!pieces_.empty()) {
|
|
Piece &last = pieces_.back();
|
|
if (last.src == src) {
|
|
std::size_t expectStart = last.start + last.len;
|
|
|
|
if (expectStart == start) {
|
|
last.len += len;
|
|
total_size_ += len;
|
|
dirty_ = true;
|
|
InvalidateLineIndex();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
pieces_.push_back(Piece{src, start, len});
|
|
total_size_ += len;
|
|
dirty_ = true;
|
|
InvalidateLineIndex();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::addPieceFront(Source src, std::size_t start, std::size_t len)
|
|
{
|
|
if (len == 0) {
|
|
return;
|
|
}
|
|
|
|
// Attempt to coalesce with first piece if contiguous and same source
|
|
if (!pieces_.empty()) {
|
|
Piece &first = pieces_.front();
|
|
if (first.src == src && start + len == first.start) {
|
|
first.start = start;
|
|
first.len += len;
|
|
total_size_ += len;
|
|
dirty_ = true;
|
|
InvalidateLineIndex();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
return;
|
|
}
|
|
}
|
|
pieces_.insert(pieces_.begin(), Piece{src, start, len});
|
|
total_size_ += len;
|
|
dirty_ = true;
|
|
InvalidateLineIndex();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::materialize() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!dirty_) {
|
|
return;
|
|
}
|
|
materialized_.clear();
|
|
materialized_.reserve(total_size_ + 1);
|
|
for (const auto &p: pieces_) {
|
|
const std::string &src = p.src == Source::Original ? original_ : add_;
|
|
if (p.len == 0) {
|
|
continue;
|
|
}
|
|
|
|
materialized_.append(src.data() + static_cast<std::ptrdiff_t>(p.start), p.len);
|
|
}
|
|
// Ensure there is a null terminator present via std::string invariants
|
|
dirty_ = false;
|
|
}
|
|
|
|
|
|
// ===== New Phase 1 implementation =====
|
|
|
|
std::pair<std::size_t, std::size_t>
|
|
PieceTable::locate(const std::size_t byte_offset) const
|
|
{
|
|
if (byte_offset >= total_size_) {
|
|
return {pieces_.size(), 0};
|
|
}
|
|
std::size_t off = byte_offset;
|
|
for (std::size_t i = 0; i < pieces_.size(); ++i) {
|
|
const auto &p = pieces_[i];
|
|
if (off < p.len) {
|
|
return {i, off};
|
|
}
|
|
off -= p.len;
|
|
}
|
|
// Should not reach here unless inconsistency; return end
|
|
return {pieces_.size(), 0};
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::coalesceNeighbors(std::size_t index)
|
|
{
|
|
if (pieces_.empty())
|
|
return;
|
|
if (index >= pieces_.size())
|
|
index = pieces_.size() - 1;
|
|
// Merge repeatedly with previous while contiguous and same source
|
|
while (index > 0) {
|
|
auto &prev = pieces_[index - 1];
|
|
auto &curr = pieces_[index];
|
|
if (prev.src == curr.src && prev.start + prev.len == curr.start) {
|
|
prev.len += curr.len;
|
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(index));
|
|
index -= 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
// Merge repeatedly with next while contiguous and same source
|
|
while (index + 1 < pieces_.size()) {
|
|
auto &curr = pieces_[index];
|
|
auto &next = pieces_[index + 1];
|
|
if (curr.src == next.src && curr.start + curr.len == next.start) {
|
|
curr.len += next.len;
|
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(index + 1));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::InvalidateLineIndex() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
line_index_dirty_ = true;
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::lineIndexOnInsert(std::size_t offset, const char *text, std::size_t len) const
|
|
{
|
|
std::lock_guard<std::mutex> 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<std::size_t> added;
|
|
const char *end = text + len;
|
|
for (const char *p = text; (p = static_cast<const char *>(std::memchr(p, '\n', end - p))) != nullptr; ++p)
|
|
added.push_back(offset + static_cast<std::size_t>(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<std::mutex> 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
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
if (!line_index_dirty_) {
|
|
return;
|
|
}
|
|
line_index_.clear();
|
|
line_index_.push_back(0);
|
|
|
|
std::size_t pos = 0;
|
|
for (const auto &pc: pieces_) {
|
|
const std::string &src = pc.src == Source::Original ? original_ : add_;
|
|
const char *base = src.data() + static_cast<std::ptrdiff_t>(pc.start);
|
|
|
|
const char *end = base + pc.len;
|
|
// The next line starts after each newline.
|
|
for (const char *p = base; (p = static_cast<const char *>(std::memchr(p, '\n', end - p))) != nullptr; ++p)
|
|
line_index_.push_back(pos + static_cast<std::size_t>(p - base) + 1);
|
|
|
|
pos += pc.len;
|
|
}
|
|
|
|
line_index_dirty_ = false;
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::Insert(std::size_t byte_offset, const char *text, std::size_t len)
|
|
{
|
|
if (len == 0) {
|
|
return;
|
|
}
|
|
if (byte_offset > total_size_) {
|
|
byte_offset = total_size_;
|
|
}
|
|
|
|
last_deleted_ = {};
|
|
const std::size_t add_start = add_.size();
|
|
add_.append(text, len);
|
|
|
|
if (pieces_.empty()) {
|
|
pieces_.push_back(Piece{Source::Add, add_start, len});
|
|
total_size_ += len;
|
|
dirty_ = true;
|
|
lineIndexOnInsert(byte_offset, add_.data() + add_start, len);
|
|
maybeConsolidate();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
return;
|
|
}
|
|
|
|
auto [idx, inner] = locate(byte_offset);
|
|
if (idx == pieces_.size()) {
|
|
// insert at end
|
|
pieces_.push_back(Piece{Source::Add, add_start, len});
|
|
total_size_ += len;
|
|
dirty_ = true;
|
|
lineIndexOnInsert(byte_offset, add_.data() + add_start, len);
|
|
coalesceNeighbors(pieces_.size() - 1);
|
|
maybeConsolidate();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
return;
|
|
}
|
|
|
|
Piece target = pieces_[idx];
|
|
// Build replacement sequence: left, inserted, right
|
|
std::vector<Piece> repl;
|
|
repl.reserve(3);
|
|
if (inner > 0) {
|
|
repl.push_back(Piece{target.src, target.start, inner});
|
|
}
|
|
repl.push_back(Piece{Source::Add, add_start, len});
|
|
const std::size_t right_len = target.len - inner;
|
|
if (right_len > 0) {
|
|
repl.push_back(Piece{target.src, target.start + inner, right_len});
|
|
}
|
|
|
|
// Replace target with repl
|
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(idx));
|
|
pieces_.insert(pieces_.begin() + static_cast<std::ptrdiff_t>(idx), repl.begin(), repl.end());
|
|
|
|
total_size_ += len;
|
|
dirty_ = true;
|
|
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);
|
|
maybeConsolidate();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::Delete(std::size_t byte_offset, std::size_t len)
|
|
{
|
|
if (len == 0) {
|
|
return;
|
|
}
|
|
if (byte_offset >= total_size_) {
|
|
return;
|
|
}
|
|
if (byte_offset + len > total_size_) {
|
|
len = total_size_ - byte_offset;
|
|
}
|
|
|
|
// Remember what a large deletion removed, so undo can refer to the
|
|
// stored bytes instead of keeping its own copy (see TakeDeletedSpans).
|
|
last_deleted_ = {};
|
|
if (len >= kCaptureDeletedMin) {
|
|
last_deleted_.spans = SpansInRange(byte_offset, len);
|
|
last_deleted_.offset = byte_offset;
|
|
last_deleted_.len = len;
|
|
last_deleted_.valid = true;
|
|
}
|
|
|
|
auto [idx, inner] = locate(byte_offset);
|
|
std::size_t remaining = len;
|
|
|
|
while (remaining > 0 && idx < pieces_.size()) {
|
|
Piece &pc = pieces_[idx];
|
|
std::size_t available = pc.len - inner; // bytes we can remove from this piece starting at inner
|
|
std::size_t take = std::min(available, remaining);
|
|
|
|
// Compute lengths for left and right remnants
|
|
std::size_t left_len = inner;
|
|
std::size_t right_len = pc.len - inner - take;
|
|
Source src = pc.src;
|
|
std::size_t start = pc.start;
|
|
|
|
// Replace current piece with up to two remnants
|
|
if (left_len > 0 && right_len > 0) {
|
|
pc.len = left_len; // keep left in place
|
|
Piece right{src, start + inner + take, right_len};
|
|
pieces_.insert(pieces_.begin() + static_cast<std::ptrdiff_t>(idx + 1), right);
|
|
idx += 1; // move to right for next iteration decision
|
|
} else if (left_len > 0) {
|
|
pc.len = left_len;
|
|
// no insertion; idx now points to left; move to next piece
|
|
} else if (right_len > 0) {
|
|
pc.start = start + inner + take;
|
|
pc.len = right_len;
|
|
} else {
|
|
// entire piece removed
|
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(idx));
|
|
// stay at same idx for next piece
|
|
inner = 0;
|
|
remaining -= take;
|
|
continue;
|
|
}
|
|
|
|
// After modifying current idx, next deletion continues at beginning of the next logical region
|
|
inner = 0;
|
|
remaining -= take;
|
|
if (remaining == 0)
|
|
break;
|
|
// Move to next piece
|
|
idx += 1;
|
|
}
|
|
|
|
total_size_ -= len;
|
|
dirty_ = true;
|
|
lineIndexOnDelete(byte_offset, len);
|
|
if (idx < pieces_.size())
|
|
coalesceNeighbors(idx);
|
|
if (idx > 0)
|
|
coalesceNeighbors(idx - 1);
|
|
maybeConsolidate();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
}
|
|
|
|
|
|
// ===== Span access =====
|
|
|
|
std::vector<TextSpan>
|
|
PieceTable::SpansInRange(std::size_t byte_offset, std::size_t len) const
|
|
{
|
|
std::vector<TextSpan> out;
|
|
if (byte_offset >= total_size_ || len == 0)
|
|
return out;
|
|
len = std::min(len, total_size_ - byte_offset);
|
|
auto [idx, inner] = locate(byte_offset);
|
|
while (len > 0 && idx < pieces_.size()) {
|
|
const Piece &p = pieces_[idx];
|
|
const std::size_t take = std::min(p.len - inner, len);
|
|
out.push_back(TextSpan{p.src == Source::Add, p.start + inner, take});
|
|
len -= take;
|
|
inner = 0;
|
|
++idx;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
bool
|
|
PieceTable::SpansEqual(const std::vector<TextSpan> &spans, std::string_view text) const
|
|
{
|
|
std::size_t pos = 0;
|
|
bool equal = true;
|
|
VisitSpans(spans, [&](const char *data, std::size_t n) {
|
|
if (!equal || pos + n > text.size() || std::memcmp(data, text.data() + pos, n) != 0)
|
|
equal = false;
|
|
pos += n;
|
|
});
|
|
return equal && pos == text.size();
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::InsertSpans(std::size_t byte_offset, const std::vector<TextSpan> &spans)
|
|
{
|
|
if (byte_offset > total_size_)
|
|
byte_offset = total_size_;
|
|
std::vector<Piece> ins;
|
|
std::size_t len = 0;
|
|
for (const TextSpan &sp: spans) {
|
|
const std::string &src = sp.add ? add_ : original_;
|
|
if (sp.len == 0 || sp.start + sp.len > src.size())
|
|
continue;
|
|
ins.push_back(Piece{sp.add ? Source::Add : Source::Original, sp.start, sp.len});
|
|
len += sp.len;
|
|
}
|
|
if (len == 0)
|
|
return;
|
|
|
|
std::size_t at = pieces_.size();
|
|
if (byte_offset < total_size_) {
|
|
auto [idx, inner] = locate(byte_offset);
|
|
if (inner > 0) {
|
|
// Split the piece containing the insertion point.
|
|
const Piece target = pieces_[idx];
|
|
pieces_[idx].len = inner;
|
|
pieces_.insert(pieces_.begin() + static_cast<std::ptrdiff_t>(idx + 1),
|
|
Piece{target.src, target.start + inner, target.len - inner});
|
|
at = idx + 1;
|
|
} else {
|
|
at = idx;
|
|
}
|
|
}
|
|
pieces_.insert(pieces_.begin() + static_cast<std::ptrdiff_t>(at), ins.begin(), ins.end());
|
|
total_size_ += len;
|
|
dirty_ = true;
|
|
std::size_t off = byte_offset;
|
|
for (const Piece &p: ins) {
|
|
const std::string &src = p.src == Source::Add ? add_ : original_;
|
|
lineIndexOnInsert(off, src.data() + p.start, p.len);
|
|
off += p.len;
|
|
}
|
|
coalesceNeighbors(std::min(at + ins.size(), pieces_.size() - 1));
|
|
if (at > 0)
|
|
coalesceNeighbors(at - 1);
|
|
maybeConsolidate();
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
last_deleted_ = {};
|
|
}
|
|
|
|
|
|
std::vector<TextSpan>
|
|
PieceTable::TakeDeletedSpans(std::size_t byte_offset, std::size_t len)
|
|
{
|
|
std::vector<TextSpan> out;
|
|
if (last_deleted_.valid && last_deleted_.offset == byte_offset && last_deleted_.len == len)
|
|
out = std::move(last_deleted_.spans);
|
|
last_deleted_ = {};
|
|
return out;
|
|
}
|
|
|
|
|
|
// ===== Consolidation implementation =====
|
|
|
|
void
|
|
PieceTable::appendPieceDataTo(std::string &out, const Piece &p) const
|
|
{
|
|
if (p.len == 0)
|
|
return;
|
|
const std::string &src = p.src == Source::Original ? original_ : add_;
|
|
out.append(src.data() + static_cast<std::ptrdiff_t>(p.start), p.len);
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::consolidateRange(std::size_t start_idx, std::size_t end_idx)
|
|
{
|
|
if (start_idx >= end_idx || start_idx >= pieces_.size())
|
|
return;
|
|
end_idx = std::min(end_idx, pieces_.size());
|
|
std::size_t total = 0;
|
|
for (std::size_t i = start_idx; i < end_idx; ++i)
|
|
total += pieces_[i].len;
|
|
if (total == 0)
|
|
return;
|
|
|
|
const std::size_t add_start = add_.size();
|
|
std::string tmp;
|
|
tmp.reserve(std::min<std::size_t>(total, max_consolidation_bytes_));
|
|
for (std::size_t i = start_idx; i < end_idx; ++i)
|
|
appendPieceDataTo(tmp, pieces_[i]);
|
|
add_.append(tmp);
|
|
|
|
// Replace [start_idx, end_idx) with single Add piece
|
|
Piece consolidated{Source::Add, add_start, tmp.size()};
|
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(start_idx),
|
|
pieces_.begin() + static_cast<std::ptrdiff_t>(end_idx));
|
|
pieces_.insert(pieces_.begin() + static_cast<std::ptrdiff_t>(start_idx), consolidated);
|
|
|
|
// 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;
|
|
coalesceNeighbors(start_idx);
|
|
// Layout changed; invalidate caches/version
|
|
version_++;
|
|
range_cache_ = {};
|
|
find_cache_ = {};
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::maybeConsolidate()
|
|
{
|
|
if (pieces_.size() <= piece_limit_)
|
|
return;
|
|
|
|
// Find the first run of small pieces to consolidate
|
|
std::size_t n = pieces_.size();
|
|
std::size_t best_start = n, best_end = n;
|
|
std::size_t i = 0;
|
|
while (i < n) {
|
|
// Skip large pieces quickly
|
|
if (pieces_[i].len > small_piece_threshold_) {
|
|
i++;
|
|
continue;
|
|
}
|
|
std::size_t j = i;
|
|
std::size_t bytes = 0;
|
|
while (j < n) {
|
|
const auto &p = pieces_[j];
|
|
if (p.len > small_piece_threshold_)
|
|
break;
|
|
if (bytes + p.len > max_consolidation_bytes_)
|
|
break;
|
|
bytes += p.len;
|
|
j++;
|
|
}
|
|
if (j - i >= 2 && bytes > 0) {
|
|
// consolidate runs of at least 2 pieces
|
|
best_start = i;
|
|
best_end = j;
|
|
break; // do one run per call; subsequent ops can repeat if still over limit
|
|
}
|
|
i = j + 1;
|
|
}
|
|
|
|
if (best_start < best_end) {
|
|
consolidateRange(best_start, best_end);
|
|
}
|
|
}
|
|
|
|
|
|
std::size_t
|
|
PieceTable::LineCount() const
|
|
{
|
|
RebuildLineIndex();
|
|
return line_index_.empty() ? 0 : line_index_.size();
|
|
}
|
|
|
|
|
|
std::pair<std::size_t, std::size_t>
|
|
PieceTable::GetLineRange(std::size_t line_num) const
|
|
{
|
|
RebuildLineIndex();
|
|
if (line_index_.empty())
|
|
return {0, 0};
|
|
if (line_num >= line_index_.size())
|
|
return {0, 0};
|
|
std::size_t start = line_index_[line_num];
|
|
std::size_t end = (line_num + 1 < line_index_.size()) ? line_index_[line_num + 1] : total_size_;
|
|
return {start, end};
|
|
}
|
|
|
|
|
|
std::string
|
|
PieceTable::GetLine(std::size_t line_num) const
|
|
{
|
|
auto [start, end] = GetLineRange(line_num);
|
|
if (end < start)
|
|
return std::string();
|
|
// Every line but the last ends at a newline (each newline starts an
|
|
// index entry); leave it out.
|
|
if (end > start && line_num + 1 < line_index_.size())
|
|
end -= 1;
|
|
return GetRange(start, end - start);
|
|
}
|
|
|
|
|
|
std::pair<std::size_t, std::size_t>
|
|
PieceTable::ByteOffsetToLineCol(std::size_t byte_offset) const
|
|
{
|
|
if (byte_offset > total_size_)
|
|
byte_offset = total_size_;
|
|
RebuildLineIndex();
|
|
if (line_index_.empty())
|
|
return {0, 0};
|
|
auto it = std::upper_bound(line_index_.begin(), line_index_.end(), byte_offset);
|
|
std::size_t row = (it == line_index_.begin()) ? 0 : static_cast<std::size_t>((it - line_index_.begin()) - 1);
|
|
std::size_t col = byte_offset - line_index_[row];
|
|
return {row, col};
|
|
}
|
|
|
|
|
|
std::size_t
|
|
PieceTable::LineColToByteOffset(std::size_t row, std::size_t col) const
|
|
{
|
|
RebuildLineIndex();
|
|
if (line_index_.empty())
|
|
return 0;
|
|
if (row >= line_index_.size())
|
|
return total_size_;
|
|
std::size_t start = line_index_[row];
|
|
std::size_t end = (row + 1 < line_index_.size()) ? line_index_[row + 1] : total_size_;
|
|
// Clamp col to the line's length without its newline (every line but
|
|
// the last has one).
|
|
if (end > start && row + 1 < line_index_.size())
|
|
end -= 1;
|
|
std::size_t target = start + std::min(col, end - start);
|
|
return target;
|
|
}
|
|
|
|
|
|
std::string
|
|
PieceTable::GetRange(std::size_t byte_offset, std::size_t len) const
|
|
{
|
|
if (byte_offset >= total_size_ || len == 0)
|
|
return std::string();
|
|
if (byte_offset + len > total_size_)
|
|
len = total_size_ - byte_offset;
|
|
|
|
// Fast path: return cached value if version/offset/len match
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (range_cache_.valid && range_cache_.version == version_ &&
|
|
range_cache_.off == byte_offset && range_cache_.len == len) {
|
|
return range_cache_.data;
|
|
}
|
|
}
|
|
|
|
std::string out;
|
|
out.reserve(len);
|
|
if (!dirty_) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
// Already materialized; slice directly
|
|
out.assign(materialized_.data() + static_cast<std::ptrdiff_t>(byte_offset), len);
|
|
} else {
|
|
// Assemble substring directly from pieces without full materialization
|
|
auto [idx, inner] = locate(byte_offset);
|
|
std::size_t remaining = len;
|
|
while (remaining > 0 && idx < pieces_.size()) {
|
|
const auto &p = pieces_[idx];
|
|
const std::string &src = (p.src == Source::Original) ? original_ : add_;
|
|
std::size_t take = std::min<std::size_t>(p.len - inner, remaining);
|
|
if (take > 0) {
|
|
const char *base = src.data() + static_cast<std::ptrdiff_t>(p.start + inner);
|
|
out.append(base, take);
|
|
remaining -= take;
|
|
inner = 0;
|
|
idx += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update cache
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
range_cache_.valid = true;
|
|
range_cache_.version = version_;
|
|
range_cache_.off = byte_offset;
|
|
range_cache_.len = len;
|
|
range_cache_.data = out;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
std::size_t
|
|
PieceTable::Find(const std::string &needle, std::size_t start) const
|
|
{
|
|
if (needle.empty())
|
|
return start <= total_size_ ? start : std::numeric_limits<std::size_t>::max();
|
|
if (start > total_size_)
|
|
return std::numeric_limits<std::size_t>::max();
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (find_cache_.valid &&
|
|
find_cache_.version == version_ &&
|
|
find_cache_.needle == needle &&
|
|
find_cache_.start == start) {
|
|
return find_cache_.result;
|
|
}
|
|
}
|
|
|
|
materialize();
|
|
std::size_t pos;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
pos = materialized_.find(needle, start);
|
|
if (pos == std::string::npos)
|
|
pos = std::numeric_limits<std::size_t>::max();
|
|
// Update cache
|
|
find_cache_.valid = true;
|
|
find_cache_.version = version_;
|
|
find_cache_.needle = needle;
|
|
find_cache_.start = start;
|
|
find_cache_.result = pos;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
|
|
void
|
|
PieceTable::WriteToStream(std::ostream &out) const
|
|
{
|
|
// Stream the content piece-by-piece without forcing full materialization
|
|
// No lock needed for original_ and add_ if they are not being modified.
|
|
// Since this is a const method and kte's piece table isn't modified by multiple threads
|
|
// (only queried), we just iterate pieces_.
|
|
for (const auto &p: pieces_) {
|
|
if (p.len == 0)
|
|
continue;
|
|
const std::string &src = (p.src == Source::Original) ? original_ : add_;
|
|
const char *base = src.data() + static_cast<std::ptrdiff_t>(p.start);
|
|
out.write(base, static_cast<std::streamsize>(p.len));
|
|
}
|
|
}
|