Add UndoSystem implementation and refactor UndoNode for simplicity.

This commit is contained in:
2025-11-30 00:04:29 -08:00
parent e91a32dd90
commit 35ffe6d11c
20 changed files with 13950 additions and 1479 deletions

8
.idea/kte.iml generated
View File

@@ -1,2 +1,8 @@
<?xml version="1.0" encoding="UTF-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
View File

@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14 (kte)" />
</component>
<component name="CMakePythonSetting"> <component name="CMakePythonSetting">
<option name="pythonIntegrationState" value="YES" /> <option name="pythonIntegrationState" value="YES" />
</component> </component>

61
.idea/workspace.xml generated
View File

@@ -23,8 +23,6 @@
<component name="CMakeRunConfigurationManager"> <component name="CMakeRunConfigurationManager">
<generated> <generated>
<config projectName="kte" targetName="kte" /> <config projectName="kte" targetName="kte" />
<config projectName="kte" targetName="imgui" />
<config projectName="kte" targetName="kge" />
</generated> </generated>
</component> </component>
<component name="CMakeSettings" AUTO_RELOAD="true"> <component name="CMakeSettings" AUTO_RELOAD="true">
@@ -33,9 +31,27 @@
</configurations> </configurations>
</component> </component>
<component name="ChangeListManager"> <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.&#10;&#10;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$/.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.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> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -103,37 +119,27 @@
"nodejs_package_manager_path": "npm", "nodejs_package_manager_path": "npm",
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp", "onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
"rearrange.code.on.save": "true", "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", "to.speed.mode.migration.done": "true",
"vue.rearranger.settings.migration": "true" "vue.rearranger.settings.migration": "true"
} }
}]]></component> }]]></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"> <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"> <method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method> </method>
</configuration> </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"> <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"> <method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method> </method>
</configuration> </configuration>
<list>
<item itemvalue="CMake Application.imgui" />
<item itemvalue="CMake Application.kge" />
<item itemvalue="CMake Application.kte" />
</list>
</component> </component>
<component name="TaskManager"> <component name="TaskManager">
<task active="true" id="Default" summary="Default task"> <task active="true" id="Default" summary="Default task">
@@ -142,7 +148,7 @@
<option name="number" value="Default" /> <option name="number" value="Default" />
<option name="presentableId" value="Default" /> <option name="presentableId" value="Default" />
<updated>1764457173148</updated> <updated>1764457173148</updated>
<workItem from="1764457174208" duration="27972000" /> <workItem from="1764457174208" duration="31043000" />
</task> </task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions."> <task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -160,7 +166,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1764486011231</updated> <updated>1764486011231</updated>
</task> </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 /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -175,7 +189,8 @@
<MESSAGE value="Refactoring" /> <MESSAGE value="Refactoring" />
<MESSAGE value="Add undo/redo infrastructure and buffer management additions." /> <MESSAGE value="Add undo/redo infrastructure and buffer management additions." />
<MESSAGE value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations.&#10;&#10;This uses either a GapBuffer or PieceTable depending on the compilation." /> <MESSAGE value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations.&#10;&#10;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.&#10;&#10;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>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />

346
Buffer.cc
View File

