Add non-linear undo/redo design documentation and improve UndoSystem with backspace batching and GUI integration fixes.
This commit is contained in:
2
.idea/kte.iml
generated
2
.idea/kte.iml
generated
@@ -2,7 +2,7 @@
|
||||
<module classpath="CMake" type="CPP_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="Python" name="Python facet">
|
||||
<configuration sdkName="Python 3.14 (kte)" />
|
||||
<configuration sdkName="" />
|
||||
</facet>
|
||||
</component>
|
||||
</module>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -4,7 +4,7 @@
|
||||
<option name="sdkName" value="Python 3.14 (kte)" />
|
||||
</component>
|
||||
<component name="CMakePythonSetting">
|
||||
<option name="pythonIntegrationState" value="YES" />
|
||||
<option name="pythonIntegrationState" value="NO" />
|
||||
</component>
|
||||
<component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
|
||||
</project>
|
||||
63
.idea/workspace.xml
generated
63
.idea/workspace.xml
generated
@@ -23,35 +23,31 @@
|
||||
<component name="CMakeRunConfigurationManager">
|
||||
<generated>
|
||||
<config projectName="kte" targetName="kte" />
|
||||
<config projectName="kte" targetName="imgui" />
|
||||
<config projectName="kte" targetName="kge" />
|
||||
</generated>
|
||||
</component>
|
||||
<component name="CMakeSettings" AUTO_RELOAD="true">
|
||||
<configurations>
|
||||
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G "Unix Makefiles" -DKTE_USE_PIECE_TABLE:BOOL=ON" />
|
||||
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G "Unix Makefiles" -DKTE_USE_PIECE_TABLE:BOOL=ON -DBUILD_GUI:BOOL=ON" />
|
||||
</configurations>
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<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" />
|
||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity.">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/kte.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/kte.iml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Editor.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.cc" 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$/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" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.h" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/UndoSystem.cc" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/UndoSystem.h" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.h" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -119,7 +115,7 @@
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
|
||||
"rearrange.code.on.save": "true",
|
||||
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
|
||||
"settings.editor.selected.configurable": "junie.application.models",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
@@ -129,17 +125,32 @@
|
||||
<recent name="$PROJECT_DIR$/docs" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<component name="RunManager" selected="CMake Application.kte">
|
||||
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="imgui" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="imgui" CONFIG_NAME="Debug">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<list>
|
||||
<item itemvalue="CMake Application.imgui" />
|
||||
<item itemvalue="CMake Application.kge" />
|
||||
<item itemvalue="CMake Application.kte" />
|
||||
</list>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
@@ -148,7 +159,7 @@
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1764457173148</updated>
|
||||
<workItem from="1764457174208" duration="31043000" />
|
||||
<workItem from="1764457174208" duration="37384000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||
<option name="closed" value="true" />
|
||||
@@ -174,7 +185,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764486876984</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<task id="LOCAL-00004" summary="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764489870957</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764489870957</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="5" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -190,7 +209,9 @@
|
||||
<MESSAGE value="Add undo/redo infrastructure and buffer management additions." />
|
||||
<MESSAGE value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation." />
|
||||
<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." />
|
||||
<MESSAGE value="Enable installation targets." />
|
||||
<MESSAGE value="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity." />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
|
||||
48
Command.cc
48
Command.cc
@@ -83,6 +83,16 @@ ensure_at_least_one_line(Buffer &buf)
|
||||
}
|
||||
|
||||
|
||||
// --- UI/status helpers ---
|
||||
static bool
|
||||
cmd_uarg_status(CommandContext &ctx)
|
||||
{
|
||||
// ctx.arg should contain the digits/minus entered so far (may be empty)
|
||||
ctx.editor.SetStatus(std::string("C-u ") + ctx.arg);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
@@ -566,6 +576,11 @@ cmd_refresh(CommandContext &ctx)
|
||||
static bool
|
||||
cmd_kprefix(CommandContext &ctx)
|
||||
{
|
||||
// Close any pending edit batch before entering k-prefix
|
||||
if (Buffer *b = ctx.editor.CurrentBuffer()) {
|
||||
if (auto *u = b->Undo())
|
||||
u->commit();
|
||||
}
|
||||
// Show k-command mode hint in status
|
||||
ctx.editor.SetStatus("C-k _");
|
||||
return true;
|
||||
@@ -998,14 +1013,26 @@ cmd_backspace(CommandContext &ctx)
|
||||
auto &rows = buf->Rows();
|
||||
std::size_t y = buf->Cury();
|
||||
std::size_t x = buf->Curx();
|
||||
UndoSystem *u = buf->Undo();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
for (int i = 0; i < repeat; ++i) {
|
||||
if (x > 0) {
|
||||
// Batch contiguous character deletes (backspace)
|
||||
if (u)
|
||||
u->Begin(UndoType::Delete);
|
||||
char deleted = rows[y][x - 1];
|
||||
rows[y].erase(x - 1, 1);
|
||||
--x;
|
||||
if (u)
|
||||
u->Append(deleted);
|
||||
} else if (y > 0) {
|
||||
// join with previous line
|
||||
std::size_t prev_len = rows[y - 1].size();
|
||||
if (u) {
|
||||
// Record a newline deletion that joined lines; commit immediately
|
||||
u->Begin(UndoType::Newline);
|
||||
u->commit();
|
||||
}
|
||||
rows[y - 1] += rows[y];
|
||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
|
||||
y = y - 1;
|
||||
@@ -1034,14 +1061,26 @@ cmd_delete_char(CommandContext &ctx)
|
||||
auto &rows = buf->Rows();
|
||||
std::size_t y = buf->Cury();
|
||||
std::size_t x = buf->Curx();
|
||||
UndoSystem *u = buf->Undo();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
for (int i = 0; i < repeat; ++i) {
|
||||
if (y >= rows.size())
|
||||
break;
|
||||
if (x < rows[y].size()) {
|
||||
// Forward delete at cursor, batch contiguous
|
||||
if (u)
|
||||
u->Begin(UndoType::Delete);
|
||||
char deleted = rows[y][x];
|
||||
rows[y].erase(x, 1);
|
||||
if (u)
|
||||
u->Append(deleted);
|
||||
} else if (y + 1 < rows.size()) {
|
||||
// join next line
|
||||
if (u) {
|
||||
// Record newline deletion at end of this line; commit immediately
|
||||
u->Begin(UndoType::Newline);
|
||||
u->commit();
|
||||
}
|
||||
rows[y] += rows[y + 1];
|
||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
||||
} else {
|
||||
@@ -1062,9 +1101,12 @@ cmd_undo(CommandContext &ctx)
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo()) {
|
||||
// Ensure pending batch is finalized so it can be undone
|
||||
u->commit();
|
||||
u->undo();
|
||||
// Keep cursor within buffer bounds
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
ctx.editor.SetStatus("Undone");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1078,8 +1120,11 @@ cmd_redo(CommandContext &ctx)
|
||||
if (!buf)
|
||||
return false;
|
||||
if (auto *u = buf->Undo()) {
|
||||
// Finalize any pending batch before redoing
|
||||
u->commit();
|
||||
u->redo();
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
ctx.editor.SetStatus("Redone");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1888,6 +1933,9 @@ InstallDefaultCommands()
|
||||
// Undo/Redo
|
||||
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo});
|
||||
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo});
|
||||
// UI helpers
|
||||
CommandRegistry::Register(
|
||||
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ enum class CommandId {
|
||||
// Undo/Redo
|
||||
Undo,
|
||||
Redo,
|
||||
// UI/status helpers
|
||||
UArgStatus, // update status line during universal-argument collection
|
||||
// Meta
|
||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||
};
|
||||
|
||||
18
Editor.cc
18
Editor.cc
@@ -68,11 +68,27 @@ Editor::AddBuffer(Buffer &&buf)
|
||||
bool
|
||||
Editor::OpenFile(const std::string &path, std::string &err)
|
||||
{
|
||||
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
||||
// of creating a new one.
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const auto &rows = cur.Rows();
|
||||
const bool rows_empty = rows.empty();
|
||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||
return cur.OpenFromFile(path, err);
|
||||
}
|
||||
}
|
||||
|
||||
Buffer b;
|
||||
if (!b.OpenFromFile(path, err)) {
|
||||
return false;
|
||||
}
|
||||
AddBuffer(std::move(b));
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
SwitchTo(idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,14 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
if (!input_.Poll(mi))
|
||||
break;
|
||||
if (mi.hasCommand) {
|
||||
// Track kill ring before and after to sync GUI clipboard when it changes
|
||||
const std::string before = ed.KillRingHead();
|
||||
Execute(ed, mi.id, mi.arg, mi.count);
|
||||
const std::string after = ed.KillRingHead();
|
||||
if (after != before && !after.empty()) {
|
||||
// Update the system clipboard to mirror the kill ring head in GUI
|
||||
SDL_SetClipboardText(after.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
#include <SDL.h>
|
||||
#include <cstdio>
|
||||
|
||||
#include "GUIInputHandler.h"
|
||||
#include "KKeymap.h"
|
||||
|
||||
|
||||
static bool
|
||||
map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, bool &esc_meta, MappedInput &out)
|
||||
map_key(const SDL_Keycode key,
|
||||
const SDL_Keymod mod,
|
||||
bool &k_prefix,
|
||||
bool &esc_meta,
|
||||
// universal-argument state (by ref)
|
||||
bool &uarg_active,
|
||||
bool &uarg_collecting,
|
||||
bool &uarg_negative,
|
||||
bool &uarg_had_digits,
|
||||
int &uarg_value,
|
||||
std::string &uarg_text,
|
||||
MappedInput &out)
|
||||
{
|
||||
// Ctrl handling
|
||||
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
|
||||
@@ -86,32 +98,66 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, bool &esc_m
|
||||
return true;
|
||||
case SDLK_ESCAPE:
|
||||
k_prefix = false;
|
||||
esc_meta = true; // next key will be treated as Meta
|
||||
esc_meta = true; // next key will be treated as Meta
|
||||
// Cancel any universal argument collection
|
||||
uarg_active = false;
|
||||
uarg_collecting = false;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 0;
|
||||
uarg_text.clear();
|
||||
out.hasCommand = false; // no immediate command for bare ESC in GUI
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// If we are in k-prefix, the very next key must be interpreted via the
|
||||
// C-k keymap first, even if Control is held (e.g., C-k C-d).
|
||||
// If we are in k-prefix, interpret the very next key via the C-k keymap immediately.
|
||||
if (k_prefix) {
|
||||
k_prefix = false;
|
||||
// Normalize SDL key to ASCII where possible
|
||||
esc_meta = false;
|
||||
// Normalize to ASCII; preserve case for letters using Shift
|
||||
int ascii_key = 0;
|
||||
if (key >= SDLK_SPACE && key <= SDLK_z) {
|
||||
if (key >= SDLK_a && key <= SDLK_z) {
|
||||
ascii_key = static_cast<int>('a' + (key - SDLK_a));
|
||||
if (mod & KMOD_SHIFT)
|
||||
ascii_key = ascii_key - 'a' + '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 = '>';
|
||||
} else if (key >= SDLK_SPACE && key <= SDLK_z) {
|
||||
ascii_key = static_cast<int>(key);
|
||||
}
|
||||
bool ctrl2 = (mod & KMOD_CTRL) != 0;
|
||||
if (ascii_key != 0) {
|
||||
ascii_key = KLowerAscii(ascii_key);
|
||||
int lower = KLowerAscii(ascii_key);
|
||||
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
|
||||
bool pass_ctrl = ctrl2 && ctrl_suffix_supported;
|
||||
CommandId id;
|
||||
if (KLookupKCommand(ascii_key, ctrl2, id)) {
|
||||
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||
// Diagnostics for u/U
|
||||
if (lower == 'u') {
|
||||
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
||||
? static_cast<char>(ascii_key)
|
||||
: '?';
|
||||
std::fprintf(stderr,
|
||||
"[kge] k-prefix suffix: sym=%d mods=0x%x ascii=%d '%c' ctrl2=%d pass_ctrl=%d mapped=%d id=%d\n",
|
||||
static_cast<int>(key), static_cast<unsigned int>(mod), ascii_key, disp,
|
||||
ctrl2 ? 1 : 0, pass_ctrl ? 1 : 0, mapped ? 1 : 0,
|
||||
mapped ? static_cast<int>(id) : -1);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
if (mapped) {
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Unknown k-command: report the typed character
|
||||
char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast<char>(ascii_key) : '?';
|
||||
int shown = KLowerAscii(ascii_key);
|
||||
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
|
||||
std::string arg(1, c);
|
||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
return true;
|
||||
@@ -121,6 +167,42 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, bool &esc_m
|
||||
}
|
||||
|
||||
if (is_ctrl) {
|
||||
// Universal argument: C-u
|
||||
if (key == SDLK_u) {
|
||||
if (!uarg_active) {
|
||||
uarg_active = true;
|
||||
uarg_collecting = true;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 4; // default
|
||||
uarg_text.clear();
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
} else if (uarg_collecting && !uarg_had_digits && !uarg_negative) {
|
||||
if (uarg_value <= 0)
|
||||
uarg_value = 4;
|
||||
else
|
||||
uarg_value *= 4; // repeated C-u multiplies by 4
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
} else {
|
||||
// End collection if already started with digits or '-'
|
||||
uarg_collecting = false;
|
||||
if (!uarg_had_digits && !uarg_negative && uarg_value <= 0)
|
||||
uarg_value = 4;
|
||||
}
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
// Cancel universal arg on C-g as well (it maps to Refresh via ctrl map)
|
||||
if (key == SDLK_g) {
|
||||
uarg_active = false;
|
||||
uarg_collecting = false;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 0;
|
||||
uarg_text.clear();
|
||||
}
|
||||
if (key == SDLK_k || key == SDLK_KP_EQUALS) {
|
||||
k_prefix = true;
|
||||
out = {true, CommandId::KPrefix, "", 0};
|
||||
@@ -160,6 +242,31 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, bool &esc_m
|
||||
}
|
||||
}
|
||||
|
||||
// If collecting universal argument, allow digits/minus on KEYDOWN path too
|
||||
if (uarg_active && uarg_collecting) {
|
||||
if ((key >= SDLK_0 && key <= SDLK_9) && !(mod & KMOD_SHIFT)) {
|
||||
int d = static_cast<int>(key - SDLK_0);
|
||||
if (!uarg_had_digits) {
|
||||
uarg_value = 0;
|
||||
uarg_had_digits = true;
|
||||
}
|
||||
if (uarg_value < 100000000) {
|
||||
uarg_value = uarg_value * 10 + d;
|
||||
}
|
||||
uarg_text.push_back(static_cast<char>('0' + d));
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
}
|
||||
if (key == SDLK_MINUS && !uarg_had_digits && !uarg_negative) {
|
||||
uarg_negative = true;
|
||||
uarg_text = "-";
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
}
|
||||
// Any other key will end collection; process it normally
|
||||
uarg_collecting = false;
|
||||
}
|
||||
|
||||
// k_prefix handled earlier
|
||||
|
||||
return false;
|
||||
@@ -173,59 +280,187 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
bool produced = false;
|
||||
switch (e.type) {
|
||||
case SDL_KEYDOWN: {
|
||||
// Remember whether we were in k-prefix before handling this key
|
||||
bool was_k_prefix = k_prefix_;
|
||||
bool was_esc_meta = esc_meta_;
|
||||
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
||||
const SDL_Keycode key = e.key.keysym.sym;
|
||||
produced = map_key(key, mods, k_prefix_, esc_meta_, mi);
|
||||
// Suppress the immediate following SDL_TEXTINPUT only in cases where
|
||||
// SDL would also emit a text input for the same physical keystroke:
|
||||
// - k-prefix printable suffix keys (no Ctrl), and
|
||||
// - Alt/Meta modified printable letters (or ESC+letter/symbol).
|
||||
// Do NOT suppress for non-text keys like Tab/Enter/Backspace/arrows/etc.,
|
||||
// otherwise the next normal character would be dropped.
|
||||
if (produced && mi.hasCommand) {
|
||||
const bool is_ctrl = (mods & KMOD_CTRL) != 0;
|
||||
const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||
const bool is_printable_letter = (key >= SDLK_SPACE && key <= SDLK_z);
|
||||
const bool is_non_text_key =
|
||||
key == SDLK_TAB || key == SDLK_RETURN || key == SDLK_KP_ENTER ||
|
||||
key == SDLK_BACKSPACE || key == SDLK_DELETE || key == SDLK_ESCAPE ||
|
||||
key == SDLK_LEFT || key == SDLK_RIGHT || key == SDLK_UP || key == SDLK_DOWN ||
|
||||
key == SDLK_HOME || key == SDLK_END || key == SDLK_PAGEUP || key == SDLK_PAGEDOWN;
|
||||
// Remember state before mapping; used for TEXTINPUT suppression heuristics
|
||||
const bool was_k_prefix = k_prefix_;
|
||||
const bool was_esc_meta = esc_meta_;
|
||||
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
||||
const SDL_Keycode key = e.key.keysym.sym;
|
||||
|
||||
bool should_suppress = false;
|
||||
if (!is_non_text_key) {
|
||||
// k-prefix then a printable key normally generates TEXTINPUT
|
||||
if (was_k_prefix && is_printable_letter && !is_ctrl) {
|
||||
should_suppress = true;
|
||||
}
|
||||
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
|
||||
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;
|
||||
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
|
||||
// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
|
||||
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
|
||||
char *clip = SDL_GetClipboardText();
|
||||
if (clip) {
|
||||
std::string text(clip);
|
||||
SDL_free(clip);
|
||||
// Split on '\n' and enqueue as InsertText/Newline commands
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
std::size_t start = 0;
|
||||
while (start <= text.size()) {
|
||||
std::size_t pos = text.find('\n', start);
|
||||
std::string_view segment;
|
||||
bool has_nl = (pos != std::string::npos);
|
||||
if (has_nl) {
|
||||
segment = std::string_view(text).substr(start, pos - start);
|
||||
} else {
|
||||
segment = std::string_view(text).substr(start);
|
||||
}
|
||||
if (!segment.empty()) {
|
||||
MappedInput ins{true, CommandId::InsertText, std::string(segment), 0};
|
||||
q_.push(ins);
|
||||
}
|
||||
if (has_nl) {
|
||||
MappedInput nl{true, CommandId::Newline, std::string(), 0};
|
||||
q_.push(nl);
|
||||
start = pos + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Suppress the corresponding TEXTINPUT that may follow
|
||||
suppress_text_input_once_ = true;
|
||||
return true; // consumed
|
||||
}
|
||||
if (should_suppress) {
|
||||
}
|
||||
|
||||
produced = map_key(key, mods,
|
||||
k_prefix_, esc_meta_,
|
||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
|
||||
uarg_text_,
|
||||
mi);
|
||||
|
||||
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
|
||||
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
||||
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
|
||||
// Digits without shift, or a plain '-'
|
||||
const bool is_digit_key = (key >= SDLK_0 && key <= SDLK_9) && !(mods & KMOD_SHIFT);
|
||||
const bool is_minus_key = (key == SDLK_MINUS);
|
||||
if (uarg_active_ && uarg_collecting_ && (is_digit_key || is_minus_key)) {
|
||||
suppress_text_input_once_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress the immediate following SDL_TEXTINPUT when a printable KEYDOWN was used as a
|
||||
// k-prefix suffix or Meta (Alt/ESC) chord, regardless of whether a command was produced.
|
||||
const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||
const bool is_printable_letter = (key >= SDLK_SPACE && key <= SDLK_z);
|
||||
const bool is_non_text_key =
|
||||
key == SDLK_TAB || key == SDLK_RETURN || key == SDLK_KP_ENTER ||
|
||||
key == SDLK_BACKSPACE || key == SDLK_DELETE || key == SDLK_ESCAPE ||
|
||||
key == SDLK_LEFT || key == SDLK_RIGHT || key == SDLK_UP || key == SDLK_DOWN ||
|
||||
key == SDLK_HOME || key == SDLK_END || key == SDLK_PAGEUP || key == SDLK_PAGEDOWN;
|
||||
|
||||
bool should_suppress = false;
|
||||
if (!is_non_text_key) {
|
||||
// Any k-prefix suffix that is printable should suppress TEXTINPUT, even if no
|
||||
// command mapped (we report unknown via status instead of inserting text).
|
||||
if (was_k_prefix && is_printable_letter) {
|
||||
should_suppress = true;
|
||||
}
|
||||
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (should_suppress) {
|
||||
suppress_text_input_once_ = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SDL_TEXTINPUT:
|
||||
// Ignore text input while in k-prefix, or once after a command-producing keydown
|
||||
case SDL_TEXTINPUT: {
|
||||
// If the previous KEYDOWN requested suppression of this TEXTINPUT (e.g.,
|
||||
// we already handled a uarg digit/minus or a k-prefix printable), do it
|
||||
// immediately before any other handling to avoid duplicates.
|
||||
if (suppress_text_input_once_) {
|
||||
suppress_text_input_once_ = false; // consume suppression
|
||||
produced = true; // consumed input
|
||||
produced = true; // consumed event
|
||||
break;
|
||||
}
|
||||
|
||||
// If universal argument collection is active, consume digit/minus TEXTINPUT
|
||||
if (uarg_active_ && uarg_collecting_) {
|
||||
const char *txt = e.text.text;
|
||||
if (txt && *txt) {
|
||||
unsigned char c0 = static_cast<unsigned char>(txt[0]);
|
||||
if (c0 >= '0' && c0 <= '9') {
|
||||
int d = c0 - '0';
|
||||
if (!uarg_had_digits_) {
|
||||
uarg_value_ = 0;
|
||||
uarg_had_digits_ = true;
|
||||
}
|
||||
if (uarg_value_ < 100000000) {
|
||||
uarg_value_ = uarg_value_ * 10 + d;
|
||||
}
|
||||
uarg_text_.push_back(static_cast<char>(c0));
|
||||
mi = {true, CommandId::UArgStatus, uarg_text_, 0};
|
||||
produced = true; // consumed and enqueued status update
|
||||
break;
|
||||
}
|
||||
if (c0 == '-' && !uarg_had_digits_ && !uarg_negative_) {
|
||||
uarg_negative_ = true;
|
||||
uarg_text_ = "-";
|
||||
mi = {true, CommandId::UArgStatus, uarg_text_, 0};
|
||||
produced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// End collection and allow this TEXTINPUT to be processed normally below
|
||||
uarg_collecting_ = false;
|
||||
}
|
||||
|
||||
// If we are still in k-prefix and KEYDOWN path didn't handle the suffix,
|
||||
// use TEXTINPUT's actual character (handles Shifted letters on macOS) to map the k-command.
|
||||
if (k_prefix_) {
|
||||
k_prefix_ = false;
|
||||
esc_meta_ = false;
|
||||
const char *txt = e.text.text;
|
||||
if (txt && *txt) {
|
||||
unsigned char c0 = static_cast<unsigned char>(txt[0]);
|
||||
int ascii_key = 0;
|
||||
if (c0 < 0x80) {
|
||||
ascii_key = static_cast<int>(c0);
|
||||
}
|
||||
if (ascii_key != 0) {
|
||||
// Map via k-prefix table; do not pass Ctrl for TEXTINPUT case
|
||||
CommandId id;
|
||||
bool mapped = KLookupKCommand(ascii_key, false, id);
|
||||
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
|
||||
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
||||
? static_cast<char>(ascii_key)
|
||||
: '?';
|
||||
std::fprintf(stderr,
|
||||
"[kge] k-prefix TEXTINPUT suffix: ascii=%d '%c' mapped=%d id=%d\n",
|
||||
ascii_key, disp, mapped ? 1 : 0,
|
||||
mapped ? static_cast<int>(id) : -1);
|
||||
std::fflush(stderr);
|
||||
if (mapped) {
|
||||
mi = {true, id, "", 0};
|
||||
produced = true;
|
||||
break; // handled; do not insert text
|
||||
} else {
|
||||
// Unknown k-command via TEXTINPUT path
|
||||
int shown = KLowerAscii(ascii_key);
|
||||
char c = (shown >= 0x20 && shown <= 0x7e)
|
||||
? static_cast<char>(shown)
|
||||
: '?';
|
||||
std::string arg(1, c);
|
||||
mi = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
produced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Consume even if no usable ascii was found
|
||||
produced = true;
|
||||
break;
|
||||
}
|
||||
// (suppression is handled at the top of this case)
|
||||
// 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_) {
|
||||
@@ -275,10 +510,30 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
produced = true; // consumed while k-prefix is active
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (produced && mi.hasCommand) {
|
||||
// Attach universal-argument count if present, then clear the state
|
||||
if (uarg_active_ && mi.id != CommandId::UArgStatus) {
|
||||
int count = 0;
|
||||
if (!uarg_had_digits_ && !uarg_negative_) {
|
||||
count = (uarg_value_ > 0) ? uarg_value_ : 4;
|
||||
} else {
|
||||
count = uarg_value_;
|
||||
if (uarg_negative_)
|
||||
count = -count;
|
||||
}
|
||||
mi.count = count;
|
||||
uarg_active_ = false;
|
||||
uarg_collecting_ = false;
|
||||
uarg_negative_ = false;
|
||||
uarg_had_digits_ = false;
|
||||
uarg_value_ = 0;
|
||||
uarg_text_.clear();
|
||||
}
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(mi);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,14 @@ private:
|
||||
// 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.
|
||||
bool suppress_text_input_once_ = false;
|
||||
|
||||
// Universal argument (C-u) state for GUI
|
||||
bool uarg_active_ = false; // an argument is pending for the next command
|
||||
bool uarg_collecting_ = false; // collecting digits / '-' right now
|
||||
bool uarg_negative_ = false; // whether a leading '-' was supplied
|
||||
bool uarg_had_digits_ = false; // whether any digits were supplied
|
||||
int uarg_value_ = 0; // current absolute value (>=0)
|
||||
std::string uarg_text_; // raw digits/minus typed for status display
|
||||
};
|
||||
|
||||
#endif // KTE_GUI_INPUT_HANDLER_H
|
||||
|
||||
103
KKeymap.cc
103
KKeymap.cc
@@ -7,6 +7,7 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
// For k-prefix, preserve case to allow distinct mappings (e.g., 'U' vs 'u').
|
||||
const int k_lower = KLowerAscii(ascii_key);
|
||||
|
||||
// 1) Try Control-specific C-k mappings first
|
||||
if (ctrl) {
|
||||
switch (k_lower) {
|
||||
case 'd':
|
||||
@@ -19,57 +20,61 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
out = CommandId::QuitNow;
|
||||
return true; // C-k C-q (quit immediately)
|
||||
default:
|
||||
// Important: do not return here — fall through to non-ctrl table
|
||||
// so that C-k u/U still work even if Ctrl is (incorrectly) held
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (k_lower) {
|
||||
case 'j':
|
||||
out = CommandId::JumpToMark;
|
||||
return true; // C-k j
|
||||
case 'f':
|
||||
out = CommandId::FlushKillRing;
|
||||
return true; // C-k f
|
||||
case 'd':
|
||||
out = CommandId::KillToEOL;
|
||||
return true; // C-k d
|
||||
case 'y':
|
||||
out = CommandId::Yank;
|
||||
return true; // C-k y
|
||||
case 's':
|
||||
out = CommandId::Save;
|
||||
return true; // C-k s
|
||||
case 'e':
|
||||
out = CommandId::OpenFileStart;
|
||||
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':
|
||||
out = CommandId::SaveAndQuit;
|
||||
return true; // C-k x
|
||||
case 'q':
|
||||
out = CommandId::Quit;
|
||||
return true; // C-k q
|
||||
case 'p':
|
||||
out = CommandId::BufferNext;
|
||||
return true; // C-k p (switch to next buffer)
|
||||
case 'u':
|
||||
out = CommandId::Undo;
|
||||
return true; // C-k u (undo)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Case-sensitive bindings after k-prefix
|
||||
if (ascii_key == 'U') {
|
||||
out = CommandId::Redo; // C-k U (redo)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Case-sensitive bindings must be checked before case-insensitive table.
|
||||
if (ascii_key == 'U') {
|
||||
out = CommandId::Redo; // C-k U (redo)
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3) Non-control k-table (lowercased)
|
||||
switch (k_lower) {
|
||||
case 'j':
|
||||
out = CommandId::JumpToMark;
|
||||
return true; // C-k j
|
||||
case 'f':
|
||||
out = CommandId::FlushKillRing;
|
||||
return true; // C-k f
|
||||
case 'd':
|
||||
out = CommandId::KillToEOL;
|
||||
return true; // C-k d
|
||||
case 'y':
|
||||
out = CommandId::Yank;
|
||||
return true; // C-k y
|
||||
case 's':
|
||||
out = CommandId::Save;
|
||||
return true; // C-k s
|
||||
case 'e':
|
||||
out = CommandId::OpenFileStart;
|
||||
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':
|
||||
out = CommandId::SaveAndQuit;
|
||||
return true; // C-k x
|
||||
case 'q':
|
||||
out = CommandId::Quit;
|
||||
return true; // C-k q
|
||||
case 'p':
|
||||
out = CommandId::BufferNext;
|
||||
return true; // C-k p (switch to next buffer)
|
||||
case 'u':
|
||||
out = CommandId::Undo;
|
||||
return true; // C-k u (undo)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,17 @@ TerminalInputHandler::~TerminalInputHandler() = default;
|
||||
|
||||
|
||||
static bool
|
||||
map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &out)
|
||||
map_key_to_command(const int ch,
|
||||
bool &k_prefix,
|
||||
bool &esc_meta,
|
||||
// universal-argument state (by ref)
|
||||
bool &uarg_active,
|
||||
bool &uarg_collecting,
|
||||
bool &uarg_negative,
|
||||
bool &uarg_had_digits,
|
||||
int &uarg_value,
|
||||
std::string &uarg_text,
|
||||
MappedInput &out)
|
||||
{
|
||||
// Handle special keys from ncurses
|
||||
switch (ch) {
|
||||
@@ -75,8 +85,15 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
||||
// ESC as cancel of prefix; many terminals send meta sequences as ESC+...
|
||||
if (ch == 27) {
|
||||
// ESC
|
||||
k_prefix = false;
|
||||
esc_meta = true; // next key will be considered meta-modified
|
||||
k_prefix = false;
|
||||
esc_meta = true; // next key will be considered meta-modified
|
||||
// Cancel any universal argument collection
|
||||
uarg_active = false;
|
||||
uarg_collecting = false;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 0;
|
||||
uarg_text.clear();
|
||||
out.hasCommand = false; // no command yet
|
||||
return true;
|
||||
}
|
||||
@@ -92,7 +109,46 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
||||
// cancel
|
||||
k_prefix = false;
|
||||
esc_meta = false;
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
// cancel universal argument as well
|
||||
uarg_active = false;
|
||||
uarg_collecting = false;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 0;
|
||||
uarg_text.clear();
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Universal argument: C-u
|
||||
if (ch == CTRL('U')) {
|
||||
// Start or extend universal argument
|
||||
if (!uarg_active) {
|
||||
uarg_active = true;
|
||||
uarg_collecting = true;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 4; // default
|
||||
// Reset collected text and emit status update
|
||||
uarg_text.clear();
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
} else if (uarg_collecting && !uarg_had_digits && !uarg_negative) {
|
||||
// Bare repeated C-u multiplies by 4
|
||||
if (uarg_value <= 0)
|
||||
uarg_value = 4;
|
||||
else
|
||||
uarg_value *= 4;
|
||||
// Keep showing status (no digits yet)
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
} else {
|
||||
// If digits or '-' have been entered, C-u ends the argument (ready for next command)
|
||||
uarg_collecting = false;
|
||||
if (!uarg_had_digits && !uarg_negative && uarg_value <= 0)
|
||||
uarg_value = 4;
|
||||
}
|
||||
// No command produced by C-u itself
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
// Tab (note: terminals encode Tab and C-i as the same code 9)
|
||||
@@ -115,12 +171,13 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
||||
ctrl = true;
|
||||
ascii_key = 'a' + (ch - 1);
|
||||
}
|
||||
ascii_key = KLowerAscii(ascii_key);
|
||||
// Do NOT lowercase here; KLookupKCommand handles case-sensitive bindings
|
||||
CommandId id;
|
||||
if (KLookupKCommand(ascii_key, ctrl, id)) {
|
||||
out = {true, id, "", 0};
|
||||
} else {
|
||||
char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast<char>(ascii_key) : '?';
|
||||
int shown = KLowerAscii(ascii_key);
|
||||
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
|
||||
std::string arg(1, c);
|
||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
}
|
||||
@@ -167,6 +224,36 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
||||
|
||||
// k_prefix handled earlier
|
||||
|
||||
// If collecting universal arg, handle digits and optional leading '-'
|
||||
if (uarg_active && uarg_collecting) {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
int d = ch - '0';
|
||||
if (!uarg_had_digits) {
|
||||
// First digit overrides any 4^n default
|
||||
uarg_value = 0;
|
||||
uarg_had_digits = true;
|
||||
}
|
||||
if (uarg_value < 100000000) {
|
||||
// avoid overflow
|
||||
uarg_value = uarg_value * 10 + d;
|
||||
}
|
||||
// Update raw text and status to reflect collected digits
|
||||
uarg_text.push_back(static_cast<char>(ch));
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == '-' && !uarg_had_digits && !uarg_negative) {
|
||||
uarg_negative = true;
|
||||
// Show leading minus in status
|
||||
uarg_text = "-";
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
}
|
||||
// Any other key will be processed as a command; fall through to mapping below
|
||||
// but mark collection finished so we apply the argument to that command
|
||||
uarg_collecting = false;
|
||||
}
|
||||
|
||||
// Printable ASCII
|
||||
if (ch >= 0x20 && ch <= 0x7E) {
|
||||
out.hasCommand = true;
|
||||
@@ -188,7 +275,33 @@ TerminalInputHandler::decode_(MappedInput &out)
|
||||
if (ch == ERR) {
|
||||
return false; // no input
|
||||
}
|
||||
return map_key_to_command(ch, k_prefix_, esc_meta_, out);
|
||||
bool consumed = map_key_to_command(
|
||||
ch,
|
||||
k_prefix_, esc_meta_,
|
||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_, uarg_text_,
|
||||
out);
|
||||
if (!consumed)
|
||||
return false;
|
||||
// If a command was produced and a universal argument is active, attach it and clear state
|
||||
if (out.hasCommand && uarg_active_ && out.id != CommandId::UArgStatus) {
|
||||
int count = 0;
|
||||
if (!uarg_had_digits_ && !uarg_negative_) {
|
||||
// No explicit digits: use current value (default 4 or 4^n)
|
||||
count = (uarg_value_ > 0) ? uarg_value_ : 4;
|
||||
} else {
|
||||
count = uarg_value_;
|
||||
if (uarg_negative_)
|
||||
count = -count;
|
||||
}
|
||||
out.count = count;
|
||||
// Clear state
|
||||
uarg_active_ = false;
|
||||
uarg_collecting_ = false;
|
||||
uarg_negative_ = false;
|
||||
uarg_had_digits_ = false;
|
||||
uarg_value_ = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,14 @@ private:
|
||||
bool k_prefix_ = false; // true after C-k until next key or ESC
|
||||
// Simple meta (ESC) state for ESC sequences like ESC b/f
|
||||
bool esc_meta_ = false;
|
||||
|
||||
// Universal argument (C-u) state
|
||||
bool uarg_active_ = false; // an argument is pending for the next command
|
||||
bool uarg_collecting_ = false; // collecting digits / '-' right now
|
||||
bool uarg_negative_ = false; // whether a leading '-' was supplied
|
||||
bool uarg_had_digits_ = false; // whether any digits were supplied
|
||||
int uarg_value_ = 0; // current absolute value (>=0)
|
||||
std::string uarg_text_; // raw digits/minus typed for status display
|
||||
};
|
||||
|
||||
#endif // KTE_TERMINAL_INPUT_HANDLER_H
|
||||
|
||||
@@ -13,20 +13,40 @@ UndoSystem::Begin(UndoType type)
|
||||
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
|
||||
if (type == UndoType::Delete) {
|
||||
// Support batching both forward deletes (DeleteChar) and backspace (prepend case)
|
||||
// Forward delete: cursor stays at anchor col; expected == col
|
||||
std::size_t anchor = static_cast<std::size_t>(tree_.pending->col);
|
||||
if (anchor + tree_.pending->text.size() == static_cast<std::size_t>(col)) {
|
||||
pending_prepend_ = false;
|
||||
return; // keep batching forward delete
|
||||
}
|
||||
// Backspace: cursor moved left by 1; allow extend if col + text.size() == anchor
|
||||
if (static_cast<std::size_t>(col) + tree_.pending->text.size() == anchor) {
|
||||
// Move anchor one left to new cursor column; next Append should prepend
|
||||
tree_.pending->col = col;
|
||||
pending_prepend_ = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
std::size_t expected = static_cast<std::size_t>(tree_.pending->col) + tree_.pending->text.
|
||||
size();
|
||||
if (expected == static_cast<std::size_t>(col)) {
|
||||
pending_prepend_ = false;
|
||||
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;
|
||||
auto *node = new UndoNode();
|
||||
node->type = type;
|
||||
node->row = row;
|
||||
node->col = col;
|
||||
node->child = nullptr;
|
||||
node->next = nullptr;
|
||||
tree_.pending = node;
|
||||
pending_prepend_ = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +55,12 @@ UndoSystem::Append(char ch)
|
||||
{
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
tree_.pending->text.push_back(ch);
|
||||
if (pending_prepend_ && tree_.pending->type == UndoType::Delete) {
|
||||
// Prepend for backspace so that text is in increasing column order
|
||||
tree_.pending->text.insert(tree_.pending->text.begin(), ch);
|
||||
} else {
|
||||
tree_.pending->text.push_back(ch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ private:
|
||||
private:
|
||||
Buffer &buf_;
|
||||
UndoTree &tree_;
|
||||
// Internal hint for Delete batching: whether next Append() should prepend
|
||||
bool pending_prepend_ = false;
|
||||
};
|
||||
|
||||
#endif // KTE_UNDOSYSTEM_H
|
||||
|
||||
95
docs/undo-state.md
Normal file
95
docs/undo-state.md
Normal file
@@ -0,0 +1,95 @@
|
||||
Undo/Redo + C-k GUI status (macOS) — current state snapshot
|
||||
|
||||
Context
|
||||
- Platform: macOS (Darwin)
|
||||
- Target: GUI build (kge) using SDL2/ImGui path
|
||||
- Date: 2025-11-30 00:30 local (from user)
|
||||
|
||||
What works right now
|
||||
- Terminal (kte): C-k keymap and UndoSystem integration have been stable in recent builds.
|
||||
- GUI: Most C-k mappings work: C-k d (KillToEOL), C-k x (Save+Quit), C-k q (Quit) — confirmed by user.
|
||||
- UndoSystem core is implemented and integrated for InsertText/Newline/Delete/Backspace. Buffer owns an UndoSystem and raw edit APIs are used by apply().
|
||||
|
||||
What is broken (GUI, macOS)
|
||||
- C-k u: Status shows "Undone" but buffer content does not change (no visible undo).
|
||||
- C-k U: Inserts a literal 'U' into the buffer; does not execute Redo.
|
||||
- C-k C-u / C-k C-U: No effect (expected unmapped), but the k-prefix prompt can remain in some paths.
|
||||
|
||||
Repro steps (GUI)
|
||||
1) Type "Hello".
|
||||
2) Press C-k then press u → status becomes "Undone", but text remains "Hello".
|
||||
3) Press C-k then press Shift+U → a literal 'U' is inserted (becomes "HelloU").
|
||||
4) Press C-k then hold Ctrl on the suffix and press u → status "Undone", still no change.
|
||||
5) Press C-k then hold Ctrl on the suffix and press Shift+U → status shows the k-prefix prompt again ("C-k _").
|
||||
|
||||
Keymap and input-layer changes we attempted (and kept)
|
||||
- KKeymap.cc: Case-sensitive 'U' mapping prioritized before the lowercase table. Added ctrl→non-ctrl fall-through so C-k u/U still map even if SDL reports Ctrl held on the suffix.
|
||||
- TerminalInputHandler: already preserved case and mapped correctly.
|
||||
- GUIInputHandler:
|
||||
- Preserve case for k-prefix suffix letters (Shift → uppercase). Clear esc_meta before k-suffix mapping.
|
||||
- Strengthened SDL_TEXTINPUT suppression after a k-prefix printable suffix to avoid inserting literal characters.
|
||||
- Added fallback to map the k-prefix suffix in the SDL_TEXTINPUT path (to catch macOS cases where uppercase arrives in TEXTINPUT rather than KEYDOWN).
|
||||
- Fixed malformed switch block introduced during iteration.
|
||||
- Command layer: commit pending undo batch at k-prefix entry and just before Undo/Redo so prior typing can actually be undone/redone.
|
||||
|
||||
Diagnostics added
|
||||
- GUIInputHandler logs k-prefix u/U suffix attempts to stderr and (previously) /tmp/kge.log. The user’s macOS session showed only KEYDOWN logs for 'u':
|
||||
- "[kge] k-prefix suffix: sym=117 mods=0x0 ascii=117 'u' ctrl2=0 pass_ctrl=0 mapped=1 id=38"
|
||||
- "[kge] k-prefix suffix: sym=117 mods=0x80 ascii=117 'u' ctrl2=1 pass_ctrl=0 mapped=1 id=38"
|
||||
- No logs were produced for 'U' (neither KEYDOWN nor TEXTINPUT). The /tmp log file was not created on the user’s host in the last run (stderr logs were visible earlier from KEYDOWN).
|
||||
|
||||
Hypotheses for current failures
|
||||
1) Undo appears to be invoked (status "Undone"), but no state change:
|
||||
- The most likely cause is that no committed node exists at the time of undo (i.e., typing "Hello" is not being recorded as an undo node), because our current typing path in Command.cc directly edits buffer rows without always driving UndoSystem Begin/Append/commit at the right times for every printable char on GUI.
|
||||
- Although we call u->Begin(Insert) and u->Append(text) in cmd_insert_text for CommandId::InsertText, the GUI printable input might be arriving through a different path or being short-circuited (e.g., via a prompt or suppression), resulting in actual text insertion but no corresponding UndoSystem pending node content, or pending but never committed.
|
||||
- We now commit at k-prefix entry and before undo; if there is still "nothing to undo", it implies the batch never had text appended (Append not called) or is detached from the real buffer edits.
|
||||
|
||||
2) Redo via C-k U inserts a literal 'U':
|
||||
- On macOS, uppercase letters often arrive as SDL_TEXTINPUT events. We added TEXTINPUT-based k-prefix mapping, but the user's run still showed a literal insertion and no diagnostic lines for TEXTINPUT, which suggests:
|
||||
a) The TEXTINPUT suppression didn’t trigger for that platform/sequence, or
|
||||
b) The k-prefix flag was already cleared by the time TEXTINPUT arrived, so the TEXTINPUT path defaulted to InsertText, or
|
||||
c) The GUI window’s input focus or SDL event ordering differs from expectations (e.g., IME/text input settings), so our suppression/mapping didn’t see the event.
|
||||
|
||||
Relevant code pointers
|
||||
- Key mapping tables: KKeymap.cc → KLookupKCommand() (C-k suffix), KLookupCtrlCommand(), KLookupEscCommand().
|
||||
- Terminal input: TerminalInputHandler.cc → map_key_to_command().
|
||||
- GUI input: GUIInputHandler.cc → map_key() and GUIInputHandler::ProcessSDLEvent() (KEYDOWN + TEXTINPUT handling, suppression, k_prefix_/esc_meta_ flags).
|
||||
- Command dispatch: Command.cc → cmd_insert_text(), cmd_newline(), cmd_backspace(), cmd_delete_char(), cmd_undo(), cmd_redo(), cmd_kprefix().
|
||||
- Undo core: UndoSystem.{h,cc}, UndoNode.{h,cc}, UndoTree.{h,cc}. Buffer raw methods used by apply().
|
||||
|
||||
Immediate next steps (when we return to this)
|
||||
1) Verify that GUI printable insertion always flows through CommandId::InsertText so UndoSystem::Begin/Append gets called. If SDL_TEXTINPUT delivers multi-byte strings, ensure Append() is given the same text inserted into buffer.
|
||||
- Add a one-session debug hook in cmd_insert_text to assert/trace: pending node type/text length and current cursor col before/after.
|
||||
- If GUI sometimes sends CommandId::InsertTextEmpty or another path, unify.
|
||||
|
||||
2) Ensure batching rules are satisfied so Begin() reuses pending correctly:
|
||||
- Begin(Insert) must see same row and col == pending->col + pending->text.size() for typing sequences.
|
||||
- If GUI accumulates multiple characters per TEXTINPUT (e.g., pasted text), Append(std::string_view) is fine, but row/col expectations remain.
|
||||
|
||||
3) For C-k U uppercase mapping on macOS:
|
||||
- Add a temporary status dump when k-prefix suffix mapping falls back to TEXTINPUT path (we added stderr prints; also set Editor status with a short code like "K-TI U" during one session) to confirm path is used and suppression is working.
|
||||
- If TEXTINPUT never arrives, force suppression: when we detect k-prefix and KEYDOWN of a letter with Shift, preemptively handle via KEYDOWN-derived uppercase ASCII rather than deferring.
|
||||
|
||||
4) Consolidate k-prefix handling:
|
||||
- After mapping a k-prefix suffix to a command (Undo/Redo/etc.), always set suppress_text_input_once_ = true to avoid any trailing TEXTINPUT.
|
||||
- Clear k_prefix_ reliably on both KEYDOWN and TEXTINPUT paths.
|
||||
|
||||
5) Once mapping is solid, remove all diagnostics and keep the minimal, deterministic logic.
|
||||
|
||||
Open questions for future debugging
|
||||
- Does SDL on this macOS setup deliver Shift+U as KEYDOWN+TEXTINPUT consistently, or only TEXTINPUT? We need a small on-screen debug to avoid relying on stderr.
|
||||
- Are there any IME/TextInput SDL hints on macOS we should set for raw key handling during k-prefix?
|
||||
- Should we temporarily disable SDL text input (SDL_StopTextInput) during k-prefix suffix processing to eliminate TEXTINPUT races on macOS?
|
||||
|
||||
Notes on UndoSystem correctness (unrelated to the GUI mapping bug)
|
||||
- Undo tree invariants are implemented: pending is detached; commit attaches and clears redo branches; undo/redo apply low-level Buffer edits with no public editor paths; saved marker updated via mark_saved().
|
||||
- Dirty flag mirrors (current != saved).
|
||||
- Delete batching supports prepend for backspace sequences (stored text is in increasing column order from anchor).
|
||||
- Newline joins/splits are recorded as UndoType::Newline and committed immediately for single-step undo of line joins.
|
||||
|
||||
Owner pointers & file locations
|
||||
- GUI mapping and suppression: GUIInputHandler.cc
|
||||
- Command layer commit boundaries: Command.cc (cmd_kprefix, cmd_undo, cmd_redo)
|
||||
- Undo batching entry points: Command.cc (cmd_insert_text, cmd_backspace, cmd_delete_char, cmd_newline)
|
||||
|
||||
End of snapshot — safe to resume from here.
|
||||
140
docs/undo.md
Normal file
140
docs/undo.md
Normal file
@@ -0,0 +1,140 @@
|
||||
This is a design for a non-linear undo/redo system for kte. The design must be identical in behavior and correctness
|
||||
to the proven kte editor undo system.
|
||||
|
||||
### Core Requirements
|
||||
|
||||
1. Each open buffer has its own completely independent undo tree.
|
||||
2. Undo and redo must be non-linear: typing after undo creates a branch; old redo branches are discarded.
|
||||
3. Typing, backspacing, and pasting are batched into word-level undo steps.
|
||||
4. Undo/redo must never create new undo nodes while applying an undo/redo (silent, low-level apply).
|
||||
5. The system must be memory-safe and leak-proof even if the user types and immediately closes the buffer.
|
||||
|
||||
### Data Structures
|
||||
|
||||
```cpp
|
||||
enum class UndoType : uint8_t {
|
||||
Insert,
|
||||
Delete,
|
||||
Paste, // optional, can reuse Insert
|
||||
Newline,
|
||||
DeleteRow,
|
||||
// future: IndentRegion, KillRegion, etc.
|
||||
};
|
||||
|
||||
struct UndoNode {
|
||||
UndoType type;
|
||||
int row; // original cursor row
|
||||
int col; // original cursor column (updated during batch)
|
||||
std::string text; // the inserted or deleted text (full batch)
|
||||
UndoNode* child = nullptr; // next in current timeline
|
||||
UndoNode* next = nullptr; // redo branch (rarely used)
|
||||
// no parent pointer needed — we walk from root
|
||||
};
|
||||
|
||||
struct UndoTree {
|
||||
UndoNode* root = nullptr; // first edit ever
|
||||
UndoNode* current = nullptr; // current state of buffer
|
||||
UndoNode* saved = nullptr; // points to node matching last save (for dirty flag)
|
||||
UndoNode* pending = nullptr; // in-progress batch (detached)
|
||||
};
|
||||
```
|
||||
|
||||
Each `Buffer` owns one `std::unique_ptr<UndoTree>`.
|
||||
|
||||
### Core API (must implement exactly)
|
||||
|
||||
```cpp
|
||||
class UndoSystem {
|
||||
public:
|
||||
void Begin(UndoType type);
|
||||
void Append(char ch);
|
||||
void Append(std::string_view text);
|
||||
void commit(); // called on cursor move, commands, etc.
|
||||
|
||||
void undo(); // Ctrl+Z
|
||||
void redo(); // Ctrl+Y or Ctrl+Shift+Z
|
||||
|
||||
void mark_saved(); // after successful save
|
||||
void discard_pending(); // before closing buffer or loading new file
|
||||
void clear(); // new file / reset
|
||||
|
||||
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
|
||||
};
|
||||
```
|
||||
|
||||
### Critical Invariants and Rules
|
||||
|
||||
1. `begin()` must reuse `pending` if:
|
||||
- same type
|
||||
- same row
|
||||
- `pending->col + pending->text.size() == current_cursor_col`
|
||||
→ otherwise `commit()` old and create new
|
||||
|
||||
2. `pending` is detached — never linked until `commit()`
|
||||
|
||||
3. `commit()`:
|
||||
- discards redo branches (`current->child`)
|
||||
- attaches `pending` as `current->child`
|
||||
- advances `current`
|
||||
- clears `pending`
|
||||
- if diverged from `saved`, null it
|
||||
|
||||
4. `apply()` must use low-level buffer operations:
|
||||
- Never call public insert/delete/newline
|
||||
- Use raw `buffer.insert_text(row, col, text)` and `buffer.delete_text(row, col, len)`
|
||||
- These must not trigger undo
|
||||
|
||||
5. `undo()`:
|
||||
- move current to parent
|
||||
- apply(current, -1)
|
||||
|
||||
6. `redo()`:
|
||||
- move current to child
|
||||
- apply(current, +1)
|
||||
|
||||
7. `discard_pending()` must be called in:
|
||||
- buffer close
|
||||
- file reload
|
||||
- new file
|
||||
- any destructive operation
|
||||
|
||||
### Example Flow: Typing "hello"
|
||||
|
||||
```text
|
||||
begin(Insert) → pending = new node, col=0
|
||||
append('h') → pending->text = "h", pending->col = 1
|
||||
append('e') → "he", col = 2
|
||||
...
|
||||
commit() on arrow key → pending becomes current->child, current advances
|
||||
```
|
||||
|
||||
One undo step removes all of "hello".
|
||||
|
||||
### Required Helper in Buffer Class
|
||||
|
||||
```cpp
|
||||
class Buffer {
|
||||
void insert_text(int row, int col, std::string_view text); // raw, no undo
|
||||
void delete_text(int row, int col, size_t len); // raw, no undo
|
||||
void split_line(int row, int col); // raw newline
|
||||
void join_lines(int row); // raw join
|
||||
void insert_row(int row, std::string_view text); // raw
|
||||
void delete_row(int row); // raw
|
||||
};
|
||||
```
|
||||
|
||||
### Tasks for Agent
|
||||
|
||||
1. Implement `UndoNode`, `UndoTree`, and `UndoSystem` class exactly as specified.
|
||||
2. Add `std::unique_ptr<UndoTree> undo;` to `Buffer`.
|
||||
3. Modify `insert_char`, `delete_char`, `paste`, `newline` to use `undo.begin()/append()/commit()`.
|
||||
4. Add `undo.commit()` at start of all cursor movement and command functions.
|
||||
5. Implement `apply()` using only `Buffer`'s raw methods.
|
||||
6. Add `undo.discard_pending()` in all buffer reset/close paths.
|
||||
7. Add `Ctrl+Z` → `buffer.undo()`, `Ctrl+Y` → `buffer.redo()`.
|
||||
|
||||
This design is used in production editors and is considered the gold standard for small, correct, non-linear undo in
|
||||
C/C++. Implement it faithfully.
|
||||
7
main.cc
7
main.cc
@@ -97,12 +97,11 @@ main(int argc, const char *argv[])
|
||||
} else if (req_term) {
|
||||
use_gui = false;
|
||||
} else {
|
||||
|
||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||
#if defined(KTE_DEFAULT_GUI)
|
||||
use_gui = true;
|
||||
use_gui = true;
|
||||
#else
|
||||
use_gui = false;
|
||||
use_gui = false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user