Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f57cf23dc | |||
| 9312550be4 | |||
| f734f98891 | |||
| 1191e14ce9 | |||
| 12cc04d7e0 | |||
| 3f4c60d311 | |||
| 71c1c9e50b | |||
| afb6888c31 | |||
| 222f73252b | |||
| 51ea473a91 | |||
| fd517b5d57 |
2
.idea/codeStyles/codeStyleConfig.xml
generated
2
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<state>
|
<state>
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="sccl" />
|
||||||
</state>
|
</state>
|
||||||
</component>
|
</component>
|
||||||
@@ -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
|
|
||||||
374
Buffer.cc
374
Buffer.cc
@@ -2,6 +2,10 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <limits>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
@@ -29,20 +33,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_;
|
||||||
filename_ = other.filename_;
|
content_ = other.content_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
dirty_ = other.dirty_;
|
filename_ = other.filename_;
|
||||||
read_only_ = other.read_only_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
mark_set_ = other.mark_set_;
|
dirty_ = other.dirty_;
|
||||||
mark_curx_ = other.mark_curx_;
|
read_only_ = other.read_only_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_set_ = other.mark_set_;
|
||||||
|
mark_curx_ = other.mark_curx_;
|
||||||
|
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_;
|
||||||
@@ -77,23 +83,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_;
|
||||||
filename_ = other.filename_;
|
content_ = other.content_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
dirty_ = other.dirty_;
|
filename_ = other.filename_;
|
||||||
read_only_ = other.read_only_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
mark_set_ = other.mark_set_;
|
dirty_ = other.dirty_;
|
||||||
mark_curx_ = other.mark_curx_;
|
read_only_ = other.read_only_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_set_ = other.mark_set_;
|
||||||
version_ = other.version_;
|
mark_curx_ = other.mark_curx_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
mark_cury_ = other.mark_cury_;
|
||||||
filetype_ = other.filetype_;
|
version_ = other.version_;
|
||||||
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
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_);
|
||||||
@@ -137,10 +145,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_);
|
||||||
|
content_ = std::move(other.content_);
|
||||||
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
// 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);
|
||||||
@@ -173,11 +183,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_);
|
||||||
|
content_ = std::move(other.content_);
|
||||||
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
// 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 +240,10 @@ 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;
|
||||||
|
|
||||||
|
// Empty PieceTable
|
||||||
|
content_.Clear();
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,50 +253,23 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect if file ends with a newline so we can preserve a final empty line
|
// Read entire file into PieceTable as-is
|
||||||
// in our in-memory representation (mg-style semantics).
|
std::string data;
|
||||||
bool ends_with_nl = false;
|
in.seekg(0, std::ios::end);
|
||||||
{
|
auto sz = in.tellg();
|
||||||
in.seekg(0, std::ios::end);
|
if (sz > 0) {
|
||||||
std::streamoff sz = in.tellg();
|
data.resize(static_cast<std::size_t>(sz));
|
||||||
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);
|
in.seekg(0, std::ios::beg);
|
||||||
|
in.read(data.data(), static_cast<std::streamsize>(data.size()));
|
||||||
}
|
}
|
||||||
|
content_.Clear();
|
||||||
rows_.clear();
|
if (!data.empty())
|
||||||
std::string line;
|
content_.Append(data.data(), data.size());
|
||||||
while (std::getline(in, line)) {
|
rows_cache_dirty_ = true;
|
||||||
// std::getline strips the '\n', keep raw line content only
|
nrows_ = 0; // not used under PieceTable
|
||||||
// Handle potential Windows CRLF: strip trailing '\r'
|
filename_ = norm;
|
||||||
if (!line.empty() && line.back() == '\r') {
|
is_file_backed_ = true;
|
||||||
line.pop_back();
|
dirty_ = false;
|
||||||
}
|
|
||||||
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();
|
|
||||||
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_)
|
||||||
@@ -310,20 +298,17 @@ Buffer::Save(std::string &err) const
|
|||||||
}
|
}
|
||||||
std::ofstream out(filename_, std::ios::out | std::ios::binary | std::ios::trunc);
|
std::ofstream out(filename_, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||||
if (!out) {
|
if (!out) {
|
||||||
err = "Failed to open for write: " + filename_;
|
err = "Failed to open for write: " + filename_ + ". Error: " + std::string(std::strerror(errno));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
// Write the entire buffer in a single block to minimize I/O calls.
|
||||||
const char *d = rows_[i].Data();
|
const char *data = content_.Data();
|
||||||
std::size_t n = rows_[i].Size();
|
const auto size = static_cast<std::streamsize>(content_.Size());
|
||||||
if (d && n)
|
if (data != nullptr && size > 0) {
|
||||||
out.write(d, static_cast<std::streamsize>(n));
|
out.write(data, size);
|
||||||
if (i + 1 < rows_.size()) {
|
|
||||||
out.put('\n');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!out.good()) {
|
if (!out.good()) {
|
||||||
err = "Write error";
|
err = "Write error: " + filename_ + ". Error: " + std::string(std::strerror(errno));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Note: const method cannot change dirty_. Intentionally const to allow UI code
|
// Note: const method cannot change dirty_. Intentionally const to allow UI code
|
||||||
@@ -357,20 +342,17 @@ Buffer::SaveAs(const std::string &path, std::string &err)
|
|||||||
// Write to the given path
|
// Write to the given path
|
||||||
std::ofstream out(out_path, std::ios::out | std::ios::binary | std::ios::trunc);
|
std::ofstream out(out_path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||||
if (!out) {
|
if (!out) {
|
||||||
err = "Failed to open for write: " + out_path;
|
err = "Failed to open for write: " + out_path + ". Error: " + std::string(std::strerror(errno));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
// Write whole content in a single I/O operation
|
||||||
const char *d = rows_[i].Data();
|
const char *data = content_.Data();
|
||||||
std::size_t n = rows_[i].Size();
|
const auto size = static_cast<std::streamsize>(content_.Size());
|
||||||
if (d && n)
|
if (data != nullptr && size > 0) {
|
||||||
out.write(d, static_cast<std::streamsize>(n));
|
out.write(data, size);
|
||||||
if (i + 1 < rows_.size()) {
|
|
||||||
out.put('\n');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!out.good()) {
|
if (!out.good()) {
|
||||||
err = "Write error";
|
err = "Write error: " + out_path + ". Error: " + std::string(std::strerror(errno));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +371,7 @@ Buffer::AsString() const
|
|||||||
if (this->Dirty()) {
|
if (this->Dirty()) {
|
||||||
ss << "*";
|
ss << "*";
|
||||||
}
|
}
|
||||||
ss << ">: " << rows_.size() << " lines";
|
ss << ">: " << content_.LineCount() << " lines";
|
||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,111 +382,135 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
|||||||
{
|
{
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
if (col < 0)
|
||||||
row = static_cast<int>(rows_.size());
|
col = 0;
|
||||||
if (rows_.empty())
|
const std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row),
|
||||||
rows_.emplace_back("");
|
static_cast<std::size_t>(col));
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
if (!text.empty()) {
|
||||||
rows_.emplace_back("");
|
content_.Insert(off, text.data(), text.size());
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
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
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Adapter helpers for PieceTable-backed Buffer =====
|
||||||
|
std::string_view
|
||||||
|
Buffer::GetLineView(std::size_t row) const
|
||||||
|
{
|
||||||
|
// Get byte range for the logical line and return a view into materialized data
|
||||||
|
auto range = content_.GetLineRange(row); // [start,end) in bytes
|
||||||
|
const char *base = content_.Data(); // materializes if needed
|
||||||
|
if (!base)
|
||||||
|
return std::string_view();
|
||||||
|
const std::size_t start = range.first;
|
||||||
|
const std::size_t len = (range.second > range.first) ? (range.second - range.first) : 0;
|
||||||
|
return std::string_view(base + start, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Buffer::delete_text(int row, int col, std::size_t len)
|
Buffer::delete_text(int row, int col, std::size_t len)
|
||||||
{
|
{
|
||||||
if (rows_.empty() || len == 0)
|
if (len == 0)
|
||||||
return;
|
return;
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
if (col < 0)
|
||||||
return;
|
col = 0;
|
||||||
const auto y = static_cast<std::size_t>(row);
|
const std::size_t start = content_.LineColToByteOffset(static_cast<std::size_t>(row),
|
||||||
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
static_cast<std::size_t>(col));
|
||||||
|
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;
|
std::size_t remaining = len;
|
||||||
while (remaining > 0 && y < rows_.size()) {
|
const std::size_t lc = content_.LineCount();
|
||||||
auto &line = rows_[y];
|
|
||||||
const std::size_t in_line = std::min<std::size_t>(remaining, line.size() - std::min(x, line.size()));
|
while (remaining > 0 && r < lc) {
|
||||||
if (x < line.size() && in_line > 0) {
|
const std::string line = content_.GetLine(r); // logical line (without trailing '\n')
|
||||||
line.erase(x, in_line);
|
const std::size_t L = line.size();
|
||||||
remaining -= in_line;
|
if (c < L) {
|
||||||
|
const std::size_t take = std::min(remaining, L - c);
|
||||||
|
c += take;
|
||||||
|
remaining -= take;
|
||||||
}
|
}
|
||||||
if (remaining == 0)
|
if (remaining == 0)
|
||||||
break;
|
break;
|
||||||
// If at or beyond end of line and there is a next line, join it (deleting the implied '\n')
|
// Consume newline between lines as one char, if there is a next line
|
||||||
if (y + 1 < rows_.size()) {
|
if (r + 1 < lc) {
|
||||||
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) {
|
if (remaining > 0) {
|
||||||
// Treat the newline as one deletion unit if len spans it
|
remaining -= 1; // the newline
|
||||||
// We already joined, so nothing else to do here.
|
r += 1;
|
||||||
|
c = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break;
|
// At last line and still remaining: delete to EOF
|
||||||
|
std::size_t total = content_.Size();
|
||||||
|
content_.Delete(start, total - start);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute end offset at (r,c)
|
||||||
|
std::size_t end = content_.LineColToByteOffset(r, c);
|
||||||
|
if (end > start) {
|
||||||
|
content_.Delete(start, end - start);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Buffer::split_line(int row, const int col)
|
Buffer::split_line(int row, const int col)
|
||||||
{
|
{
|
||||||
if (row < 0) {
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
}
|
if (col < 0)
|
||||||
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size()) {
|
const std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row),
|
||||||
rows_.resize(static_cast<std::size_t>(row) + 1);
|
static_cast<std::size_t>(col));
|
||||||
}
|
const char nl = '\n';
|
||||||
const auto y = static_cast<std::size_t>(row);
|
content_.Insert(off, &nl, 1);
|
||||||
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
rows_cache_dirty_ = true;
|
||||||
const auto tail = rows_[y].substr(x);
|
|
||||||
rows_[y].erase(x);
|
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Buffer::join_lines(int row)
|
Buffer::join_lines(int row)
|
||||||
{
|
{
|
||||||
if (row < 0) {
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
}
|
std::size_t r = static_cast<std::size_t>(row);
|
||||||
|
if (r + 1 >= content_.LineCount())
|
||||||
const auto y = static_cast<std::size_t>(row);
|
|
||||||
if (y + 1 >= rows_.size()) {
|
|
||||||
return;
|
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());
|
||||||
rows_[y] += rows_[y + 1];
|
// end_of_line now equals line end (clamped before newline). The newline should be exactly at this position.
|
||||||
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
content_.Delete(end_of_line, 1);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -513,9 +519,12 @@ Buffer::insert_row(int row, const std::string_view text)
|
|||||||
{
|
{
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row), 0);
|
||||||
row = static_cast<int>(rows_.size());
|
if (!text.empty())
|
||||||
rows_.insert(rows_.begin() + row, Line(std::string(text)));
|
content_.Insert(off, text.data(), text.size());
|
||||||
|
const char nl = '\n';
|
||||||
|
content_.Insert(off + text.size(), &nl, 1);
|
||||||
|
rows_cache_dirty_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -524,9 +533,16 @@ Buffer::delete_row(int row)
|
|||||||
{
|
{
|
||||||
if (row < 0)
|
if (row < 0)
|
||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
std::size_t r = static_cast<std::size_t>(row);
|
||||||
|
if (r >= content_.LineCount())
|
||||||
return;
|
return;
|
||||||
rows_.erase(rows_.begin() + row);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
117
Buffer.h
117
Buffer.h
@@ -9,10 +9,9 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include "AppendBuffer.h"
|
#include "PieceTable.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
|
||||||
#include "syntax/HighlighterEngine.h"
|
#include "syntax/HighlighterEngine.h"
|
||||||
#include "Highlight.h"
|
#include "Highlight.h"
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] std::size_t Nrows() const
|
[[nodiscard]] std::size_t Nrows() const
|
||||||
{
|
{
|
||||||
return nrows_;
|
return content_LineCount_();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -79,7 +78,8 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Line wrapper backed by AppendBuffer (GapBuffer/PieceTable)
|
// Line wrapper used by legacy command paths.
|
||||||
|
// Keep this lightweight: store materialized bytes only for that line.
|
||||||
class Line {
|
class Line {
|
||||||
public:
|
public:
|
||||||
Line() = default;
|
Line() = default;
|
||||||
@@ -108,119 +108,102 @@ public:
|
|||||||
// capacity helpers
|
// capacity helpers
|
||||||
void Clear()
|
void Clear()
|
||||||
{
|
{
|
||||||
buf_.Clear();
|
s_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// size/access
|
// size/access
|
||||||
[[nodiscard]] std::size_t size() const
|
[[nodiscard]] std::size_t size() const
|
||||||
{
|
{
|
||||||
return buf_.Size();
|
return s_.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] bool empty() const
|
[[nodiscard]] bool empty() const
|
||||||
{
|
{
|
||||||
return size() == 0;
|
return s_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// read-only raw view
|
// read-only raw view
|
||||||
[[nodiscard]] const char *Data() const
|
[[nodiscard]] const char *Data() const
|
||||||
{
|
{
|
||||||
return buf_.Data();
|
return s_.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] std::size_t Size() const
|
[[nodiscard]] std::size_t Size() const
|
||||||
{
|
{
|
||||||
return buf_.Size();
|
return s_.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// element access (read-only)
|
// element access (read-only)
|
||||||
[[nodiscard]] char operator[](std::size_t i) const
|
[[nodiscard]] char operator[](std::size_t i) const
|
||||||
{
|
{
|
||||||
const char *d = buf_.Data();
|
return (i < s_.size()) ? s_[i] : '\0';
|
||||||
return (i < buf_.Size() && d) ? d[i] : '\0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// conversions
|
// conversions
|
||||||
explicit operator std::string() const
|
explicit operator std::string() const
|
||||||
{
|
{
|
||||||
return {buf_.Data() ? buf_.Data() : "", buf_.Size()};
|
return s_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// string-like API used by command/renderer layers (implemented via materialization for now)
|
// string-like API used by command/renderer layers (implemented via materialization for now)
|
||||||
[[nodiscard]] std::string substr(std::size_t pos) const
|
[[nodiscard]] std::string substr(std::size_t pos) const
|
||||||
{
|
{
|
||||||
const std::size_t n = buf_.Size();
|
return pos < s_.size() ? s_.substr(pos) : std::string();
|
||||||
if (pos >= n)
|
|
||||||
return {};
|
|
||||||
return {buf_.Data() + pos, n - pos};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] std::string substr(std::size_t pos, std::size_t len) const
|
[[nodiscard]] std::string substr(std::size_t pos, std::size_t len) const
|
||||||
{
|
{
|
||||||
const std::size_t n = buf_.Size();
|
return pos < s_.size() ? s_.substr(pos, len) : std::string();
|
||||||
if (pos >= n)
|
|
||||||
return {};
|
|
||||||
const std::size_t take = (pos + len > n) ? (n - pos) : len;
|
|
||||||
return {buf_.Data() + pos, take};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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
|
return s_.find(needle, pos);
|
||||||
const auto s = static_cast<std::string>(*this);
|
|
||||||
return s.find(needle, pos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void erase(std::size_t pos)
|
void erase(std::size_t pos)
|
||||||
{
|
{
|
||||||
// erase to end
|
if (pos < s_.size())
|
||||||
material_edit([&](std::string &s) {
|
s_.erase(pos);
|
||||||
if (pos < s.size())
|
|
||||||
s.erase(pos);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void erase(std::size_t pos, std::size_t len)
|
void erase(std::size_t pos, std::size_t len)
|
||||||
{
|
{
|
||||||
material_edit([&](std::string &s) {
|
if (pos < s_.size())
|
||||||
if (pos < s.size())
|
s_.erase(pos, len);
|
||||||
s.erase(pos, len);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void insert(std::size_t pos, const std::string &seg)
|
void insert(std::size_t pos, const std::string &seg)
|
||||||
{
|
{
|
||||||
material_edit([&](std::string &s) {
|
if (pos > s_.size())
|
||||||
if (pos > s.size())
|
pos = s_.size();
|
||||||
pos = s.size();
|
s_.insert(pos, seg);
|
||||||
s.insert(pos, seg);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Line &operator+=(const Line &other)
|
Line &operator+=(const Line &other)
|
||||||
{
|
{
|
||||||
buf_.Append(other.buf_.Data(), other.buf_.Size());
|
s_ += other.s_;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Line &operator+=(const std::string &s)
|
Line &operator+=(const std::string &s)
|
||||||
{
|
{
|
||||||
buf_.Append(s.data(), s.size());
|
s_ += s;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,37 +217,47 @@ public:
|
|||||||
private:
|
private:
|
||||||
void assign_from(const std::string &s)
|
void assign_from(const std::string &s)
|
||||||
{
|
{
|
||||||
buf_.Clear();
|
s_ = s;
|
||||||
if (!s.empty())
|
|
||||||
buf_.Append(s.data(), s.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
template<typename F>
|
std::string s_;
|
||||||
void material_edit(F fn)
|
|
||||||
{
|
|
||||||
std::string tmp = static_cast<std::string>(*this);
|
|
||||||
fn(tmp);
|
|
||||||
assign_from(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AppendBuffer buf_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] const std::vector<Line> &Rows() const
|
[[nodiscard]] const std::vector<Line> &Rows() const
|
||||||
{
|
{
|
||||||
|
ensure_rows_cache();
|
||||||
return rows_;
|
return rows_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] std::vector<Line> &Rows()
|
[[nodiscard]] std::vector<Line> &Rows()
|
||||||
{
|
{
|
||||||
|
ensure_rows_cache();
|
||||||
return rows_;
|
return rows_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Lightweight, lazy per-line accessors that avoid materializing all rows.
|
||||||
|
// Prefer these over Rows() in hot paths to reduce memory overhead on large files.
|
||||||
|
[[nodiscard]] std::string GetLineString(std::size_t row) const
|
||||||
|
{
|
||||||
|
return content_.GetLine(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] std::pair<std::size_t, std::size_t> GetLineRange(std::size_t row) const
|
||||||
|
{
|
||||||
|
return content_.GetLineRange(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Zero-copy view of a line. Points into the materialized backing store; becomes
|
||||||
|
// invalid after subsequent edits. Use immediately.
|
||||||
|
[[nodiscard]] std::string_view GetLineView(std::size_t row) const;
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] const std::string &Filename() const
|
[[nodiscard]] const std::string &Filename() const
|
||||||
{
|
{
|
||||||
return filename_;
|
return filename_;
|
||||||
@@ -409,13 +402,13 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
kte::HighlighterEngine *Highlighter()
|
[[nodiscard]] kte::HighlighterEngine *Highlighter()
|
||||||
{
|
{
|
||||||
return highlighter_.get();
|
return highlighter_.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const kte::HighlighterEngine *Highlighter() const
|
[[nodiscard]] const kte::HighlighterEngine *Highlighter() const
|
||||||
{
|
{
|
||||||
return highlighter_.get();
|
return highlighter_.get();
|
||||||
}
|
}
|
||||||
@@ -450,7 +443,7 @@ public:
|
|||||||
void delete_row(int row);
|
void delete_row(int row);
|
||||||
|
|
||||||
// Undo system accessors (created per-buffer)
|
// Undo system accessors (created per-buffer)
|
||||||
UndoSystem *Undo();
|
[[nodiscard]] UndoSystem *Undo();
|
||||||
|
|
||||||
[[nodiscard]] const UndoSystem *Undo() const;
|
[[nodiscard]] const UndoSystem *Undo() const;
|
||||||
|
|
||||||
@@ -460,7 +453,17 @@ 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
|
||||||
std::vector<Line> rows_; // buffer rows (without trailing newlines)
|
mutable std::vector<Line> rows_; // materialized cache of rows (without trailing newlines)
|
||||||
|
// 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;
|
||||||
|
|
||||||
std::string filename_;
|
std::string filename_;
|
||||||
bool is_file_backed_ = false;
|
bool is_file_backed_ = false;
|
||||||
bool dirty_ = false;
|
bool dirty_ = false;
|
||||||
|
|||||||
@@ -4,14 +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.5.1")
|
||||||
|
|
||||||
# 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)
|
|
||||||
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)
|
||||||
@@ -128,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
|
||||||
@@ -213,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
|
||||||
@@ -270,9 +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_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 ()
|
||||||
@@ -306,10 +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_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 ()
|
||||||
|
|||||||
158
Command.cc
158
Command.cc
@@ -6,6 +6,7 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "syntax/HighlighterRegistry.h"
|
#include "syntax/HighlighterRegistry.h"
|
||||||
@@ -48,7 +49,7 @@ bool gFontDialogRequested = false;
|
|||||||
// window based on the editor's current dimensions. The bottom row is reserved
|
// window based on the editor's current dimensions. The bottom row is reserved
|
||||||
// for the status line.
|
// for the status line.
|
||||||
static std::size_t
|
static std::size_t
|
||||||
compute_render_x(const std::string &line, const std::size_t curx, const std::size_t tabw)
|
compute_render_x(std::string_view line, const std::size_t curx, const std::size_t tabw)
|
||||||
{
|
{
|
||||||
std::size_t rx = 0;
|
std::size_t rx = 0;
|
||||||
for (std::size_t i = 0; i < curx && i < line.size(); ++i) {
|
for (std::size_t i = 0; i < curx && i < line.size(); ++i) {
|
||||||
@@ -82,7 +83,11 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
|
|||||||
rowoffs = cury - content_rows + 1;
|
rowoffs = cury - content_rows + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp vertical offset to available content
|
// Clamp vertical offset to available content. Use the materialized rows cache
|
||||||
|
// because some legacy editing commands still modify Buffer::Rows() directly.
|
||||||
|
// TerminalRenderer also renders from Buffer::Rows(), so keeping viewport math
|
||||||
|
// consistent with that avoids desync where the cursor goes off-screen when
|
||||||
|
// inserting newlines at EOF.
|
||||||
const auto total_rows = buf.Rows().size();
|
const auto total_rows = buf.Rows().size();
|
||||||
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;
|
||||||
@@ -93,10 +98,11 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal scrolling (use rendered columns with tabs expanded)
|
// Horizontal scrolling (use rendered columns with tabs expanded)
|
||||||
std::size_t rx = 0;
|
std::size_t rx = 0;
|
||||||
const auto &lines = buf.Rows();
|
const auto total = buf.Nrows();
|
||||||
if (cury < lines.size()) {
|
if (cury < total) {
|
||||||
rx = compute_render_x(static_cast<std::string>(lines[cury]), curx, 8);
|
// Avoid materializing all rows and copying strings; get a zero-copy view
|
||||||
|
rx = compute_render_x(buf.GetLineView(cury), curx, 8);
|
||||||
}
|
}
|
||||||
if (rx < coloffs) {
|
if (rx < coloffs) {
|
||||||
coloffs = rx;
|
coloffs = rx;
|
||||||
@@ -115,8 +121,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 +144,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 +259,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 +320,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 +342,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 +446,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 +464,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 +2154,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);
|
||||||
|
|||||||
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
|
|
||||||
};
|
|
||||||
@@ -294,25 +294,34 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
bool produced = false;
|
bool produced = false;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case SDL_MOUSEWHEEL: {
|
case SDL_MOUSEWHEEL: {
|
||||||
// Let ImGui handle mouse wheel when it wants to capture the mouse
|
// High-resolution trackpads can deliver fractional wheel deltas. Accumulate
|
||||||
// (e.g., when hovering the editor child window with scrollbars).
|
// precise values and emit one scroll step per whole unit.
|
||||||
// This enables native vertical and horizontal scrolling behavior in GUI.
|
float dy = 0.0f;
|
||||||
if (ImGui::GetIO().WantCaptureMouse)
|
#if SDL_VERSION_ATLEAST(2,0,18)
|
||||||
return false;
|
dy = e.wheel.preciseY;
|
||||||
// Otherwise, fallback to mapping vertical wheel to editor scroll commands.
|
#else
|
||||||
int dy = e.wheel.y;
|
dy = static_cast<float>(e.wheel.y);
|
||||||
|
#endif
|
||||||
#ifdef SDL_MOUSEWHEEL_FLIPPED
|
#ifdef SDL_MOUSEWHEEL_FLIPPED
|
||||||
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
|
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
|
||||||
dy = -dy;
|
dy = -dy;
|
||||||
#endif
|
#endif
|
||||||
if (dy != 0) {
|
if (dy != 0.0f) {
|
||||||
int repeat = dy > 0 ? dy : -dy;
|
wheel_accum_y_ += dy;
|
||||||
CommandId id = dy > 0 ? CommandId::ScrollUp : CommandId::ScrollDown;
|
float abs_accum = wheel_accum_y_ >= 0.0f ? wheel_accum_y_ : -wheel_accum_y_;
|
||||||
std::lock_guard<std::mutex> lk(mu_);
|
int steps = static_cast<int>(abs_accum);
|
||||||
for (int i = 0; i < repeat; ++i) {
|
if (steps > 0) {
|
||||||
q_.push(MappedInput{true, id, std::string(), 0});
|
CommandId id = (wheel_accum_y_ > 0.0f) ? CommandId::ScrollUp : CommandId::ScrollDown;
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
for (int i = 0; i < steps; ++i) {
|
||||||
|
q_.push(MappedInput{true, id, std::string(), 0});
|
||||||
|
}
|
||||||
|
// remove the whole steps, keep fractional remainder
|
||||||
|
wheel_accum_y_ += (wheel_accum_y_ > 0.0f)
|
||||||
|
? -static_cast<float>(steps)
|
||||||
|
: static_cast<float>(steps);
|
||||||
|
return true; // consumed
|
||||||
}
|
}
|
||||||
return true; // consumed
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,9 @@ private:
|
|||||||
bool suppress_text_input_once_ = false;
|
bool suppress_text_input_once_ = false;
|
||||||
|
|
||||||
Editor *ed_ = nullptr; // attached editor for editor-owned uarg handling
|
Editor *ed_ = nullptr; // attached editor for editor-owned uarg handling
|
||||||
|
|
||||||
|
// Accumulators for high-resolution (trackpad) scrolling. We emit one scroll
|
||||||
|
// command per whole step and keep the fractional remainder.
|
||||||
|
float wheel_accum_y_ = 0.0f;
|
||||||
|
float wheel_accum_x_ = 0.0f; // reserved for future horizontal scrolling
|
||||||
};
|
};
|
||||||
534
PieceTable.cc
534
PieceTable.cc
@@ -1,5 +1,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include "PieceTable.h"
|
#include "PieceTable.h"
|
||||||
|
|
||||||
@@ -14,13 +15,32 @@ PieceTable::PieceTable(const std::size_t initialCapacity)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PieceTable::PieceTable(const std::size_t initialCapacity,
|
||||||
|
const std::size_t piece_limit,
|
||||||
|
const std::size_t small_piece_threshold,
|
||||||
|
const std::size_t max_consolidation_bytes)
|
||||||
|
{
|
||||||
|
add_.reserve(initialCapacity);
|
||||||
|
materialized_.reserve(initialCapacity);
|
||||||
|
piece_limit_ = piece_limit;
|
||||||
|
small_piece_threshold_ = small_piece_threshold;
|
||||||
|
max_consolidation_bytes_ = max_consolidation_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
PieceTable::PieceTable(const PieceTable &other)
|
PieceTable::PieceTable(const PieceTable &other)
|
||||||
: original_(other.original_),
|
: original_(other.original_),
|
||||||
add_(other.add_),
|
add_(other.add_),
|
||||||
pieces_(other.pieces_),
|
pieces_(other.pieces_),
|
||||||
materialized_(other.materialized_),
|
materialized_(other.materialized_),
|
||||||
dirty_(other.dirty_),
|
dirty_(other.dirty_),
|
||||||
total_size_(other.total_size_) {}
|
total_size_(other.total_size_)
|
||||||
|
{
|
||||||
|
version_ = other.version_;
|
||||||
|
// caches are per-instance, mark invalid
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
PieceTable &
|
PieceTable &
|
||||||
@@ -34,6 +54,9 @@ PieceTable::operator=(const PieceTable &other)
|
|||||||
materialized_ = other.materialized_;
|
materialized_ = other.materialized_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
total_size_ = other.total_size_;
|
total_size_ = other.total_size_;
|
||||||
|
version_ = other.version_;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +71,9 @@ PieceTable::PieceTable(PieceTable &&other) noexcept
|
|||||||
{
|
{
|
||||||
other.dirty_ = true;
|
other.dirty_ = true;
|
||||||
other.total_size_ = 0;
|
other.total_size_ = 0;
|
||||||
|
version_ = other.version_;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -64,6 +90,9 @@ PieceTable::operator=(PieceTable &&other) noexcept
|
|||||||
total_size_ = other.total_size_;
|
total_size_ = other.total_size_;
|
||||||
other.dirty_ = true;
|
other.dirty_ = true;
|
||||||
other.total_size_ = 0;
|
other.total_size_ = 0;
|
||||||
|
version_ = other.version_;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +108,21 @@ PieceTable::Reserve(const std::size_t newCapacity)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Setter to allow tuning consolidation heuristics
|
||||||
|
void
|
||||||
|
PieceTable::SetConsolidationParams(const std::size_t piece_limit,
|
||||||
|
const std::size_t small_piece_threshold,
|
||||||
|
const std::size_t max_consolidation_bytes)
|
||||||
|
{
|
||||||
|
piece_limit_ = piece_limit;
|
||||||
|
small_piece_threshold_ = small_piece_threshold;
|
||||||
|
max_consolidation_bytes_ = max_consolidation_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// (removed helper) — we'll invalidate caches inline inside mutating methods
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
PieceTable::AppendChar(char c)
|
PieceTable::AppendChar(char c)
|
||||||
{
|
{
|
||||||
@@ -151,6 +195,11 @@ PieceTable::Clear()
|
|||||||
materialized_.clear();
|
materialized_.clear();
|
||||||
total_size_ = 0;
|
total_size_ = 0;
|
||||||
dirty_ = true;
|
dirty_ = true;
|
||||||
|
line_index_.clear();
|
||||||
|
line_index_dirty_ = true;
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -171,6 +220,9 @@ PieceTable::addPieceBack(const Source src, const std::size_t start, const std::s
|
|||||||
last.len += len;
|
last.len += len;
|
||||||
total_size_ += len;
|
total_size_ += len;
|
||||||
dirty_ = true;
|
dirty_ = true;
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,6 +231,10 @@ 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();
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -197,12 +253,19 @@ PieceTable::addPieceFront(Source src, std::size_t start, std::size_t len)
|
|||||||
first.len += len;
|
first.len += len;
|
||||||
total_size_ += len;
|
total_size_ += len;
|
||||||
dirty_ = true;
|
dirty_ = true;
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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();
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -225,3 +288,472 @@ 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;
|
||||||
|
// Merge repeatedly with previous while contiguous and same source
|
||||||
|
while (index > 0) {
|
||||||
|
auto &prev = pieces_[index - 1];
|
||||||
|
auto &curr = pieces_[index];
|
||||||
|
if (prev.src == curr.src && prev.start + prev.len == curr.start) {
|
||||||
|
prev.len += curr.len;
|
||||||
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
|
index -= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Merge repeatedly with next while contiguous and same source
|
||||||
|
while (index + 1 < pieces_.size()) {
|
||||||
|
auto &curr = pieces_[index];
|
||||||
|
auto &next = pieces_[index + 1];
|
||||||
|
if (curr.src == next.src && curr.start + curr.len == next.start) {
|
||||||
|
curr.len += next.len;
|
||||||
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(index + 1));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PieceTable::InvalidateLineIndex() const
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
maybeConsolidate();
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [idx, inner] = locate(byte_offset);
|
||||||
|
if (idx == pieces_.size()) {
|
||||||
|
// insert at end
|
||||||
|
pieces_.push_back(Piece{Source::Add, add_start, len});
|
||||||
|
total_size_ += len;
|
||||||
|
dirty_ = true;
|
||||||
|
InvalidateLineIndex();
|
||||||
|
coalesceNeighbors(pieces_.size() - 1);
|
||||||
|
maybeConsolidate();
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Piece target = pieces_[idx];
|
||||||
|
// Build replacement sequence: left, inserted, right
|
||||||
|
std::vector<Piece> repl;
|
||||||
|
repl.reserve(3);
|
||||||
|
if (inner > 0) {
|
||||||
|
repl.push_back(Piece{target.src, target.start, inner});
|
||||||
|
}
|
||||||
|
repl.push_back(Piece{Source::Add, add_start, len});
|
||||||
|
const std::size_t right_len = target.len - inner;
|
||||||
|
if (right_len > 0) {
|
||||||
|
repl.push_back(Piece{target.src, target.start + inner, right_len});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace target with repl
|
||||||
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(idx));
|
||||||
|
pieces_.insert(pieces_.begin() + static_cast<std::ptrdiff_t>(idx), repl.begin(), repl.end());
|
||||||
|
|
||||||
|
total_size_ += len;
|
||||||
|
dirty_ = true;
|
||||||
|
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);
|
||||||
|
maybeConsolidate();
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PieceTable::Delete(std::size_t byte_offset, std::size_t len)
|
||||||
|
{
|
||||||
|
if (len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (byte_offset >= total_size_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (byte_offset + len > total_size_) {
|
||||||
|
len = total_size_ - byte_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
maybeConsolidate();
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Consolidation implementation =====
|
||||||
|
|
||||||
|
void
|
||||||
|
PieceTable::appendPieceDataTo(std::string &out, const Piece &p) const
|
||||||
|
{
|
||||||
|
if (p.len == 0)
|
||||||
|
return;
|
||||||
|
const std::string &src = p.src == Source::Original ? original_ : add_;
|
||||||
|
out.append(src.data() + static_cast<std::ptrdiff_t>(p.start), p.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PieceTable::consolidateRange(std::size_t start_idx, std::size_t end_idx)
|
||||||
|
{
|
||||||
|
if (start_idx >= end_idx || start_idx >= pieces_.size())
|
||||||
|
return;
|
||||||
|
end_idx = std::min(end_idx, pieces_.size());
|
||||||
|
std::size_t total = 0;
|
||||||
|
for (std::size_t i = start_idx; i < end_idx; ++i)
|
||||||
|
total += pieces_[i].len;
|
||||||
|
if (total == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::size_t add_start = add_.size();
|
||||||
|
std::string tmp;
|
||||||
|
tmp.reserve(std::min<std::size_t>(total, max_consolidation_bytes_));
|
||||||
|
for (std::size_t i = start_idx; i < end_idx; ++i)
|
||||||
|
appendPieceDataTo(tmp, pieces_[i]);
|
||||||
|
add_.append(tmp);
|
||||||
|
|
||||||
|
// Replace [start_idx, end_idx) with single Add piece
|
||||||
|
Piece consolidated{Source::Add, add_start, tmp.size()};
|
||||||
|
pieces_.erase(pieces_.begin() + static_cast<std::ptrdiff_t>(start_idx),
|
||||||
|
pieces_.begin() + static_cast<std::ptrdiff_t>(end_idx));
|
||||||
|
pieces_.insert(pieces_.begin() + static_cast<std::ptrdiff_t>(start_idx), consolidated);
|
||||||
|
|
||||||
|
// total_size_ unchanged
|
||||||
|
dirty_ = true;
|
||||||
|
InvalidateLineIndex();
|
||||||
|
coalesceNeighbors(start_idx);
|
||||||
|
// Layout changed; invalidate caches/version
|
||||||
|
version_++;
|
||||||
|
range_cache_ = {};
|
||||||
|
find_cache_ = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PieceTable::maybeConsolidate()
|
||||||
|
{
|
||||||
|
if (pieces_.size() <= piece_limit_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Find the first run of small pieces to consolidate
|
||||||
|
std::size_t n = pieces_.size();
|
||||||
|
std::size_t best_start = n, best_end = n;
|
||||||
|
std::size_t i = 0;
|
||||||
|
while (i < n) {
|
||||||
|
// Skip large pieces quickly
|
||||||
|
if (pieces_[i].len > small_piece_threshold_) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::size_t j = i;
|
||||||
|
std::size_t bytes = 0;
|
||||||
|
while (j < n) {
|
||||||
|
const auto &p = pieces_[j];
|
||||||
|
if (p.len > small_piece_threshold_)
|
||||||
|
break;
|
||||||
|
if (bytes + p.len > max_consolidation_bytes_)
|
||||||
|
break;
|
||||||
|
bytes += p.len;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
if (j - i >= 2 && bytes > 0) {
|
||||||
|
// consolidate runs of at least 2 pieces
|
||||||
|
best_start = i;
|
||||||
|
best_end = j;
|
||||||
|
break; // do one run per call; subsequent ops can repeat if still over limit
|
||||||
|
}
|
||||||
|
i = j + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best_start < best_end) {
|
||||||
|
consolidateRange(best_start, best_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
PieceTable::LineCount() const
|
||||||
|
{
|
||||||
|
RebuildLineIndex();
|
||||||
|
return line_index_.empty() ? 0 : line_index_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::pair<std::size_t, std::size_t>
|
||||||
|
PieceTable::GetLineRange(std::size_t line_num) const
|
||||||
|
{
|
||||||
|
RebuildLineIndex();
|
||||||
|
if (line_index_.empty())
|
||||||
|
return {0, 0};
|
||||||
|
if (line_num >= line_index_.size())
|
||||||
|
return {0, 0};
|
||||||
|
std::size_t start = line_index_[line_num];
|
||||||
|
std::size_t end = (line_num + 1 < line_index_.size()) ? line_index_[line_num + 1] : total_size_;
|
||||||
|
return {start, end};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
PieceTable::GetLine(std::size_t line_num) const
|
||||||
|
{
|
||||||
|
auto [start, end] = GetLineRange(line_num);
|
||||||
|
if (end < start)
|
||||||
|
return std::string();
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Fast path: return cached value if version/offset/len match
|
||||||
|
if (range_cache_.valid && range_cache_.version == version_ &&
|
||||||
|
range_cache_.off == byte_offset && range_cache_.len == len) {
|
||||||
|
return range_cache_.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out;
|
||||||
|
out.reserve(len);
|
||||||
|
if (!dirty_) {
|
||||||
|
// Already materialized; slice directly
|
||||||
|
out.assign(materialized_.data() + static_cast<std::ptrdiff_t>(byte_offset), len);
|
||||||
|
} else {
|
||||||
|
// Assemble substring directly from pieces without full materialization
|
||||||
|
auto [idx, inner] = locate(byte_offset);
|
||||||
|
std::size_t remaining = len;
|
||||||
|
while (remaining > 0 && idx < pieces_.size()) {
|
||||||
|
const auto &p = pieces_[idx];
|
||||||
|
const std::string &src = (p.src == Source::Original) ? original_ : add_;
|
||||||
|
std::size_t take = std::min<std::size_t>(p.len - inner, remaining);
|
||||||
|
if (take > 0) {
|
||||||
|
const char *base = src.data() + static_cast<std::ptrdiff_t>(p.start + inner);
|
||||||
|
out.append(base, take);
|
||||||
|
remaining -= take;
|
||||||
|
inner = 0;
|
||||||
|
idx += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
range_cache_.valid = true;
|
||||||
|
range_cache_.version = version_;
|
||||||
|
range_cache_.off = byte_offset;
|
||||||
|
range_cache_.len = len;
|
||||||
|
range_cache_.data = out;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
PieceTable::Find(const std::string &needle, std::size_t start) const
|
||||||
|
{
|
||||||
|
if (needle.empty())
|
||||||
|
return start <= total_size_ ? start : std::numeric_limits<std::size_t>::max();
|
||||||
|
if (start > total_size_)
|
||||||
|
return std::numeric_limits<std::size_t>::max();
|
||||||
|
if (find_cache_.valid &&
|
||||||
|
find_cache_.version == version_ &&
|
||||||
|
find_cache_.needle == needle &&
|
||||||
|
find_cache_.start == start) {
|
||||||
|
return find_cache_.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
materialize();
|
||||||
|
auto pos = materialized_.find(needle, start);
|
||||||
|
if (pos == std::string::npos)
|
||||||
|
pos = std::numeric_limits<std::size_t>::max();
|
||||||
|
// Update cache
|
||||||
|
find_cache_.valid = true;
|
||||||
|
find_cache_.version = version_;
|
||||||
|
find_cache_.needle = needle;
|
||||||
|
find_cache_.start = start;
|
||||||
|
find_cache_.result = pos;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|||||||
90
PieceTable.h
90
PieceTable.h
@@ -3,8 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
|
||||||
class PieceTable {
|
class PieceTable {
|
||||||
@@ -13,6 +15,12 @@ public:
|
|||||||
|
|
||||||
explicit PieceTable(std::size_t initialCapacity);
|
explicit PieceTable(std::size_t initialCapacity);
|
||||||
|
|
||||||
|
// Advanced constructor allowing configuration of consolidation heuristics
|
||||||
|
PieceTable(std::size_t initialCapacity,
|
||||||
|
std::size_t piece_limit,
|
||||||
|
std::size_t small_piece_threshold,
|
||||||
|
std::size_t max_consolidation_bytes);
|
||||||
|
|
||||||
PieceTable(const PieceTable &other);
|
PieceTable(const PieceTable &other);
|
||||||
|
|
||||||
PieceTable &operator=(const PieceTable &other);
|
PieceTable &operator=(const PieceTable &other);
|
||||||
@@ -68,6 +76,35 @@ 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;
|
||||||
|
|
||||||
|
// Heuristic configuration
|
||||||
|
void SetConsolidationParams(std::size_t piece_limit,
|
||||||
|
std::size_t small_piece_threshold,
|
||||||
|
std::size_t max_consolidation_bytes);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Source : unsigned char { Original, Add };
|
enum class Source : unsigned char { Original, Add };
|
||||||
|
|
||||||
@@ -83,12 +120,61 @@ 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);
|
||||||
|
|
||||||
|
// Consolidation helpers and heuristics
|
||||||
|
void maybeConsolidate();
|
||||||
|
|
||||||
|
void consolidateRange(std::size_t start_idx, std::size_t end_idx);
|
||||||
|
|
||||||
|
void appendPieceDataTo(std::string &out, const Piece &p) const;
|
||||||
|
|
||||||
|
// 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_;
|
||||||
std::vector<Piece> pieces_;
|
std::vector<Piece> pieces_;
|
||||||
|
|
||||||
mutable std::string materialized_;
|
mutable std::string materialized_;
|
||||||
mutable bool dirty_ = true;
|
mutable bool dirty_ = true;
|
||||||
std::size_t total_size_ = 0;
|
// Monotonic content version. Increment on any mutation that affects content layout
|
||||||
|
mutable std::uint64_t version_ = 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;
|
||||||
|
|
||||||
|
// Heuristic knobs
|
||||||
|
std::size_t piece_limit_ = 4096; // trigger consolidation when exceeded
|
||||||
|
std::size_t small_piece_threshold_ = 64; // bytes
|
||||||
|
std::size_t max_consolidation_bytes_ = 4096; // cap per consolidation run
|
||||||
|
|
||||||
|
// Lightweight caches to avoid redundant work when callers query the same range repeatedly
|
||||||
|
struct RangeCache {
|
||||||
|
bool valid = false;
|
||||||
|
std::uint64_t version = 0;
|
||||||
|
std::size_t off = 0;
|
||||||
|
std::size_t len = 0;
|
||||||
|
std::string data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FindCache {
|
||||||
|
bool valid = false;
|
||||||
|
std::uint64_t version = 0;
|
||||||
|
std::string needle;
|
||||||
|
std::size_t start = 0;
|
||||||
|
std::size_t result = std::numeric_limits<std::size_t>::max();
|
||||||
|
};
|
||||||
|
|
||||||
|
mutable RangeCache range_cache_;
|
||||||
|
mutable FindCache find_cache_;
|
||||||
};
|
};
|
||||||
@@ -142,9 +142,11 @@ protected:
|
|||||||
p.save();
|
p.save();
|
||||||
p.setClipRect(viewport);
|
p.setClipRect(viewport);
|
||||||
|
|
||||||
// Iterate visible lines
|
// Iterate visible lines
|
||||||
for (std::size_t i = rowoffs, vis_idx = 0; i < last_row; ++i, ++vis_idx) {
|
for (std::size_t i = rowoffs, vis_idx = 0; i < last_row; ++i, ++vis_idx) {
|
||||||
const auto &line = static_cast<const std::string &>(lines[i]);
|
// Materialize the Buffer::Line into a std::string for
|
||||||
|
// regex/iterator usage and general string ops.
|
||||||
|
const std::string line = static_cast<std::string>(lines[i]);
|
||||||
const int y = viewport.y() + static_cast<int>(vis_idx) * line_h;
|
const int y = viewport.y() + static_cast<int>(vis_idx) * line_h;
|
||||||
const int baseline = y + fm.ascent();
|
const int baseline = y + fm.ascent();
|
||||||
|
|
||||||
|
|||||||
2502
REWRITE.md
Normal file
2502
REWRITE.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,13 +42,15 @@ TerminalFrontend::Init(Editor &ed)
|
|||||||
meta(stdscr, TRUE);
|
meta(stdscr, TRUE);
|
||||||
// Make ESC key sequences resolve quickly so ESC+<key> works as meta
|
// Make ESC key sequences resolve quickly so ESC+<key> works as meta
|
||||||
#ifdef set_escdelay
|
#ifdef set_escdelay
|
||||||
set_escdelay(50);
|
set_escdelay(TerminalFrontend::kEscDelayMs);
|
||||||
#endif
|
#endif
|
||||||
nodelay(stdscr, TRUE);
|
// Make getch() block briefly instead of busy-looping; reduces CPU when idle
|
||||||
|
// Equivalent to nodelay(FALSE) with a small timeout.
|
||||||
|
timeout(16); // ~16ms (about 60Hz)
|
||||||
curs_set(1);
|
curs_set(1);
|
||||||
// Enable mouse support if available
|
// Enable mouse support if available
|
||||||
mouseinterval(0);
|
mouseinterval(0);
|
||||||
mousemask(ALL_MOUSE_EVENTS, nullptr);
|
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, nullptr);
|
||||||
|
|
||||||
int r = 0, c = 0;
|
int r = 0, c = 0;
|
||||||
getmaxyx(stdscr, r, c);
|
getmaxyx(stdscr, r, c);
|
||||||
@@ -57,6 +59,20 @@ TerminalFrontend::Init(Editor &ed)
|
|||||||
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
||||||
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
||||||
input_.Attach(&ed);
|
input_.Attach(&ed);
|
||||||
|
|
||||||
|
// Ignore SIGINT (Ctrl-C) so it doesn't terminate the TUI.
|
||||||
|
// We'll restore the previous handler on Shutdown().
|
||||||
|
{
|
||||||
|
struct sigaction sa{};
|
||||||
|
sa.sa_handler = SIG_IGN;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = 0;
|
||||||
|
struct sigaction old{};
|
||||||
|
if (sigaction(SIGINT, &sa, &old) == 0) {
|
||||||
|
old_sigint_ = old;
|
||||||
|
have_old_sigint_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +96,6 @@ TerminalFrontend::Step(Editor &ed, bool &running)
|
|||||||
if (mi.hasCommand) {
|
if (mi.hasCommand) {
|
||||||
Execute(ed, mi.id, mi.arg, mi.count);
|
Execute(ed, mi.id, mi.arg, mi.count);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Avoid busy loop
|
|
||||||
usleep(1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ed.QuitRequested()) {
|
if (ed.QuitRequested()) {
|
||||||
@@ -101,5 +114,10 @@ TerminalFrontend::Shutdown()
|
|||||||
(void) tcsetattr(STDIN_FILENO, TCSANOW, &orig_tio_);
|
(void) tcsetattr(STDIN_FILENO, TCSANOW, &orig_tio_);
|
||||||
have_orig_tio_ = false;
|
have_orig_tio_ = false;
|
||||||
}
|
}
|
||||||
|
// Restore previous SIGINT handler
|
||||||
|
if (have_old_sigint_) {
|
||||||
|
(void) sigaction(SIGINT, &old_sigint_, nullptr);
|
||||||
|
have_old_sigint_ = false;
|
||||||
|
}
|
||||||
endwin();
|
endwin();
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
#include "Frontend.h"
|
#include "Frontend.h"
|
||||||
#include "TerminalInputHandler.h"
|
#include "TerminalInputHandler.h"
|
||||||
@@ -15,6 +16,11 @@ public:
|
|||||||
|
|
||||||
~TerminalFrontend() override = default;
|
~TerminalFrontend() override = default;
|
||||||
|
|
||||||
|
// Configurable ESC key delay (ms) for ncurses' set_escdelay().
|
||||||
|
// Controls how long ncurses waits to distinguish ESC vs. meta sequences.
|
||||||
|
// Adjust if your terminal needs a different threshold.
|
||||||
|
static constexpr int kEscDelayMs = 50;
|
||||||
|
|
||||||
bool Init(Editor &ed) override;
|
bool Init(Editor &ed) override;
|
||||||
|
|
||||||
void Step(Editor &ed, bool &running) override;
|
void Step(Editor &ed, bool &running) override;
|
||||||
@@ -29,4 +35,7 @@ private:
|
|||||||
// Saved terminal attributes to restore on shutdown
|
// Saved terminal attributes to restore on shutdown
|
||||||
bool have_orig_tio_ = false;
|
bool have_orig_tio_ = false;
|
||||||
struct termios orig_tio_{};
|
struct termios orig_tio_{};
|
||||||
|
// Saved SIGINT handler to restore on shutdown
|
||||||
|
bool have_old_sigint_ = false;
|
||||||
|
struct sigaction old_sigint_{};
|
||||||
};
|
};
|
||||||
@@ -29,89 +29,95 @@ map_key_to_command(const int ch,
|
|||||||
// Handle special keys from ncurses
|
// Handle special keys from ncurses
|
||||||
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
||||||
switch (ch) {
|
switch (ch) {
|
||||||
case KEY_MOUSE: {
|
case KEY_ENTER:
|
||||||
k_prefix = false;
|
// Some terminals send KEY_ENTER distinct from '\n'/'\r'
|
||||||
k_ctrl_pending = false;
|
k_prefix = false;
|
||||||
MEVENT ev{};
|
k_ctrl_pending = false;
|
||||||
if (getmouse(&ev) == OK) {
|
out = {true, CommandId::Newline, "", 0};
|
||||||
// Mouse wheel → scroll viewport without moving cursor
|
return true;
|
||||||
|
case KEY_MOUSE: {
|
||||||
|
k_prefix = false;
|
||||||
|
k_ctrl_pending = false;
|
||||||
|
MEVENT ev{};
|
||||||
|
if (getmouse(&ev) == OK) {
|
||||||
|
// Mouse wheel → scroll viewport without moving cursor
|
||||||
#ifdef BUTTON4_PRESSED
|
#ifdef BUTTON4_PRESSED
|
||||||
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
||||||
out = {true, CommandId::ScrollUp, "", 0};
|
out = {true, CommandId::ScrollUp, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef BUTTON5_PRESSED
|
#ifdef BUTTON5_PRESSED
|
||||||
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
||||||
out = {true, CommandId::ScrollDown, "", 0};
|
out = {true, CommandId::ScrollDown, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// React to left button click/press
|
// React to left button click/press
|
||||||
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
||||||
char buf[64];
|
char buf[64];
|
||||||
// Use screen coordinates; command handler will translate via offsets
|
// Use screen coordinates; command handler will translate via offsets
|
||||||
std::snprintf(buf, sizeof(buf), "@%d:%d", ev.y, ev.x);
|
std::snprintf(buf, sizeof(buf), "@%d:%d", ev.y, ev.x);
|
||||||
out = {true, CommandId::MoveCursorTo, std::string(buf), 0};
|
out = {true, CommandId::MoveCursorTo, std::string(buf), 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// No actionable mouse event
|
|
||||||
out.hasCommand = false;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
case KEY_LEFT:
|
// No actionable mouse event
|
||||||
k_prefix = false;
|
out.hasCommand = false;
|
||||||
k_ctrl_pending = false;
|
return true;
|
||||||
out = {true, CommandId::MoveLeft, "", 0};
|
}
|
||||||
return true;
|
case KEY_LEFT:
|
||||||
case KEY_RIGHT:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::MoveLeft, "", 0};
|
||||||
out = {true, CommandId::MoveRight, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_RIGHT:
|
||||||
case KEY_UP:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::MoveRight, "", 0};
|
||||||
out = {true, CommandId::MoveUp, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_UP:
|
||||||
case KEY_DOWN:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::MoveUp, "", 0};
|
||||||
out = {true, CommandId::MoveDown, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_DOWN:
|
||||||
case KEY_HOME:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::MoveDown, "", 0};
|
||||||
out = {true, CommandId::MoveHome, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_HOME:
|
||||||
case KEY_END:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::MoveHome, "", 0};
|
||||||
out = {true, CommandId::MoveEnd, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_END:
|
||||||
case KEY_PPAGE:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::MoveEnd, "", 0};
|
||||||
out = {true, CommandId::PageUp, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_PPAGE:
|
||||||
case KEY_NPAGE:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::PageUp, "", 0};
|
||||||
out = {true, CommandId::PageDown, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_NPAGE:
|
||||||
case KEY_DC:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::PageDown, "", 0};
|
||||||
out = {true, CommandId::DeleteChar, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_DC:
|
||||||
case KEY_RESIZE:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
k_ctrl_pending = false;
|
out = {true, CommandId::DeleteChar, "", 0};
|
||||||
out = {true, CommandId::Refresh, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_RESIZE:
|
||||||
default:
|
k_prefix = false;
|
||||||
break;
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::Refresh, "", 0};
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ESC as cancel of prefix; many terminals send meta sequences as ESC+...
|
// ESC as cancel of prefix; many terminals send meta sequences as ESC+...
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
34
default.nix
34
default.nix
@@ -6,8 +6,9 @@
|
|||||||
SDL2,
|
SDL2,
|
||||||
libGL,
|
libGL,
|
||||||
xorg,
|
xorg,
|
||||||
|
kdePackages,
|
||||||
|
qt6Packages ? kdePackages.qt6Packages,
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
|
|
||||||
graphical ? false,
|
graphical ? false,
|
||||||
graphical-qt ? false,
|
graphical-qt ? false,
|
||||||
...
|
...
|
||||||
@@ -37,12 +38,13 @@ stdenv.mkDerivation {
|
|||||||
xorg.libX11
|
xorg.libX11
|
||||||
]
|
]
|
||||||
++ lib.optionals graphical-qt [
|
++ lib.optionals graphical-qt [
|
||||||
qt5Full
|
kdePackages.qt6ct
|
||||||
qtcreator ## not sure if this is actually needed
|
qt6Packages.qtbase
|
||||||
|
qt6Packages.wrapQtAppsHook
|
||||||
];
|
];
|
||||||
|
|
||||||
cmakeFlags = [
|
cmakeFlags = [
|
||||||
"-DBUILD_GUI=${if graphical or graphical-qt then "ON" else "OFF"}"
|
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
|
||||||
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
|
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
|
||||||
"-DCMAKE_BUILD_TYPE=Debug"
|
"-DCMAKE_BUILD_TYPE=Debug"
|
||||||
];
|
];
|
||||||
@@ -52,17 +54,23 @@ stdenv.mkDerivation {
|
|||||||
|
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
cp kte $out/bin/
|
cp kte $out/bin/
|
||||||
|
|
||||||
installManPage ../docs/kte.1
|
installManPage ../docs/kte.1
|
||||||
|
|
||||||
''
|
${lib.optionalString graphical ''
|
||||||
+ lib.optionalString graphical ''
|
mkdir -p $out/bin
|
||||||
cp kge $out/bin/
|
|
||||||
installManPage ../docs/kge.1
|
${if graphical-qt then ''
|
||||||
mkdir -p $out/share/icons
|
cp kge $out/bin/kge-qt
|
||||||
cp ../kge.png $out/share/icons/
|
'' else ''
|
||||||
''
|
cp kge $out/bin/kge
|
||||||
+ ''
|
''}
|
||||||
|
|
||||||
|
installManPage ../docs/kge.1
|
||||||
|
|
||||||
|
mkdir -p $out/share/icons/hicolor/256x256/apps
|
||||||
|
cp ../kge.png $out/share/icons/hicolor/256x256/apps/kge.png
|
||||||
|
''}
|
||||||
|
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
601
docs/plans/piece-table-migration.md
Normal file
601
docs/plans/piece-table-migration.md
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
# PieceTable Migration Plan
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document outlines the plan to remove GapBuffer support from kte and
|
||||||
|
migrate to using a **single PieceTable per Buffer**, rather than the
|
||||||
|
current vector-of-Lines architecture where each Line contains either a
|
||||||
|
GapBuffer or PieceTable.
|
||||||
|
|
||||||
|
## Current Architecture Analysis
|
||||||
|
|
||||||
|
### Text Storage
|
||||||
|
|
||||||
|
**Current Implementation:**
|
||||||
|
|
||||||
|
- `Buffer` contains `std::vector<Line> rows_`
|
||||||
|
- Each `Line` wraps an `AppendBuffer` (type alias)
|
||||||
|
- `AppendBuffer` is either `GapBuffer` (default) or `PieceTable` (via
|
||||||
|
`KTE_USE_PIECE_TABLE`)
|
||||||
|
- Each line is independently managed with its own buffer
|
||||||
|
- Operations are line-based with coordinate pairs (row, col)
|
||||||
|
|
||||||
|
**Key Files:**
|
||||||
|
|
||||||
|
- `Buffer.h/cc` - Buffer class with vector of Lines
|
||||||
|
- `AppendBuffer.h` - Type selector (GapBuffer vs PieceTable)
|
||||||
|
- `GapBuffer.h/cc` - Per-line gap buffer implementation
|
||||||
|
- `PieceTable.h/cc` - Per-line piece table implementation
|
||||||
|
- `UndoSystem.h/cc` - Records operations with (row, col, text)
|
||||||
|
- `UndoNode.h` - Undo operation types (Insert, Delete, Paste, Newline,
|
||||||
|
DeleteRow)
|
||||||
|
- `Command.cc` - High-level editing commands
|
||||||
|
|
||||||
|
### Current Buffer API
|
||||||
|
|
||||||
|
**Low-level editing operations (used by UndoSystem):**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void insert_text(int row, int col, std::string_view text);
|
||||||
|
void delete_text(int row, int col, std::size_t len);
|
||||||
|
void split_line(int row, int col);
|
||||||
|
void join_lines(int row);
|
||||||
|
void insert_row(int row, std::string_view text);
|
||||||
|
void delete_row(int row);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Line access:**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::vector<Line> &Rows();
|
||||||
|
const std::vector<Line> &Rows() const;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Line API (Buffer::Line):**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::size_t size() const;
|
||||||
|
const char *Data() const;
|
||||||
|
char operator[](std::size_t i) const;
|
||||||
|
std::string substr(std::size_t pos, std::size_t len) const;
|
||||||
|
std::size_t find(const std::string &needle, std::size_t pos) const;
|
||||||
|
void erase(std::size_t pos, std::size_t len);
|
||||||
|
void insert(std::size_t pos, const std::string &seg);
|
||||||
|
Line &operator+=(const Line &other);
|
||||||
|
Line &operator+=(const std::string &s);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current PieceTable Limitations
|
||||||
|
|
||||||
|
The existing `PieceTable` class only supports:
|
||||||
|
|
||||||
|
- `Append(char/string)` - add to end
|
||||||
|
- `Prepend(char/string)` - add to beginning
|
||||||
|
- `Clear()` - empty the buffer
|
||||||
|
- `Data()` / `Size()` - access content (materializes on demand)
|
||||||
|
|
||||||
|
**Missing capabilities needed for buffer-wide storage:**
|
||||||
|
|
||||||
|
- Insert at arbitrary byte position
|
||||||
|
- Delete at arbitrary byte position
|
||||||
|
- Line indexing and line-based queries
|
||||||
|
- Position conversion (byte offset ↔ line/col)
|
||||||
|
- Efficient line boundary tracking
|
||||||
|
|
||||||
|
## Target Architecture
|
||||||
|
|
||||||
|
### Design Overview
|
||||||
|
|
||||||
|
**Single PieceTable per Buffer:**
|
||||||
|
|
||||||
|
- `Buffer` contains one `PieceTable content_` (replaces
|
||||||
|
`std::vector<Line> rows_`)
|
||||||
|
- Text stored as continuous byte sequence with `\n` as line separators
|
||||||
|
- Line index cached for efficient line-based operations
|
||||||
|
- All operations work on byte offsets internally
|
||||||
|
- Buffer provides line/column API as convenience layer
|
||||||
|
|
||||||
|
### Enhanced PieceTable Design
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class PieceTable {
|
||||||
|
public:
|
||||||
|
// Existing API (keep for compatibility if needed)
|
||||||
|
void Append(const char *s, std::size_t len);
|
||||||
|
void Prepend(const char *s, std::size_t len);
|
||||||
|
void Clear();
|
||||||
|
const char *Data() const;
|
||||||
|
std::size_t Size() const;
|
||||||
|
|
||||||
|
// NEW: Core 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);
|
||||||
|
|
||||||
|
// NEW: Line-based queries
|
||||||
|
std::size_t LineCount() const;
|
||||||
|
std::string GetLine(std::size_t line_num) const;
|
||||||
|
std::pair<std::size_t, std::size_t> GetLineRange(std::size_t line_num) const; // (start, end) byte offsets
|
||||||
|
|
||||||
|
// NEW: Position conversion
|
||||||
|
std::pair<std::size_t, std::size_t> ByteOffsetToLineCol(std::size_t byte_offset) const;
|
||||||
|
std::size_t LineColToByteOffset(std::size_t row, std::size_t col) const;
|
||||||
|
|
||||||
|
// NEW: Substring extraction
|
||||||
|
std::string GetRange(std::size_t byte_offset, std::size_t len) const;
|
||||||
|
|
||||||
|
// NEW: Search support
|
||||||
|
std::size_t Find(const std::string &needle, std::size_t start_offset) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Existing members
|
||||||
|
std::string original_;
|
||||||
|
std::string add_;
|
||||||
|
std::vector<Piece> pieces_;
|
||||||
|
mutable std::string materialized_;
|
||||||
|
mutable bool dirty_;
|
||||||
|
std::size_t total_size_;
|
||||||
|
|
||||||
|
// NEW: Line index for efficient line operations
|
||||||
|
struct LineInfo {
|
||||||
|
std::size_t byte_offset; // absolute byte offset from buffer start
|
||||||
|
std::size_t piece_idx; // which piece contains line start
|
||||||
|
std::size_t offset_in_piece; // byte offset within that piece
|
||||||
|
};
|
||||||
|
mutable std::vector<LineInfo> line_index_;
|
||||||
|
mutable bool line_index_dirty_;
|
||||||
|
|
||||||
|
// NEW: Line index management
|
||||||
|
void RebuildLineIndex() const;
|
||||||
|
void InvalidateLineIndex();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Buffer API Changes
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Buffer {
|
||||||
|
public:
|
||||||
|
// NEW: Direct content access
|
||||||
|
PieceTable &Content() { return content_; }
|
||||||
|
const PieceTable &Content() const { return content_; }
|
||||||
|
|
||||||
|
// MODIFIED: Keep existing API but implement via PieceTable
|
||||||
|
void insert_text(int row, int col, std::string_view text);
|
||||||
|
void delete_text(int row, int col, std::size_t len);
|
||||||
|
void split_line(int row, int col);
|
||||||
|
void join_lines(int row);
|
||||||
|
void insert_row(int row, std::string_view text);
|
||||||
|
void delete_row(int row);
|
||||||
|
|
||||||
|
// MODIFIED: Line access - return line from PieceTable
|
||||||
|
std::size_t Nrows() const { return content_.LineCount(); }
|
||||||
|
std::string GetLine(std::size_t row) const { return content_.GetLine(row); }
|
||||||
|
|
||||||
|
// REMOVED: Rows() - no longer have vector of Lines
|
||||||
|
// std::vector<Line> &Rows(); // REMOVE
|
||||||
|
|
||||||
|
private:
|
||||||
|
// REMOVED: std::vector<Line> rows_;
|
||||||
|
// NEW: Single piece table for all content
|
||||||
|
PieceTable content_;
|
||||||
|
|
||||||
|
// Keep existing members
|
||||||
|
std::size_t curx_, cury_, rx_;
|
||||||
|
std::size_t nrows_; // cached from content_.LineCount()
|
||||||
|
std::size_t rowoffs_, coloffs_;
|
||||||
|
std::string filename_;
|
||||||
|
bool is_file_backed_;
|
||||||
|
bool dirty_;
|
||||||
|
bool read_only_;
|
||||||
|
bool mark_set_;
|
||||||
|
std::size_t mark_curx_, mark_cury_;
|
||||||
|
std::unique_ptr<UndoTree> undo_tree_;
|
||||||
|
std::unique_ptr<UndoSystem> undo_sys_;
|
||||||
|
std::uint64_t version_;
|
||||||
|
bool syntax_enabled_;
|
||||||
|
std::string filetype_;
|
||||||
|
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||||
|
kte::SwapRecorder *swap_rec_;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Phases
|
||||||
|
|
||||||
|
### Phase 1: Extend PieceTable (Foundation)
|
||||||
|
|
||||||
|
**Goal:** Add buffer-wide capabilities to PieceTable without breaking
|
||||||
|
existing per-line usage.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
|
||||||
|
1. Add line indexing infrastructure to PieceTable
|
||||||
|
- Add `LineInfo` struct and `line_index_` member
|
||||||
|
- Implement `RebuildLineIndex()` that scans pieces for '\n'
|
||||||
|
characters
|
||||||
|
- Implement `InvalidateLineIndex()` called by Insert/Delete
|
||||||
|
|
||||||
|
2. Implement core byte-based operations
|
||||||
|
- `Insert(byte_offset, text, len)` - split piece at offset, insert
|
||||||
|
new piece
|
||||||
|
- `Delete(byte_offset, len)` - split pieces, remove/truncate as
|
||||||
|
needed
|
||||||
|
|
||||||
|
3. Implement line-based query methods
|
||||||
|
- `LineCount()` - return line_index_.size()
|
||||||
|
- `GetLine(line_num)` - extract text between line boundaries
|
||||||
|
- `GetLineRange(line_num)` - return (start, end) byte offsets
|
||||||
|
|
||||||
|
4. Implement position conversion
|
||||||
|
- `ByteOffsetToLineCol(offset)` - binary search in line_index_
|
||||||
|
- `LineColToByteOffset(row, col)` - lookup line start, add col
|
||||||
|
|
||||||
|
5. Implement utility methods
|
||||||
|
- `GetRange(offset, len)` - extract substring
|
||||||
|
- `Find(needle, start)` - search across pieces
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
|
||||||
|
- Write unit tests for new PieceTable methods
|
||||||
|
- Test with multi-line content
|
||||||
|
- Verify line index correctness after edits
|
||||||
|
- Benchmark performance vs current line-based approach
|
||||||
|
|
||||||
|
**Estimated Effort:** 3-5 days
|
||||||
|
|
||||||
|
### Phase 2: Create Buffer Adapter Layer (Compatibility)
|
||||||
|
|
||||||
|
**Goal:** Create compatibility layer in Buffer to use PieceTable while
|
||||||
|
maintaining existing API.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
|
||||||
|
1. Add `PieceTable content_` member to Buffer (alongside existing
|
||||||
|
`rows_`)
|
||||||
|
2. Add compilation flag `KTE_USE_BUFFER_PIECE_TABLE` (like existing
|
||||||
|
`KTE_USE_PIECE_TABLE`)
|
||||||
|
3. Implement Buffer methods to delegate to content_:
|
||||||
|
```cpp
|
||||||
|
#ifdef KTE_USE_BUFFER_PIECE_TABLE
|
||||||
|
void insert_text(int row, int col, std::string_view text) {
|
||||||
|
std::size_t offset = content_.LineColToByteOffset(row, col);
|
||||||
|
content_.Insert(offset, text.data(), text.size());
|
||||||
|
}
|
||||||
|
// ... similar for other methods
|
||||||
|
#else
|
||||||
|
// Existing line-based implementation
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
4. Update file I/O to work with PieceTable
|
||||||
|
- `OpenFromFile()` - load into content_ instead of rows_
|
||||||
|
- `Save()` - serialize content_ instead of rows_
|
||||||
|
5. Update `AsString()` to materialize from content_
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
|
||||||
|
- Run existing buffer correctness tests with new flag
|
||||||
|
- Verify undo/redo still works
|
||||||
|
- Test file I/O round-tripping
|
||||||
|
- Test with existing command operations
|
||||||
|
|
||||||
|
**Estimated Effort:** 3-4 days
|
||||||
|
|
||||||
|
### Phase 3: Migrate Command Layer (High-level Operations)
|
||||||
|
|
||||||
|
**Goal:** Update commands that directly access Rows() to use new API.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
|
||||||
|
1. Audit all usages of `buf.Rows()` in Command.cc
|
||||||
|
2. Refactor helper functions:
|
||||||
|
- `extract_region_text()` - use content_.GetRange()
|
||||||
|
- `delete_region()` - convert to byte offsets, use content_.Delete()
|
||||||
|
- `insert_text_at_cursor()` - convert position, use content_
|
||||||
|
.Insert()
|
||||||
|
3. Update commands that iterate over lines:
|
||||||
|
- Use `buf.GetLine(i)` instead of `buf.Rows()[i]`
|
||||||
|
- Update line count queries to use `buf.Nrows()`
|
||||||
|
4. Update search/replace operations:
|
||||||
|
- Modify `search_compute_matches()` to work with GetLine()
|
||||||
|
- Update regex matching to work line-by-line or use content directly
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
|
||||||
|
- Test all editing commands (insert, delete, newline, backspace)
|
||||||
|
- Test region operations (mark, copy, kill)
|
||||||
|
- Test search and replace
|
||||||
|
- Test word navigation and deletion
|
||||||
|
- Run through common editing workflows
|
||||||
|
|
||||||
|
**Estimated Effort:** 4-6 days
|
||||||
|
|
||||||
|
### Phase 4: Update Renderer and Frontend (Display)
|
||||||
|
|
||||||
|
**Goal:** Ensure all renderers work with new Buffer structure.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
|
||||||
|
1. Audit renderer implementations:
|
||||||
|
- `TerminalRenderer.cc`
|
||||||
|
- `ImGuiRenderer.cc`
|
||||||
|
- `QtRenderer.cc`
|
||||||
|
- `TestRenderer.cc`
|
||||||
|
2. Update line access patterns:
|
||||||
|
- Replace `buf.Rows()[y]` with `buf.GetLine(y)`
|
||||||
|
- Handle string return instead of Line object
|
||||||
|
3. Update syntax highlighting integration:
|
||||||
|
- Ensure HighlighterEngine works with GetLine()
|
||||||
|
- Update any line-based caching
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
|
||||||
|
- Test rendering in terminal
|
||||||
|
- Test ImGui frontend (if enabled)
|
||||||
|
- Test Qt frontend (if enabled)
|
||||||
|
- Verify syntax highlighting displays correctly
|
||||||
|
- Test scrolling and viewport updates
|
||||||
|
|
||||||
|
**Estimated Effort:** 2-3 days
|
||||||
|
|
||||||
|
### Phase 5: Remove Old Infrastructure (Cleanup) ✅ COMPLETED
|
||||||
|
|
||||||
|
**Goal:** Remove GapBuffer, AppendBuffer, and Line class completely.
|
||||||
|
|
||||||
|
**Status:** Completed on 2025-12-05
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
|
||||||
|
1. ✅ Remove conditional compilation:
|
||||||
|
- Removed `#ifdef KTE_USE_BUFFER_PIECE_TABLE` (PieceTable is now the
|
||||||
|
only way)
|
||||||
|
- Removed `#ifdef KTE_USE_PIECE_TABLE`
|
||||||
|
- Removed `AppendBuffer.h`
|
||||||
|
2. ✅ Delete obsolete code:
|
||||||
|
- Deleted `GapBuffer.h/cc`
|
||||||
|
- Line class now uses PieceTable internally (kept for API
|
||||||
|
compatibility)
|
||||||
|
- `rows_` kept as mutable cache rebuilt from `content_` PieceTable
|
||||||
|
3. ✅ Update CMakeLists.txt:
|
||||||
|
- Removed GapBuffer from sources
|
||||||
|
- Removed AppendBuffer.h from headers
|
||||||
|
- Removed KTE_USE_PIECE_TABLE and KTE_USE_BUFFER_PIECE_TABLE options
|
||||||
|
4. ✅ Clean up includes and dependencies
|
||||||
|
5. ✅ Update documentation
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
|
||||||
|
- Full regression test suite
|
||||||
|
- Verify clean compilation
|
||||||
|
- Check for any lingering references
|
||||||
|
|
||||||
|
**Estimated Effort:** 1-2 days
|
||||||
|
|
||||||
|
### Phase 6: Performance Optimization (Polish)
|
||||||
|
|
||||||
|
**Goal:** Optimize the new implementation for real-world usage.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
|
||||||
|
1. Profile common operations:
|
||||||
|
- Measure line access patterns
|
||||||
|
- Identify hot paths in editing
|
||||||
|
- Benchmark against old implementation
|
||||||
|
2. Optimize line index:
|
||||||
|
- Consider incremental updates instead of full rebuild
|
||||||
|
- Tune rebuild threshold
|
||||||
|
- Cache frequently accessed lines
|
||||||
|
3. Optimize piece table:
|
||||||
|
- Tune piece coalescing heuristics
|
||||||
|
- Consider piece count limits and consolidation
|
||||||
|
4. Memory optimization:
|
||||||
|
- Review materialization frequency
|
||||||
|
- Consider lazy materialization strategies
|
||||||
|
- Profile memory usage on large files
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
|
||||||
|
- Benchmark suite with various file sizes
|
||||||
|
- Memory profiling
|
||||||
|
- Real-world usage testing
|
||||||
|
|
||||||
|
**Estimated Effort:** 3-5 days
|
||||||
|
|
||||||
|
## Files Requiring Modification
|
||||||
|
|
||||||
|
### Core Files (Must Change)
|
||||||
|
|
||||||
|
- `PieceTable.h/cc` - Add new methods (Phase 1)
|
||||||
|
- `Buffer.h/cc` - Replace rows_ with content_ (Phase 2)
|
||||||
|
- `Command.cc` - Update line access (Phase 3)
|
||||||
|
- `UndoSystem.cc` - May need updates for new Buffer API
|
||||||
|
|
||||||
|
### Renderer Files (Will Change)
|
||||||
|
|
||||||
|
- `TerminalRenderer.cc` - Update line access (Phase 4)
|
||||||
|
- `ImGuiRenderer.cc` - Update line access (Phase 4)
|
||||||
|
- `QtRenderer.cc` - Update line access (Phase 4)
|
||||||
|
- `TestRenderer.cc` - Update line access (Phase 4)
|
||||||
|
|
||||||
|
### Files Removed (Phase 5 - Completed)
|
||||||
|
|
||||||
|
- `GapBuffer.h/cc` - ✅ Deleted
|
||||||
|
- `AppendBuffer.h` - ✅ Deleted
|
||||||
|
- `test_buffer_correctness.cc` - ✅ Deleted (obsolete GapBuffer
|
||||||
|
comparison test)
|
||||||
|
- `bench/BufferBench.cc` - ✅ Deleted (obsolete GapBuffer benchmarks)
|
||||||
|
- `bench/PerformanceSuite.cc` - ✅ Deleted (obsolete GapBuffer
|
||||||
|
benchmarks)
|
||||||
|
- `Buffer::Line` class - ✅ Updated to use PieceTable internally (kept
|
||||||
|
for API compatibility)
|
||||||
|
|
||||||
|
### Build Files
|
||||||
|
|
||||||
|
- `CMakeLists.txt` - Update sources (Phase 5)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- `README.md` - Update architecture notes
|
||||||
|
- `docs/` - Update any architectural documentation
|
||||||
|
- `REWRITE.md` - Note C++ now matches Rust design
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
- **PieceTable Tests:** New file `test_piece_table.cc`
|
||||||
|
- Test Insert/Delete at various positions
|
||||||
|
- Test line indexing correctness
|
||||||
|
- Test position conversion
|
||||||
|
- Test with edge cases (empty, single line, large files)
|
||||||
|
|
||||||
|
- **Buffer Tests:** Extend `test_buffer_correctness.cc`
|
||||||
|
- Test new Buffer API with PieceTable backend
|
||||||
|
- Test file I/O round-tripping
|
||||||
|
- Test multi-line operations
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
- **Undo Tests:** `test_undo.cc` should still pass
|
||||||
|
- Verify undo/redo across all operation types
|
||||||
|
- Test undo tree navigation
|
||||||
|
|
||||||
|
- **Search Tests:** `test_search_correctness.cc` should still pass
|
||||||
|
- Verify search across multiple lines
|
||||||
|
- Test regex search
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
|
||||||
|
- Load and edit large files (>10MB)
|
||||||
|
- Perform complex editing sequences
|
||||||
|
- Test all keybindings and commands
|
||||||
|
- Verify syntax highlighting
|
||||||
|
- Test crash recovery (swap files)
|
||||||
|
|
||||||
|
### Regression Testing
|
||||||
|
|
||||||
|
- All existing tests must pass with new implementation
|
||||||
|
- No observable behavior changes for users
|
||||||
|
- Performance should be comparable or better
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### High Risk
|
||||||
|
|
||||||
|
- **Undo System Integration:** Undo records operations with
|
||||||
|
row/col/text. Need to ensure compatibility or refactor.
|
||||||
|
- *Mitigation:* Carefully preserve undo semantics, extensive testing
|
||||||
|
|
||||||
|
- **Performance Regression:** Line index rebuilding could be expensive
|
||||||
|
on large files.
|
||||||
|
- *Mitigation:* Profile early, optimize incrementally, consider
|
||||||
|
caching strategies
|
||||||
|
|
||||||
|
### Medium Risk
|
||||||
|
|
||||||
|
- **Syntax Highlighting:** Highlighters may depend on line-based access
|
||||||
|
patterns.
|
||||||
|
- *Mitigation:* Review highlighter integration, test thoroughly
|
||||||
|
|
||||||
|
- **Renderer Updates:** Multiple renderers need updating, risk of
|
||||||
|
inconsistency.
|
||||||
|
- *Mitigation:* Update all renderers in same phase, test each
|
||||||
|
|
||||||
|
### Low Risk
|
||||||
|
|
||||||
|
- **Search/Replace:** Should work naturally with new GetLine() API.
|
||||||
|
- *Mitigation:* Test thoroughly with existing test suite
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- ✓ All existing tests pass
|
||||||
|
- ✓ All commands work identically to before
|
||||||
|
- ✓ File I/O works correctly
|
||||||
|
- ✓ Undo/redo functionality preserved
|
||||||
|
- ✓ Syntax highlighting works
|
||||||
|
- ✓ All frontends (terminal, ImGui, Qt) work
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- ✓ GapBuffer completely removed
|
||||||
|
- ✓ No conditional compilation for buffer type
|
||||||
|
- ✓ Clean, maintainable code
|
||||||
|
- ✓ Good test coverage for new PieceTable methods
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- ✓ Editing operations at least as fast as current
|
||||||
|
- ✓ Line access within 2x of current performance
|
||||||
|
- ✓ Memory usage reasonable (no excessive materialization)
|
||||||
|
- ✓ Large file handling acceptable (tested up to 100MB)
|
||||||
|
|
||||||
|
## Timeline Estimate
|
||||||
|
|
||||||
|
| Phase | Duration | Dependencies |
|
||||||
|
|----------------------------|----------------|--------------|
|
||||||
|
| Phase 1: Extend PieceTable | 3-5 days | None |
|
||||||
|
| Phase 2: Buffer Adapter | 3-4 days | Phase 1 |
|
||||||
|
| Phase 3: Command Layer | 4-6 days | Phase 2 |
|
||||||
|
| Phase 4: Renderer Updates | 2-3 days | Phase 3 |
|
||||||
|
| Phase 5: Cleanup | 1-2 days | Phase 4 |
|
||||||
|
| Phase 6: Optimization | 3-5 days | Phase 5 |
|
||||||
|
| **Total** | **16-25 days** | |
|
||||||
|
|
||||||
|
**Note:** Timeline assumes one developer working full-time. Actual
|
||||||
|
duration may vary based on:
|
||||||
|
|
||||||
|
- Unforeseen integration issues
|
||||||
|
- Performance optimization needs
|
||||||
|
- Testing thoroughness
|
||||||
|
- Code review iterations
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Alternative 1: Keep Line-based but unify GapBuffer/PieceTable
|
||||||
|
|
||||||
|
- Keep vector of Lines, but make each Line always use PieceTable
|
||||||
|
- Remove GapBuffer, remove AppendBuffer selector
|
||||||
|
- **Pros:** Smaller change, less risk
|
||||||
|
- **Cons:** Doesn't achieve architectural goal, still have per-line
|
||||||
|
overhead
|
||||||
|
|
||||||
|
### Alternative 2: Hybrid approach
|
||||||
|
|
||||||
|
- Use PieceTable for buffer, but maintain materialized Line objects as
|
||||||
|
cache
|
||||||
|
- **Pros:** Easier migration, maintains some compatibility
|
||||||
|
- **Cons:** Complex dual representation, cache invalidation issues
|
||||||
|
|
||||||
|
### Alternative 3: Complete rewrite
|
||||||
|
|
||||||
|
- Follow REWRITE.md exactly, implement in Rust
|
||||||
|
- **Pros:** Modern language, better architecture
|
||||||
|
- **Cons:** Much larger effort, different project
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
**Proceed with planned migration** (single PieceTable per Buffer)
|
||||||
|
because:
|
||||||
|
|
||||||
|
1. Aligns with long-term architecture vision (REWRITE.md)
|
||||||
|
2. Removes unnecessary per-line buffer overhead
|
||||||
|
3. Simplifies codebase (one text representation)
|
||||||
|
4. Enables future optimizations (better undo, swap files, etc.)
|
||||||
|
5. Reasonable effort (16-25 days) for significant improvement
|
||||||
|
|
||||||
|
**Suggested Approach:**
|
||||||
|
|
||||||
|
- Start with Phase 1 (extend PieceTable) in isolated branch
|
||||||
|
- Thoroughly test new PieceTable functionality
|
||||||
|
- Proceed incrementally through phases
|
||||||
|
- Maintain working editor at end of each phase
|
||||||
|
- Merge to main after Phase 4 (before cleanup) to get testing
|
||||||
|
- Complete Phase 5-6 based on feedback
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `REWRITE.md` - Rust architecture specification (lines 54-157)
|
||||||
|
- Current buffer implementation: `Buffer.h/cc`
|
||||||
|
- Current piece table: `PieceTable.h/cc`
|
||||||
|
- Undo system: `UndoSystem.h/cc`, `UndoNode.h`
|
||||||
|
- Commands: `Command.cc`
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
packages = eachSystem (system: rec {
|
packages = eachSystem (system: rec {
|
||||||
default = kte;
|
default = kte;
|
||||||
full = kge;
|
full = kge;
|
||||||
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
|
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; graphical-qt = false; };
|
||||||
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
|
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = false; };
|
||||||
qt = (pkgsFor system).callPackage ./default.nix { graphical-qt = true; }
|
qt = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = true; };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,21 @@ InstallDefaultFonts()
|
|||||||
));
|
));
|
||||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
"brassmono",
|
"brassmono",
|
||||||
BrassMono::DefaultFontBoldCompressedData,
|
BrassMono::DefaultFontRegularCompressedData,
|
||||||
BrassMono::DefaultFontBoldCompressedSize
|
BrassMono::DefaultFontRegularCompressedSize
|
||||||
));
|
));
|
||||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
"brassmonocode",
|
"brassmono-bold",
|
||||||
|
BrassMono::DefaultFontBoldCompressedData,
|
||||||
|
BrassMono::DefaultFontBoldCompressedSize
|
||||||
|
));
|
||||||
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
|
"brassmonocode",
|
||||||
|
BrassMonoCode::DefaultFontRegularCompressedData,
|
||||||
|
BrassMonoCode::DefaultFontRegularCompressedSize
|
||||||
|
));
|
||||||
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
|
"brassmonocode-bold",
|
||||||
BrassMonoCode::DefaultFontBoldCompressedData,
|
BrassMonoCode::DefaultFontBoldCompressedData,
|
||||||
BrassMonoCode::DefaultFontBoldCompressedSize
|
BrassMonoCode::DefaultFontBoldCompressedSize
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ make clean
|
|||||||
rm -fr kge.app* kge-qt.app*
|
rm -fr kge.app* kge-qt.app*
|
||||||
make
|
make
|
||||||
mv kge.app kge-qt.app
|
mv kge.app kge-qt.app
|
||||||
|
macdeployqt kge-qt.app -always-overwrite
|
||||||
zip -r kge-qt.app.zip kge-qt.app
|
zip -r kge-qt.app.zip kge-qt.app
|
||||||
sha256sum kge-qt.app.zip
|
sha256sum kge-qt.app.zip
|
||||||
open .
|
open .
|
||||||
|
|||||||
@@ -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