Introduce PieceTable-based buffer backend (Phase 1)
- Added `PieceTable` class for efficient text manipulation and implemented core editing APIs (`Insert`, `Delete`, `Find`, etc.). - Integrated `PieceTable` into `Buffer` class with an adapter for rows caching. - Enabled seamless switching between legacy row-based and new PieceTable-backed editing via `KTE_USE_BUFFER_PIECE_TABLE`. - Updated file I/O, line-based queries, and cursor operations to support PieceTable-based storage. - Lazy rebuilding of line index and improved management of edit state for performance.
This commit is contained in:
262
Buffer.cc
262
Buffer.cc
@@ -2,6 +2,7 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
@@ -29,13 +30,17 @@ Buffer::Buffer(const std::string &path)
|
|||||||
// Copy constructor/assignment: perform a deep copy of core fields; reinitialize undo for the new buffer.
|
// Copy constructor/assignment: perform a deep copy of core fields; reinitialize undo for the new buffer.
|
||||||
Buffer::Buffer(const Buffer &other)
|
Buffer::Buffer(const Buffer &other)
|
||||||
{
|
{
|
||||||
curx_ = other.curx_;
|
curx_ = other.curx_;
|
||||||
cury_ = other.cury_;
|
cury_ = other.cury_;
|
||||||
rx_ = other.rx_;
|
rx_ = other.rx_;
|
||||||
nrows_ = other.nrows_;
|
nrows_ = other.nrows_;
|
||||||
rowoffs_ = other.rowoffs_;
|
rowoffs_ = other.rowoffs_;
|
||||||
coloffs_ = other.coloffs_;
|
coloffs_ = other.coloffs_;
|
||||||
rows_ = other.rows_;
|
rows_ = other.rows_;
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
content_ = other.content_;
|
||||||
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
|
#endif
|
||||||
filename_ = other.filename_;
|
filename_ = other.filename_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
@@ -77,13 +82,17 @@ Buffer::operator=(const Buffer &other)
|
|||||||
{
|
{
|
||||||
if (this == &other)
|
if (this == &other)
|
||||||
return *this;
|
return *this;
|
||||||
curx_ = other.curx_;
|
curx_ = other.curx_;
|
||||||
cury_ = other.cury_;
|
cury_ = other.cury_;
|
||||||
rx_ = other.rx_;
|
rx_ = other.rx_;
|
||||||
nrows_ = other.nrows_;
|
nrows_ = other.nrows_;
|
||||||
rowoffs_ = other.rowoffs_;
|
rowoffs_ = other.rowoffs_;
|
||||||
coloffs_ = other.coloffs_;
|
coloffs_ = other.coloffs_;
|
||||||
rows_ = other.rows_;
|
rows_ = other.rows_;
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
content_ = other.content_;
|
||||||
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
|
#endif
|
||||||
filename_ = other.filename_;
|
filename_ = other.filename_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
@@ -141,6 +150,10 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
filetype_ = std::move(other.filetype_);
|
filetype_ = std::move(other.filetype_);
|
||||||
highlighter_ = std::move(other.highlighter_);
|
highlighter_ = std::move(other.highlighter_);
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
content_ = std::move(other.content_);
|
||||||
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
|
#endif
|
||||||
// Update UndoSystem's buffer reference to point to this object
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
if (undo_sys_) {
|
if (undo_sys_) {
|
||||||
undo_sys_->UpdateBufferReference(*this);
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
@@ -178,6 +191,10 @@ Buffer::operator=(Buffer &&other) noexcept
|
|||||||
filetype_ = std::move(other.filetype_);
|
filetype_ = std::move(other.filetype_);
|
||||||
highlighter_ = std::move(other.highlighter_);
|
highlighter_ = std::move(other.highlighter_);
|
||||||
|
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
content_ = std::move(other.content_);
|
||||||
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
|
#endif
|
||||||
// Update UndoSystem's buffer reference to point to this object
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
if (undo_sys_) {
|
if (undo_sys_) {
|
||||||
undo_sys_->UpdateBufferReference(*this);
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
@@ -229,6 +246,12 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
|
|||||||
mark_set_ = false;
|
mark_set_ = false;
|
||||||
mark_curx_ = mark_cury_ = 0;
|
mark_curx_ = mark_cury_ = 0;
|
||||||
|
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
// Empty PieceTable
|
||||||
|
content_.Clear();
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +261,22 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
// Read entire file into PieceTable as-is
|
||||||
|
std::string data;
|
||||||
|
in.seekg(0, std::ios::end);
|
||||||
|
auto sz = in.tellg();
|
||||||
|
if (sz > 0) {
|
||||||
|
data.resize(static_cast<std::size_t>(sz));
|
||||||
|
in.seekg(0, std::ios::beg);
|
||||||
|
in.read(data.data(), static_cast<std::streamsize>(data.size()));
|
||||||
|
}
|
||||||
|
content_.Clear();
|
||||||
|
if (!data.empty())
|
||||||
|
content_.Append(data.data(), data.size());
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
nrows_ = 0; // not used under adapter
|
||||||
|
#else
|
||||||
// Detect if file ends with a newline so we can preserve a final empty line
|
// Detect if file ends with a newline so we can preserve a final empty line
|
||||||
// in our in-memory representation (mg-style semantics).
|
// in our in-memory representation (mg-style semantics).
|
||||||
bool ends_with_nl = false;
|
bool ends_with_nl = false;
|
||||||
@@ -278,7 +317,8 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nrows_ = rows_.size();
|
nrows_ = rows_.size();
|
||||||
|
#endif
|
||||||
filename_ = norm;
|
filename_ = norm;
|
||||||
is_file_backed_ = true;
|
is_file_backed_ = true;
|
||||||
dirty_ = false;
|
dirty_ = false;
|
||||||
@@ -313,6 +353,12 @@ Buffer::Save(std::string &err) const
|
|||||||
err = "Failed to open for write: " + filename_;
|
err = "Failed to open for write: " + filename_;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
const char *d = content_.Data();
|
||||||
|
std::size_t n = content_.Size();
|
||||||
|
if (d && n)
|
||||||
|
out.write(d, static_cast<std::streamsize>(n));
|
||||||
|
#else
|
||||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||||
const char *d = rows_[i].Data();
|
const char *d = rows_[i].Data();
|
||||||
std::size_t n = rows_[i].Size();
|
std::size_t n = rows_[i].Size();
|
||||||
@@ -322,6 +368,7 @@ Buffer::Save(std::string &err) const
|
|||||||
out.put('\n');
|
out.put('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (!out.good()) {
|
if (!out.good()) {
|
||||||
err = "Write error";
|
err = "Write error";
|
||||||
return false;
|
return false;
|
||||||
@@ -360,6 +407,14 @@ Buffer::SaveAs(const std::string &path, std::string &err)
|
|||||||
err = "Failed to open for write: " + out_path;
|
err = "Failed to open for write: " + out_path;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
{
|
||||||
|
const char *d = content_.Data();
|
||||||
|
std::size_t n = content_.Size();
|
||||||
|
if (d && n)
|
||||||
|
out.write(d, static_cast<std::streamsize>(n));
|
||||||
|
}
|
||||||
|
#else
|
||||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||||
const char *d = rows_[i].Data();
|
const char *d = rows_[i].Data();
|
||||||
std::size_t n = rows_[i].Size();
|
std::size_t n = rows_[i].Size();
|
||||||
@@ -369,6 +424,7 @@ Buffer::SaveAs(const std::string &path, std::string &err)
|
|||||||
out.put('\n');
|
out.put('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (!out.good()) {
|
if (!out.good()) {
|
||||||
err = "Write error";
|
err = "Write error";
|
||||||
return false;
|
return false;
|
||||||
@@ -389,7 +445,11 @@ Buffer::AsString() const
|
|||||||
if (this->Dirty()) {
|
if (this->Dirty()) {
|
||||||
ss << "*";
|
ss << "*";
|
||||||
}
|
}
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
ss << ">: " << content_.LineCount() << " lines";
|
||||||
|
#else
|
||||||
ss << ">: " << rows_.size() << " lines";
|
ss << ">: " << rows_.size() << " lines";
|
||||||
|
#endif
|
||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,6 +458,19 @@ Buffer::AsString() const
|
|||||||
void
|
void
|
||||||
Buffer::insert_text(int row, int col, std::string_view text)
|
Buffer::insert_text(int row, int col, std::string_view text)
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
if (row < 0)
|
||||||
|
row = 0;
|
||||||
|
if (col < 0)
|
||||||
|
col = 0;
|
||||||
|
const std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row),
|
||||||
|
static_cast<std::size_t>(col));
|
||||||
|
if (!text.empty()) {
|
||||||
|
content_.Insert(off, text.data(), text.size());
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
#else
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
if (static_cast<std::size_t>(row) > rows_.size())
|
||||||
@@ -409,8 +482,9 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
|||||||
|
|
||||||
auto y = static_cast<std::size_t>(row);
|
auto y = static_cast<std::size_t>(row);
|
||||||
auto x = static_cast<std::size_t>(col);
|
auto x = static_cast<std::size_t>(col);
|
||||||
if (x > rows_[y].size())
|
if (x > rows_[y].size()) {
|
||||||
x = rows_[y].size();
|
x = rows_[y].size();
|
||||||
|
}
|
||||||
|
|
||||||
std::string remain(text);
|
std::string remain(text);
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -432,12 +506,110 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
|||||||
remain.erase(0, pos + 1);
|
remain.erase(0, pos + 1);
|
||||||
}
|
}
|
||||||
// Do not set dirty here; UndoSystem will manage state/dirty externally
|
// Do not set dirty here; UndoSystem will manage state/dirty externally
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
// ===== Adapter helpers for PieceTable-backed Buffer =====
|
||||||
|
void
|
||||||
|
Buffer::ensure_rows_cache() const
|
||||||
|
{
|
||||||
|
if (!rows_cache_dirty_)
|
||||||
|
return;
|
||||||
|
rows_.clear();
|
||||||
|
const std::size_t lc = content_.LineCount();
|
||||||
|
rows_.reserve(lc);
|
||||||
|
for (std::size_t i = 0; i < lc; ++i) {
|
||||||
|
rows_.emplace_back(content_.GetLine(i));
|
||||||
|
}
|
||||||
|
// Keep nrows_ in sync for any legacy code that still reads it
|
||||||
|
const_cast<Buffer *>(this)->nrows_ = rows_.size();
|
||||||
|
rows_cache_dirty_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Buffer::content_LineCount_() const
|
||||||
|
{
|
||||||
|
return content_.LineCount();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Buffer::delete_text(int row, int col, std::size_t len)
|
Buffer::delete_text(int row, int col, std::size_t len)
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
if (len == 0)
|
||||||
|
return;
|
||||||
|
if (row < 0)
|
||||||
|
row = 0;
|
||||||
|
if (col < 0)
|
||||||
|
col = 0;
|
||||||
|
std::size_t start = content_.LineColToByteOffset(static_cast<std::size_t>(row), static_cast<std::size_t>(col));
|
||||||
|
// Walk len logical characters across lines to compute end offset
|
||||||
|
std::size_t r = static_cast<std::size_t>(row);
|
||||||
|
std::size_t c = static_cast<std::size_t>(col);
|
||||||
|
std::size_t remaining = len;
|
||||||
|
const std::size_t line_count = content_.LineCount();
|
||||||
|
while (remaining > 0 && r < line_count) {
|
||||||
|
auto range = content_.GetLineRange(r); // [start,end)
|
||||||
|
// Compute end of line excluding trailing '\n'
|
||||||
|
std::size_t line_end = range.second;
|
||||||
|
if (line_end > range.first) {
|
||||||
|
// If last char is '\n', don't count in-column span
|
||||||
|
std::string last = content_.GetRange(line_end - 1, 1);
|
||||||
|
if (!last.empty() && last[0] == '\n') {
|
||||||
|
line_end -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::size_t cur_off = content_.LineColToByteOffset(r, c);
|
||||||
|
std::size_t in_line = (cur_off < line_end) ? (line_end - cur_off) : 0;
|
||||||
|
if (remaining <= in_line) {
|
||||||
|
// All within current line
|
||||||
|
std::size_t end = cur_off + remaining;
|
||||||
|
content_.Delete(start, end - start);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Consume rest of line
|
||||||
|
remaining -= in_line;
|
||||||
|
std::size_t end = cur_off + in_line;
|
||||||
|
// If there is a next line and remaining > 0, consider consuming the newline as 1
|
||||||
|
if (r + 1 < line_count) {
|
||||||
|
if (remaining > 0) {
|
||||||
|
// newline
|
||||||
|
end += 1;
|
||||||
|
remaining -= 1;
|
||||||
|
}
|
||||||
|
// Move to next line
|
||||||
|
r += 1;
|
||||||
|
c = 0;
|
||||||
|
// Update start deletion length so far by postponing until we know final end; we keep start fixed
|
||||||
|
if (remaining == 0) {
|
||||||
|
content_.Delete(start, end - start);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Continue loop with updated r/c; but also keep track of 'end' as current consumed position
|
||||||
|
// Rather than tracking incrementally, we will recompute cur_off at top of loop.
|
||||||
|
// However, we need to carry forward the consumed part; we can temporarily store 'end' in start_of_next
|
||||||
|
// To simplify, after loop finishes we will compute final end using current r/c using remaining.
|
||||||
|
} else {
|
||||||
|
// No next line; delete to file end
|
||||||
|
std::size_t total = content_.Size();
|
||||||
|
content_.Delete(start, total - start);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If loop ended because remaining==0 at a line boundary
|
||||||
|
if (remaining == 0) {
|
||||||
|
std::size_t end = content_.LineColToByteOffset(r, c);
|
||||||
|
content_.Delete(start, end - start);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
#else
|
||||||
if (rows_.empty() || len == 0)
|
if (rows_.empty() || len == 0)
|
||||||
return;
|
return;
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
@@ -470,12 +642,25 @@ Buffer::delete_text(int row, int col, std::size_t len)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Buffer::split_line(int row, const int col)
|
Buffer::split_line(int row, const int col)
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
if (row < 0)
|
||||||
|
row = 0;
|
||||||
|
if (col < 0)
|
||||||
|
row = 0;
|
||||||
|
const std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row),
|
||||||
|
static_cast<std::size_t>(col));
|
||||||
|
const char nl = '\n';
|
||||||
|
content_.Insert(off, &nl, 1);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
return;
|
||||||
|
#else
|
||||||
if (row < 0) {
|
if (row < 0) {
|
||||||
row = 0;
|
row = 0;
|
||||||
}
|
}
|
||||||
@@ -488,12 +673,26 @@ Buffer::split_line(int row, const int col)
|
|||||||
const auto tail = rows_[y].substr(x);
|
const auto tail = rows_[y].substr(x);
|
||||||
rows_[y].erase(x);
|
rows_[y].erase(x);
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Buffer::join_lines(int row)
|
Buffer::join_lines(int row)
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
if (row < 0)
|
||||||
|
row = 0;
|
||||||
|
std::size_t r = static_cast<std::size_t>(row);
|
||||||
|
if (r + 1 >= content_.LineCount())
|
||||||
|
return;
|
||||||
|
// Delete the newline between line r and r+1
|
||||||
|
std::size_t end_of_line = content_.LineColToByteOffset(r, std::numeric_limits<std::size_t>::max());
|
||||||
|
// end_of_line now equals line end (clamped before newline). The newline should be exactly at this position.
|
||||||
|
content_.Delete(end_of_line, 1);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
return;
|
||||||
|
#else
|
||||||
if (row < 0) {
|
if (row < 0) {
|
||||||
row = 0;
|
row = 0;
|
||||||
}
|
}
|
||||||
@@ -505,28 +704,57 @@ Buffer::join_lines(int row)
|
|||||||
|
|
||||||
rows_[y] += rows_[y + 1];
|
rows_[y] += rows_[y + 1];
|
||||||
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Buffer::insert_row(int row, const std::string_view text)
|
Buffer::insert_row(int row, const std::string_view text)
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
if (row < 0)
|
||||||
|
row = 0;
|
||||||
|
std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row), 0);
|
||||||
|
if (!text.empty())
|
||||||
|
content_.Insert(off, text.data(), text.size());
|
||||||
|
const char nl = '\n';
|
||||||
|
content_.Insert(off + text.size(), &nl, 1);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
return;
|
||||||
|
#else
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
if (static_cast<std::size_t>(row) > rows_.size())
|
||||||
row = static_cast<int>(rows_.size());
|
row = static_cast<int>(rows_.size());
|
||||||
rows_.insert(rows_.begin() + row, Line(std::string(text)));
|
rows_.insert(rows_.begin() + row, Line(std::string(text)));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Buffer::delete_row(int row)
|
Buffer::delete_row(int row)
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
if (row < 0)
|
||||||
|
row = 0;
|
||||||
|
std::size_t r = static_cast<std::size_t>(row);
|
||||||
|
if (r >= content_.LineCount())
|
||||||
|
return;
|
||||||
|
auto range = content_.GetLineRange(r); // [start,end)
|
||||||
|
// If not last line, ensure we include the separating newline by using end as-is (which points to next line start)
|
||||||
|
// If last line, end may equal total_size_. We still delete [start,end) which removes the last line content.
|
||||||
|
std::size_t start = range.first;
|
||||||
|
std::size_t end = range.second;
|
||||||
|
content_.Delete(start, end - start);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
return;
|
||||||
|
#else
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
if (static_cast<std::size_t>(row) >= rows_.size())
|
||||||
return;
|
return;
|
||||||
rows_.erase(rows_.begin() + row);
|
rows_.erase(rows_.begin() + row);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -542,4 +770,4 @@ const UndoSystem *
|
|||||||
Buffer::Undo() const
|
Buffer::Undo() const
|
||||||
{
|
{
|
||||||
return undo_sys_.get();
|
return undo_sys_.get();
|
||||||
}
|
}
|
||||||
32
Buffer.h
32
Buffer.h
@@ -10,6 +10,9 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include "AppendBuffer.h"
|
#include "AppendBuffer.h"
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
#include "PieceTable.h"
|
||||||
|
#endif
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -63,7 +66,11 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] std::size_t Nrows() const
|
[[nodiscard]] std::size_t Nrows() const
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
return content_LineCount_();
|
||||||
|
#else
|
||||||
return nrows_;
|
return nrows_;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -255,13 +262,23 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] const std::vector<Line> &Rows() const
|
[[nodiscard]] const std::vector<Line> &Rows() const
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
ensure_rows_cache();
|
||||||
return rows_;
|
return rows_;
|
||||||
|
#else
|
||||||
|
return rows_;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] std::vector<Line> &Rows()
|
[[nodiscard]] std::vector<Line> &Rows()
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
ensure_rows_cache();
|
||||||
return rows_;
|
return rows_;
|
||||||
|
#else
|
||||||
|
return rows_;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -460,7 +477,22 @@ private:
|
|||||||
std::size_t rx_ = 0; // render x (tabs expanded)
|
std::size_t rx_ = 0; // render x (tabs expanded)
|
||||||
std::size_t nrows_ = 0; // number of rows
|
std::size_t nrows_ = 0; // number of rows
|
||||||
std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets
|
std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
mutable std::vector<Line> rows_; // materialized cache of rows (without trailing newlines)
|
||||||
|
#else
|
||||||
std::vector<Line> rows_; // buffer rows (without trailing newlines)
|
std::vector<Line> rows_; // buffer rows (without trailing newlines)
|
||||||
|
#endif
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
// When using the adapter, PieceTable is the source of truth.
|
||||||
|
PieceTable content_{};
|
||||||
|
mutable bool rows_cache_dirty_ = true; // invalidate on edits / I/O
|
||||||
|
|
||||||
|
// Helper to rebuild rows_ from content_
|
||||||
|
void ensure_rows_cache() const;
|
||||||
|
|
||||||
|
// Helper to query content_.LineCount() while keeping header minimal
|
||||||
|
std::size_t content_LineCount_() const;
|
||||||
|
#endif
|
||||||
std::string filename_;
|
std::string filename_;
|
||||||
bool is_file_backed_ = false;
|
bool is_file_backed_ = false;
|
||||||
bool dirty_ = false;
|
bool dirty_ = false;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
|||||||
set(KTE_USE_QT OFF CACHE BOOL "Build the QT frontend instead of ImGui.")
|
set(KTE_USE_QT OFF CACHE BOOL "Build the QT frontend instead of ImGui.")
|
||||||
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
||||||
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
||||||
|
option(KTE_USE_BUFFER_PIECE_TABLE "Use PieceTable inside Buffer adapter (Phase 2)" OFF)
|
||||||
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||||
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
||||||
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
||||||
@@ -273,6 +274,9 @@ add_executable(kte
|
|||||||
if (KTE_USE_PIECE_TABLE)
|
if (KTE_USE_PIECE_TABLE)
|
||||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
endif ()
|
endif ()
|
||||||
|
if (KTE_USE_BUFFER_PIECE_TABLE)
|
||||||
|
target_compile_definitions(kte PRIVATE KTE_USE_BUFFER_PIECE_TABLE=1)
|
||||||
|
endif ()
|
||||||
if (KTE_UNDO_DEBUG)
|
if (KTE_UNDO_DEBUG)
|
||||||
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
|
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
endif ()
|
endif ()
|
||||||
@@ -309,6 +313,9 @@ if (BUILD_TESTS)
|
|||||||
if (KTE_USE_PIECE_TABLE)
|
if (KTE_USE_PIECE_TABLE)
|
||||||
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
endif ()
|
endif ()
|
||||||
|
if (KTE_USE_BUFFER_PIECE_TABLE)
|
||||||
|
target_compile_definitions(test_undo PRIVATE KTE_USE_BUFFER_PIECE_TABLE=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (KTE_UNDO_DEBUG)
|
if (KTE_UNDO_DEBUG)
|
||||||
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
|
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
@@ -357,6 +364,9 @@ if (${BUILD_GUI})
|
|||||||
if (KTE_UNDO_DEBUG)
|
if (KTE_UNDO_DEBUG)
|
||||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
endif ()
|
endif ()
|
||||||
|
if (KTE_USE_BUFFER_PIECE_TABLE)
|
||||||
|
target_compile_definitions(kge PRIVATE KTE_USE_BUFFER_PIECE_TABLE=1)
|
||||||
|
endif ()
|
||||||
if (KTE_USE_QT)
|
if (KTE_USE_QT)
|
||||||
target_link_libraries(kge ${CURSES_LIBRARIES} Qt6::Widgets)
|
target_link_libraries(kge ${CURSES_LIBRARIES} Qt6::Widgets)
|
||||||
else ()
|
else ()
|
||||||
|
|||||||
142
Command.cc
142
Command.cc
@@ -83,7 +83,7 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clamp vertical offset to available content
|
// Clamp vertical offset to available content
|
||||||
const auto total_rows = buf.Rows().size();
|
const auto total_rows = buf.Nrows();
|
||||||
if (content_rows < total_rows) {
|
if (content_rows < total_rows) {
|
||||||
std::size_t max_rowoffs = total_rows - content_rows;
|
std::size_t max_rowoffs = total_rows - content_rows;
|
||||||
if (rowoffs > max_rowoffs)
|
if (rowoffs > max_rowoffs)
|
||||||
@@ -115,8 +115,7 @@ cmd_center_on_cursor(CommandContext &ctx)
|
|||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf)
|
if (!buf)
|
||||||
return false;
|
return false;
|
||||||
const auto &rows = buf->Rows();
|
std::size_t total = buf->Nrows();
|
||||||
std::size_t total = rows.size();
|
|
||||||
std::size_t content = ctx.editor.ContentRows();
|
std::size_t content = ctx.editor.ContentRows();
|
||||||
if (content == 0)
|
if (content == 0)
|
||||||
content = 1;
|
content = 1;
|
||||||
@@ -139,8 +138,8 @@ cmd_center_on_cursor(CommandContext &ctx)
|
|||||||
static void
|
static void
|
||||||
ensure_at_least_one_line(Buffer &buf)
|
ensure_at_least_one_line(Buffer &buf)
|
||||||
{
|
{
|
||||||
if (buf.Rows().empty()) {
|
if (buf.Nrows() == 0) {
|
||||||
buf.Rows().emplace_back("");
|
buf.insert_row(0, "");
|
||||||
buf.SetDirty(true);
|
buf.SetDirty(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,33 +253,57 @@ extract_region_text(const Buffer &buf, std::size_t sx, std::size_t sy, std::size
|
|||||||
static void
|
static void
|
||||||
delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey)
|
delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey)
|
||||||
{
|
{
|
||||||
auto &rows = buf.Rows();
|
std::size_t nrows = buf.Nrows();
|
||||||
if (rows.empty())
|
if (nrows == 0)
|
||||||
return;
|
return;
|
||||||
if (sy >= rows.size())
|
if (sy >= nrows)
|
||||||
return;
|
return;
|
||||||
if (ey >= rows.size())
|
if (ey >= nrows)
|
||||||
ey = rows.size() - 1;
|
ey = nrows - 1;
|
||||||
if (sy == ey) {
|
if (sy == ey) {
|
||||||
auto &line = rows[sy];
|
// Single line: delete text from xs to xe
|
||||||
std::size_t xs = std::min(sx, line.size());
|
const auto &rows = buf.Rows();
|
||||||
std::size_t xe = std::min(ex, line.size());
|
const auto &line = rows[sy];
|
||||||
|
std::size_t xs = std::min(sx, line.size());
|
||||||
|
std::size_t xe = std::min(ex, line.size());
|
||||||
if (xe < xs)
|
if (xe < xs)
|
||||||
std::swap(xs, xe);
|
std::swap(xs, xe);
|
||||||
line.erase(xs, xe - xs);
|
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), xe - xs);
|
||||||
} else {
|
} else {
|
||||||
// Keep prefix of first and suffix of last then join
|
// Multi-line: delete from (sx,sy) to (ex,ey)
|
||||||
std::string prefix = rows[sy].substr(0, std::min(sx, rows[sy].size()));
|
// Strategy:
|
||||||
std::string suffix;
|
// 1. Save suffix of last line (from ex to end)
|
||||||
{
|
// 2. Delete tail of first line (from sx to end)
|
||||||
const auto &last = rows[ey];
|
// 3. Delete all lines from sy+1 to ey (inclusive)
|
||||||
std::size_t xe = std::min(ex, last.size());
|
// 4. Insert saved suffix at end of first line
|
||||||
suffix = last.substr(xe);
|
// 5. Join if needed (no, suffix is appended directly)
|
||||||
|
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
std::size_t first_line_len = rows[sy].size();
|
||||||
|
std::size_t last_line_len = rows[ey].size();
|
||||||
|
std::size_t xs = std::min(sx, first_line_len);
|
||||||
|
std::size_t xe = std::min(ex, last_line_len);
|
||||||
|
|
||||||
|
// Save suffix of last line before any modifications
|
||||||
|
std::string suffix = rows[ey].substr(xe);
|
||||||
|
|
||||||
|
// Delete tail of first line (from xs to end)
|
||||||
|
if (xs < first_line_len) {
|
||||||
|
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), first_line_len - xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete lines from ey down to sy+1 (reverse order to preserve indices)
|
||||||
|
for (std::size_t i = ey; i > sy; --i) {
|
||||||
|
buf.delete_row(static_cast<int>(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append saved suffix to first line
|
||||||
|
if (!suffix.empty()) {
|
||||||
|
// Get current length of line sy after deletions
|
||||||
|
const auto &rows_after = buf.Rows();
|
||||||
|
std::size_t line_len = rows_after[sy].size();
|
||||||
|
buf.insert_text(static_cast<int>(sy), static_cast<int>(line_len), suffix);
|
||||||
}
|
}
|
||||||
rows[sy] = prefix + suffix;
|
|
||||||
// erase middle lines and the last line
|
|
||||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(sy + 1),
|
|
||||||
rows.begin() + static_cast<std::ptrdiff_t>(ey + 1));
|
|
||||||
}
|
}
|
||||||
buf.SetCursor(sx, sy);
|
buf.SetCursor(sx, sy);
|
||||||
buf.SetDirty(true);
|
buf.SetDirty(true);
|
||||||
@@ -291,15 +314,19 @@ delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::
|
|||||||
static void
|
static void
|
||||||
insert_text_at_cursor(Buffer &buf, const std::string &text)
|
insert_text_at_cursor(Buffer &buf, const std::string &text)
|
||||||
{
|
{
|
||||||
auto &rows = buf.Rows();
|
std::size_t nrows = buf.Nrows();
|
||||||
std::size_t y = buf.Cury();
|
std::size_t y = buf.Cury();
|
||||||
std::size_t x = buf.Curx();
|
std::size_t x = buf.Curx();
|
||||||
if (y > rows.size())
|
if (y > nrows)
|
||||||
y = rows.size();
|
y = nrows;
|
||||||
if (rows.empty())
|
if (nrows == 0) {
|
||||||
rows.emplace_back("");
|
buf.insert_row(0, "");
|
||||||
if (y >= rows.size())
|
nrows = 1;
|
||||||
rows.emplace_back("");
|
}
|
||||||
|
if (y >= nrows) {
|
||||||
|
buf.insert_row(static_cast<int>(nrows), "");
|
||||||
|
nrows = buf.Nrows();
|
||||||
|
}
|
||||||
|
|
||||||
std::size_t cur_y = y;
|
std::size_t cur_y = y;
|
||||||
std::size_t cur_x = x;
|
std::size_t cur_x = x;
|
||||||
@@ -309,25 +336,28 @@ insert_text_at_cursor(Buffer &buf, const std::string &text)
|
|||||||
auto pos = remain.find('\n');
|
auto pos = remain.find('\n');
|
||||||
if (pos == std::string::npos) {
|
if (pos == std::string::npos) {
|
||||||
// insert remaining into current line
|
// insert remaining into current line
|
||||||
if (cur_y >= rows.size())
|
nrows = buf.Nrows();
|
||||||
rows.emplace_back("");
|
if (cur_y >= nrows) {
|
||||||
|
buf.insert_row(static_cast<int>(nrows), "");
|
||||||
|
}
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
if (cur_x > rows[cur_y].size())
|
if (cur_x > rows[cur_y].size())
|
||||||
cur_x = rows[cur_y].size();
|
cur_x = rows[cur_y].size();
|
||||||
rows[cur_y].insert(cur_x, remain);
|
buf.insert_text(static_cast<int>(cur_y), static_cast<int>(cur_x), remain);
|
||||||
cur_x += remain.size();
|
cur_x += remain.size();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// insert segment before newline
|
// insert segment before newline
|
||||||
std::string seg = remain.substr(0, pos);
|
std::string seg = remain.substr(0, pos);
|
||||||
if (cur_x > rows[cur_y].size())
|
{
|
||||||
cur_x = rows[cur_y].size();
|
const auto &rows = buf.Rows();
|
||||||
rows[cur_y].insert(cur_x, seg);
|
if (cur_x > rows[cur_y].size())
|
||||||
|
cur_x = rows[cur_y].size();
|
||||||
|
}
|
||||||
|
buf.insert_text(static_cast<int>(cur_y), static_cast<int>(cur_x), seg);
|
||||||
// split line at cur_x + seg.size()
|
// split line at cur_x + seg.size()
|
||||||
cur_x += seg.size();
|
cur_x += seg.size();
|
||||||
std::string after = rows[cur_y].substr(cur_x);
|
buf.split_line(static_cast<int>(cur_y), static_cast<int>(cur_x));
|
||||||
rows[cur_y].erase(cur_x);
|
|
||||||
// create new line after current with the 'after' tail
|
|
||||||
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(cur_y + 1), Buffer::Line(after));
|
|
||||||
// move to start of next line
|
// move to start of next line
|
||||||
cur_y += 1;
|
cur_y += 1;
|
||||||
cur_x = 0;
|
cur_x = 0;
|
||||||
@@ -410,10 +440,8 @@ cmd_move_cursor_to(CommandContext &ctx)
|
|||||||
std::size_t bco = buf->Coloffs();
|
std::size_t bco = buf->Coloffs();
|
||||||
std::size_t by = bro + vy;
|
std::size_t by = bro + vy;
|
||||||
// Clamp by to existing lines later
|
// Clamp by to existing lines later
|
||||||
auto &lines2 = buf->Rows();
|
ensure_at_least_one_line(*buf);
|
||||||
if (lines2.empty()) {
|
const auto &lines2 = buf->Rows();
|
||||||
lines2.emplace_back("");
|
|
||||||
}
|
|
||||||
if (by >= lines2.size())
|
if (by >= lines2.size())
|
||||||
by = lines2.size() - 1;
|
by = lines2.size() - 1;
|
||||||
std::string line2 = static_cast<std::string>(lines2[by]);
|
std::string line2 = static_cast<std::string>(lines2[by]);
|
||||||
@@ -430,10 +458,8 @@ cmd_move_cursor_to(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto &lines = buf->Rows();
|
ensure_at_least_one_line(*buf);
|
||||||
if (lines.empty()) {
|
const auto &lines = buf->Rows();
|
||||||
lines.emplace_back("");
|
|
||||||
}
|
|
||||||
if (row >= lines.size())
|
if (row >= lines.size())
|
||||||
row = lines.size() - 1;
|
row = lines.size() - 1;
|
||||||
std::string line = static_cast<std::string>(lines[row]);
|
std::string line = static_cast<std::string>(lines[row]);
|
||||||
@@ -2122,20 +2148,24 @@ cmd_show_help(CommandContext &ctx)
|
|||||||
};
|
};
|
||||||
|
|
||||||
auto populate_from_text = [](Buffer &b, const std::string &text) {
|
auto populate_from_text = [](Buffer &b, const std::string &text) {
|
||||||
auto &rows = b.Rows();
|
// Clear existing rows
|
||||||
rows.clear();
|
while (b.Nrows() > 0) {
|
||||||
|
b.delete_row(0);
|
||||||
|
}
|
||||||
|
// Parse text and insert rows
|
||||||
std::string line;
|
std::string line;
|
||||||
line.reserve(128);
|
line.reserve(128);
|
||||||
|
int row_idx = 0;
|
||||||
for (char ch: text) {
|
for (char ch: text) {
|
||||||
if (ch == '\n') {
|
if (ch == '\n') {
|
||||||
rows.emplace_back(line);
|
b.insert_row(row_idx++, line);
|
||||||
line.clear();
|
line.clear();
|
||||||
} else if (ch != '\r') {
|
} else if (ch != '\r') {
|
||||||
line.push_back(ch);
|
line.push_back(ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Add last line (even if empty)
|
// Add last line (even if empty)
|
||||||
rows.emplace_back(line);
|
b.insert_row(row_idx, line);
|
||||||
b.SetDirty(false);
|
b.SetDirty(false);
|
||||||
b.SetCursor(0, 0);
|
b.SetCursor(0, 0);
|
||||||
b.SetOffsets(0, 0);
|
b.SetOffsets(0, 0);
|
||||||
|
|||||||
310
PieceTable.cc
310
PieceTable.cc
@@ -1,5 +1,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include "PieceTable.h"
|
#include "PieceTable.h"
|
||||||
|
|
||||||
@@ -151,6 +152,8 @@ PieceTable::Clear()
|
|||||||
materialized_.clear();
|
materialized_.clear();
|
||||||
total_size_ = 0;
|
total_size_ = 0;
|
||||||
dirty_ = true;
|
dirty_ = true;
|
||||||
|
line_index_.clear();
|
||||||
|
line_index_dirty_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -179,6 +182,7 @@ PieceTable::addPieceBack(const Source src, const std::size_t start, const std::s
|
|||||||
pieces_.push_back(Piece{src, start, len});
|
pieces_.push_back(Piece{src, start, len});
|
||||||
total_size_ += len;
|
total_size_ += len;
|
||||||
dirty_ = true;
|
dirty_ = true;
|
||||||
|
InvalidateLineIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -203,6 +207,7 @@ PieceTable::addPieceFront(Source src, std::size_t start, std::size_t len)
|
|||||||
pieces_.insert(pieces_.begin(), Piece{src, start, len});
|
pieces_.insert(pieces_.begin(), Piece{src, start, len});
|
||||||
total_size_ += len;
|
total_size_ += len;
|
||||||
dirty_ = true;
|
dirty_ = true;
|
||||||
|
InvalidateLineIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -225,3 +230,308 @@ PieceTable::materialize() const
|
|||||||
// Ensure there is a null terminator present via std::string invariants
|
// Ensure there is a null terminator present via std::string invariants
|
||||||
dirty_ = false;
|
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;
|
||||||
|
// Try merge with previous
|
||||||
|
if (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));
|
||||||
|
if (index > 0)
|
||||||
|
index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Try merge with next (index may have shifted)
|
||||||
|
if (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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PieceTable::InvalidateLineIndex() const
|
||||||
|
{
|
||||||
|
line_index_dirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PieceTable::RebuildLineIndex() const
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
for (std::size_t j = 0; j < pc.len; ++j) {
|
||||||
|
if (base[j] == '\n') {
|
||||||
|
// next line starts after the newline
|
||||||
|
line_index_.push_back(pos + j + 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_;
|
||||||
|
|
||||||
|
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;
|
||||||
|
InvalidateLineIndex();
|
||||||
|
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;
|
||||||
|
InvalidateLineIndex();
|
||||||
|
coalesceNeighbors(pieces_.size() - 1);
|
||||||
|
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;
|
||||||
|
InvalidateLineIndex();
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
InvalidateLineIndex();
|
||||||
|
if (idx < pieces_.size())
|
||||||
|
coalesceNeighbors(idx);
|
||||||
|
if (idx > 0)
|
||||||
|
coalesceNeighbors(idx - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
// Trim trailing '\n'
|
||||||
|
if (end > start) {
|
||||||
|
// To check last char, we can get it via GetRange of len 1 at end-1 without materializing whole
|
||||||
|
std::string last = GetRange(end - 1, 1);
|
||||||
|
if (!last.empty() && last[0] == '\n') {
|
||||||
|
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 line length excluding trailing newline
|
||||||
|
if (end > start) {
|
||||||
|
std::string last = GetRange(end - 1, 1);
|
||||||
|
if (!last.empty() && last[0] == '\n') {
|
||||||
|
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;
|
||||||
|
materialize();
|
||||||
|
return materialized_.substr(byte_offset, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
materialize();
|
||||||
|
auto pos = materialized_.find(needle, start);
|
||||||
|
if (pos == std::string::npos)
|
||||||
|
return std::numeric_limits<std::size_t>::max();
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
39
PieceTable.h
39
PieceTable.h
@@ -68,6 +68,30 @@ public:
|
|||||||
return materialized_.capacity();
|
return materialized_.capacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== New buffer-wide API (Phase 1) =====
|
||||||
|
// Byte-based editing operations
|
||||||
|
void Insert(std::size_t byte_offset, const char *text, std::size_t len);
|
||||||
|
|
||||||
|
void Delete(std::size_t byte_offset, std::size_t len);
|
||||||
|
|
||||||
|
// Line-based queries
|
||||||
|
[[nodiscard]] std::size_t LineCount() const; // number of logical lines
|
||||||
|
[[nodiscard]] std::string GetLine(std::size_t line_num) const;
|
||||||
|
|
||||||
|
[[nodiscard]] std::pair<std::size_t, std::size_t> GetLineRange(std::size_t line_num) const; // [start,end)
|
||||||
|
|
||||||
|
// Position conversion
|
||||||
|
[[nodiscard]] std::pair<std::size_t, std::size_t> ByteOffsetToLineCol(std::size_t byte_offset) const;
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t LineColToByteOffset(std::size_t row, std::size_t col) const;
|
||||||
|
|
||||||
|
// Substring extraction
|
||||||
|
[[nodiscard]] std::string GetRange(std::size_t byte_offset, std::size_t len) const;
|
||||||
|
|
||||||
|
// Simple search utility; returns byte offset or npos
|
||||||
|
[[nodiscard]] std::size_t Find(const std::string &needle, std::size_t start = 0) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Source : unsigned char { Original, Add };
|
enum class Source : unsigned char { Original, Add };
|
||||||
|
|
||||||
@@ -83,6 +107,17 @@ private:
|
|||||||
|
|
||||||
void materialize() const;
|
void materialize() const;
|
||||||
|
|
||||||
|
// Helper: locate piece index and inner offset for a global byte offset
|
||||||
|
[[nodiscard]] std::pair<std::size_t, std::size_t> locate(std::size_t byte_offset) const;
|
||||||
|
|
||||||
|
// Helper: try to coalesce neighboring pieces around index
|
||||||
|
void coalesceNeighbors(std::size_t index);
|
||||||
|
|
||||||
|
// Line index support (rebuilt lazily on demand)
|
||||||
|
void InvalidateLineIndex() const;
|
||||||
|
|
||||||
|
void RebuildLineIndex() const;
|
||||||
|
|
||||||
// Underlying storages
|
// Underlying storages
|
||||||
std::string original_; // unused for builder use-case, but kept for API symmetry
|
std::string original_; // unused for builder use-case, but kept for API symmetry
|
||||||
std::string add_;
|
std::string add_;
|
||||||
@@ -91,4 +126,8 @@ private:
|
|||||||
mutable std::string materialized_;
|
mutable std::string materialized_;
|
||||||
mutable bool dirty_ = true;
|
mutable bool dirty_ = true;
|
||||||
std::size_t total_size_ = 0;
|
std::size_t total_size_ = 0;
|
||||||
|
|
||||||
|
// Cached line index: starting byte offset of each line (always contains at least 1 entry: 0)
|
||||||
|
mutable std::vector<std::size_t> line_index_;
|
||||||
|
mutable bool line_index_dirty_ = true;
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user