Add UndoSystem implementation and refactor UndoNode for simplicity.
This commit is contained in:
8
.idea/kte.iml
generated
8
.idea/kte.iml
generated
@@ -1,2 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module classpath="CMake" type="CPP_MODULE" version="4" />
|
||||
<module classpath="CMake" type="CPP_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="Python" name="Python facet">
|
||||
<configuration sdkName="Python 3.14 (kte)" />
|
||||
</facet>
|
||||
</component>
|
||||
</module>
|
||||
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.14 (kte)" />
|
||||
</component>
|
||||
<component name="CMakePythonSetting">
|
||||
<option name="pythonIntegrationState" value="YES" />
|
||||
</component>
|
||||
|
||||
61
.idea/workspace.xml
generated
61
.idea/workspace.xml
generated
@@ -23,8 +23,6 @@
|
||||
<component name="CMakeRunConfigurationManager">
|
||||
<generated>
|
||||
<config projectName="kte" targetName="kte" />
|
||||
<config projectName="kte" targetName="imgui" />
|
||||
<config projectName="kte" targetName="kge" />
|
||||
</generated>
|
||||
</component>
|
||||
<component name="CMakeSettings" AUTO_RELOAD="true">
|
||||
@@ -33,9 +31,27 @@
|
||||
</configurations>
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation.">
|
||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Handle end-of-file newline semantics and improve scroll alignment logic.">
|
||||
<change afterPath="$PROJECT_DIR$/UndoSystem.cc" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/UndoSystem.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/kte.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/kte.iml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/UndoNode.cc" beforeDir="false" afterPath="$PROJECT_DIR$/UndoNode.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/UndoNode.h" beforeDir="false" afterPath="$PROJECT_DIR$/UndoNode.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/UndoTree.cc" beforeDir="false" afterPath="$PROJECT_DIR$/UndoTree.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/UndoTree.h" beforeDir="false" afterPath="$PROJECT_DIR$/UndoTree.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/cmake/packaging.cmake" beforeDir="false" afterPath="$PROJECT_DIR$/cmake/packaging.cmake" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/fonts/b612_mono.h" beforeDir="false" afterPath="$PROJECT_DIR$/fonts/b612_mono.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/fonts/brassmono.h" beforeDir="false" afterPath="$PROJECT_DIR$/fonts/brassmono.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/main.cc" beforeDir="false" afterPath="$PROJECT_DIR$/main.cc" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -103,37 +119,27 @@
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
|
||||
"rearrange.code.on.save": "true",
|
||||
"settings.editor.selected.configurable": "CMakeSettings",
|
||||
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunManager" selected="CMake Application.kge">
|
||||
<component name="RecentsManager">
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/docs" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="imgui" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="imgui" CONFIG_NAME="Debug">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<list>
|
||||
<item itemvalue="CMake Application.imgui" />
|
||||
<item itemvalue="CMake Application.kge" />
|
||||
<item itemvalue="CMake Application.kte" />
|
||||
</list>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
@@ -142,7 +148,7 @@
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1764457173148</updated>
|
||||
<workItem from="1764457174208" duration="27972000" />
|
||||
<workItem from="1764457174208" duration="31043000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||
<option name="closed" value="true" />
|
||||
@@ -160,7 +166,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764486011231</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="3" />
|
||||
<task id="LOCAL-00003" summary="Handle end-of-file newline semantics and improve scroll alignment logic.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764486876984</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764486876984</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -175,7 +189,8 @@
|
||||
<MESSAGE value="Refactoring" />
|
||||
<MESSAGE value="Add undo/redo infrastructure and buffer management additions." />
|
||||
<MESSAGE value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation." />
|
||||
<MESSAGE value="Handle end-of-file newline semantics and improve scroll alignment logic." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Handle end-of-file newline semantics and improve scroll alignment logic." />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
|
||||
346
Buffer.cc
346
Buffer.cc
@@ -1,11 +1,18 @@
|
||||
#include "Buffer.h"
|
||||
#include "UndoSystem.h"
|
||||
#include "UndoTree.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
|
||||
|
||||
Buffer::Buffer() = default;
|
||||
Buffer::Buffer()
|
||||
{
|
||||
// Initialize undo system per buffer
|
||||
undo_tree_ = std::make_unique<UndoTree>();
|
||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||
}
|
||||
|
||||
|
||||
Buffer::Buffer(const std::string &path)
|
||||
@@ -15,78 +22,133 @@ Buffer::Buffer(const std::string &path)
|
||||
}
|
||||
|
||||
|
||||
// Copy constructor/assignment: perform a deep copy of core fields; reinitialize undo for the new buffer.
|
||||
Buffer::Buffer(const Buffer &other)
|
||||
{
|
||||
curx_ = other.curx_;
|
||||
cury_ = other.cury_;
|
||||
rx_ = other.rx_;
|
||||
nrows_ = other.nrows_;
|
||||
rowoffs_ = other.rowoffs_;
|
||||
coloffs_ = other.coloffs_;
|
||||
rows_ = other.rows_;
|
||||
filename_ = other.filename_;
|
||||
is_file_backed_ = other.is_file_backed_;
|
||||
dirty_ = other.dirty_;
|
||||
mark_set_ = other.mark_set_;
|
||||
mark_curx_ = other.mark_curx_;
|
||||
mark_cury_ = other.mark_cury_;
|
||||
// Fresh undo system for the copy
|
||||
undo_tree_ = std::make_unique<UndoTree>();
|
||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||
}
|
||||
|
||||
|
||||
Buffer &
|
||||
Buffer::operator=(const Buffer &other)
|
||||
{
|
||||
if (this == &other)
|
||||
return *this;
|
||||
curx_ = other.curx_;
|
||||
cury_ = other.cury_;
|
||||
rx_ = other.rx_;
|
||||
nrows_ = other.nrows_;
|
||||
rowoffs_ = other.rowoffs_;
|
||||
coloffs_ = other.coloffs_;
|
||||
rows_ = other.rows_;
|
||||
filename_ = other.filename_;
|
||||
is_file_backed_ = other.is_file_backed_;
|
||||
dirty_ = other.dirty_;
|
||||
mark_set_ = other.mark_set_;
|
||||
mark_curx_ = other.mark_curx_;
|
||||
mark_cury_ = other.mark_cury_;
|
||||
// Recreate undo system for this instance
|
||||
undo_tree_ = std::make_unique<UndoTree>();
|
||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Buffer::OpenFromFile(const std::string &path, std::string &err)
|
||||
{
|
||||
// If the file doesn't exist, initialize an empty, non-file-backed buffer
|
||||
// with the provided filename. Do not touch the filesystem until Save/SaveAs.
|
||||
if (!std::filesystem::exists(path)) {
|
||||
rows_.clear();
|
||||
nrows_ = 0;
|
||||
filename_ = path;
|
||||
is_file_backed_ = false;
|
||||
dirty_ = false;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
rows_.clear();
|
||||
nrows_ = 0;
|
||||
filename_ = path;
|
||||
is_file_backed_ = false;
|
||||
dirty_ = false;
|
||||
|
||||
// Reset cursor/viewport state
|
||||
curx_ = cury_ = rx_ = 0;
|
||||
rowoffs_ = coloffs_ = 0;
|
||||
mark_set_ = false;
|
||||
mark_curx_ = mark_cury_ = 0;
|
||||
// Reset cursor/viewport state
|
||||
curx_ = cury_ = rx_ = 0;
|
||||
rowoffs_ = coloffs_ = 0;
|
||||
mark_set_ = false;
|
||||
mark_curx_ = mark_cury_ = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::ifstream in(path, std::ios::in | std::ios::binary);
|
||||
if (!in) {
|
||||
err = "Failed to open file: " + path;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect if file ends with a newline so we can preserve a final empty line
|
||||
// in our in-memory representation (mg-style semantics).
|
||||
bool ends_with_nl = false;
|
||||
{
|
||||
in.seekg(0, std::ios::end);
|
||||
std::streamoff sz = in.tellg();
|
||||
if (sz > 0) {
|
||||
in.seekg(-1, std::ios::end);
|
||||
char last = 0;
|
||||
in.read(&last, 1);
|
||||
ends_with_nl = (last == '\n');
|
||||
} else {
|
||||
in.clear();
|
||||
}
|
||||
// Rewind to start for line-by-line read
|
||||
in.clear();
|
||||
in.seekg(0, std::ios::beg);
|
||||
}
|
||||
|
||||
rows_.clear();
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
// std::getline strips the '\n', keep raw line content only
|
||||
// Handle potential Windows CRLF: strip trailing '\r'
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
rows_.emplace_back(line);
|
||||
}
|
||||
// Detect if file ends with a newline so we can preserve a final empty line
|
||||
// in our in-memory representation (mg-style semantics).
|
||||
bool ends_with_nl = false;
|
||||
{
|
||||
in.seekg(0, std::ios::end);
|
||||
std::streamoff sz = in.tellg();
|
||||
if (sz > 0) {
|
||||
in.seekg(-1, std::ios::end);
|
||||
char last = 0;
|
||||
in.read(&last, 1);
|
||||
ends_with_nl = (last == '\n');
|
||||
} else {
|
||||
in.clear();
|
||||
}
|
||||
// Rewind to start for line-by-line read
|
||||
in.clear();
|
||||
in.seekg(0, std::ios::beg);
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
rows_.clear();
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
// std::getline strips the '\n', keep raw line content only
|
||||
// Handle potential Windows CRLF: strip trailing '\r'
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
rows_.emplace_back(line);
|
||||
}
|
||||
|
||||
nrows_ = rows_.size();
|
||||
// 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_ = path;
|
||||
is_file_backed_ = true;
|
||||
dirty_ = false;
|
||||
|
||||
// Reset/initialize undo system for this loaded file
|
||||
if (!undo_tree_)
|
||||
undo_tree_ = std::make_unique<UndoTree>();
|
||||
if (!undo_sys_)
|
||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||
// Clear any existing history for a fresh load
|
||||
undo_sys_->clear();
|
||||
|
||||
// Reset cursor/viewport state
|
||||
curx_ = cury_ = rx_ = 0;
|
||||
rowoffs_ = coloffs_ = 0;
|
||||
@@ -109,14 +171,15 @@ Buffer::Save(std::string &err) const
|
||||
err = "Failed to open for write: " + filename_;
|
||||
return false;
|
||||
}
|
||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||
const char *d = rows_[i].Data();
|
||||
std::size_t n = rows_[i].Size();
|
||||
if (d && n) out.write(d, static_cast<std::streamsize>(n));
|
||||
if (i + 1 < rows_.size()) {
|
||||
out.put('\n');
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||
const char *d = rows_[i].Data();
|
||||
std::size_t n = rows_[i].Size();
|
||||
if (d && n)
|
||||
out.write(d, static_cast<std::streamsize>(n));
|
||||
if (i + 1 < rows_.size()) {
|
||||
out.put('\n');
|
||||
}
|
||||
}
|
||||
if (!out.good()) {
|
||||
err = "Write error";
|
||||
return false;
|
||||
@@ -136,14 +199,15 @@ Buffer::SaveAs(const std::string &path, std::string &err)
|
||||
err = "Failed to open for write: " + path;
|
||||
return false;
|
||||
}
|
||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||
const char *d = rows_[i].Data();
|
||||
std::size_t n = rows_[i].Size();
|
||||
if (d && n) out.write(d, static_cast<std::streamsize>(n));
|
||||
if (i + 1 < rows_.size()) {
|
||||
out.put('\n');
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||
const char *d = rows_[i].Data();
|
||||
std::size_t n = rows_[i].Size();
|
||||
if (d && n)
|
||||
out.write(d, static_cast<std::streamsize>(n));
|
||||
if (i + 1 < rows_.size()) {
|
||||
out.put('\n');
|
||||
}
|
||||
}
|
||||
if (!out.good()) {
|
||||
err = "Write error";
|
||||
return false;
|
||||
@@ -167,3 +231,147 @@ Buffer::AsString() const
|
||||
ss << ">: " << rows_.size() << " lines";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
|
||||
// --- Raw editing APIs (no undo recording, cursor untouched) ---
|
||||
void
|
||||
Buffer::insert_text(int row, int col, std::string_view text)
|
||||
{
|
||||
if (row < 0)
|
||||
row = 0;
|
||||
if (static_cast<std::size_t>(row) > rows_.size())
|
||||
row = static_cast<int>(rows_.size());
|
||||
if (rows_.empty())
|
||||
rows_.emplace_back("");
|
||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
||||
rows_.emplace_back("");
|
||||
|
||||
std::size_t y = static_cast<std::size_t>(row);
|
||||
std::size_t 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), tail);
|
||||
y += 1;
|
||||
x = 0;
|
||||
remain.erase(0, pos + 1);
|
||||
}
|
||||
// Do not set dirty here; UndoSystem will manage state/dirty externally
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Buffer::delete_text(int row, int col, std::size_t len)
|
||||
{
|
||||
if (rows_.empty() || len == 0)
|
||||
return;
|
||||
if (row < 0)
|
||||
row = 0;
|
||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
||||
return;
|
||||
std::size_t y = static_cast<std::size_t>(row);
|
||||
std::size_t x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
||||
|
||||
std::size_t remaining = len;
|
||||
while (remaining > 0 && y < rows_.size()) {
|
||||
auto &line = rows_[y];
|
||||
std::size_t in_line = std::min<std::size_t>(remaining, line.size() - std::min(x, line.size()));
|
||||
if (x < line.size() && in_line > 0) {
|
||||
line.erase(x, in_line);
|
||||
remaining -= in_line;
|
||||
}
|
||||
if (remaining == 0)
|
||||
break;
|
||||
// If at or beyond end of line and there is a next line, join it (deleting the implied '\n')
|
||||
if (y + 1 < rows_.size()) {
|
||||
line += rows_[y + 1];
|
||||
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
||||
// deleting the newline consumes one virtual character
|
||||
if (remaining > 0) {
|
||||
// Treat the newline as one deletion unit if len spans it
|
||||
// We already joined, so nothing else to do here.
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Buffer::split_line(int row, int col)
|
||||
{
|
||||
if (row < 0)
|
||||
row = 0;
|
||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
||||
rows_.resize(static_cast<std::size_t>(row) + 1);
|
||||
std::size_t y = static_cast<std::size_t>(row);
|
||||
std::size_t x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
||||
std::string tail = rows_[y].substr(x);
|
||||
rows_[y].erase(x);
|
||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Buffer::join_lines(int row)
|
||||
{
|
||||
if (row < 0)
|
||||
row = 0;
|
||||
std::size_t y = static_cast<std::size_t>(row);
|
||||
if (y + 1 >= rows_.size())
|
||||
return;
|
||||
rows_[y] += rows_[y + 1];
|
||||
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Buffer::insert_row(int row, std::string_view text)
|
||||
{
|
||||
if (row < 0)
|
||||
row = 0;
|
||||
if (static_cast<std::size_t>(row) > rows_.size())
|
||||
row = static_cast<int>(rows_.size());
|
||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(row), std::string(text));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Buffer::delete_row(int row)
|
||||
{
|
||||
if (row < 0)
|
||||
row = 0;
|
||||
if (static_cast<std::size_t>(row) >= rows_.size())
|
||||
return;
|
||||
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(row));
|
||||
}
|
||||
|
||||
|
||||
// Undo system accessors
|
||||
UndoSystem *
|
||||
Buffer::Undo()
|
||||
{
|
||||
return undo_sys_.get();
|
||||
}
|
||||
|
||||
|
||||
const UndoSystem *
|
||||
Buffer::Undo() const
|
||||
{
|
||||
return undo_sys_.get();
|
||||
}
|
||||
|
||||
108
Buffer.h
108
Buffer.h
@@ -7,13 +7,23 @@
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
|
||||
#include "AppendBuffer.h"
|
||||
#include "UndoSystem.h"
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
Buffer();
|
||||
|
||||
Buffer(const Buffer &other);
|
||||
|
||||
Buffer &operator=(const Buffer &other);
|
||||
|
||||
Buffer(Buffer &&) noexcept = default;
|
||||
|
||||
Buffer &operator=(Buffer &&) noexcept = default;
|
||||
|
||||
explicit Buffer(const std::string &path);
|
||||
|
||||
// File operations
|
||||
@@ -64,31 +74,59 @@ public:
|
||||
public:
|
||||
Line() = default;
|
||||
|
||||
|
||||
Line(const char *s)
|
||||
{
|
||||
assign_from(s ? std::string(s) : std::string());
|
||||
}
|
||||
|
||||
|
||||
Line(const std::string &s)
|
||||
{
|
||||
assign_from(s);
|
||||
}
|
||||
|
||||
|
||||
Line(const Line &other) = default;
|
||||
|
||||
Line &operator=(const Line &other) = default;
|
||||
|
||||
Line(Line &&other) noexcept = default;
|
||||
|
||||
Line &operator=(Line &&other) noexcept = default;
|
||||
|
||||
// capacity helpers
|
||||
void Clear() { buf_.Clear(); }
|
||||
void Clear()
|
||||
{
|
||||
buf_.Clear();
|
||||
}
|
||||
|
||||
|
||||
// size/access
|
||||
[[nodiscard]] std::size_t size() const { return buf_.Size(); }
|
||||
[[nodiscard]] bool empty() const { return size() == 0; }
|
||||
[[nodiscard]] std::size_t size() const
|
||||
{
|
||||
return buf_.Size();
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool empty() const
|
||||
{
|
||||
return size() == 0;
|
||||
}
|
||||
|
||||
|
||||
// read-only raw view
|
||||
[[nodiscard]] const char *Data() const { return buf_.Data(); }
|
||||
[[nodiscard]] std::size_t Size() const { return buf_.Size(); }
|
||||
[[nodiscard]] const char *Data() const
|
||||
{
|
||||
return buf_.Data();
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::size_t Size() const
|
||||
{
|
||||
return buf_.Size();
|
||||
}
|
||||
|
||||
|
||||
// element access (read-only)
|
||||
[[nodiscard]] char operator[](std::size_t i) const
|
||||
@@ -97,60 +135,77 @@ public:
|
||||
return (i < buf_.Size() && d) ? d[i] : '\0';
|
||||
}
|
||||
|
||||
|
||||
// conversions
|
||||
operator std::string() const { return std::string(buf_.Data() ? buf_.Data() : "", buf_.Size()); }
|
||||
operator std::string() const
|
||||
{
|
||||
return std::string(buf_.Data() ? buf_.Data() : "", buf_.Size());
|
||||
}
|
||||
|
||||
|
||||
// string-like API used by command/renderer layers (implemented via materialization for now)
|
||||
std::string substr(std::size_t pos) const
|
||||
{
|
||||
const std::size_t n = buf_.Size();
|
||||
if (pos >= n) return std::string();
|
||||
if (pos >= n)
|
||||
return std::string();
|
||||
return std::string(buf_.Data() + pos, n - pos);
|
||||
}
|
||||
|
||||
|
||||
std::string substr(std::size_t pos, std::size_t len) const
|
||||
{
|
||||
const std::size_t n = buf_.Size();
|
||||
if (pos >= n) return std::string();
|
||||
if (pos >= n)
|
||||
return std::string();
|
||||
const std::size_t take = (pos + len > n) ? (n - pos) : len;
|
||||
return std::string(buf_.Data() + pos, take);
|
||||
}
|
||||
|
||||
|
||||
void erase(std::size_t pos)
|
||||
{
|
||||
// erase to end
|
||||
material_edit([&](std::string &s) {
|
||||
if (pos < s.size()) s.erase(pos);
|
||||
if (pos < s.size())
|
||||
s.erase(pos);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void erase(std::size_t pos, std::size_t len)
|
||||
{
|
||||
material_edit([&](std::string &s) {
|
||||
if (pos < s.size()) s.erase(pos, len);
|
||||
if (pos < s.size())
|
||||
s.erase(pos, len);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void insert(std::size_t pos, const std::string &seg)
|
||||
{
|
||||
material_edit([&](std::string &s) {
|
||||
if (pos > s.size()) pos = s.size();
|
||||
if (pos > s.size())
|
||||
pos = s.size();
|
||||
s.insert(pos, seg);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Line &operator+=(const Line &other)
|
||||
{
|
||||
buf_.Append(other.buf_.Data(), other.buf_.Size());
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
Line &operator+=(const std::string &s)
|
||||
{
|
||||
buf_.Append(s.data(), s.size());
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
Line &operator=(const std::string &s)
|
||||
{
|
||||
assign_from(s);
|
||||
@@ -161,10 +216,12 @@ public:
|
||||
void assign_from(const std::string &s)
|
||||
{
|
||||
buf_.Clear();
|
||||
if (!s.empty()) buf_.Append(s.data(), s.size());
|
||||
if (!s.empty())
|
||||
buf_.Append(s.data(), s.size());
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
|
||||
template<typename F>
|
||||
void material_edit(F fn)
|
||||
{
|
||||
std::string tmp = static_cast<std::string>(*this);
|
||||
@@ -172,9 +229,11 @@ public:
|
||||
assign_from(tmp);
|
||||
}
|
||||
|
||||
|
||||
AppendBuffer buf_;
|
||||
};
|
||||
|
||||
|
||||
[[nodiscard]] const std::vector<Line> &Rows() const
|
||||
{
|
||||
return rows_;
|
||||
@@ -266,6 +325,25 @@ public:
|
||||
|
||||
[[nodiscard]] std::string AsString() const;
|
||||
|
||||
// Raw, low-level editing APIs used by UndoSystem apply().
|
||||
// These must NOT trigger undo recording. They also do not move the cursor.
|
||||
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);
|
||||
|
||||
// Undo system accessors (created per-buffer)
|
||||
UndoSystem *Undo();
|
||||
|
||||
const UndoSystem *Undo() const;
|
||||
|
||||
private:
|
||||
// State mirroring original C struct (without undo_tree)
|
||||
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
|
||||
@@ -278,6 +356,10 @@ private:
|
||||
bool dirty_ = false;
|
||||
bool mark_set_ = false;
|
||||
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
||||
|
||||
// Per-buffer undo state
|
||||
std::unique_ptr<struct UndoTree> undo_tree_;
|
||||
std::unique_ptr<UndoSystem> undo_sys_;
|
||||
};
|
||||
|
||||
#endif // KTE_BUFFER_H
|
||||
|
||||
@@ -55,8 +55,9 @@ set(COMMON_SOURCES
|
||||
TerminalInputHandler.cc
|
||||
TerminalRenderer.cc
|
||||
TerminalFrontend.cc
|
||||
# UndoNode.cc
|
||||
# UndoTree.cc
|
||||
UndoNode.cc
|
||||
UndoTree.cc
|
||||
UndoSystem.cc
|
||||
)
|
||||
|
||||
set(COMMON_HEADERS
|
||||
@@ -73,8 +74,9 @@ set(COMMON_HEADERS
|
||||
TerminalRenderer.h
|
||||
Frontend.h
|
||||
TerminalFrontend.h
|
||||
# UndoNode.h
|
||||
# UndoTree.h
|
||||
UndoNode.h
|
||||
UndoTree.h
|
||||
UndoSystem.h
|
||||
)
|
||||
|
||||
# kte (terminal-first) executable
|
||||
|
||||
81
Command.cc
81
Command.cc
@@ -3,6 +3,7 @@
|
||||
#include "Command.h"
|
||||
#include "Editor.h"
|
||||
#include "Buffer.h"
|
||||
#include "UndoSystem.h"
|
||||
// Note: Command layer must remain UI-agnostic. Do not include frontend/IO headers here.
|
||||
|
||||
|
||||
@@ -424,6 +425,8 @@ cmd_save(CommandContext &ctx)
|
||||
}
|
||||
buf->SetDirty(false);
|
||||
ctx.editor.SetStatus("Saved " + buf->Filename());
|
||||
if (auto *u = buf->Undo())
|
||||
u->mark_saved();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -446,6 +449,8 @@ cmd_save_as(CommandContext &ctx)
|
||||
return false;
|
||||
}
|
||||
ctx.editor.SetStatus("Saved as " + ctx.arg);
|
||||
if (auto *u = buf->Undo())
|
||||
u->mark_saved();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -691,8 +696,10 @@ cmd_buffer_close(CommandContext &ctx)
|
||||
if (ctx.editor.BufferCount() == 0)
|
||||
return true;
|
||||
std::size_t idx = ctx.editor.CurrentBufferIndex();
|
||||
const Buffer *b = ctx.editor.CurrentBuffer();
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
std::string name = b ? buffer_display_name(*b) : std::string("");
|
||||
if (b && b->Undo())
|
||||
b->Undo()->discard_pending();
|
||||
ctx.editor.CloseBuffer(idx);
|
||||
if (ctx.editor.BufferCount() == 0) {
|
||||
// Open a fresh empty buffer
|
||||
@@ -716,6 +723,11 @@ cmd_insert_text(CommandContext &ctx)
|
||||
ctx.editor.SetStatus("No buffer to edit");
|
||||
return false;
|
||||
}
|
||||
// Start/extend an insert batch for undo
|
||||
if (auto *u = buf->Undo()) {
|
||||
u->Begin(UndoType::Insert);
|
||||
u->Append(std::string_view(ctx.arg));
|
||||
}
|
||||
// If a prompt is active, edit prompt text
|
||||
if (ctx.editor.PromptActive()) {
|
||||
// Special-case: buffer switch prompt supports Tab-completion
|
||||
@@ -916,15 +928,19 @@ cmd_newline(CommandContext &ctx)
|
||||
ctx.editor.SetStatus("No buffer to edit");
|
||||
return false;
|
||||
}
|
||||
// Start a newline batch for undo at current cursor
|
||||
if (auto *u = buf->Undo()) {
|
||||
u->Begin(UndoType::Newline);
|
||||
}
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
std::size_t y = buf->Cury();
|
||||
std::size_t x = buf->Curx();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
for (int i = 0; i < repeat; ++i) {
|
||||
if (y >= rows.size())
|
||||
rows.resize(y + 1);
|
||||
auto &line = rows[y];
|
||||
if (y >= rows.size())
|
||||
rows.resize(y + 1);
|
||||
auto &line = rows[y];
|
||||
std::string tail;
|
||||
if (x < line.size()) {
|
||||
tail = line.substr(x);
|
||||
@@ -1038,6 +1054,38 @@ cmd_delete_char(CommandContext &ctx)
|
||||
}
|
||||
|
||||
|
||||
// --- Undo/Redo ---
|
||||
static bool
|
||||
cmd_undo(CommandContext &ctx)
|
||||
{
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo()) {
|
||||
u->undo();
|
||||
// Keep cursor within buffer bounds
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_redo(CommandContext &ctx)
|
||||
{
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo()) {
|
||||
u->redo();
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_kill_to_eol(CommandContext &ctx)
|
||||
{
|
||||
@@ -1103,7 +1151,7 @@ cmd_kill_line(CommandContext &ctx)
|
||||
if (rows.size() == 1) {
|
||||
// last remaining line: clear its contents
|
||||
killed_total += rows[0];
|
||||
rows[0].Clear();
|
||||
rows[0].Clear();
|
||||
y = 0;
|
||||
} else if (y < rows.size()) {
|
||||
// erase current line; keep y pointing at the next line
|
||||
@@ -1299,6 +1347,8 @@ cmd_move_left(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
// If a prompt is active and it's search, go to previous match
|
||||
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
|
||||
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
|
||||
@@ -1353,6 +1403,8 @@ cmd_move_right(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
|
||||
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
|
||||
if (!matches.empty()) {
|
||||
@@ -1406,6 +1458,8 @@ cmd_move_up(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor.
|
||||
SearchActive()) {
|
||||
// Up == previous match
|
||||
@@ -1444,6 +1498,8 @@ cmd_move_down(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor.
|
||||
SearchActive()) {
|
||||
// Down == next match
|
||||
@@ -1483,6 +1539,8 @@ cmd_move_home(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
ensure_at_least_one_line(*buf);
|
||||
std::size_t y = buf->Cury();
|
||||
buf->SetCursor(0, y);
|
||||
@@ -1497,6 +1555,8 @@ cmd_move_end(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
std::size_t y = buf->Cury();
|
||||
@@ -1513,6 +1573,8 @@ cmd_page_up(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
@@ -1553,6 +1615,8 @@ cmd_page_down(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
@@ -1597,6 +1661,8 @@ cmd_word_prev(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
std::size_t y = buf->Cury();
|
||||
@@ -1652,6 +1718,8 @@ cmd_word_next(CommandContext &ctx)
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
std::size_t y = buf->Cury();
|
||||
@@ -1817,6 +1885,9 @@ InstallDefaultCommands()
|
||||
CommandRegistry::Register({
|
||||
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to
|
||||
});
|
||||
// Undo/Redo
|
||||
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo});
|
||||
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ enum class CommandId {
|
||||
WordNext,
|
||||
// Direct cursor placement
|
||||
MoveCursorTo, // arg: "y:x" (zero-based row:col)
|
||||
// Undo/Redo
|
||||
Undo,
|
||||
Redo,
|
||||
// Meta
|
||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||
};
|
||||
|
||||
@@ -82,14 +82,14 @@ GUIRenderer::Draw(Editor &ed)
|
||||
vis_rows = 1;
|
||||
long last_row = first_row + vis_rows - 1;
|
||||
|
||||
// A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row
|
||||
// Skip this when we just forced a scroll alignment this frame (programmatic change).
|
||||
if (!forced_scroll && prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
long new_row = (cyr < first_row) ? first_row : last_row;
|
||||
if (new_row < 0)
|
||||
new_row = 0;
|
||||
// A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row
|
||||
// Skip this when we just forced a scroll alignment this frame (programmatic change).
|
||||
if (!forced_scroll && prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
long new_row = (cyr < first_row) ? first_row : last_row;
|
||||
if (new_row < 0)
|
||||
new_row = 0;
|
||||
if (new_row >= static_cast<long>(lines.size()))
|
||||
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
|
||||
// Clamp column to line length
|
||||
|
||||
16
KKeymap.cc
16
KKeymap.cc
@@ -4,11 +4,11 @@
|
||||
auto
|
||||
KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
{
|
||||
// Normalize to lowercase letter if applicable
|
||||
int k = KLowerAscii(ascii_key);
|
||||
// For k-prefix, preserve case to allow distinct mappings (e.g., 'U' vs 'u').
|
||||
const int k_lower = KLowerAscii(ascii_key);
|
||||
|
||||
if (ctrl) {
|
||||
switch (k) {
|
||||
switch (k_lower) {
|
||||
case 'd':
|
||||
out = CommandId::KillLine;
|
||||
return true; // C-k C-d
|
||||
@@ -22,7 +22,7 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (k) {
|
||||
switch (k_lower) {
|
||||
case 'j':
|
||||
out = CommandId::JumpToMark;
|
||||
return true; // C-k j
|
||||
@@ -59,9 +59,17 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
case 'p':
|
||||
out = CommandId::BufferNext;
|
||||
return true; // C-k p (switch to next buffer)
|
||||
case 'u':
|
||||
out = CommandId::Undo;
|
||||
return true; // C-k u (undo)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Case-sensitive bindings after k-prefix
|
||||
if (ascii_key == 'U') {
|
||||
out = CommandId::Redo; // C-k U (redo)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
15
UndoNode.cc
15
UndoNode.cc
@@ -1,15 +1,2 @@
|
||||
// Placeholder translation unit for UndoNode struct definition.
|
||||
#include "UndoNode.h"
|
||||
|
||||
|
||||
void
|
||||
UndoNode::DeleteNext() const
|
||||
{
|
||||
const UndoNode *node = next_;
|
||||
const UndoNode *next = nullptr;
|
||||
|
||||
while (node != nullptr) {
|
||||
next = node->Next();
|
||||
delete node;
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
|
||||
86
UndoNode.h
86
UndoNode.h
@@ -2,81 +2,25 @@
|
||||
#define KTE_UNDONODE_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
|
||||
enum UndoKind {
|
||||
UNDO_INSERT,
|
||||
UNDO_DELETE,
|
||||
enum class UndoType : uint8_t {
|
||||
Insert,
|
||||
Delete,
|
||||
Paste,
|
||||
Newline,
|
||||
DeleteRow,
|
||||
};
|
||||
|
||||
|
||||
class UndoNode {
|
||||
public:
|
||||
explicit UndoNode(const UndoKind kind, const size_t row, const size_t col)
|
||||
: kind_(kind), row_(row), col_(col) {}
|
||||
|
||||
|
||||
~UndoNode() = default;
|
||||
|
||||
|
||||
[[nodiscard]] UndoKind Kind() const
|
||||
{
|
||||
return kind_;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] UndoNode *Next() const
|
||||
{
|
||||
return next_;
|
||||
}
|
||||
|
||||
|
||||
void Next(UndoNode *next)
|
||||
{
|
||||
next_ = next;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] UndoNode *Child() const
|
||||
{
|
||||
return child_;
|
||||
}
|
||||
|
||||
|
||||
void Child(UndoNode *child)
|
||||
{
|
||||
child_ = child;
|
||||
}
|
||||
|
||||
|
||||
void SetRowCol(const std::size_t row, const std::size_t col)
|
||||
{
|
||||
this->row_ = row;
|
||||
this->col_ = col;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::size_t Row() const
|
||||
{
|
||||
return row_;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::size_t Col() const
|
||||
{
|
||||
return col_;
|
||||
}
|
||||
|
||||
|
||||
void DeleteNext() const;
|
||||
|
||||
private:
|
||||
[[maybe_unused]] UndoKind kind_;
|
||||
[[maybe_unused]] UndoNode *next_{nullptr};
|
||||
[[maybe_unused]] UndoNode *child_{nullptr};
|
||||
|
||||
[[maybe_unused]] std::size_t row_{}, col_{};
|
||||
struct UndoNode {
|
||||
UndoType type{};
|
||||
int row{};
|
||||
int col{};
|
||||
std::string text;
|
||||
UndoNode *child = nullptr; // next in current timeline
|
||||
UndoNode *next = nullptr; // redo branch
|
||||
};
|
||||
|
||||
|
||||
|
||||
263
UndoSystem.cc
Normal file
263
UndoSystem.cc
Normal file
@@ -0,0 +1,263 @@
|
||||
#include "UndoSystem.h"
|
||||
#include "Buffer.h"
|
||||
|
||||
|
||||
UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
||||
: buf_(owner), tree_(tree) {}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::Begin(UndoType type)
|
||||
{
|
||||
// Reuse pending if batching conditions are met
|
||||
const int row = static_cast<int>(buf_.Cury());
|
||||
const int col = static_cast<int>(buf_.Curx());
|
||||
if (tree_.pending && tree_.pending->type == type && tree_.pending->row == row) {
|
||||
std::size_t expected = static_cast<std::size_t>(tree_.pending->col) + tree_.pending->text.size();
|
||||
if (expected == static_cast<std::size_t>(col)) {
|
||||
return; // keep batching
|
||||
}
|
||||
}
|
||||
// Otherwise commit any existing batch and start a new node
|
||||
commit();
|
||||
auto *node = new UndoNode();
|
||||
node->type = type;
|
||||
node->row = row;
|
||||
node->col = col;
|
||||
node->child = nullptr;
|
||||
node->next = nullptr;
|
||||
tree_.pending = node;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::Append(char ch)
|
||||
{
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
tree_.pending->text.push_back(ch);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::Append(std::string_view text)
|
||||
{
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
tree_.pending->text.append(text.data(), text.size());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::commit()
|
||||
{
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
|
||||
// If we have redo branches from current, discard them (non-linear behavior)
|
||||
if (tree_.current && tree_.current->child) {
|
||||
free_node(tree_.current->child);
|
||||
tree_.current->child = nullptr;
|
||||
// We diverged; saved snapshot cannot be on discarded branch anymore
|
||||
if (tree_.saved) {
|
||||
// If saved is not equal to current, keep it; if it was on discarded branch we cannot easily detect now.
|
||||
// For simplicity, leave saved as-is; dirty flag uses pointer equality.
|
||||
}
|
||||
}
|
||||
|
||||
// Attach pending as next state
|
||||
if (!tree_.root) {
|
||||
tree_.root = tree_.pending;
|
||||
tree_.current = tree_.pending;
|
||||
} else if (!tree_.current) {
|
||||
// Should not happen if root exists, but handle gracefully
|
||||
tree_.current = tree_.pending;
|
||||
} else {
|
||||
// Attach as primary child (head of redo list)
|
||||
tree_.pending->next = nullptr;
|
||||
tree_.current->child = tree_.pending;
|
||||
tree_.current = tree_.pending;
|
||||
}
|
||||
tree_.pending = nullptr;
|
||||
update_dirty_flag();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::undo()
|
||||
{
|
||||
// Close any pending batch
|
||||
commit();
|
||||
if (!tree_.current)
|
||||
return;
|
||||
UndoNode *parent = find_parent(tree_.root, tree_.current);
|
||||
UndoNode *node = tree_.current;
|
||||
// Apply inverse of current node
|
||||
apply(node, -1);
|
||||
tree_.current = parent;
|
||||
update_dirty_flag();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::redo()
|
||||
{
|
||||
// Redo next child along current timeline
|
||||
if (tree_.pending) {
|
||||
// If app added pending edits, finalize them before redo chain
|
||||
commit();
|
||||
}
|
||||
UndoNode *next = nullptr;
|
||||
if (!tree_.current) {
|
||||
next = tree_.root; // if nothing yet, try applying first node
|
||||
} else {
|
||||
next = tree_.current->child;
|
||||
}
|
||||
if (!next)
|
||||
return;
|
||||
apply(next, +1);
|
||||
tree_.current = next;
|
||||
update_dirty_flag();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::mark_saved()
|
||||
{
|
||||
tree_.saved = tree_.current;
|
||||
update_dirty_flag();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::discard_pending()
|
||||
{
|
||||
if (tree_.pending) {
|
||||
delete tree_.pending;
|
||||
tree_.pending = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::clear()
|
||||
{
|
||||
if (tree_.root) {
|
||||
free_node(tree_.root);
|
||||
}
|
||||
if (tree_.pending) {
|
||||
delete tree_.pending;
|
||||
}
|
||||
tree_.root = tree_.current = tree_.saved = tree_.pending = nullptr;
|
||||
update_dirty_flag();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::apply(const UndoNode *node, int direction)
|
||||
{
|
||||
if (!node)
|
||||
return;
|
||||
switch (node->type) {
|
||||
case UndoType::Insert:
|
||||
case UndoType::Paste:
|
||||
if (direction > 0) {
|
||||
buf_.insert_text(node->row, node->col, node->text);
|
||||
} else {
|
||||
buf_.delete_text(node->row, node->col, node->text.size());
|
||||
}
|
||||
break;
|
||||
case UndoType::Delete:
|
||||
if (direction > 0) {
|
||||
buf_.delete_text(node->row, node->col, node->text.size());
|
||||
} else {
|
||||
buf_.insert_text(node->row, node->col, node->text);
|
||||
}
|
||||
break;
|
||||
case UndoType::Newline:
|
||||
if (direction > 0) {
|
||||
buf_.split_line(node->row, node->col);
|
||||
} else {
|
||||
buf_.join_lines(node->row);
|
||||
}
|
||||
break;
|
||||
case UndoType::DeleteRow:
|
||||
if (direction > 0) {
|
||||
buf_.delete_row(node->row);
|
||||
} else {
|
||||
buf_.insert_row(node->row, node->text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::free_node(UndoNode *node)
|
||||
{
|
||||
if (!node)
|
||||
return;
|
||||
// Free child subtree(s) and sibling branches
|
||||
if (node->child) {
|
||||
// Free entire redo list starting at child, including each subtree
|
||||
UndoNode *branch = node->child;
|
||||
while (branch) {
|
||||
UndoNode *next = branch->next;
|
||||
free_node(branch);
|
||||
branch = next;
|
||||
}
|
||||
node->child = nullptr;
|
||||
}
|
||||
delete node;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::free_branch(UndoNode *node)
|
||||
{
|
||||
// Free a branch list (node and its next siblings) including their subtrees
|
||||
while (node) {
|
||||
UndoNode *next = node->next;
|
||||
free_node(node);
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
dfs_find_parent(UndoNode *cur, UndoNode *target, UndoNode *&out_parent)
|
||||
{
|
||||
if (!cur)
|
||||
return false;
|
||||
for (UndoNode *child = cur->child; child != nullptr; child = child->next) {
|
||||
if (child == target) {
|
||||
out_parent = cur;
|
||||
return true;
|
||||
}
|
||||
if (dfs_find_parent(child, target, out_parent))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
UndoNode *
|
||||
UndoSystem::find_parent(UndoNode *from, UndoNode *target)
|
||||
{
|
||||
if (!from || !target)
|
||||
return nullptr;
|
||||
if (from == target)
|
||||
return nullptr;
|
||||
UndoNode *parent = nullptr;
|
||||
dfs_find_parent(from, target, parent);
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::update_dirty_flag()
|
||||
{
|
||||
// dirty if current != saved
|
||||
bool dirty = (tree_.current != tree_.saved);
|
||||
buf_.SetDirty(dirty);
|
||||
}
|
||||
45
UndoSystem.h
Normal file
45
UndoSystem.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef KTE_UNDOSYSTEM_H
|
||||
#define KTE_UNDOSYSTEM_H
|
||||
|
||||
#include <string_view>
|
||||
#include "UndoTree.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
class UndoSystem {
|
||||
public:
|
||||
explicit UndoSystem(Buffer &owner, UndoTree &tree);
|
||||
|
||||
void Begin(UndoType type);
|
||||
|
||||
void Append(char ch);
|
||||
|
||||
void Append(std::string_view text);
|
||||
|
||||
void commit();
|
||||
|
||||
void undo();
|
||||
|
||||
void redo();
|
||||
|
||||
void mark_saved();
|
||||
|
||||
void discard_pending();
|
||||
|
||||
void clear();
|
||||
|
||||
private:
|
||||
void apply(const UndoNode *node, int direction); // +1 redo, -1 undo
|
||||
void free_node(UndoNode *node);
|
||||
|
||||
void free_branch(UndoNode *node); // frees redo siblings only
|
||||
UndoNode *find_parent(UndoNode *from, UndoNode *target);
|
||||
|
||||
void update_dirty_flag();
|
||||
|
||||
private:
|
||||
Buffer &buf_;
|
||||
UndoTree &tree_;
|
||||
};
|
||||
|
||||
#endif // KTE_UNDOSYSTEM_H
|
||||
48
UndoTree.cc
48
UndoTree.cc
@@ -1,47 +1,3 @@
|
||||
// Placeholder translation unit for UndoTree struct definition.
|
||||
// Undo logic is implemented in UndoSystem.
|
||||
#include "UndoTree.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
|
||||
void
|
||||
UndoTree::Begin(const UndoKind kind, const size_t row, const size_t col)
|
||||
{
|
||||
if (this->pending != nullptr) {
|
||||
if (this->pending->Kind() == kind) {
|
||||
return;
|
||||
}
|
||||
this->Commit();
|
||||
}
|
||||
|
||||
assert(this->pending == nullptr);
|
||||
this->pending = new UndoNode(kind, row, col);
|
||||
assert(this->pending != nullptr);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoTree::Commit()
|
||||
{
|
||||
if (this->pending == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->root == nullptr) {
|
||||
assert(this->current == nullptr);
|
||||
|
||||
this->root = this->pending;
|
||||
this->current = this->pending;
|
||||
this->pending = nullptr;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
assert(this->current != nullptr);
|
||||
if (this->current->Next() != nullptr) {
|
||||
this->current->DeleteNext();
|
||||
}
|
||||
|
||||
this->current->Next(this->pending);
|
||||
this->current = this->pending;
|
||||
this->pending = nullptr;
|
||||
}
|
||||
|
||||
17
UndoTree.h
17
UndoTree.h
@@ -2,18 +2,13 @@
|
||||
#define KTE_UNDOTREE_H
|
||||
|
||||
#include "UndoNode.h"
|
||||
#include <memory>
|
||||
|
||||
class UndoTree {
|
||||
UndoTree() : root{nullptr}, current{nullptr}, pending{nullptr} {}
|
||||
|
||||
void Begin(UndoKind kind, size_t row, size_t col);
|
||||
|
||||
void Commit();
|
||||
|
||||
private:
|
||||
UndoNode *root{nullptr};
|
||||
UndoNode *current{nullptr};
|
||||
UndoNode *pending{nullptr};
|
||||
struct UndoTree {
|
||||
UndoNode *root = nullptr; // first edit ever
|
||||
UndoNode *current = nullptr; // current state of buffer
|
||||
UndoNode *saved = nullptr; // points to node matching last save (for dirty flag)
|
||||
UndoNode *pending = nullptr; // in-progress batch (detached)
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ include(InstallRequiredSystemLibraries)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEBUG ON)
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
set(CPACK_PACKAGE_VENDOR "Shimmering Clarity")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "kyle's editor")
|
||||
@@ -14,11 +14,11 @@ set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
|
||||
###################
|
||||
### DEBIANESQUE ###
|
||||
###################
|
||||
if(${BUILD_GUI})
|
||||
if (${BUILD_GUI})
|
||||
set(CPACK_COMPONENTS_ALL gui nox)
|
||||
else()
|
||||
else ()
|
||||
set(CPACK_COMPONENTS_ALL nox)
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
set(CPACK_COMPONENTS_GROUPING ONE_PER_GROUP)
|
||||
set(CPACK_DEBIAN_ENABLE_COMPONENT_DEPENDS ON)
|
||||
@@ -31,25 +31,25 @@ set(CPACK_PACKAGE_nox_DESCRIPTION ${CPACK_PACKAGE_DESCRIPTION})
|
||||
set(CPACK_PACKAGE_nox_PACKAGE_NAME "kte")
|
||||
set(CPACK_DEBIAN_nox_PACKAGE_NAME "ke")
|
||||
|
||||
if(BUILD_GUI)
|
||||
set(CPACK_PACKAGE_gui_PACKAGE_NAME "kte")
|
||||
set(CPACK_DEBIAN_gui_PACKAGE_NAME "kte")
|
||||
if (BUILD_GUI)
|
||||
set(CPACK_PACKAGE_gui_PACKAGE_NAME "kge")
|
||||
set(CPACK_DEBIAN_gui_PACKAGE_NAME "kge")
|
||||
set(CPACK_PACKAGE_gui_DESCRIPTION_SUMMARY " graphical front-end for kyle's editor")
|
||||
set(CPACK_PACKAGE_gui_DESCRIPTION "graphical front-end for ${CPACK_PACKAGE_DESCRIPTION} ")
|
||||
endif()
|
||||
endif ()
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON)
|
||||
|
||||
|
||||
if(LINUX)
|
||||
if (LINUX)
|
||||
set(CPACK_GENERATOR "DEB;STGZ;TGZ")
|
||||
elseif(APPLE)
|
||||
elseif (APPLE)
|
||||
set(CPACK_GENERATOR "productbuild;TGZ")
|
||||
elseif(MSVC OR MSYS OR MINGW)
|
||||
elseif (MSVC OR MSYS OR MINGW)
|
||||
set(CPACK_GENERATOR "NSIS;ZIP")
|
||||
else()
|
||||
else ()
|
||||
set(CPACK_GENERATOR "ZIP")
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
set(CPACK_SOURCE_GENERATOR "TGZ;ZIP ")
|
||||
set(CPACK_SOURCE_IGNORE_FILES
|
||||
|
||||
11882
fonts/b612_mono.h
11882
fonts/b612_mono.h
File diff suppressed because it is too large
Load Diff
2388
fonts/brassmono.h
2388
fonts/brassmono.h
File diff suppressed because it is too large
Load Diff
7
main.cc
7
main.cc
@@ -97,11 +97,12 @@ main(int argc, const char *argv[])
|
||||
} else if (req_term) {
|
||||
use_gui = false;
|
||||
} else {
|
||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||
|
||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||
#if defined(KTE_DEFAULT_GUI)
|
||||
use_gui = true;
|
||||
use_gui = true;
|
||||
#else
|
||||
use_gui = false;
|
||||
use_gui = false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user