Add undo/redo infrastructure and buffer management additions.
This commit is contained in:
24
.idea/workspace.xml
generated
24
.idea/workspace.xml
generated
@@ -34,10 +34,30 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
|
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
|
||||||
|
<change afterPath="$PROJECT_DIR$/UndoNode.cc" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/UndoNode.h" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/UndoTree.cc" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/UndoTree.h" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/kte-cloc" 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$/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.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$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Editor.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GUIFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GUIInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GUIInputHandler.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GapBuffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GapBuffer.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/PieceTable.cc" beforeDir="false" afterPath="$PROJECT_DIR$/PieceTable.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/TerminalFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalFrontend.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/TerminalFrontend.h" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalFrontend.h" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" 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" />
|
||||||
@@ -73,6 +93,8 @@
|
|||||||
<component name="ProjectViewState">
|
<component name="ProjectViewState">
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
|
<option name="sortByType" value="true" />
|
||||||
|
<option name="sortKey" value="BY_TYPE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
@@ -142,7 +164,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="22512000" />
|
<workItem from="1764457174208" duration="26658000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ set(COMMON_SOURCES
|
|||||||
TerminalInputHandler.cc
|
TerminalInputHandler.cc
|
||||||
TerminalRenderer.cc
|
TerminalRenderer.cc
|
||||||
TerminalFrontend.cc
|
TerminalFrontend.cc
|
||||||
|
# UndoNode.cc
|
||||||
|
# UndoTree.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
set(COMMON_HEADERS
|
set(COMMON_HEADERS
|
||||||
@@ -74,6 +76,8 @@ set(COMMON_HEADERS
|
|||||||
TerminalRenderer.h
|
TerminalRenderer.h
|
||||||
Frontend.h
|
Frontend.h
|
||||||
TerminalFrontend.h
|
TerminalFrontend.h
|
||||||
|
# UndoNode.h
|
||||||
|
# UndoTree.h
|
||||||
)
|
)
|
||||||
|
|
||||||
# kte (terminal-first) executable
|
# kte (terminal-first) executable
|
||||||
|
|||||||
648
Command.cc
648
Command.cc
@@ -82,6 +82,163 @@ ensure_at_least_one_line(Buffer &buf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helper: compute ordered region between mark and cursor. Returns false if no mark set or zero-length.
|
||||||
|
static bool
|
||||||
|
compute_mark_region(Buffer &buf, std::size_t &sx, std::size_t &sy, std::size_t &ex, std::size_t &ey)
|
||||||
|
{
|
||||||
|
if (!buf.MarkSet())
|
||||||
|
return false;
|
||||||
|
std::size_t cx = buf.Curx();
|
||||||
|
std::size_t cy = buf.Cury();
|
||||||
|
std::size_t mx = buf.MarkCurx();
|
||||||
|
std::size_t my = buf.MarkCury();
|
||||||
|
if (cy < my || (cy == my && cx < mx)) {
|
||||||
|
sy = cy;
|
||||||
|
sx = cx;
|
||||||
|
ey = my;
|
||||||
|
ex = mx;
|
||||||
|
} else {
|
||||||
|
sy = my;
|
||||||
|
sx = mx;
|
||||||
|
ey = cy;
|
||||||
|
ex = cx;
|
||||||
|
}
|
||||||
|
if (sy == ey && sx == ex)
|
||||||
|
return false; // empty region
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helper: extract text from [sx,sy) to [ex,ey) without modifying buffer. Newlines inserted between lines.
|
||||||
|
static std::string
|
||||||
|
extract_region_text(const Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey)
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (sy >= rows.size())
|
||||||
|
return std::string();
|
||||||
|
if (ey >= rows.size())
|
||||||
|
ey = rows.size() - 1;
|
||||||
|
if (sy == ey) {
|
||||||
|
const auto &line = rows[sy];
|
||||||
|
std::size_t xs = std::min(sx, line.size());
|
||||||
|
std::size_t xe = std::min(ex, line.size());
|
||||||
|
if (xe < xs)
|
||||||
|
std::swap(xs, xe);
|
||||||
|
return line.substr(xs, xe - xs);
|
||||||
|
}
|
||||||
|
std::string out;
|
||||||
|
// first line tail
|
||||||
|
{
|
||||||
|
const auto &line = rows[sy];
|
||||||
|
std::size_t xs = std::min(sx, line.size());
|
||||||
|
out += line.substr(xs);
|
||||||
|
out += '\n';
|
||||||
|
}
|
||||||
|
// middle lines full
|
||||||
|
for (std::size_t y = sy + 1; y < ey; ++y) {
|
||||||
|
out += rows[y];
|
||||||
|
out += '\n';
|
||||||
|
}
|
||||||
|
// last line head
|
||||||
|
{
|
||||||
|
const auto &line = rows[ey];
|
||||||
|
std::size_t xe = std::min(ex, line.size());
|
||||||
|
out += line.substr(0, xe);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helper: delete region and leave cursor at start (sx,sy). Adjust lines appropriately.
|
||||||
|
static void
|
||||||
|
delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey)
|
||||||
|
{
|
||||||
|
auto &rows = buf.Rows();
|
||||||
|
if (rows.empty())
|
||||||
|
return;
|
||||||
|
if (sy >= rows.size())
|
||||||
|
return;
|
||||||
|
if (ey >= rows.size())
|
||||||
|
ey = rows.size() - 1;
|
||||||
|
if (sy == ey) {
|
||||||
|
auto &line = rows[sy];
|
||||||
|
std::size_t xs = std::min(sx, line.size());
|
||||||
|
std::size_t xe = std::min(ex, line.size());
|
||||||
|
if (xe < xs)
|
||||||
|
std::swap(xs, xe);
|
||||||
|
line.erase(xs, xe - xs);
|
||||||
|
} else {
|
||||||
|
// Keep prefix of first and suffix of last then join
|
||||||
|
std::string prefix = rows[sy].substr(0, std::min(sx, rows[sy].size()));
|
||||||
|
std::string suffix;
|
||||||
|
{
|
||||||
|
const auto &last = rows[ey];
|
||||||
|
std::size_t xe = std::min(ex, last.size());
|
||||||
|
suffix = last.substr(xe);
|
||||||
|
}
|
||||||
|
rows[sy] = prefix + suffix;
|
||||||
|
// erase middle lines and the last line
|
||||||
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(sy + 1),
|
||||||
|
rows.begin() + static_cast<std::ptrdiff_t>(ey + 1));
|
||||||
|
}
|
||||||
|
buf.SetCursor(sx, sy);
|
||||||
|
buf.SetDirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Insert arbitrary text at cursor, supporting newlines. Updates cursor position.
|
||||||
|
static void
|
||||||
|
insert_text_at_cursor(Buffer &buf, const std::string &text)
|
||||||
|
{
|
||||||
|
auto &rows = buf.Rows();
|
||||||
|
std::size_t y = buf.Cury();
|
||||||
|
std::size_t x = buf.Curx();
|
||||||
|
if (y > rows.size())
|
||||||
|
y = rows.size();
|
||||||
|
if (rows.empty())
|
||||||
|
rows.emplace_back("");
|
||||||
|
if (y >= rows.size())
|
||||||
|
rows.emplace_back("");
|
||||||
|
|
||||||
|
std::size_t cur_y = y;
|
||||||
|
std::size_t cur_x = x;
|
||||||
|
|
||||||
|
std::string remain = text;
|
||||||
|
while (true) {
|
||||||
|
auto pos = remain.find('\n');
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
// insert remaining into current line
|
||||||
|
if (cur_y >= rows.size())
|
||||||
|
rows.emplace_back("");
|
||||||
|
if (cur_x > rows[cur_y].size())
|
||||||
|
cur_x = rows[cur_y].size();
|
||||||
|
rows[cur_y].insert(cur_x, remain);
|
||||||
|
cur_x += remain.size();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// insert segment before newline
|
||||||
|
std::string seg = remain.substr(0, pos);
|
||||||
|
if (cur_x > rows[cur_y].size())
|
||||||
|
cur_x = rows[cur_y].size();
|
||||||
|
rows[cur_y].insert(cur_x, seg);
|
||||||
|
// split line at cur_x + seg.size()
|
||||||
|
cur_x += seg.size();
|
||||||
|
std::string after = rows[cur_y].substr(cur_x);
|
||||||
|
rows[cur_y].erase(cur_x);
|
||||||
|
// create new line after current with the 'after' tail
|
||||||
|
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(cur_y + 1), after);
|
||||||
|
// move to start of next line
|
||||||
|
cur_y += 1;
|
||||||
|
cur_x = 0;
|
||||||
|
// advance remain after newline
|
||||||
|
remain.erase(0, pos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.SetCursor(cur_x, cur_y);
|
||||||
|
buf.SetDirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static std::size_t
|
static std::size_t
|
||||||
inverse_render_to_source_col(const std::string &line, std::size_t rx_target, std::size_t tabw)
|
inverse_render_to_source_col(const std::string &line, std::size_t rx_target, std::size_t tabw)
|
||||||
{
|
{
|
||||||
@@ -457,6 +614,99 @@ cmd_open_file_start(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Buffers: switch/next/prev/close ---
|
||||||
|
static bool
|
||||||
|
cmd_buffer_switch_start(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
// If only one (or zero) buffer is open, do nothing per spec
|
||||||
|
if (ctx.editor.BufferCount() <= 1) {
|
||||||
|
ctx.editor.SetStatus("No other buffers open.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ctx.editor.StartPrompt(Editor::PromptKind::BufferSwitch, "Buffer", "");
|
||||||
|
ctx.editor.SetStatus("Buffer: ");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::string
|
||||||
|
buffer_display_name(const Buffer &b)
|
||||||
|
{
|
||||||
|
if (!b.Filename().empty())
|
||||||
|
return b.Filename();
|
||||||
|
return std::string("<untitled>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::string
|
||||||
|
buffer_basename(const Buffer &b)
|
||||||
|
{
|
||||||
|
const std::string &p = b.Filename();
|
||||||
|
if (p.empty())
|
||||||
|
return std::string("<untitled>");
|
||||||
|
auto pos = p.find_last_of("/\\");
|
||||||
|
if (pos == std::string::npos)
|
||||||
|
return p;
|
||||||
|
return p.substr(pos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_buffer_next(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
const auto cnt = ctx.editor.BufferCount();
|
||||||
|
if (cnt <= 1) {
|
||||||
|
ctx.editor.SetStatus("No other buffers open.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::size_t idx = ctx.editor.CurrentBufferIndex();
|
||||||
|
idx = (idx + 1) % cnt;
|
||||||
|
ctx.editor.SwitchTo(idx);
|
||||||
|
const Buffer *b = ctx.editor.CurrentBuffer();
|
||||||
|
ctx.editor.SetStatus(std::string("Switched: ") + (b ? buffer_display_name(*b) : std::string("")));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_buffer_prev(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
const auto cnt = ctx.editor.BufferCount();
|
||||||
|
if (cnt <= 1) {
|
||||||
|
ctx.editor.SetStatus("No other buffers open.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::size_t idx = ctx.editor.CurrentBufferIndex();
|
||||||
|
idx = (idx + cnt - 1) % cnt;
|
||||||
|
ctx.editor.SwitchTo(idx);
|
||||||
|
const Buffer *b = ctx.editor.CurrentBuffer();
|
||||||
|
ctx.editor.SetStatus(std::string("Switched: ") + (b ? buffer_display_name(*b) : std::string("")));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
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();
|
||||||
|
std::string name = b ? buffer_display_name(*b) : std::string("");
|
||||||
|
ctx.editor.CloseBuffer(idx);
|
||||||
|
if (ctx.editor.BufferCount() == 0) {
|
||||||
|
// Open a fresh empty buffer
|
||||||
|
Buffer empty;
|
||||||
|
ctx.editor.AddBuffer(std::move(empty));
|
||||||
|
ctx.editor.SwitchTo(0);
|
||||||
|
}
|
||||||
|
const Buffer *cur = ctx.editor.CurrentBuffer();
|
||||||
|
ctx.editor.SetStatus(std::string("Closed: ") + name + std::string(" Now: ")
|
||||||
|
+ (cur ? buffer_display_name(*cur) : std::string("")));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Editing ---
|
// --- Editing ---
|
||||||
static bool
|
static bool
|
||||||
cmd_insert_text(CommandContext &ctx)
|
cmd_insert_text(CommandContext &ctx)
|
||||||
@@ -468,6 +718,45 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
// 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
|
||||||
|
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::BufferSwitch && ctx.arg == "\t") {
|
||||||
|
// Complete against buffer names (path and basename)
|
||||||
|
const std::string prefix = ctx.editor.PromptText();
|
||||||
|
std::vector<std::pair<std::string, std::size_t> > cands; // name, index
|
||||||
|
const auto &bs = ctx.editor.Buffers();
|
||||||
|
for (std::size_t i = 0; i < bs.size(); ++i) {
|
||||||
|
std::string full = buffer_display_name(bs[i]);
|
||||||
|
std::string base = buffer_basename(bs[i]);
|
||||||
|
if (full.rfind(prefix, 0) == 0) {
|
||||||
|
cands.emplace_back(full, i);
|
||||||
|
}
|
||||||
|
if (base.rfind(prefix, 0) == 0 && base != full) {
|
||||||
|
cands.emplace_back(base, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cands.empty()) {
|
||||||
|
// no change
|
||||||
|
} else if (cands.size() == 1) {
|
||||||
|
ctx.editor.SetPromptText(cands[0].first);
|
||||||
|
} else {
|
||||||
|
// extend to longest common prefix
|
||||||
|
std::string lcp = cands[0].first;
|
||||||
|
for (std::size_t i = 1; i < cands.size(); ++i) {
|
||||||
|
const std::string &s = cands[i].first;
|
||||||
|
std::size_t j = 0;
|
||||||
|
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
|
||||||
|
++j;
|
||||||
|
lcp.resize(j);
|
||||||
|
if (lcp.empty())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!lcp.empty() && lcp != ctx.editor.PromptText())
|
||||||
|
ctx.editor.SetPromptText(lcp);
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.editor.AppendPromptText(ctx.arg);
|
ctx.editor.AppendPromptText(ctx.arg);
|
||||||
// If it's a search prompt, mirror text to search state
|
// If it's a search prompt, mirror text to search state
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
|
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
|
||||||
@@ -576,6 +865,35 @@ cmd_newline(CommandContext &ctx)
|
|||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus(std::string("Opened ") + value);
|
ctx.editor.SetStatus(std::string("Opened ") + value);
|
||||||
}
|
}
|
||||||
|
} else if (kind == Editor::PromptKind::BufferSwitch) {
|
||||||
|
// Resolve to a buffer index by exact match against path or basename;
|
||||||
|
// if multiple partial matches, prefer exact; if none, keep status.
|
||||||
|
const auto &bs = ctx.editor.Buffers();
|
||||||
|
std::vector<std::size_t> matches;
|
||||||
|
for (std::size_t i = 0; i < bs.size(); ++i) {
|
||||||
|
if (value == buffer_display_name(bs[i]) || value == buffer_basename(bs[i])) {
|
||||||
|
matches.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches.empty()) {
|
||||||
|
// Try prefix match if no exact
|
||||||
|
for (std::size_t i = 0; i < bs.size(); ++i) {
|
||||||
|
const std::string full = buffer_display_name(bs[i]);
|
||||||
|
const std::string base = buffer_basename(bs[i]);
|
||||||
|
if ((!value.empty() && full.rfind(value, 0) == 0) || (
|
||||||
|
!value.empty() && base.rfind(value, 0) == 0)) {
|
||||||
|
matches.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches.empty()) {
|
||||||
|
ctx.editor.SetStatus("No such buffer: " + value);
|
||||||
|
} else {
|
||||||
|
ctx.editor.SwitchTo(matches[0]);
|
||||||
|
const Buffer *cur = ctx.editor.CurrentBuffer();
|
||||||
|
ctx.editor.SetStatus(std::string("Switched: ")
|
||||||
|
+ (cur ? buffer_display_name(*cur) : std::string("")));
|
||||||
|
}
|
||||||
} else if (kind == Editor::PromptKind::SaveAs) {
|
} else if (kind == Editor::PromptKind::SaveAs) {
|
||||||
// Optional: not wired yet
|
// Optional: not wired yet
|
||||||
ctx.editor.SetStatus("Save As not implemented");
|
ctx.editor.SetStatus("Save As not implemented");
|
||||||
@@ -723,74 +1041,252 @@ cmd_delete_char(CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_kill_to_eol(CommandContext &ctx)
|
cmd_kill_to_eol(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
ctx.editor.SetStatus("No buffer to edit");
|
ctx.editor.SetStatus("No buffer to edit");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
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) {
|
std::string killed_total;
|
||||||
if (y >= rows.size())
|
for (int i = 0; i < repeat; ++i) {
|
||||||
break;
|
if (y >= rows.size())
|
||||||
if (x < rows[y].size()) {
|
break;
|
||||||
// delete from cursor to end of line
|
if (x < rows[y].size()) {
|
||||||
rows[y].erase(x);
|
// delete from cursor to end of line
|
||||||
} else if (y + 1 < rows.size()) {
|
killed_total += rows[y].substr(x);
|
||||||
// at EOL: delete the newline (join with next line)
|
rows[y].erase(x);
|
||||||
rows[y] += rows[y + 1];
|
} else if (y + 1 < rows.size()) {
|
||||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
// at EOL: delete the newline (join with next line)
|
||||||
} else {
|
killed_total += "\n";
|
||||||
// nothing to delete
|
rows[y] += rows[y + 1];
|
||||||
break;
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
||||||
}
|
} else {
|
||||||
}
|
// nothing to delete
|
||||||
buf->SetDirty(true);
|
break;
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
}
|
||||||
return true;
|
}
|
||||||
|
buf->SetDirty(true);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
if (!killed_total.empty()) {
|
||||||
|
if (ctx.editor.KillChain())
|
||||||
|
ctx.editor.KillRingAppend(killed_total);
|
||||||
|
else
|
||||||
|
ctx.editor.KillRingPush(killed_total);
|
||||||
|
ctx.editor.SetKillChain(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_kill_line(CommandContext &ctx)
|
cmd_kill_line(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
ctx.editor.SetStatus("No buffer to edit");
|
ctx.editor.SetStatus("No buffer to edit");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
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();
|
||||||
(void)x; // cursor x will be reset to 0
|
(void) x; // cursor x will be reset to 0
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
for (int i = 0; i < repeat; ++i) {
|
std::string killed_total;
|
||||||
if (rows.empty())
|
for (int i = 0; i < repeat; ++i) {
|
||||||
break;
|
if (rows.empty())
|
||||||
if (rows.size() == 1) {
|
break;
|
||||||
// last remaining line: clear its contents
|
if (rows.size() == 1) {
|
||||||
rows[0].clear();
|
// last remaining line: clear its contents
|
||||||
y = 0;
|
killed_total += rows[0];
|
||||||
} else if (y < rows.size()) {
|
rows[0].clear();
|
||||||
// erase current line; keep y pointing at the next line
|
y = 0;
|
||||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
|
} else if (y < rows.size()) {
|
||||||
if (y >= rows.size()) {
|
// erase current line; keep y pointing at the next line
|
||||||
// deleted last line; move to previous
|
killed_total += rows[y];
|
||||||
y = rows.size() - 1;
|
killed_total += "\n";
|
||||||
}
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
|
||||||
} else {
|
if (y >= rows.size()) {
|
||||||
// out of range
|
// deleted last line; move to previous
|
||||||
y = rows.empty() ? 0 : rows.size() - 1;
|
y = rows.size() - 1;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
buf->SetCursor(0, y);
|
// out of range
|
||||||
buf->SetDirty(true);
|
y = rows.empty() ? 0 : rows.size() - 1;
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
}
|
||||||
return true;
|
}
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
buf->SetDirty(true);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
if (!killed_total.empty()) {
|
||||||
|
if (ctx.editor.KillChain())
|
||||||
|
ctx.editor.KillRingAppend(killed_total);
|
||||||
|
else
|
||||||
|
ctx.editor.KillRingPush(killed_total);
|
||||||
|
ctx.editor.SetKillChain(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_yank(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf) {
|
||||||
|
ctx.editor.SetStatus("No buffer to edit");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string text = ctx.editor.KillRingHead();
|
||||||
|
if (text.empty()) {
|
||||||
|
ctx.editor.SetStatus("Kill ring is empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
insert_text_at_cursor(*buf, text);
|
||||||
|
}
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
// Start a new kill chain only from kill commands; yanking should break it
|
||||||
|
ctx.editor.SetKillChain(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Marks/Regions and File boundaries ---
|
||||||
|
static bool
|
||||||
|
cmd_move_file_start(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
buf->SetCursor(0, 0);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_move_file_end(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
auto &rows = buf->Rows();
|
||||||
|
std::size_t y = rows.empty() ? 0 : rows.size() - 1;
|
||||||
|
std::size_t x = rows.empty() ? 0 : rows[y].size();
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_toggle_mark(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
if (buf->MarkSet()) {
|
||||||
|
buf->ClearMark();
|
||||||
|
ctx.editor.SetStatus("Mark cleared");
|
||||||
|
} else {
|
||||||
|
buf->SetMark(buf->Curx(), buf->Cury());
|
||||||
|
ctx.editor.SetStatus("Mark set");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_jump_to_mark(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
if (!buf->MarkSet()) {
|
||||||
|
ctx.editor.SetStatus("Mark not set");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::size_t cx = buf->Curx();
|
||||||
|
std::size_t cy = buf->Cury();
|
||||||
|
std::size_t mx = buf->MarkCurx();
|
||||||
|
std::size_t my = buf->MarkCury();
|
||||||
|
buf->SetCursor(mx, my);
|
||||||
|
buf->SetMark(cx, cy);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_kill_region(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
std::size_t sx, sy, ex, ey;
|
||||||
|
if (!compute_mark_region(*buf, sx, sy, ex, ey)) {
|
||||||
|
ctx.editor.SetStatus("No region to kill");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string text = extract_region_text(*buf, sx, sy, ex, ey);
|
||||||
|
delete_region(*buf, sx, sy, ex, ey);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
if (!text.empty()) {
|
||||||
|
if (ctx.editor.KillChain())
|
||||||
|
ctx.editor.KillRingAppend(text);
|
||||||
|
else
|
||||||
|
ctx.editor.KillRingPush(text);
|
||||||
|
ctx.editor.SetKillChain(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf->ClearMark();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_copy_region(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
std::size_t sx, sy, ex, ey;
|
||||||
|
if (!compute_mark_region(*buf, sx, sy, ex, ey)) {
|
||||||
|
ctx.editor.SetStatus("No region to copy");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string text = extract_region_text(*buf, sx, sy, ex, ey);
|
||||||
|
if (!text.empty()) {
|
||||||
|
if (ctx.editor.KillChain())
|
||||||
|
ctx.editor.KillRingAppend(text);
|
||||||
|
else
|
||||||
|
ctx.editor.KillRingPush(text);
|
||||||
|
ctx.editor.SetKillChain(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf->ClearMark();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_flush_kill_ring(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
ctx.editor.KillRingClear();
|
||||||
|
ctx.editor.SetKillChain(false);
|
||||||
|
ctx.editor.SetStatus("Kill ring cleared");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1275,6 +1771,14 @@ InstallDefaultCommands()
|
|||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start
|
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start
|
||||||
});
|
});
|
||||||
|
// Buffers
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::BufferSwitchStart, "buffer-switch-start", "Begin buffer switch prompt",
|
||||||
|
cmd_buffer_switch_start
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({CommandId::BufferNext, "buffer-next", "Switch to next buffer", cmd_buffer_next});
|
||||||
|
CommandRegistry::Register({CommandId::BufferPrev, "buffer-prev", "Switch to previous buffer", cmd_buffer_prev});
|
||||||
|
CommandRegistry::Register({CommandId::BufferClose, "buffer-close", "Close current buffer", cmd_buffer_close});
|
||||||
// Editing
|
// Editing
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text
|
CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text
|
||||||
@@ -1284,6 +1788,21 @@ InstallDefaultCommands()
|
|||||||
CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char});
|
CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char});
|
||||||
CommandRegistry::Register({CommandId::KillToEOL, "kill-to-eol", "Delete to end of line", cmd_kill_to_eol});
|
CommandRegistry::Register({CommandId::KillToEOL, "kill-to-eol", "Delete to end of line", cmd_kill_to_eol});
|
||||||
CommandRegistry::Register({CommandId::KillLine, "kill-line", "Delete entire line", cmd_kill_line});
|
CommandRegistry::Register({CommandId::KillLine, "kill-line", "Delete entire line", cmd_kill_line});
|
||||||
|
CommandRegistry::Register({CommandId::Yank, "yank", "Yank from kill ring", cmd_yank});
|
||||||
|
// Marks/regions and file boundaries
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::MoveFileStart, "file-start", "Move to beginning of file", cmd_move_file_start
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({CommandId::MoveFileEnd, "file-end", "Move to end of file", cmd_move_file_end});
|
||||||
|
CommandRegistry::Register({CommandId::ToggleMark, "toggle-mark", "Toggle mark at cursor", cmd_toggle_mark});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::JumpToMark, "jump-to-mark", "Jump to mark (swap mark)", cmd_jump_to_mark
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({CommandId::KillRegion, "kill-region", "Kill region to kill ring", cmd_kill_region});
|
||||||
|
CommandRegistry::Register({CommandId::CopyRegion, "copy-region", "Copy region to kill ring", cmd_copy_region});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::FlushKillRing, "flush-kill-ring", "Flush kill ring", cmd_flush_kill_ring
|
||||||
|
});
|
||||||
// Navigation
|
// Navigation
|
||||||
CommandRegistry::Register({CommandId::MoveLeft, "left", "Move cursor left", cmd_move_left});
|
CommandRegistry::Register({CommandId::MoveLeft, "left", "Move cursor left", cmd_move_left});
|
||||||
CommandRegistry::Register({CommandId::MoveRight, "right", "Move cursor right", cmd_move_right});
|
CommandRegistry::Register({CommandId::MoveRight, "right", "Move cursor right", cmd_move_right});
|
||||||
@@ -1312,6 +1831,11 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count)
|
|||||||
if (ed.QuitConfirmPending() && id != CommandId::Quit && id != CommandId::KPrefix) {
|
if (ed.QuitConfirmPending() && id != CommandId::Quit && id != CommandId::KPrefix) {
|
||||||
ed.SetQuitConfirmPending(false);
|
ed.SetQuitConfirmPending(false);
|
||||||
}
|
}
|
||||||
|
// Reset kill chain unless this is a kill-like command (so consecutive kills append)
|
||||||
|
if (id != CommandId::KillToEOL && id != CommandId::KillLine && id != CommandId::KillRegion && id !=
|
||||||
|
CommandId::CopyRegion) {
|
||||||
|
ed.SetKillChain(false);
|
||||||
|
}
|
||||||
CommandContext ctx{ed, arg, count};
|
CommandContext ctx{ed, arg, count};
|
||||||
return cmd->handler ? cmd->handler(ctx) : false;
|
return cmd->handler ? cmd->handler(ctx) : false;
|
||||||
}
|
}
|
||||||
|
|||||||
16
Command.h
16
Command.h
@@ -24,13 +24,27 @@ enum class CommandId {
|
|||||||
KPrefix, // show "C-k _" prompt in status when entering k-command
|
KPrefix, // show "C-k _" prompt in status when entering k-command
|
||||||
FindStart, // begin incremental search (placeholder)
|
FindStart, // begin incremental search (placeholder)
|
||||||
OpenFileStart, // begin open-file prompt
|
OpenFileStart, // begin open-file prompt
|
||||||
|
// Buffers
|
||||||
|
BufferSwitchStart, // begin buffer switch prompt
|
||||||
|
BufferClose,
|
||||||
|
BufferNext,
|
||||||
|
BufferPrev,
|
||||||
// Editing
|
// Editing
|
||||||
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
|
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
|
||||||
Newline, // insert a newline at cursor
|
Newline, // insert a newline at cursor
|
||||||
Backspace, // delete char before cursor (may join lines)
|
Backspace, // delete char before cursor (may join lines)
|
||||||
DeleteChar, // delete char at cursor (may join lines)
|
DeleteChar, // delete char at cursor (may join lines)
|
||||||
KillToEOL, // delete from cursor to end of line; if at EOL, delete newline
|
KillToEOL, // delete from cursor to end of line; if at EOL, delete newline
|
||||||
KillLine, // delete the entire current line (including newline)
|
KillLine, // delete the entire current line (including newline)
|
||||||
|
Yank, // insert most recently killed text at cursor
|
||||||
|
// Region/mark and file-boundary navigation
|
||||||
|
MoveFileStart, // move to beginning of file
|
||||||
|
MoveFileEnd, // move to end of file
|
||||||
|
ToggleMark, // toggle mark at cursor
|
||||||
|
JumpToMark, // jump to mark, set mark to previous cursor
|
||||||
|
KillRegion, // kill region between mark and cursor (to kill ring)
|
||||||
|
CopyRegion, // copy region to kill ring (Alt-w)
|
||||||
|
FlushKillRing, // clear kill ring
|
||||||
// Navigation (basic)
|
// Navigation (basic)
|
||||||
MoveLeft,
|
MoveLeft,
|
||||||
MoveRight,
|
MoveRight,
|
||||||
|
|||||||
42
Editor.h
42
Editor.h
@@ -69,6 +69,42 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Kill ring API
|
||||||
|
void KillRingClear()
|
||||||
|
{
|
||||||
|
kill_ring_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void KillRingPush(const std::string &text)
|
||||||
|
{
|
||||||
|
if (text.empty())
|
||||||
|
return;
|
||||||
|
// push to front
|
||||||
|
kill_ring_.insert(kill_ring_.begin(), text);
|
||||||
|
if (kill_ring_.size() > kill_ring_max_)
|
||||||
|
kill_ring_.resize(kill_ring_max_);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void KillRingAppend(const std::string &text)
|
||||||
|
{
|
||||||
|
if (text.empty())
|
||||||
|
return;
|
||||||
|
if (kill_ring_.empty()) {
|
||||||
|
KillRingPush(text);
|
||||||
|
} else {
|
||||||
|
kill_ring_.front() += text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] std::string KillRingHead() const
|
||||||
|
{
|
||||||
|
return kill_ring_.empty() ? std::string() : kill_ring_.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetDirtyEx(int d)
|
void SetDirtyEx(int d)
|
||||||
{
|
{
|
||||||
dirtyex_ = d;
|
dirtyex_ = d;
|
||||||
@@ -254,7 +290,7 @@ public:
|
|||||||
|
|
||||||
|
|
||||||
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
||||||
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm };
|
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch };
|
||||||
|
|
||||||
|
|
||||||
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
||||||
@@ -382,6 +418,10 @@ private:
|
|||||||
std::vector<Buffer> buffers_;
|
std::vector<Buffer> buffers_;
|
||||||
std::size_t curbuf_ = 0; // index into buffers_
|
std::size_t curbuf_ = 0; // index into buffers_
|
||||||
|
|
||||||
|
// Kill ring (Emacs-like)
|
||||||
|
std::vector<std::string> kill_ring_;
|
||||||
|
std::size_t kill_ring_max_ = 60;
|
||||||
|
|
||||||
// Quit state
|
// Quit state
|
||||||
bool quit_requested_ = false;
|
bool quit_requested_ = false;
|
||||||
bool quit_confirm_pending_ = false;
|
bool quit_confirm_pending_ = false;
|
||||||
|
|||||||
@@ -5,12 +5,42 @@
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput &out)
|
map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, bool &esc_meta, MappedInput &out)
|
||||||
{
|
{
|
||||||
// Ctrl handling
|
// Ctrl handling
|
||||||
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
|
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
|
||||||
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||||
|
|
||||||
|
// If previous key was ESC, interpret this as Meta via ESC keymap
|
||||||
|
if (esc_meta) {
|
||||||
|
int ascii_key = 0;
|
||||||
|
if (key >= SDLK_a && key <= SDLK_z) {
|
||||||
|
ascii_key = static_cast<int>('a' + (key - SDLK_a));
|
||||||
|
} else if (key == SDLK_COMMA) {
|
||||||
|
ascii_key = '<';
|
||||||
|
} else if (key == SDLK_PERIOD) {
|
||||||
|
ascii_key = '>';
|
||||||
|
} else if (key == SDLK_LESS) {
|
||||||
|
ascii_key = '<';
|
||||||
|
} else if (key == SDLK_GREATER) {
|
||||||
|
ascii_key = '>';
|
||||||
|
}
|
||||||
|
if (ascii_key != 0) {
|
||||||
|
ascii_key = KLowerAscii(ascii_key);
|
||||||
|
CommandId id;
|
||||||
|
if (KLookupEscCommand(ascii_key, id)) {
|
||||||
|
// Only consume the ESC-meta prefix if we actually mapped a command
|
||||||
|
esc_meta = false;
|
||||||
|
out = {true, id, "", 0};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unhandled meta chord at KEYDOWN: do not clear esc_meta here.
|
||||||
|
// Leave it set so SDL_TEXTINPUT fallback can translate and suppress insertion.
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Movement and basic keys
|
// Movement and basic keys
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case SDLK_LEFT:
|
case SDLK_LEFT:
|
||||||
@@ -56,7 +86,8 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput
|
|||||||
return true;
|
return true;
|
||||||
case SDLK_ESCAPE:
|
case SDLK_ESCAPE:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::Refresh, "", 0};
|
esc_meta = true; // next key will be treated as Meta
|
||||||
|
out.hasCommand = false; // no immediate command for bare ESC in GUI
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -108,8 +139,19 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput
|
|||||||
|
|
||||||
// Alt/Meta bindings (ESC f/b equivalent)
|
// Alt/Meta bindings (ESC f/b equivalent)
|
||||||
if (is_alt) {
|
if (is_alt) {
|
||||||
|
int ascii_key = 0;
|
||||||
if (key >= SDLK_a && key <= SDLK_z) {
|
if (key >= SDLK_a && key <= SDLK_z) {
|
||||||
int ascii_key = static_cast<int>('a' + (key - SDLK_a));
|
ascii_key = static_cast<int>('a' + (key - SDLK_a));
|
||||||
|
} else if (key == SDLK_COMMA) {
|
||||||
|
ascii_key = '<';
|
||||||
|
} else if (key == SDLK_PERIOD) {
|
||||||
|
ascii_key = '>';
|
||||||
|
} else if (key == SDLK_LESS) {
|
||||||
|
ascii_key = '<';
|
||||||
|
} else if (key == SDLK_GREATER) {
|
||||||
|
ascii_key = '>';
|
||||||
|
}
|
||||||
|
if (ascii_key != 0) {
|
||||||
CommandId id;
|
CommandId id;
|
||||||
if (KLookupEscCommand(ascii_key, id)) {
|
if (KLookupEscCommand(ascii_key, id)) {
|
||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
@@ -133,13 +175,14 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
case SDL_KEYDOWN: {
|
case SDL_KEYDOWN: {
|
||||||
// Remember whether we were in k-prefix before handling this key
|
// Remember whether we were in k-prefix before handling this key
|
||||||
bool was_k_prefix = k_prefix_;
|
bool was_k_prefix = k_prefix_;
|
||||||
|
bool was_esc_meta = esc_meta_;
|
||||||
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
||||||
const SDL_Keycode key = e.key.keysym.sym;
|
const SDL_Keycode key = e.key.keysym.sym;
|
||||||
produced = map_key(key, mods, k_prefix_, mi);
|
produced = map_key(key, mods, k_prefix_, esc_meta_, mi);
|
||||||
// Suppress the immediate following SDL_TEXTINPUT only in cases where
|
// Suppress the immediate following SDL_TEXTINPUT only in cases where
|
||||||
// SDL would also emit a text input for the same physical keystroke:
|
// SDL would also emit a text input for the same physical keystroke:
|
||||||
// - k-prefix printable suffix keys (no Ctrl), and
|
// - k-prefix printable suffix keys (no Ctrl), and
|
||||||
// - Alt/Meta modified printable letters.
|
// - Alt/Meta modified printable letters (or ESC+letter/symbol).
|
||||||
// Do NOT suppress for non-text keys like Tab/Enter/Backspace/arrows/etc.,
|
// Do NOT suppress for non-text keys like Tab/Enter/Backspace/arrows/etc.,
|
||||||
// otherwise the next normal character would be dropped.
|
// otherwise the next normal character would be dropped.
|
||||||
if (produced && mi.hasCommand) {
|
if (produced && mi.hasCommand) {
|
||||||
@@ -159,7 +202,14 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
should_suppress = true;
|
should_suppress = true;
|
||||||
}
|
}
|
||||||
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
|
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
|
||||||
if (is_alt && key >= SDLK_a && key <= SDLK_z) {
|
const bool is_meta_symbol = (
|
||||||
|
key == SDLK_COMMA || key == SDLK_PERIOD || key == SDLK_LESS || key ==
|
||||||
|
SDLK_GREATER);
|
||||||
|
if (is_alt && ((key >= SDLK_a && key <= SDLK_z) || is_meta_symbol)) {
|
||||||
|
should_suppress = true;
|
||||||
|
}
|
||||||
|
// ESC-as-meta followed by printable
|
||||||
|
if (was_esc_meta && (is_printable_letter || is_meta_symbol)) {
|
||||||
should_suppress = true;
|
should_suppress = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +226,45 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
produced = true; // consumed input
|
produced = true; // consumed input
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Handle ESC-as-meta fallback on TEXTINPUT: some platforms emit only TEXTINPUT
|
||||||
|
// for the printable part after ESC. If esc_meta_ is set, translate first char.
|
||||||
|
if (esc_meta_) {
|
||||||
|
esc_meta_ = false; // consume meta prefix
|
||||||
|
const char *txt = e.text.text;
|
||||||
|
if (txt && *txt) {
|
||||||
|
// Parse first UTF-8 codepoint (we care only about common ASCII cases)
|
||||||
|
unsigned char c0 = static_cast<unsigned char>(txt[0]);
|
||||||
|
// Map a few common symbols/letters used in our ESC map
|
||||||
|
int ascii_key = 0;
|
||||||
|
if (c0 < 0x80) {
|
||||||
|
// ASCII path
|
||||||
|
ascii_key = static_cast<int>(c0);
|
||||||
|
ascii_key = KLowerAscii(ascii_key);
|
||||||
|
} else {
|
||||||
|
// Basic handling for macOS Option combos that might produce ≤/≥
|
||||||
|
// Compare the UTF-8 prefix for these two symbols
|
||||||
|
std::string s(txt);
|
||||||
|
if (s.rfind("\xE2\x89\xA4", 0) == 0) {
|
||||||
|
// U+2264 '≤'
|
||||||
|
ascii_key = '<';
|
||||||
|
} else if (s.rfind("\xE2\x89\xA5", 0) == 0) {
|
||||||
|
// U+2265 '≥'
|
||||||
|
ascii_key = '>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ascii_key != 0) {
|
||||||
|
CommandId id;
|
||||||
|
if (KLookupEscCommand(ascii_key, id)) {
|
||||||
|
mi = {true, id, "", 0};
|
||||||
|
produced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we get here, swallow the TEXTINPUT (do not insert stray char)
|
||||||
|
produced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (!k_prefix_ && e.text.text[0] != '\0') {
|
if (!k_prefix_ && e.text.text[0] != '\0') {
|
||||||
mi.hasCommand = true;
|
mi.hasCommand = true;
|
||||||
mi.id = CommandId::InsertText;
|
mi.id = CommandId::InsertText;
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ private:
|
|||||||
std::mutex mu_;
|
std::mutex mu_;
|
||||||
std::queue<MappedInput> q_;
|
std::queue<MappedInput> q_;
|
||||||
bool k_prefix_ = false;
|
bool k_prefix_ = false;
|
||||||
|
// Treat ESC as a Meta prefix: next key is looked up via ESC keymap
|
||||||
|
bool esc_meta_ = false;
|
||||||
// When a printable keydown generated a non-text command, suppress the very next SDL_TEXTINPUT
|
// When a printable keydown generated a non-text command, suppress the very next SDL_TEXTINPUT
|
||||||
// event produced by SDL for the same keystroke to avoid inserting stray characters.
|
// event produced by SDL for the same keystroke to avoid inserting stray characters.
|
||||||
bool suppress_text_input_once_ = false;
|
bool suppress_text_input_once_ = false;
|
||||||
|
|||||||
39
KKeymap.cc
39
KKeymap.cc
@@ -23,21 +23,42 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (k) {
|
switch (k) {
|
||||||
|
case 'j':
|
||||||
|
out = CommandId::JumpToMark;
|
||||||
|
return true; // C-k j
|
||||||
|
case 'f':
|
||||||
|
out = CommandId::FlushKillRing;
|
||||||
|
return true; // C-k f
|
||||||
case 'd':
|
case 'd':
|
||||||
out = CommandId::KillToEOL;
|
out = CommandId::KillToEOL;
|
||||||
return true; // C-k d
|
return true; // C-k d
|
||||||
|
case 'y':
|
||||||
|
out = CommandId::Yank;
|
||||||
|
return true; // C-k y
|
||||||
case 's':
|
case 's':
|
||||||
out = CommandId::Save;
|
out = CommandId::Save;
|
||||||
return true; // C-k s
|
return true; // C-k s
|
||||||
case 'e':
|
case 'e':
|
||||||
out = CommandId::OpenFileStart;
|
out = CommandId::OpenFileStart;
|
||||||
return true; // C-k e (open file)
|
return true; // C-k e (open file)
|
||||||
|
case 'b':
|
||||||
|
out = CommandId::BufferSwitchStart;
|
||||||
|
return true; // C-k b (switch buffer by name)
|
||||||
|
case 'c':
|
||||||
|
out = CommandId::BufferClose;
|
||||||
|
return true; // C-k c (close current buffer)
|
||||||
|
case 'n':
|
||||||
|
out = CommandId::BufferPrev;
|
||||||
|
return true; // C-k n (switch to previous buffer)
|
||||||
case 'x':
|
case 'x':
|
||||||
out = CommandId::SaveAndQuit;
|
out = CommandId::SaveAndQuit;
|
||||||
return true; // C-k x
|
return true; // C-k x
|
||||||
case 'q':
|
case 'q':
|
||||||
out = CommandId::Quit;
|
out = CommandId::Quit;
|
||||||
return true; // C-k q
|
return true; // C-k q
|
||||||
|
case 'p':
|
||||||
|
out = CommandId::BufferNext;
|
||||||
|
return true; // C-k p (switch to next buffer)
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -51,6 +72,12 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
{
|
{
|
||||||
const int k = KLowerAscii(ascii_key);
|
const int k = KLowerAscii(ascii_key);
|
||||||
switch (k) {
|
switch (k) {
|
||||||
|
case 'w':
|
||||||
|
out = CommandId::KillRegion; // C-w
|
||||||
|
return true;
|
||||||
|
case 'y':
|
||||||
|
out = CommandId::Yank; // C-y
|
||||||
|
return true;
|
||||||
case 'd':
|
case 'd':
|
||||||
out = CommandId::DeleteChar; // C-d
|
out = CommandId::DeleteChar; // C-d
|
||||||
return true;
|
return true;
|
||||||
@@ -96,6 +123,18 @@ KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
{
|
{
|
||||||
const int k = KLowerAscii(ascii_key);
|
const int k = KLowerAscii(ascii_key);
|
||||||
switch (k) {
|
switch (k) {
|
||||||
|
case '<':
|
||||||
|
out = CommandId::MoveFileStart; // Esc <
|
||||||
|
return true;
|
||||||
|
case '>':
|
||||||
|
out = CommandId::MoveFileEnd; // Esc >
|
||||||
|
return true;
|
||||||
|
case 'm':
|
||||||
|
out = CommandId::ToggleMark; // Esc m
|
||||||
|
return true;
|
||||||
|
case 'w':
|
||||||
|
out = CommandId::CopyRegion; // Esc w (Alt-w)
|
||||||
|
return true;
|
||||||
case 'b':
|
case 'b':
|
||||||
out = CommandId::WordPrev;
|
out = CommandId::WordPrev;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -10,11 +10,27 @@
|
|||||||
bool
|
bool
|
||||||
TerminalFrontend::Init(Editor &ed)
|
TerminalFrontend::Init(Editor &ed)
|
||||||
{
|
{
|
||||||
// Ensure Ctrl-S/Ctrl-Q reach the application by disabling XON/XOFF flow control
|
// Ensure Control keys reach the app: disable XON/XOFF and dsusp/susp bindings (e.g., ^S/^Q, ^Y on macOS)
|
||||||
{
|
{
|
||||||
struct termios tio{};
|
struct termios tio{};
|
||||||
if (tcgetattr(STDIN_FILENO, &tio) == 0) {
|
if (tcgetattr(STDIN_FILENO, &tio) == 0) {
|
||||||
|
// Save original to restore on shutdown
|
||||||
|
orig_tio_ = tio;
|
||||||
|
have_orig_tio_ = true;
|
||||||
|
// Disable software flow control so C-s/C-q work
|
||||||
tio.c_iflag &= static_cast<unsigned long>(~IXON);
|
tio.c_iflag &= static_cast<unsigned long>(~IXON);
|
||||||
|
#ifdef IXOFF
|
||||||
|
tio.c_iflag &= static_cast<unsigned long>(~IXOFF);
|
||||||
|
#endif
|
||||||
|
// Disable dsusp/susp characters so C-y (VDSUSP on macOS) and C-z don't signal-stop the app
|
||||||
|
#ifdef _POSIX_VDISABLE
|
||||||
|
#ifdef VSUSP
|
||||||
|
tio.c_cc[VSUSP] = _POSIX_VDISABLE;
|
||||||
|
#endif
|
||||||
|
#ifdef VDSUSP
|
||||||
|
tio.c_cc[VDSUSP] = _POSIX_VDISABLE;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
(void) tcsetattr(STDIN_FILENO, TCSANOW, &tio);
|
(void) tcsetattr(STDIN_FILENO, TCSANOW, &tio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,5 +94,10 @@ TerminalFrontend::Step(Editor &ed, bool &running)
|
|||||||
void
|
void
|
||||||
TerminalFrontend::Shutdown()
|
TerminalFrontend::Shutdown()
|
||||||
{
|
{
|
||||||
|
// Restore original terminal settings if we changed them
|
||||||
|
if (have_orig_tio_) {
|
||||||
|
(void) tcsetattr(STDIN_FILENO, TCSANOW, &orig_tio_);
|
||||||
|
have_orig_tio_ = false;
|
||||||
|
}
|
||||||
endwin();
|
endwin();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "Frontend.h"
|
#include "Frontend.h"
|
||||||
#include "TerminalInputHandler.h"
|
#include "TerminalInputHandler.h"
|
||||||
#include "TerminalRenderer.h"
|
#include "TerminalRenderer.h"
|
||||||
|
#include <termios.h>
|
||||||
|
|
||||||
|
|
||||||
class TerminalFrontend final : public Frontend {
|
class TerminalFrontend final : public Frontend {
|
||||||
@@ -26,6 +27,9 @@ private:
|
|||||||
TerminalRenderer renderer_{};
|
TerminalRenderer renderer_{};
|
||||||
int prev_r_ = 0;
|
int prev_r_ = 0;
|
||||||
int prev_c_ = 0;
|
int prev_c_ = 0;
|
||||||
|
// Saved terminal attributes to restore on shutdown
|
||||||
|
bool have_orig_tio_ = false;
|
||||||
|
struct termios orig_tio_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_TERMINAL_FRONTEND_H
|
#endif // KTE_TERMINAL_FRONTEND_H
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
|||||||
// IMPORTANT: if we're in k-prefix, the very next key must be interpreted
|
// IMPORTANT: if we're in k-prefix, the very next key must be interpreted
|
||||||
// via the C-k keymap first, even if it's a Control chord like C-d.
|
// via the C-k keymap first, even if it's a Control chord like C-d.
|
||||||
if (k_prefix) {
|
if (k_prefix) {
|
||||||
k_prefix = false; // consume the prefix for this one key
|
k_prefix = false; // consume the prefix for this one key
|
||||||
bool ctrl = false;
|
bool ctrl = false;
|
||||||
int ascii_key = ch;
|
int ascii_key = ch;
|
||||||
if (ch >= 1 && ch <= 26) {
|
if (ch >= 1 && ch <= 26) {
|
||||||
|
|||||||
15
UndoNode.cc
Normal file
15
UndoNode.cc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
UndoNode.h
Normal file
83
UndoNode.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#ifndef KTE_UNDONODE_H
|
||||||
|
#define KTE_UNDONODE_H
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
|
||||||
|
enum UndoKind {
|
||||||
|
UNDO_INSERT,
|
||||||
|
UNDO_DELETE,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif // KTE_UNDONODE_H
|
||||||
47
UndoTree.cc
Normal file
47
UndoTree.cc
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
20
UndoTree.h
Normal file
20
UndoTree.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#ifndef KTE_UNDOTREE_H
|
||||||
|
#define KTE_UNDOTREE_H
|
||||||
|
|
||||||
|
#include "UndoNode.h"
|
||||||
|
|
||||||
|
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};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif // KTE_UNDOTREE_H
|
||||||
Reference in New Issue
Block a user