Remove GapBuffer and associated legacy implementation.
- Deleted `GapBuffer` class and its API implementations. - Removed `AppendBuffer` selector and conditional `KTE_USE_PIECE_TABLE` macros. - Eliminated legacy support in buffer APIs, file I/O, benchmarks, and correctness tests. - Updated guidelines and comments to reflect PieceTable as the default and only buffer backend.
This commit is contained in:
@@ -1,28 +1,35 @@
|
|||||||
# Project Guidelines
|
# Project Guidelines
|
||||||
|
|
||||||
kte is Kyle's Text Editor — a simple, fast text editor written in C++17. It
|
kte is Kyle's Text Editor — a simple, fast text editor written in C++17.
|
||||||
replaces the earlier C implementation, ke (see the ke manual in `docs/ke.md`). The
|
It
|
||||||
design draws inspiration from Antirez' kilo, with keybindings rooted in the
|
replaces the earlier C implementation, ke (see the ke manual in
|
||||||
|
`docs/ke.md`). The
|
||||||
|
design draws inspiration from Antirez' kilo, with keybindings rooted in
|
||||||
|
the
|
||||||
WordStar/VDE family and emacs. The spiritual parent is `mg(1)`.
|
WordStar/VDE family and emacs. The spiritual parent is `mg(1)`.
|
||||||
|
|
||||||
These guidelines summarize the goals, interfaces, key operations, and current
|
These guidelines summarize the goals, interfaces, key operations, and
|
||||||
|
current
|
||||||
development practices for kte.
|
development practices for kte.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
- Keep the core small, fast, and understandable.
|
- Keep the core small, fast, and understandable.
|
||||||
- Provide an ncurses-based terminal-first editing experience, with an additional ImGui GUI.
|
- Provide an ncurses-based terminal-first editing experience, with an
|
||||||
|
additional ImGui GUI.
|
||||||
- Preserve familiar keybindings from ke while modernizing the internals.
|
- Preserve familiar keybindings from ke while modernizing the internals.
|
||||||
- Favor simple data structures (e.g., piece table) and incremental evolution.
|
- Favor simple data structures (e.g., piece table) and incremental
|
||||||
|
evolution.
|
||||||
|
|
||||||
Project entry point: `main.cpp`
|
Project entry point: `main.cpp`
|
||||||
|
|
||||||
## Core Components (current codebase)
|
## Core Components (current codebase)
|
||||||
|
|
||||||
- Buffer: editing model and file I/O (`Buffer.h/.cpp`).
|
- Buffer: editing model and file I/O (`Buffer.h/.cpp`).
|
||||||
- GapBuffer: editable in-memory text representation (`GapBuffer.h/.cpp`).
|
- PieceTable: editable in-memory text representation (
|
||||||
- PieceTable: experimental/alternative representation (`PieceTable.h/.cpp`).
|
`PieceTable.h/.cpp`).
|
||||||
- InputHandler: interface for handling text input (`InputHandler.h/`), along
|
- InputHandler: interface for handling text input (`InputHandler.h/`),
|
||||||
|
along
|
||||||
with `TerminalInputHandler` (ncurses-based) and `GUIInputHandler`.
|
with `TerminalInputHandler` (ncurses-based) and `GUIInputHandler`.
|
||||||
- Renderer: interface for rendering text (`Renderer.h`), along with
|
- Renderer: interface for rendering text (`Renderer.h`), along with
|
||||||
`TerminalRenderer` (ncurses-based) and `GUIRenderer`.
|
`TerminalRenderer` (ncurses-based) and `GUIRenderer`.
|
||||||
@@ -38,11 +45,13 @@ The file `docs/ke.md` contains the canonical reference for keybindings.
|
|||||||
|
|
||||||
- C++ standard: C++17.
|
- C++ standard: C++17.
|
||||||
- Keep dependencies minimal.
|
- Keep dependencies minimal.
|
||||||
- Prefer small, focused changes that preserve ke’s UX unless explicitly changing
|
- Prefer small, focused changes that preserve ke’s UX unless explicitly
|
||||||
|
changing
|
||||||
behavior.
|
behavior.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Previous editor manual: `ke.md` (canonical keybinding/spec reference for now).
|
- Previous editor manual: `ke.md` (canonical keybinding/spec reference
|
||||||
|
for now).
|
||||||
- Inspiration: kilo, WordStar/VDE, emacs, `mg(1)`.
|
- Inspiration: kilo, WordStar/VDE, emacs, `mg(1)`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
* AppendBuffer.h - selector header to choose GapBuffer or PieceTable
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#ifdef KTE_USE_PIECE_TABLE
|
|
||||||
#include "PieceTable.h"
|
|
||||||
using AppendBuffer = PieceTable;
|
|
||||||
#else
|
|
||||||
#include "GapBuffer.h"
|
|
||||||
using AppendBuffer = GapBuffer;
|
|
||||||
#endif
|
|
||||||
297
Buffer.cc
297
Buffer.cc
@@ -30,24 +30,22 @@ 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_;
|
content_ = other.content_;
|
||||||
rows_cache_dirty_ = other.rows_cache_dirty_;
|
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_;
|
read_only_ = other.read_only_;
|
||||||
read_only_ = other.read_only_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_set_ = other.mark_set_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_cury_ = other.mark_cury_;
|
||||||
mark_cury_ = other.mark_cury_;
|
|
||||||
// Copy syntax/highlighting flags
|
// Copy syntax/highlighting flags
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
@@ -82,27 +80,25 @@ 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_;
|
content_ = other.content_;
|
||||||
rows_cache_dirty_ = other.rows_cache_dirty_;
|
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_;
|
read_only_ = other.read_only_;
|
||||||
read_only_ = other.read_only_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_set_ = other.mark_set_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_cury_ = other.mark_cury_;
|
||||||
mark_cury_ = other.mark_cury_;
|
version_ = other.version_;
|
||||||
version_ = other.version_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
filetype_ = other.filetype_;
|
||||||
filetype_ = other.filetype_;
|
|
||||||
// Recreate undo system for this instance
|
// Recreate undo system for this instance
|
||||||
undo_tree_ = std::make_unique<UndoTree>();
|
undo_tree_ = std::make_unique<UndoTree>();
|
||||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||||
@@ -146,14 +142,12 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
undo_sys_(std::move(other.undo_sys_))
|
undo_sys_(std::move(other.undo_sys_))
|
||||||
{
|
{
|
||||||
// Move syntax/highlighting state
|
// Move syntax/highlighting state
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
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_);
|
content_ = std::move(other.content_);
|
||||||
rows_cache_dirty_ = other.rows_cache_dirty_;
|
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);
|
||||||
@@ -186,15 +180,12 @@ Buffer::operator=(Buffer &&other) noexcept
|
|||||||
undo_sys_ = std::move(other.undo_sys_);
|
undo_sys_ = std::move(other.undo_sys_);
|
||||||
|
|
||||||
// Move syntax/highlighting state
|
// Move syntax/highlighting state
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
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_);
|
content_ = std::move(other.content_);
|
||||||
rows_cache_dirty_ = other.rows_cache_dirty_;
|
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);
|
||||||
@@ -246,11 +237,9 @@ 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
|
// Empty PieceTable
|
||||||
content_.Clear();
|
content_.Clear();
|
||||||
rows_cache_dirty_ = true;
|
rows_cache_dirty_ = true;
|
||||||
#endif
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -261,7 +250,6 @@ 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
|
// Read entire file into PieceTable as-is
|
||||||
std::string data;
|
std::string data;
|
||||||
in.seekg(0, std::ios::end);
|
in.seekg(0, std::ios::end);
|
||||||
@@ -275,53 +263,10 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
|
|||||||
if (!data.empty())
|
if (!data.empty())
|
||||||
content_.Append(data.data(), data.size());
|
content_.Append(data.data(), data.size());
|
||||||
rows_cache_dirty_ = true;
|
rows_cache_dirty_ = true;
|
||||||
nrows_ = 0; // not used under adapter
|
nrows_ = 0; // not used under PieceTable
|
||||||
#else
|
filename_ = norm;
|
||||||
// Detect if file ends with a newline so we can preserve a final empty line
|
is_file_backed_ = true;
|
||||||
// in our in-memory representation (mg-style semantics).
|
dirty_ = false;
|
||||||
bool ends_with_nl = false;
|
|
||||||
{
|
|
||||||
in.seekg(0, std::ios::end);
|
|
||||||
std::streamoff sz = in.tellg();
|
|
||||||
if (sz > 0) {
|
|
||||||
in.seekg(-1, std::ios::end);
|
|
||||||
char last = 0;
|
|
||||||
in.read(&last, 1);
|
|
||||||
ends_with_nl = (last == '\n');
|
|
||||||
} else {
|
|
||||||
in.clear();
|
|
||||||
}
|
|
||||||
// Rewind to start for line-by-line read
|
|
||||||
in.clear();
|
|
||||||
in.seekg(0, std::ios::beg);
|
|
||||||
}
|
|
||||||
|
|
||||||
rows_.clear();
|
|
||||||
std::string line;
|
|
||||||
while (std::getline(in, line)) {
|
|
||||||
// std::getline strips the '\n', keep raw line content only
|
|
||||||
// Handle potential Windows CRLF: strip trailing '\r'
|
|
||||||
if (!line.empty() && line.back() == '\r') {
|
|
||||||
line.pop_back();
|
|
||||||
}
|
|
||||||
rows_.emplace_back(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the file ended with a newline and we didn't already get an
|
|
||||||
// empty final row from getline (e.g., when the last textual line
|
|
||||||
// had content followed by '\n'), append an empty row to represent
|
|
||||||
// the cursor position past the last newline.
|
|
||||||
if (ends_with_nl) {
|
|
||||||
if (rows_.empty() || !rows_.back().empty()) {
|
|
||||||
rows_.emplace_back(std::string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nrows_ = rows_.size();
|
|
||||||
#endif
|
|
||||||
filename_ = norm;
|
|
||||||
is_file_backed_ = true;
|
|
||||||
dirty_ = false;
|
|
||||||
|
|
||||||
// Reset/initialize undo system for this loaded file
|
// Reset/initialize undo system for this loaded file
|
||||||
if (!undo_tree_)
|
if (!undo_tree_)
|
||||||
@@ -353,22 +298,10 @@ 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();
|
const char *d = content_.Data();
|
||||||
std::size_t n = content_.Size();
|
std::size_t n = content_.Size();
|
||||||
if (d && n)
|
if (d && n)
|
||||||
out.write(d, static_cast<std::streamsize>(n));
|
out.write(d, static_cast<std::streamsize>(n));
|
||||||
#else
|
|
||||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
|
||||||
const char *d = rows_[i].Data();
|
|
||||||
std::size_t n = rows_[i].Size();
|
|
||||||
if (d && n)
|
|
||||||
out.write(d, static_cast<std::streamsize>(n));
|
|
||||||
if (i + 1 < rows_.size()) {
|
|
||||||
out.put('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (!out.good()) {
|
if (!out.good()) {
|
||||||
err = "Write error";
|
err = "Write error";
|
||||||
return false;
|
return false;
|
||||||
@@ -407,24 +340,12 @@ 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();
|
const char *d = content_.Data();
|
||||||
std::size_t n = content_.Size();
|
std::size_t n = content_.Size();
|
||||||
if (d && n)
|
if (d && n)
|
||||||
out.write(d, static_cast<std::streamsize>(n));
|
out.write(d, static_cast<std::streamsize>(n));
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
|
||||||
const char *d = rows_[i].Data();
|
|
||||||
std::size_t n = rows_[i].Size();
|
|
||||||
if (d && n)
|
|
||||||
out.write(d, static_cast<std::streamsize>(n));
|
|
||||||
if (i + 1 < rows_.size()) {
|
|
||||||
out.put('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (!out.good()) {
|
if (!out.good()) {
|
||||||
err = "Write error";
|
err = "Write error";
|
||||||
return false;
|
return false;
|
||||||
@@ -445,11 +366,7 @@ Buffer::AsString() const
|
|||||||
if (this->Dirty()) {
|
if (this->Dirty()) {
|
||||||
ss << "*";
|
ss << "*";
|
||||||
}
|
}
|
||||||
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
|
||||||
ss << ">: " << content_.LineCount() << " lines";
|
ss << ">: " << content_.LineCount() << " lines";
|
||||||
#else
|
|
||||||
ss << ">: " << rows_.size() << " lines";
|
|
||||||
#endif
|
|
||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +375,6 @@ 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)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (col < 0)
|
if (col < 0)
|
||||||
@@ -469,47 +385,9 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
|||||||
content_.Insert(off, text.data(), text.size());
|
content_.Insert(off, text.data(), text.size());
|
||||||
rows_cache_dirty_ = true;
|
rows_cache_dirty_ = true;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
#else
|
|
||||||
if (row < 0)
|
|
||||||
row = 0;
|
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
|
||||||
row = static_cast<int>(rows_.size());
|
|
||||||
if (rows_.empty())
|
|
||||||
rows_.emplace_back("");
|
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
|
||||||
rows_.emplace_back("");
|
|
||||||
|
|
||||||
auto y = static_cast<std::size_t>(row);
|
|
||||||
auto x = static_cast<std::size_t>(col);
|
|
||||||
if (x > rows_[y].size()) {
|
|
||||||
x = rows_[y].size();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string remain(text);
|
|
||||||
while (true) {
|
|
||||||
auto pos = remain.find('\n');
|
|
||||||
if (pos == std::string::npos) {
|
|
||||||
rows_[y].insert(x, remain);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Insert up to newline
|
|
||||||
std::string seg = remain.substr(0, pos);
|
|
||||||
rows_[y].insert(x, seg);
|
|
||||||
x += seg.size();
|
|
||||||
// Split line at x
|
|
||||||
std::string tail = rows_[y].substr(x);
|
|
||||||
rows_[y].erase(x);
|
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
|
||||||
y += 1;
|
|
||||||
x = 0;
|
|
||||||
remain.erase(0, pos + 1);
|
|
||||||
}
|
|
||||||
// Do not set dirty here; UndoSystem will manage state/dirty externally
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
|
||||||
// ===== Adapter helpers for PieceTable-backed Buffer =====
|
// ===== Adapter helpers for PieceTable-backed Buffer =====
|
||||||
void
|
void
|
||||||
Buffer::ensure_rows_cache() const
|
Buffer::ensure_rows_cache() const
|
||||||
@@ -527,18 +405,17 @@ Buffer::ensure_rows_cache() const
|
|||||||
rows_cache_dirty_ = false;
|
rows_cache_dirty_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::size_t
|
std::size_t
|
||||||
Buffer::content_LineCount_() const
|
Buffer::content_LineCount_() const
|
||||||
{
|
{
|
||||||
return content_.LineCount();
|
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)
|
if (len == 0)
|
||||||
return;
|
return;
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
@@ -608,48 +485,12 @@ Buffer::delete_text(int row, int col, std::size_t len)
|
|||||||
content_.Delete(start, end - start);
|
content_.Delete(start, end - start);
|
||||||
rows_cache_dirty_ = true;
|
rows_cache_dirty_ = true;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
#else
|
|
||||||
if (rows_.empty() || len == 0)
|
|
||||||
return;
|
|
||||||
if (row < 0)
|
|
||||||
row = 0;
|
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
|
||||||
return;
|
|
||||||
const auto y = static_cast<std::size_t>(row);
|
|
||||||
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
|
||||||
|
|
||||||
std::size_t remaining = len;
|
|
||||||
while (remaining > 0 && y < rows_.size()) {
|
|
||||||
auto &line = rows_[y];
|
|
||||||
const std::size_t in_line = std::min<std::size_t>(remaining, line.size() - std::min(x, line.size()));
|
|
||||||
if (x < line.size() && in_line > 0) {
|
|
||||||
line.erase(x, in_line);
|
|
||||||
remaining -= in_line;
|
|
||||||
}
|
|
||||||
if (remaining == 0)
|
|
||||||
break;
|
|
||||||
// If at or beyond end of line and there is a next line, join it (deleting the implied '\n')
|
|
||||||
if (y + 1 < rows_.size()) {
|
|
||||||
line += rows_[y + 1];
|
|
||||||
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
|
||||||
// deleting the newline consumes one virtual character
|
|
||||||
if (remaining > 0) {
|
|
||||||
// Treat the newline as one deletion unit if len spans it
|
|
||||||
// We already joined, so nothing else to do here.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (col < 0)
|
if (col < 0)
|
||||||
@@ -659,28 +500,12 @@ Buffer::split_line(int row, const int col)
|
|||||||
const char nl = '\n';
|
const char nl = '\n';
|
||||||
content_.Insert(off, &nl, 1);
|
content_.Insert(off, &nl, 1);
|
||||||
rows_cache_dirty_ = true;
|
rows_cache_dirty_ = true;
|
||||||
return;
|
|
||||||
#else
|
|
||||||
if (row < 0) {
|
|
||||||
row = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size()) {
|
|
||||||
rows_.resize(static_cast<std::size_t>(row) + 1);
|
|
||||||
}
|
|
||||||
const auto y = static_cast<std::size_t>(row);
|
|
||||||
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
|
||||||
const auto tail = rows_[y].substr(x);
|
|
||||||
rows_[y].erase(x);
|
|
||||||
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)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
std::size_t r = static_cast<std::size_t>(row);
|
std::size_t r = static_cast<std::size_t>(row);
|
||||||
@@ -691,27 +516,12 @@ Buffer::join_lines(int row)
|
|||||||
// end_of_line now equals line end (clamped before newline). The newline should be exactly at this position.
|
// end_of_line now equals line end (clamped before newline). The newline should be exactly at this position.
|
||||||
content_.Delete(end_of_line, 1);
|
content_.Delete(end_of_line, 1);
|
||||||
rows_cache_dirty_ = true;
|
rows_cache_dirty_ = true;
|
||||||
return;
|
|
||||||
#else
|
|
||||||
if (row < 0) {
|
|
||||||
row = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto y = static_cast<std::size_t>(row);
|
|
||||||
if (y + 1 >= rows_.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows_[y] += rows_[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)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row), 0);
|
std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row), 0);
|
||||||
@@ -720,21 +530,12 @@ Buffer::insert_row(int row, const std::string_view text)
|
|||||||
const char nl = '\n';
|
const char nl = '\n';
|
||||||
content_.Insert(off + text.size(), &nl, 1);
|
content_.Insert(off + text.size(), &nl, 1);
|
||||||
rows_cache_dirty_ = true;
|
rows_cache_dirty_ = true;
|
||||||
return;
|
|
||||||
#else
|
|
||||||
if (row < 0)
|
|
||||||
row = 0;
|
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
|
||||||
row = static_cast<int>(rows_.size());
|
|
||||||
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)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
std::size_t r = static_cast<std::size_t>(row);
|
std::size_t r = static_cast<std::size_t>(row);
|
||||||
@@ -747,14 +548,6 @@ Buffer::delete_row(int row)
|
|||||||
std::size_t end = range.second;
|
std::size_t end = range.second;
|
||||||
content_.Delete(start, end - start);
|
content_.Delete(start, end - start);
|
||||||
rows_cache_dirty_ = true;
|
rows_cache_dirty_ = true;
|
||||||
return;
|
|
||||||
#else
|
|
||||||
if (row < 0)
|
|
||||||
row = 0;
|
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
|
||||||
return;
|
|
||||||
rows_.erase(rows_.begin() + row);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
30
Buffer.h
30
Buffer.h
@@ -9,10 +9,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include "AppendBuffer.h"
|
|
||||||
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
|
||||||
#include "PieceTable.h"
|
#include "PieceTable.h"
|
||||||
#endif
|
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -66,11 +63,7 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] std::size_t Nrows() const
|
[[nodiscard]] std::size_t Nrows() const
|
||||||
{
|
{
|
||||||
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
|
||||||
return content_LineCount_();
|
return content_LineCount_();
|
||||||
#else
|
|
||||||
return nrows_;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +79,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Line wrapper backed by AppendBuffer (GapBuffer/PieceTable)
|
// Line wrapper backed by PieceTable
|
||||||
class Line {
|
class Line {
|
||||||
public:
|
public:
|
||||||
Line() = default;
|
Line() = default;
|
||||||
@@ -183,7 +176,7 @@ public:
|
|||||||
// minimal find() to support search within a line
|
// minimal find() to support search within a line
|
||||||
[[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const
|
[[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const
|
||||||
{
|
{
|
||||||
// Materialize to std::string for now; Line is backed by AppendBuffer
|
// Materialize to std::string for now; Line is backed by PieceTable
|
||||||
const auto s = static_cast<std::string>(*this);
|
const auto s = static_cast<std::string>(*this);
|
||||||
return s.find(needle, pos);
|
return s.find(needle, pos);
|
||||||
}
|
}
|
||||||
@@ -256,29 +249,21 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AppendBuffer buf_;
|
PieceTable buf_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] const std::vector<Line> &Rows() const
|
[[nodiscard]] const std::vector<Line> &Rows() const
|
||||||
{
|
{
|
||||||
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
|
||||||
ensure_rows_cache();
|
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();
|
ensure_rows_cache();
|
||||||
return rows_;
|
return rows_;
|
||||||
#else
|
|
||||||
return rows_;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -477,13 +462,8 @@ 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)
|
mutable std::vector<Line> rows_; // materialized cache of rows (without trailing newlines)
|
||||||
#else
|
// PieceTable is the source of truth.
|
||||||
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_{};
|
PieceTable content_{};
|
||||||
mutable bool rows_cache_dirty_ = true; // invalidate on edits / I/O
|
mutable bool rows_cache_dirty_ = true; // invalidate on edits / I/O
|
||||||
|
|
||||||
@@ -492,7 +472,7 @@ private:
|
|||||||
|
|
||||||
// Helper to query content_.LineCount() while keeping header minimal
|
// Helper to query content_.LineCount() while keeping header minimal
|
||||||
std::size_t content_LineCount_() const;
|
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;
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ project(kte)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(KTE_VERSION "1.4.1")
|
set(KTE_VERSION "1.4.2")
|
||||||
|
|
||||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||||
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
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_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)
|
||||||
@@ -129,7 +127,6 @@ if (BUILD_GUI)
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
set(COMMON_SOURCES
|
set(COMMON_SOURCES
|
||||||
GapBuffer.cc
|
|
||||||
PieceTable.cc
|
PieceTable.cc
|
||||||
Buffer.cc
|
Buffer.cc
|
||||||
Editor.cc
|
Editor.cc
|
||||||
@@ -214,11 +211,9 @@ set(FONT_HEADERS
|
|||||||
)
|
)
|
||||||
|
|
||||||
set(COMMON_HEADERS
|
set(COMMON_HEADERS
|
||||||
GapBuffer.h
|
|
||||||
PieceTable.h
|
PieceTable.h
|
||||||
Buffer.h
|
Buffer.h
|
||||||
Editor.h
|
Editor.h
|
||||||
AppendBuffer.h
|
|
||||||
Command.h
|
Command.h
|
||||||
HelpText.h
|
HelpText.h
|
||||||
KKeymap.h
|
KKeymap.h
|
||||||
@@ -271,12 +266,6 @@ add_executable(kte
|
|||||||
${COMMON_HEADERS}
|
${COMMON_HEADERS}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (KTE_USE_PIECE_TABLE)
|
|
||||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
|
||||||
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 ()
|
||||||
@@ -310,13 +299,6 @@ if (BUILD_TESTS)
|
|||||||
${COMMON_HEADERS}
|
${COMMON_HEADERS}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (KTE_USE_PIECE_TABLE)
|
|
||||||
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
|
||||||
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)
|
||||||
endif ()
|
endif ()
|
||||||
@@ -364,9 +346,6 @@ 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 ()
|
||||||
|
|||||||
204
GapBuffer.cc
204
GapBuffer.cc
@@ -1,204 +0,0 @@
|
|||||||
#include <algorithm>
|
|
||||||
#include <cassert>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include "GapBuffer.h"
|
|
||||||
|
|
||||||
|
|
||||||
GapBuffer::GapBuffer() = default;
|
|
||||||
|
|
||||||
|
|
||||||
GapBuffer::GapBuffer(std::size_t initialCapacity)
|
|
||||||
: buffer_(nullptr), size_(0), capacity_(0)
|
|
||||||
{
|
|
||||||
if (initialCapacity > 0) {
|
|
||||||
Reserve(initialCapacity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
GapBuffer::GapBuffer(const GapBuffer &other)
|
|
||||||
: buffer_(nullptr), size_(0), capacity_(0)
|
|
||||||
{
|
|
||||||
if (other.capacity_ > 0) {
|
|
||||||
Reserve(other.capacity_);
|
|
||||||
if (other.size_ > 0) {
|
|
||||||
std::memcpy(buffer_, other.buffer_, other.size_);
|
|
||||||
size_ = other.size_;
|
|
||||||
}
|
|
||||||
setTerminator();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
GapBuffer &
|
|
||||||
GapBuffer::operator=(const GapBuffer &other)
|
|
||||||
{
|
|
||||||
if (this == &other)
|
|
||||||
return *this;
|
|
||||||
if (other.capacity_ > capacity_) {
|
|
||||||
Reserve(other.capacity_);
|
|
||||||
}
|
|
||||||
if (other.size_ > 0) {
|
|
||||||
std::memcpy(buffer_, other.buffer_, other.size_);
|
|
||||||
}
|
|
||||||
size_ = other.size_;
|
|
||||||
setTerminator();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
GapBuffer::GapBuffer(GapBuffer &&other) noexcept
|
|
||||||
: buffer_(other.buffer_), size_(other.size_), capacity_(other.capacity_)
|
|
||||||
{
|
|
||||||
other.buffer_ = nullptr;
|
|
||||||
other.size_ = 0;
|
|
||||||
other.capacity_ = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
GapBuffer &
|
|
||||||
GapBuffer::operator=(GapBuffer &&other) noexcept
|
|
||||||
{
|
|
||||||
if (this == &other)
|
|
||||||
return *this;
|
|
||||||
delete[] buffer_;
|
|
||||||
buffer_ = other.buffer_;
|
|
||||||
size_ = other.size_;
|
|
||||||
capacity_ = other.capacity_;
|
|
||||||
other.buffer_ = nullptr;
|
|
||||||
other.size_ = 0;
|
|
||||||
other.capacity_ = 0;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
GapBuffer::~GapBuffer()
|
|
||||||
{
|
|
||||||
delete[] buffer_;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::Reserve(const std::size_t newCapacity)
|
|
||||||
{
|
|
||||||
if (newCapacity <= capacity_) [[likely]]
|
|
||||||
return;
|
|
||||||
// Allocate space for terminator as well
|
|
||||||
char *nb = new char[newCapacity + 1];
|
|
||||||
if (size_ > 0 && buffer_) {
|
|
||||||
std::memcpy(nb, buffer_, size_);
|
|
||||||
}
|
|
||||||
delete[] buffer_;
|
|
||||||
buffer_ = nb;
|
|
||||||
capacity_ = newCapacity;
|
|
||||||
setTerminator();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::AppendChar(const char c)
|
|
||||||
{
|
|
||||||
ensureCapacityFor(1);
|
|
||||||
buffer_[size_++] = c;
|
|
||||||
setTerminator();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::Append(const char *s, const std::size_t len)
|
|
||||||
{
|
|
||||||
if (!s || len == 0) [[unlikely]]
|
|
||||||
return;
|
|
||||||
ensureCapacityFor(len);
|
|
||||||
std::memcpy(buffer_ + size_, s, len);
|
|
||||||
size_ += len;
|
|
||||||
setTerminator();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::Append(const GapBuffer &other)
|
|
||||||
{
|
|
||||||
if (other.size_ == 0)
|
|
||||||
return;
|
|
||||||
Append(other.buffer_, other.size_);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::PrependChar(char c)
|
|
||||||
{
|
|
||||||
ensureCapacityFor(1);
|
|
||||||
// shift right by 1
|
|
||||||
if (size_ > 0) [[likely]] {
|
|
||||||
std::memmove(buffer_ + 1, buffer_, size_);
|
|
||||||
}
|
|
||||||
buffer_[0] = c;
|
|
||||||
++size_;
|
|
||||||
setTerminator();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::Prepend(const char *s, std::size_t len)
|
|
||||||
{
|
|
||||||
if (!s || len == 0) [[unlikely]]
|
|
||||||
return;
|
|
||||||
ensureCapacityFor(len);
|
|
||||||
if (size_ > 0) [[likely]] {
|
|
||||||
std::memmove(buffer_ + len, buffer_, size_);
|
|
||||||
}
|
|
||||||
std::memcpy(buffer_, s, len);
|
|
||||||
size_ += len;
|
|
||||||
setTerminator();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::Prepend(const GapBuffer &other)
|
|
||||||
{
|
|
||||||
if (other.size_ == 0)
|
|
||||||
return;
|
|
||||||
Prepend(other.buffer_, other.size_);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::Clear()
|
|
||||||
{
|
|
||||||
size_ = 0;
|
|
||||||
setTerminator();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::ensureCapacityFor(std::size_t delta)
|
|
||||||
{
|
|
||||||
if (capacity_ - size_ >= delta) [[likely]]
|
|
||||||
return;
|
|
||||||
auto required = size_ + delta;
|
|
||||||
Reserve(growCapacity(capacity_, required));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
std::size_t
|
|
||||||
GapBuffer::growCapacity(std::size_t current, std::size_t required)
|
|
||||||
{
|
|
||||||
// geometric growth, at least required
|
|
||||||
std::size_t newCap = current ? current : 8;
|
|
||||||
while (newCap < required)
|
|
||||||
newCap = newCap + (newCap >> 1); // 1.5x growth
|
|
||||||
return newCap;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
GapBuffer::setTerminator() const
|
|
||||||
{
|
|
||||||
if (!buffer_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer_[size_] = '\0';
|
|
||||||
}
|
|
||||||
76
GapBuffer.h
76
GapBuffer.h
@@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* GapBuffer.h - C++ replacement for abuf append/prepend buffer utilities
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
#include <cstddef>
|
|
||||||
|
|
||||||
|
|
||||||
class GapBuffer {
|
|
||||||
public:
|
|
||||||
GapBuffer();
|
|
||||||
|
|
||||||
explicit GapBuffer(std::size_t initialCapacity);
|
|
||||||
|
|
||||||
GapBuffer(const GapBuffer &other);
|
|
||||||
|
|
||||||
GapBuffer &operator=(const GapBuffer &other);
|
|
||||||
|
|
||||||
GapBuffer(GapBuffer &&other) noexcept;
|
|
||||||
|
|
||||||
GapBuffer &operator=(GapBuffer &&other) noexcept;
|
|
||||||
|
|
||||||
~GapBuffer();
|
|
||||||
|
|
||||||
void Reserve(std::size_t newCapacity);
|
|
||||||
|
|
||||||
|
|
||||||
void AppendChar(char c);
|
|
||||||
|
|
||||||
void Append(const char *s, std::size_t len);
|
|
||||||
|
|
||||||
void Append(const GapBuffer &other);
|
|
||||||
|
|
||||||
void PrependChar(char c);
|
|
||||||
|
|
||||||
void Prepend(const char *s, std::size_t len);
|
|
||||||
|
|
||||||
void Prepend(const GapBuffer &other);
|
|
||||||
|
|
||||||
// Content management
|
|
||||||
void Clear();
|
|
||||||
|
|
||||||
// Accessors
|
|
||||||
char *Data()
|
|
||||||
{
|
|
||||||
return buffer_;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] const char *Data() const
|
|
||||||
{
|
|
||||||
return buffer_;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] std::size_t Size() const
|
|
||||||
{
|
|
||||||
return size_;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] std::size_t Capacity() const
|
|
||||||
{
|
|
||||||
return capacity_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void ensureCapacityFor(std::size_t delta);
|
|
||||||
|
|
||||||
static std::size_t growCapacity(std::size_t current, std::size_t required);
|
|
||||||
|
|
||||||
void setTerminator() const;
|
|
||||||
|
|
||||||
char *buffer_ = nullptr;
|
|
||||||
std::size_t size_ = 0; // number of valid bytes (excluding terminator)
|
|
||||||
std::size_t capacity_ = 0; // capacity of buffer_ excluding space for terminator
|
|
||||||
};
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
/*
|
|
||||||
* BufferBench.cc - microbenchmarks for GapBuffer and PieceTable
|
|
||||||
*
|
|
||||||
* This benchmark exercises the public APIs shared by both structures as used
|
|
||||||
* in Buffer::Line: Reserve, AppendChar, Append, PrependChar, Prepend, Clear.
|
|
||||||
*
|
|
||||||
* Run examples:
|
|
||||||
* ./kte_bench_buffer # defaults
|
|
||||||
* ./kte_bench_buffer 200000 8 4096 # N=200k, rounds=8, chunk=4096
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstring>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <iostream>
|
|
||||||
#include <random>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <typeinfo>
|
|
||||||
|
|
||||||
#include "GapBuffer.h"
|
|
||||||
#include "PieceTable.h"
|
|
||||||
|
|
||||||
using clock_t = std::chrono::steady_clock;
|
|
||||||
using us = std::chrono::microseconds;
|
|
||||||
|
|
||||||
struct Result {
|
|
||||||
std::string name;
|
|
||||||
std::string scenario;
|
|
||||||
double micros = 0.0;
|
|
||||||
std::size_t bytes = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
print_header()
|
|
||||||
{
|
|
||||||
std::cout << std::left << std::setw(14) << "Structure"
|
|
||||||
<< std::left << std::setw(18) << "Scenario"
|
|
||||||
<< std::right << std::setw(12) << "time(us)"
|
|
||||||
<< std::right << std::setw(14) << "bytes"
|
|
||||||
<< std::right << std::setw(14) << "MB/s"
|
|
||||||
<< "\n";
|
|
||||||
std::cout << std::string(72, '-') << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
print_row(const Result &r)
|
|
||||||
{
|
|
||||||
double mb = r.bytes / (1024.0 * 1024.0);
|
|
||||||
double mbps = (r.micros > 0.0) ? (mb / (r.micros / 1'000'000.0)) : 0.0;
|
|
||||||
std::cout << std::left << std::setw(14) << r.name
|
|
||||||
<< std::left << std::setw(18) << r.scenario
|
|
||||||
<< std::right << std::setw(12) << std::fixed << std::setprecision(2) << r.micros
|
|
||||||
<< std::right << std::setw(14) << r.bytes
|
|
||||||
<< std::right << std::setw(14) << std::fixed << std::setprecision(2) << mbps
|
|
||||||
<< "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
template<typename Buf>
|
|
||||||
Result
|
|
||||||
bench_sequential_append(std::size_t N, int rounds)
|
|
||||||
{
|
|
||||||
Result r;
|
|
||||||
r.name = typeid(Buf).name();
|
|
||||||
r.scenario = "seq_append";
|
|
||||||
const char c = 'x';
|
|
||||||
auto start = clock_t::now();
|
|
||||||
std::size_t bytes = 0;
|
|
||||||
for (int t = 0; t < rounds; ++t) {
|
|
||||||
Buf b;
|
|
||||||
b.Reserve(N);
|
|
||||||
for (std::size_t i = 0; i < N; ++i) {
|
|
||||||
b.AppendChar(c);
|
|
||||||
}
|
|
||||||
bytes += N;
|
|
||||||
}
|
|
||||||
auto end = clock_t::now();
|
|
||||||
r.micros = std::chrono::duration_cast<us>(end - start).count();
|
|
||||||
r.bytes = bytes;
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
template<typename Buf>
|
|
||||||
Result
|
|
||||||
bench_sequential_prepend(std::size_t N, int rounds)
|
|
||||||
{
|
|
||||||
Result r;
|
|
||||||
r.name = typeid(Buf).name();
|
|
||||||
r.scenario = "seq_prepend";
|
|
||||||
const char c = 'x';
|
|
||||||
auto start = clock_t::now();
|
|
||||||
std::size_t bytes = 0;
|
|
||||||
for (int t = 0; t < rounds; ++t) {
|
|
||||||
Buf b;
|
|
||||||
b.Reserve(N);
|
|
||||||
for (std::size_t i = 0; i < N; ++i) {
|
|
||||||
b.PrependChar(c);
|
|
||||||
}
|
|
||||||
bytes += N;
|
|
||||||
}
|
|
||||||
auto end = clock_t::now();
|
|
||||||
r.micros = std::chrono::duration_cast<us>(end - start).count();
|
|
||||||
r.bytes = bytes;
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
template<typename Buf>
|
|
||||||
Result
|
|
||||||
bench_chunk_append(std::size_t N, std::size_t chunk, int rounds)
|
|
||||||
{
|
|
||||||
Result r;
|
|
||||||
r.name = typeid(Buf).name();
|
|
||||||
r.scenario = "chunk_append";
|
|
||||||
std::string payload(chunk, 'y');
|
|
||||||
auto start = clock_t::now();
|
|
||||||
std::size_t bytes = 0;
|
|
||||||
for (int t = 0; t < rounds; ++t) {
|
|
||||||
Buf b;
|
|
||||||
b.Reserve(N);
|
|
||||||
std::size_t written = 0;
|
|
||||||
while (written < N) {
|
|
||||||
std::size_t now = std::min(chunk, N - written);
|
|
||||||
b.Append(payload.data(), now);
|
|
||||||
written += now;
|
|
||||||
}
|
|
||||||
bytes += N;
|
|
||||||
}
|
|
||||||
auto end = clock_t::now();
|
|
||||||
r.micros = std::chrono::duration_cast<us>(end - start).count();
|
|
||||||
r.bytes = bytes;
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
template<typename Buf>
|
|
||||||
Result
|
|
||||||
bench_mixed(std::size_t N, std::size_t chunk, int rounds)
|
|
||||||
{
|
|
||||||
Result r;
|
|
||||||
r.name = typeid(Buf).name();
|
|
||||||
r.scenario = "mixed";
|
|
||||||
std::string payload(chunk, 'z');
|
|
||||||
auto start = clock_t::now();
|
|
||||||
std::size_t bytes = 0;
|
|
||||||
for (int t = 0; t < rounds; ++t) {
|
|
||||||
Buf b;
|
|
||||||
b.Reserve(N);
|
|
||||||
std::size_t written = 0;
|
|
||||||
while (written < N) {
|
|
||||||
// alternate append/prepend with small chunks
|
|
||||||
std::size_t now = std::min(chunk, N - written);
|
|
||||||
if ((written / chunk) % 2 == 0) {
|
|
||||||
b.Append(payload.data(), now);
|
|
||||||
} else {
|
|
||||||
b.Prepend(payload.data(), now);
|
|
||||||
}
|
|
||||||
written += now;
|
|
||||||
}
|
|
||||||
bytes += N;
|
|
||||||
}
|
|
||||||
auto end = clock_t::now();
|
|
||||||
r.micros = std::chrono::duration_cast<us>(end - start).count();
|
|
||||||
r.bytes = bytes;
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int
|
|
||||||
main(int argc, char **argv)
|
|
||||||
{
|
|
||||||
// Parameters
|
|
||||||
std::size_t N = 100'000; // bytes per round
|
|
||||||
int rounds = 5; // iterations
|
|
||||||
std::size_t chunk = 1024; // chunk size for chunked scenarios
|
|
||||||
if (argc >= 2)
|
|
||||||
N = static_cast<std::size_t>(std::stoull(argv[1]));
|
|
||||||
if (argc >= 3)
|
|
||||||
rounds = std::stoi(argv[2]);
|
|
||||||
if (argc >= 4)
|
|
||||||
chunk = static_cast<std::size_t>(std::stoull(argv[3]));
|
|
||||||
|
|
||||||
std::cout << "KTE Buffer Microbenchmarks" << "\n";
|
|
||||||
std::cout << "N=" << N << ", rounds=" << rounds << ", chunk=" << chunk << "\n\n";
|
|
||||||
|
|
||||||
print_header();
|
|
||||||
|
|
||||||
// Run for GapBuffer
|
|
||||||
print_row(bench_sequential_append<GapBuffer>(N, rounds));
|
|
||||||
print_row(bench_sequential_prepend<GapBuffer>(N, rounds));
|
|
||||||
print_row(bench_chunk_append<GapBuffer>(N, chunk, rounds));
|
|
||||||
print_row(bench_mixed<GapBuffer>(N, chunk, rounds));
|
|
||||||
|
|
||||||
// Run for PieceTable
|
|
||||||
print_row(bench_sequential_append<PieceTable>(N, rounds));
|
|
||||||
print_row(bench_sequential_prepend<PieceTable>(N, rounds));
|
|
||||||
print_row(bench_chunk_append<PieceTable>(N, chunk, rounds));
|
|
||||||
print_row(bench_mixed<PieceTable>(N, chunk, rounds));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
/*
|
|
||||||
* PerformanceSuite.cc - broader performance and verification benchmarks
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cassert>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstring>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <iostream>
|
|
||||||
#include <random>
|
|
||||||
#include <string>
|
|
||||||
#include <typeinfo>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "GapBuffer.h"
|
|
||||||
#include "PieceTable.h"
|
|
||||||
#include "OptimizedSearch.h"
|
|
||||||
|
|
||||||
using clock_t = std::chrono::steady_clock;
|
|
||||||
using us = std::chrono::microseconds;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
struct Stat {
|
|
||||||
double micros{0.0};
|
|
||||||
std::size_t bytes{0};
|
|
||||||
std::size_t ops{0};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
print_header(const std::string &title)
|
|
||||||
{
|
|
||||||
std::cout << "\n" << title << "\n";
|
|
||||||
std::cout << std::left << std::setw(18) << "Case"
|
|
||||||
<< std::left << std::setw(18) << "Type"
|
|
||||||
<< std::right << std::setw(12) << "time(us)"
|
|
||||||
<< std::right << std::setw(14) << "bytes"
|
|
||||||
<< std::right << std::setw(14) << "ops/s"
|
|
||||||
<< std::right << std::setw(14) << "MB/s"
|
|
||||||
<< "\n";
|
|
||||||
std::cout << std::string(90, '-') << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
print_row(const std::string &caseName, const std::string &typeName, const Stat &s)
|
|
||||||
{
|
|
||||||
double mb = s.bytes / (1024.0 * 1024.0);
|
|
||||||
double sec = s.micros / 1'000'000.0;
|
|
||||||
double mbps = sec > 0 ? (mb / sec) : 0.0;
|
|
||||||
double opss = sec > 0 ? (static_cast<double>(s.ops) / sec) : 0.0;
|
|
||||||
std::cout << std::left << std::setw(18) << caseName
|
|
||||||
<< std::left << std::setw(18) << typeName
|
|
||||||
<< std::right << std::setw(12) << std::fixed << std::setprecision(2) << s.micros
|
|
||||||
<< std::right << std::setw(14) << s.bytes
|
|
||||||
<< std::right << std::setw(14) << std::fixed << std::setprecision(2) << opss
|
|
||||||
<< std::right << std::setw(14) << std::fixed << std::setprecision(2) << mbps
|
|
||||||
<< "\n";
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
class PerformanceSuite {
|
|
||||||
public:
|
|
||||||
void benchmarkBufferOperations(std::size_t N, int rounds, std::size_t chunk)
|
|
||||||
{
|
|
||||||
print_header("Buffer Operations");
|
|
||||||
run_buffer_case<GapBuffer>("append_char", N, rounds, chunk, [&](auto &b, std::size_t count) {
|
|
||||||
for (std::size_t i = 0; i < count; ++i)
|
|
||||||
b.AppendChar('a');
|
|
||||||
});
|
|
||||||
run_buffer_case<GapBuffer>("prepend_char", N, rounds, chunk, [&](auto &b, std::size_t count) {
|
|
||||||
for (std::size_t i = 0; i < count; ++i)
|
|
||||||
b.PrependChar('a');
|
|
||||||
});
|
|
||||||
run_buffer_case<GapBuffer>("chunk_mix", N, rounds, chunk, [&](auto &b, std::size_t) {
|
|
||||||
std::string payload(chunk, 'x');
|
|
||||||
std::size_t written = 0;
|
|
||||||
while (written < N) {
|
|
||||||
std::size_t now = std::min(chunk, N - written);
|
|
||||||
if (((written / chunk) & 1) == 0)
|
|
||||||
b.Append(payload.data(), now);
|
|
||||||
else
|
|
||||||
b.Prepend(payload.data(), now);
|
|
||||||
written += now;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
run_buffer_case<PieceTable>("append_char", N, rounds, chunk, [&](auto &b, std::size_t count) {
|
|
||||||
for (std::size_t i = 0; i < count; ++i)
|
|
||||||
b.AppendChar('a');
|
|
||||||
});
|
|
||||||
run_buffer_case<PieceTable>("prepend_char", N, rounds, chunk, [&](auto &b, std::size_t count) {
|
|
||||||
for (std::size_t i = 0; i < count; ++i)
|
|
||||||
b.PrependChar('a');
|
|
||||||
});
|
|
||||||
run_buffer_case<PieceTable>("chunk_mix", N, rounds, chunk, [&](auto &b, std::size_t) {
|
|
||||||
std::string payload(chunk, 'x');
|
|
||||||
std::size_t written = 0;
|
|
||||||
while (written < N) {
|
|
||||||
std::size_t now = std::min(chunk, N - written);
|
|
||||||
if (((written / chunk) & 1) == 0)
|
|
||||||
b.Append(payload.data(), now);
|
|
||||||
else
|
|
||||||
b.Prepend(payload.data(), now);
|
|
||||||
written += now;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void benchmarkSearchOperations(std::size_t textLen, std::size_t patLen, int rounds)
|
|
||||||
{
|
|
||||||
print_header("Search Operations");
|
|
||||||
std::mt19937_64 rng(0xC0FFEE);
|
|
||||||
std::uniform_int_distribution<int> dist('a', 'z');
|
|
||||||
std::string text(textLen, '\0');
|
|
||||||
for (auto &ch: text)
|
|
||||||
ch = static_cast<char>(dist(rng));
|
|
||||||
std::string pattern(patLen, '\0');
|
|
||||||
for (auto &ch: pattern)
|
|
||||||
ch = static_cast<char>(dist(rng));
|
|
||||||
|
|
||||||
// Ensure at least one hit
|
|
||||||
if (textLen >= patLen && patLen > 0) {
|
|
||||||
std::size_t pos = textLen / 2;
|
|
||||||
std::memcpy(&text[pos], pattern.data(), patLen);
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptimizedSearch find_all vs std::string reference
|
|
||||||
OptimizedSearch os;
|
|
||||||
Stat s{};
|
|
||||||
auto start = clock_t::now();
|
|
||||||
std::size_t matches = 0;
|
|
||||||
std::size_t bytesScanned = 0;
|
|
||||||
for (int r = 0; r < rounds; ++r) {
|
|
||||||
auto hits = os.find_all(text, pattern, 0);
|
|
||||||
matches += hits.size();
|
|
||||||
bytesScanned += text.size();
|
|
||||||
// Verify with reference
|
|
||||||
std::vector<std::size_t> ref;
|
|
||||||
std::size_t from = 0;
|
|
||||||
while (true) {
|
|
||||||
auto p = text.find(pattern, from);
|
|
||||||
if (p == std::string::npos)
|
|
||||||
break;
|
|
||||||
ref.push_back(p);
|
|
||||||
from = p + (patLen ? patLen : 1);
|
|
||||||
}
|
|
||||||
assert(ref == hits);
|
|
||||||
}
|
|
||||||
auto end = clock_t::now();
|
|
||||||
s.micros = std::chrono::duration_cast<us>(end - start).count();
|
|
||||||
s.bytes = bytesScanned;
|
|
||||||
s.ops = matches;
|
|
||||||
print_row("find_all", "OptimizedSearch", s);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void benchmarkMemoryAllocation(std::size_t N, int rounds)
|
|
||||||
{
|
|
||||||
print_header("Memory Allocation (allocations during editing)");
|
|
||||||
// Measure number of allocations by simulating editing patterns.
|
|
||||||
auto run_session = [&](auto &&buffer) {
|
|
||||||
// alternate small appends and prepends
|
|
||||||
const std::size_t chunk = 32;
|
|
||||||
std::string payload(chunk, 'q');
|
|
||||||
for (int r = 0; r < rounds; ++r) {
|
|
||||||
buffer.Clear();
|
|
||||||
for (std::size_t i = 0; i < N; i += chunk)
|
|
||||||
buffer.Append(payload.data(), std::min(chunk, N - i));
|
|
||||||
for (std::size_t i = 0; i < N / 2; i += chunk)
|
|
||||||
buffer.Prepend(payload.data(), std::min(chunk, N / 2 - i));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Local allocation counters for this TU via overriding operators
|
|
||||||
reset_alloc_counters();
|
|
||||||
GapBuffer gb;
|
|
||||||
run_session(gb);
|
|
||||||
auto gap_allocs = current_allocs();
|
|
||||||
print_row("edit_session", "GapBuffer", Stat{
|
|
||||||
0.0, static_cast<std::size_t>(gap_allocs.bytes),
|
|
||||||
static_cast<std::size_t>(gap_allocs.count)
|
|
||||||
});
|
|
||||||
|
|
||||||
reset_alloc_counters();
|
|
||||||
PieceTable pt;
|
|
||||||
run_session(pt);
|
|
||||||
auto pt_allocs = current_allocs();
|
|
||||||
print_row("edit_session", "PieceTable", Stat{
|
|
||||||
0.0, static_cast<std::size_t>(pt_allocs.bytes),
|
|
||||||
static_cast<std::size_t>(pt_allocs.count)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
template<typename Buf, typename Fn>
|
|
||||||
void run_buffer_case(const std::string &caseName, std::size_t N, int rounds, std::size_t chunk, Fn fn)
|
|
||||||
{
|
|
||||||
Stat s{};
|
|
||||||
auto start = clock_t::now();
|
|
||||||
std::size_t bytes = 0;
|
|
||||||
std::size_t ops = 0;
|
|
||||||
for (int t = 0; t < rounds; ++t) {
|
|
||||||
Buf b;
|
|
||||||
b.Reserve(N);
|
|
||||||
fn(b, N);
|
|
||||||
// compare to reference string where possible (only for append_char/prepend_char)
|
|
||||||
bytes += N;
|
|
||||||
ops += N / (chunk ? chunk : 1);
|
|
||||||
}
|
|
||||||
auto end = clock_t::now();
|
|
||||||
s.micros = std::chrono::duration_cast<us>(end - start).count();
|
|
||||||
s.bytes = bytes;
|
|
||||||
s.ops = ops;
|
|
||||||
print_row(caseName, typeid(Buf).name(), s);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Simple global allocation tracking for this TU
|
|
||||||
struct AllocStats {
|
|
||||||
std::uint64_t count{0};
|
|
||||||
std::uint64_t bytes{0};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
static AllocStats &alloc_stats()
|
|
||||||
{
|
|
||||||
static AllocStats s;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void reset_alloc_counters()
|
|
||||||
{
|
|
||||||
alloc_stats() = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static AllocStats current_allocs()
|
|
||||||
{
|
|
||||||
return alloc_stats();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Friend global new/delete defined below
|
|
||||||
friend void *operator new(std::size_t sz) noexcept(false);
|
|
||||||
|
|
||||||
friend void operator delete(void *p) noexcept;
|
|
||||||
|
|
||||||
friend void *operator new[](std::size_t sz) noexcept(false);
|
|
||||||
|
|
||||||
friend void operator delete[](void *p) noexcept;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Override new/delete only in this translation unit to track allocations made here
|
|
||||||
void *
|
|
||||||
operator new(std::size_t sz) noexcept(false)
|
|
||||||
{
|
|
||||||
auto &s = PerformanceSuite::alloc_stats();
|
|
||||||
s.count++;
|
|
||||||
s.bytes += sz;
|
|
||||||
if (void *p = std::malloc(sz))
|
|
||||||
return p;
|
|
||||||
throw std::bad_alloc();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
operator delete(void *p) noexcept
|
|
||||||
{
|
|
||||||
std::free(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void *
|
|
||||||
operator new[](std::size_t sz) noexcept(false)
|
|
||||||
{
|
|
||||||
auto &s = PerformanceSuite::alloc_stats();
|
|
||||||
s.count++;
|
|
||||||
s.bytes += sz;
|
|
||||||
if (void *p = std::malloc(sz))
|
|
||||||
return p;
|
|
||||||
throw std::bad_alloc();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
operator delete[](void *p) noexcept
|
|
||||||
{
|
|
||||||
std::free(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int
|
|
||||||
main(int argc, char **argv)
|
|
||||||
{
|
|
||||||
std::size_t N = 200'000; // bytes per round for buffer cases
|
|
||||||
int rounds = 3;
|
|
||||||
std::size_t chunk = 1024;
|
|
||||||
if (argc >= 2)
|
|
||||||
N = static_cast<std::size_t>(std::stoull(argv[1]));
|
|
||||||
if (argc >= 3)
|
|
||||||
rounds = std::stoi(argv[2]);
|
|
||||||
if (argc >= 4)
|
|
||||||
chunk = static_cast<std::size_t>(std::stoull(argv[3]));
|
|
||||||
|
|
||||||
std::cout << "KTE Performance Suite" << "\n";
|
|
||||||
std::cout << "N=" << N << ", rounds=" << rounds << ", chunk=" << chunk << "\n";
|
|
||||||
|
|
||||||
PerformanceSuite suite;
|
|
||||||
suite.benchmarkBufferOperations(N, rounds, chunk);
|
|
||||||
suite.benchmarkSearchOperations(1'000'000, 16, rounds);
|
|
||||||
suite.benchmarkMemoryAllocation(N, rounds);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
// Simple buffer correctness tests comparing GapBuffer and PieceTable to std::string
|
|
||||||
#include <cassert>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstring>
|
|
||||||
#include <random>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "GapBuffer.h"
|
|
||||||
#include "PieceTable.h"
|
|
||||||
|
|
||||||
|
|
||||||
template<typename Buf>
|
|
||||||
static void
|
|
||||||
check_equals(const Buf &b, const std::string &ref)
|
|
||||||
{
|
|
||||||
assert(b.Size() == ref.size());
|
|
||||||
if (b.Size() == 0)
|
|
||||||
return;
|
|
||||||
const char *p = b.Data();
|
|
||||||
assert(p != nullptr);
|
|
||||||
assert(std::memcmp(p, ref.data(), ref.size()) == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
template<typename Buf>
|
|
||||||
static void
|
|
||||||
run_basic_cases()
|
|
||||||
{
|
|
||||||
// empty
|
|
||||||
{
|
|
||||||
Buf b;
|
|
||||||
std::string ref;
|
|
||||||
check_equals(b, ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// append chars
|
|
||||||
{
|
|
||||||
Buf b;
|
|
||||||
std::string ref;
|
|
||||||
for (int i = 0; i < 1000; ++i) {
|
|
||||||
b.AppendChar('a');
|
|
||||||
ref.push_back('a');
|
|
||||||
}
|
|
||||||
check_equals(b, ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepend chars
|
|
||||||
{
|
|
||||||
Buf b;
|
|
||||||
std::string ref;
|
|
||||||
for (int i = 0; i < 1000; ++i) {
|
|
||||||
b.PrependChar('b');
|
|
||||||
ref.insert(ref.begin(), 'b');
|
|
||||||
}
|
|
||||||
check_equals(b, ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// append/prepend strings
|
|
||||||
{
|
|
||||||
Buf b;
|
|
||||||
std::string ref;
|
|
||||||
const char *hello = "hello";
|
|
||||||
b.Append(hello, 5);
|
|
||||||
ref.append("hello");
|
|
||||||
b.Prepend(hello, 5);
|
|
||||||
ref.insert(0, "hello");
|
|
||||||
check_equals(b, ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// larger random blocks
|
|
||||||
{
|
|
||||||
std::mt19937 rng(42);
|
|
||||||
std::uniform_int_distribution<int> len_dist(0, 128);
|
|
||||||
std::uniform_int_distribution<int> coin(0, 1);
|
|
||||||
Buf b;
|
|
||||||
std::string ref;
|
|
||||||
for (int step = 0; step < 2000; ++step) {
|
|
||||||
int L = len_dist(rng);
|
|
||||||
std::string payload(L, '\0');
|
|
||||||
for (int i = 0; i < L; ++i)
|
|
||||||
payload[i] = static_cast<char>('a' + (i % 26));
|
|
||||||
if (coin(rng)) {
|
|
||||||
b.Append(payload.data(), payload.size());
|
|
||||||
ref.append(payload);
|
|
||||||
} else {
|
|
||||||
b.Prepend(payload.data(), payload.size());
|
|
||||||
ref.insert(0, payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check_equals(b, ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int
|
|
||||||
main()
|
|
||||||
{
|
|
||||||
run_basic_cases<GapBuffer>();
|
|
||||||
run_basic_cases<PieceTable>();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user