@@ -1,11 +1,18 @@
#include "Buffer.h" #include "Buffer.h"
#include "UndoSystem.h"
#include "UndoTree.h"
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <filesystem> #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) 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 bool
Buffer::OpenFromFile(const std::string &path, std::string &err) Buffer::OpenFromFile(const std::string &path, std::string &err)
{ {
// If the file doesn't exist, initialize an empty, non-file-backed buffer // 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. // with the provided filename. Do not touch the filesystem until Save/SaveAs.
if (!std::filesystem::exists(path)) { if (!std::filesystem::exists(path)) {
rows_.clear(); rows_.clear();
nrows_ = 0; nrows_ = 0;
filename_ = path; filename_ = path;
is_file_backed_ = false; is_file_backed_ = false;
dirty_ = false; dirty_ = false;
// Reset cursor/viewport state // Reset cursor/viewport state
curx_ = cury_ = rx_ = 0; curx_ = cury_ = rx_ = 0;
rowoffs_ = coloffs_ = 0; rowoffs_ = coloffs_ = 0;
mark_set_ = false; mark_set_ = false;
mark_curx_ = mark_cury_ = 0; mark_curx_ = mark_cury_ = 0;
return true; return true;
} }
std::ifstream in(path, std::ios::in | std::ios::binary); std::ifstream in(path, std::ios::in | std::ios::binary);
if (!in) { if (!in) {
err = "Failed to open file: " + path; err = "Failed to open file: " + path;
return false; 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(); // Detect if file ends with a newline so we can preserve a final empty line
std::string line; // in our in-memory representation (mg-style semantics).
while (std::getline(in, line)) { bool ends_with_nl = false;
// std::getline strips the '\n', keep raw line content only {
// Handle potential Windows CRLF: strip trailing '\r' in.seekg(0, std::ios::end);
if (!line.empty() && line.back() == '\r') { std::streamoff sz = in.tellg();
line.pop_back(); if (sz > 0) {
} in.seekg(-1, std::ios::end);
rows_.emplace_back(line); 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 rows_.clear();
// empty final row from getline (e.g., when the last textual line std::string line;
// had content followed by '\n'), append an empty row to represent while (std::getline(in, line)) {
// the cursor position past the last newline. // std::getline strips the '\n', keep raw line content only
if (ends_with_nl) { // Handle potential Windows CRLF: strip trailing '\r'
if (rows_.empty() || !rows_.back().empty()) { if (!line.empty() && line.back() == '\r') {
rows_.emplace_back(std::string()); 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; filename_ = path;
is_file_backed_ = true; is_file_backed_ = true;
dirty_ = false; 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 // Reset cursor/viewport state
curx_ = cury_ = rx_ = 0; curx_ = cury_ = rx_ = 0;
rowoffs_ = coloffs_ = 0; rowoffs_ = coloffs_ = 0;
@@ -109,14 +171,15 @@ Buffer::Save(std::string &err) const
err = "Failed to open for write: " + filename_; err = "Failed to open for write: " + filename_;
return false; return false;
} }
for (std::size_t i = 0; i < rows_.size(); ++i) { for (std::size_t i = 0; i < rows_.size(); ++i) {
const char *d = rows_[i].Data(); const char *d = rows_[i].Data();
std::size_t n = rows_[i].Size(); std::size_t n = rows_[i].Size();
if (d && n) out.write(d, static_cast<std::streamsize>(n)); if (d && n)
if (i + 1 < rows_.size()) { out.write(d, static_cast<std::streamsize>(n));
out.put('\n'); if (i + 1 < rows_.size()) {
} out.put('\n');
} }
}
if (!out.good()) { if (!out.good()) {
err = "Write error"; err = "Write error";
return false; return false;
@@ -136,14 +199,15 @@ Buffer::SaveAs(const std::string &path, std::string &err)
err = "Failed to open for write: " + path; err = "Failed to open for write: " + path;
return false; return false;
} }
for (std::size_t i = 0; i < rows_.size(); ++i) { for (std::size_t i = 0; i < rows_.size(); ++i) {
const char *d = rows_[i].Data(); const char *d = rows_[i].Data();
std::size_t n = rows_[i].Size(); std::size_t n = rows_[i].Size();
if (d && n) out.write(d, static_cast<std::streamsize>(n)); if (d && n)
if (i + 1 < rows_.size()) { out.write(d, static_cast<std::streamsize>(n));
out.put('\n'); if (i + 1 < rows_.size()) {
} out.put('\n');
} }
}
if (!out.good()) { if (!out.good()) {
err = "Write error"; err = "Write error";
return false; return false;
@@ -167,3 +231,147 @@ Buffer::AsString() const
ss << ">: " << rows_.size() << " lines"; ss << ">: " << rows_.size() << " lines";
return ss.str(); 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
View File

@@ -7,13 +7,23 @@
#include <cstddef> #include <cstddef>
#include <string> #include <string>
#include <vector> #include <vector>
#include <string_view>
#include "AppendBuffer.h" #include "AppendBuffer.h"
#include "UndoSystem.h"
class Buffer { class Buffer {
public: public:
Buffer(); 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); explicit Buffer(const std::string &path);
// File operations // File operations
@@ -64,31 +74,59 @@ public:
public: public:
Line() = default; Line() = default;
Line(const char *s) Line(const char *s)
{ {
assign_from(s ? std::string(s) : std::string()); assign_from(s ? std::string(s) : std::string());
} }
Line(const std::string &s) Line(const std::string &s)
{ {
assign_from(s); assign_from(s);
} }
Line(const Line &other) = default; Line(const Line &other) = default;
Line &operator=(const Line &other) = default; Line &operator=(const Line &other) = default;
Line(Line &&other) noexcept = default; Line(Line &&other) noexcept = default;
Line &operator=(Line &&other) noexcept = default; Line &operator=(Line &&other) noexcept = default;
// capacity helpers // capacity helpers
void Clear() { buf_.Clear(); } void Clear()
{
buf_.Clear();
}
// size/access // size/access
[[nodiscard]] std::size_t size() const { return buf_.Size(); } [[nodiscard]] std::size_t size() const
[[nodiscard]] bool empty() const { return size() == 0; } {
return buf_.Size();
}
[[nodiscard]] bool empty() const
{
return size() == 0;
}
// read-only raw view // read-only raw view
[[nodiscard]] const char *Data() const { return buf_.Data(); } [[nodiscard]] const char *Data() const
[[nodiscard]] std::size_t Size() const { return buf_.Size(); } {
return buf_.Data();
}
[[nodiscard]] std::size_t Size() const
{
return buf_.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
@@ -97,60 +135,77 @@ public:
return (i < buf_.Size() && d) ? d[i] : '\0'; return (i < buf_.Size() && d) ? d[i] : '\0';
} }
// conversions // 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) // string-like API used by command/renderer layers (implemented via materialization for now)
std::string substr(std::size_t pos) const std::string substr(std::size_t pos) const
{ {
const std::size_t n = buf_.Size(); 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); return std::string(buf_.Data() + pos, n - pos);
} }
std::string substr(std::size_t pos, std::size_t len) const std::string substr(std::size_t pos, std::size_t len) const
{ {
const std::size_t n = buf_.Size(); 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; const std::size_t take = (pos + len > n) ? (n - pos) : len;
return std::string(buf_.Data() + pos, take); return std::string(buf_.Data() + pos, take);
} }
void erase(std::size_t pos) void erase(std::size_t pos)
{ {
// erase to end // erase to end
material_edit([&](std::string &s) { 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) void erase(std::size_t pos, std::size_t len)
{ {
material_edit([&](std::string &s) { 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) void insert(std::size_t pos, const std::string &seg)
{ {
material_edit([&](std::string &s) { material_edit([&](std::string &s) {
if (pos > s.size()) pos = s.size(); if (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()); buf_.Append(other.buf_.Data(), other.buf_.Size());
return *this; return *this;
} }
Line &operator+=(const std::string &s) Line &operator+=(const std::string &s)
{ {
buf_.Append(s.data(), s.size()); buf_.Append(s.data(), s.size());
return *this; return *this;
} }
Line &operator=(const std::string &s) Line &operator=(const std::string &s)
{ {
assign_from(s); assign_from(s);
@@ -161,10 +216,12 @@ public:
void assign_from(const std::string &s) void assign_from(const std::string &s)
{ {
buf_.Clear(); 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) void material_edit(F fn)
{ {
std::string tmp = static_cast<std::string>(*this); std::string tmp = static_cast<std::string>(*this);
@@ -172,9 +229,11 @@ public:
assign_from(tmp); assign_from(tmp);
} }
AppendBuffer buf_; AppendBuffer buf_;
}; };
[[nodiscard]] const std::vector<Line> &Rows() const [[nodiscard]] const std::vector<Line> &Rows() const
{ {
return rows_; return rows_;
@@ -266,6 +325,25 @@ public:
[[nodiscard]] std::string AsString() const; [[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: private:
// State mirroring original C struct (without undo_tree) // State mirroring original C struct (without undo_tree)
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
@@ -278,6 +356,10 @@ private:
bool dirty_ = false; bool dirty_ = false;
bool mark_set_ = false; bool mark_set_ = false;
std::size_t mark_curx_ = 0, mark_cury_ = 0; 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 #endif // KTE_BUFFER_H

View File

@@ -55,8 +55,9 @@ set(COMMON_SOURCES
TerminalInputHandler.cc TerminalInputHandler.cc
TerminalRenderer.cc TerminalRenderer.cc
TerminalFrontend.cc TerminalFrontend.cc
# UndoNode.cc UndoNode.cc
# UndoTree.cc UndoTree.cc
UndoSystem.cc
) )
set(COMMON_HEADERS set(COMMON_HEADERS
@@ -73,8 +74,9 @@ set(COMMON_HEADERS
TerminalRenderer.h TerminalRenderer.h
Frontend.h Frontend.h
TerminalFrontend.h TerminalFrontend.h
# UndoNode.h UndoNode.h
# UndoTree.h UndoTree.h
UndoSystem.h
) )
# kte (terminal-first) executable # kte (terminal-first) executable

View File

@@ -3,6 +3,7 @@
#include "Command.h" #include "Command.h"
#include "Editor.h" #include "Editor.h"
#include "Buffer.h" #include "Buffer.h"
#include "UndoSystem.h"
// Note: Command layer must remain UI-agnostic. Do not include frontend/IO headers here. // 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); buf->SetDirty(false);
ctx.editor.SetStatus("Saved " + buf->Filename()); ctx.editor.SetStatus("Saved " + buf->Filename());
if (auto *u = buf->Undo())
u->mark_saved();
return true; return true;
} }
@@ -446,6 +449,8 @@ cmd_save_as(CommandContext &ctx)
return false; return false;
} }
ctx.editor.SetStatus("Saved as " + ctx.arg); ctx.editor.SetStatus("Saved as " + ctx.arg);
if (auto *u = buf->Undo())
u->mark_saved();
return true; return true;
} }
@@ -691,8 +696,10 @@ cmd_buffer_close(CommandContext &ctx)
if (ctx.editor.BufferCount() == 0) if (ctx.editor.BufferCount() == 0)
return true; return true;
std::size_t idx = ctx.editor.CurrentBufferIndex(); 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(""); std::string name = b ? buffer_display_name(*b) : std::string("");
if (b && b->Undo())
b->Undo()->discard_pending();
ctx.editor.CloseBuffer(idx); ctx.editor.CloseBuffer(idx);
if (ctx.editor.BufferCount() == 0) { if (ctx.editor.BufferCount() == 0) {
// Open a fresh empty buffer // Open a fresh empty buffer
@@ -716,6 +723,11 @@ cmd_insert_text(CommandContext &ctx)
ctx.editor.SetStatus("No buffer to edit"); ctx.editor.SetStatus("No buffer to edit");
return false; 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 a prompt is active, edit prompt text
if (ctx.editor.PromptActive()) { if (ctx.editor.PromptActive()) {
// Special-case: buffer switch prompt supports Tab-completion // Special-case: buffer switch prompt supports Tab-completion
@@ -916,15 +928,19 @@ cmd_newline(CommandContext &ctx)
ctx.editor.SetStatus("No buffer to edit"); ctx.editor.SetStatus("No buffer to edit");
return false; 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); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
std::size_t x = buf->Curx(); std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) { for (int i = 0; i < repeat; ++i) {
if (y >= rows.size()) if (y >= rows.size())
rows.resize(y + 1); rows.resize(y + 1);
auto &line = rows[y]; auto &line = rows[y];
std::string tail; std::string tail;
if (x < line.size()) { if (x < line.size()) {
tail = line.substr(x); 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 static bool
cmd_kill_to_eol(CommandContext &ctx) cmd_kill_to_eol(CommandContext &ctx)
{ {
@@ -1103,7 +1151,7 @@ cmd_kill_line(CommandContext &ctx)
if (rows.size() == 1) { if (rows.size() == 1) {
// last remaining line: clear its contents // last remaining line: clear its contents
killed_total += rows[0]; killed_total += rows[0];
rows[0].Clear(); rows[0].Clear();
y = 0; y = 0;
} else if (y < rows.size()) { } else if (y < rows.size()) {
// erase current line; keep y pointing at the next line // erase current line; keep y pointing at the next line
@@ -1299,6 +1347,8 @@ cmd_move_left(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
// If a prompt is active and it's search, go to previous match // If a prompt is active and it's search, go to previous match
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) { if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
@@ -1353,6 +1403,8 @@ cmd_move_right(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) { if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
if (!matches.empty()) { if (!matches.empty()) {
@@ -1406,6 +1458,8 @@ cmd_move_up(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor. if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor.
SearchActive()) { SearchActive()) {
// Up == previous match // Up == previous match
@@ -1444,6 +1498,8 @@ cmd_move_down(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor. if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor.
SearchActive()) { SearchActive()) {
// Down == next match // Down == next match
@@ -1483,6 +1539,8 @@ cmd_move_home(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
buf->SetCursor(0, y); buf->SetCursor(0, y);
@@ -1497,6 +1555,8 @@ cmd_move_end(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
@@ -1513,6 +1573,8 @@ cmd_page_up(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
@@ -1553,6 +1615,8 @@ cmd_page_down(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
@@ -1597,6 +1661,8 @@ cmd_word_prev(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
@@ -1652,6 +1718,8 @@ cmd_word_next(CommandContext &ctx)
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) if (!buf)
return false; return false;
if (auto *u = buf->Undo())
u->commit();
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
@@ -1817,6 +1885,9 @@ InstallDefaultCommands()
CommandRegistry::Register({ CommandRegistry::Register({
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to 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});
} }

View File

@@ -58,6 +58,9 @@ enum class CommandId {
WordNext, WordNext,
// Direct cursor placement // Direct cursor placement
MoveCursorTo, // arg: "y:x" (zero-based row:col) MoveCursorTo, // arg: "y:x" (zero-based row:col)
// Undo/Redo
Undo,
Redo,
// Meta // Meta
UnknownKCommand, // arg: single character that was not recognized after C-k UnknownKCommand, // arg: single character that was not recognized after C-k
}; };

View File

@@ -82,14 +82,14 @@ GUIRenderer::Draw(Editor &ed)
vis_rows = 1; vis_rows = 1;
long last_row = first_row + 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 // 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). // 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) { if (!forced_scroll && prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
long cyr = static_cast<long>(cy); long cyr = static_cast<long>(cy);
if (cyr < first_row || cyr > last_row) { if (cyr < first_row || cyr > last_row) {
long new_row = (cyr < first_row) ? first_row : last_row; long new_row = (cyr < first_row) ? first_row : last_row;
if (new_row < 0) if (new_row < 0)
new_row = 0; new_row = 0;
if (new_row >= static_cast<long>(lines.size())) if (new_row >= static_cast<long>(lines.size()))
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1)); new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
// Clamp column to line length // Clamp column to line length

View File

@@ -4,11 +4,11 @@
auto auto
KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
{ {
// Normalize to lowercase letter if applicable // For k-prefix, preserve case to allow distinct mappings (e.g., 'U' vs 'u').
int k = KLowerAscii(ascii_key); const int k_lower = KLowerAscii(ascii_key);
if (ctrl) { if (ctrl) {
switch (k) { switch (k_lower) {
case 'd': case 'd':
out = CommandId::KillLine; out = CommandId::KillLine;
return true; // C-k C-d return true; // C-k C-d
@@ -22,7 +22,7 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
break; break;
} }
} else { } else {
switch (k) { switch (k_lower) {
case 'j': case 'j':
out = CommandId::JumpToMark; out = CommandId::JumpToMark;
return true; // C-k j return true; // C-k j
@@ -59,9 +59,17 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'p': case 'p':
out = CommandId::BufferNext; out = CommandId::BufferNext;
return true; // C-k p (switch to next buffer) return true; // C-k p (switch to next buffer)
case 'u':
out = CommandId::Undo;
return true; // C-k u (undo)
default: default:
break; break;
} }
// Case-sensitive bindings after k-prefix
if (ascii_key == 'U') {
out = CommandId::Redo; // C-k U (redo)
return true;
}
} }
return false; return false;
} }

View File

@@ -1,15 +1,2 @@
// Placeholder translation unit for UndoNode struct definition.
#include "UndoNode.h" #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;
}
}

View File

@@ -2,81 +2,25 @@
#define KTE_UNDONODE_H #define KTE_UNDONODE_H
#include <cstddef> #include <cstddef>
#include <cstddef> #include <cstdint>
#include <string>
enum UndoKind { enum class UndoType : uint8_t {
UNDO_INSERT, Insert,
UNDO_DELETE, Delete,
Paste,
Newline,
DeleteRow,
}; };
struct UndoNode {
class UndoNode { UndoType type{};
public: int row{};
explicit UndoNode(const UndoKind kind, const size_t row, const size_t col) int col{};
: kind_(kind), row_(row), col_(col) {} std::string text;
UndoNode *child = nullptr; // next in current timeline
UndoNode *next = nullptr; // redo branch
~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_{};
}; };

263
UndoSystem.cc Normal file
View 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
View 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

View File

@@ -1,47 +1,3 @@
// Placeholder translation unit for UndoTree struct definition.
// Undo logic is implemented in UndoSystem.
#include "UndoTree.h" #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;
}

View File

@@ -2,18 +2,13 @@
#define KTE_UNDOTREE_H #define KTE_UNDOTREE_H
#include "UndoNode.h" #include "UndoNode.h"
#include <memory>
class UndoTree { struct UndoTree {
UndoTree() : root{nullptr}, current{nullptr}, pending{nullptr} {} UndoNode *root = nullptr; // first edit ever
UndoNode *current = nullptr; // current state of buffer
void Begin(UndoKind kind, size_t row, size_t col); UndoNode *saved = nullptr; // points to node matching last save (for dirty flag)
UndoNode *pending = nullptr; // in-progress batch (detached)
void Commit();
private:
UndoNode *root{nullptr};
UndoNode *current{nullptr};
UndoNode *pending{nullptr};
}; };

View File

@@ -3,7 +3,7 @@ include(InstallRequiredSystemLibraries)
if (CMAKE_BUILD_TYPE STREQUAL "Debug") if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CPACK_DEBIAN_PACKAGE_DEBUG ON) set(CPACK_DEBIAN_PACKAGE_DEBUG ON)
endif() endif ()
set(CPACK_PACKAGE_VENDOR "Shimmering Clarity") set(CPACK_PACKAGE_VENDOR "Shimmering Clarity")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "kyle's editor") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "kyle's editor")
@@ -14,11 +14,11 @@ set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
################### ###################
### DEBIANESQUE ### ### DEBIANESQUE ###
################### ###################
if(${BUILD_GUI}) if (${BUILD_GUI})
set(CPACK_COMPONENTS_ALL gui nox) set(CPACK_COMPONENTS_ALL gui nox)
else() else ()
set(CPACK_COMPONENTS_ALL nox) set(CPACK_COMPONENTS_ALL nox)
endif() endif ()
set(CPACK_COMPONENTS_GROUPING ONE_PER_GROUP) set(CPACK_COMPONENTS_GROUPING ONE_PER_GROUP)
set(CPACK_DEBIAN_ENABLE_COMPONENT_DEPENDS ON) 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_PACKAGE_nox_PACKAGE_NAME "kte")
set(CPACK_DEBIAN_nox_PACKAGE_NAME "ke") set(CPACK_DEBIAN_nox_PACKAGE_NAME "ke")
if(BUILD_GUI) if (BUILD_GUI)
set(CPACK_PACKAGE_gui_PACKAGE_NAME "kte") set(CPACK_PACKAGE_gui_PACKAGE_NAME "kge")
set(CPACK_DEBIAN_gui_PACKAGE_NAME "kte") 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_SUMMARY " graphical front-end for kyle's editor")
set(CPACK_PACKAGE_gui_DESCRIPTION "graphical front-end for ${CPACK_PACKAGE_DESCRIPTION} ") set(CPACK_PACKAGE_gui_DESCRIPTION "graphical front-end for ${CPACK_PACKAGE_DESCRIPTION} ")
endif() endif ()
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON) set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON)
if(LINUX) if (LINUX)
set(CPACK_GENERATOR "DEB;STGZ;TGZ") set(CPACK_GENERATOR "DEB;STGZ;TGZ")
elseif(APPLE) elseif (APPLE)
set(CPACK_GENERATOR "productbuild;TGZ") set(CPACK_GENERATOR "productbuild;TGZ")
elseif(MSVC OR MSYS OR MINGW) elseif (MSVC OR MSYS OR MINGW)
set(CPACK_GENERATOR "NSIS;ZIP") set(CPACK_GENERATOR "NSIS;ZIP")
else() else ()
set(CPACK_GENERATOR "ZIP") set(CPACK_GENERATOR "ZIP")
endif() endif ()
set(CPACK_SOURCE_GENERATOR "TGZ;ZIP ") set(CPACK_SOURCE_GENERATOR "TGZ;ZIP ")
set(CPACK_SOURCE_IGNORE_FILES set(CPACK_SOURCE_IGNORE_FILES

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -97,11 +97,12 @@ main(int argc, const char *argv[])
} else if (req_term) { } else if (req_term) {
use_gui = false; use_gui = false;
} else { } 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) #if defined(KTE_DEFAULT_GUI)
use_gui = true; use_gui = true;
#else #else
use_gui = false; use_gui = false;
#endif #endif
} }
#endif #endif