Undo/redo: - cmd_newline recorded the undo node's position after the cursor moved past the split point, so undoing Enter joined the wrong pair of lines. - Backspace/Delete line-joins recorded UndoType::Newline, whose apply() semantics are inverted for a join (forward=split); add UndoType::JoinLines with correct forward/backward behavior. - Regex replace-all, indent/unindent region, kill-to-eol, kill-line, kill-region, and delete-word-prev/next mutated the buffer via the no-undo-recording raw APIs; they now record undo, grouped atomically via a shared UndoGroupGuard. - Plain-text replace-all with an empty replacement advanced the scan position one character too far, skipping adjacent/overlapping matches. Crash recovery / memory safety: - Buffer's move ctor/assignment never carried over swap_rec_, on_disk_identity_, or the visual-line-mode fields, so the crash-recovery journal silently stopped tracking a buffer whenever Editor's std::vector<Buffer> reallocated or shifted (e.g. opening/closing files). Added SwapManager::Rehome() plus Flush()-before-mutate in Editor::AddBuffer/CloseBuffer so the journal's Buffer* key and the background writer thread never reference a stale address. - UndoTree leaked its entire node graph (including all edit text) on every buffer close/reload; added ~UndoTree() to free it. - main.cc didn't restore the terminal if an exception escaped the run loop; added an RAII guard around Frontend::Shutdown(). Input handling: - Terminal frontend used getch() instead of get_wch(), silently dropping non-ASCII keyboard input despite linking wide-char ncurses. - KKeymap's C-k lookup switches on an already-lowercased key, making `case 'E'` unreachable dead code; C-k Shift-E silently aliased to C-k e. - ImGui file picker wasn't modal: keystrokes typed while it was open still reached the buffer underneath as edit commands, and Escape didn't close it. - Gated ImGuiInputHandler's unconditional fprintf/fflush diagnostics behind an IMGUI_IH_DEBUG macro, matching QtInputHandler's existing convention. Syntax highlighting: - Go/Rust/SQL highlighters weren't stateful, so multi-line /* */ comments mis-highlighted as code starting on the second line. - Python's triple-quoted-string handler didn't re-scan the remainder of a line after a string closed mid-line, missing a same-line reopen. - HighlighterEngine's state_last_contig_ field was declared but unused, forcing an O(n) scan of state_cache_ on every stateful lookup; wired it up as a real fast path and fixed InvalidateFrom() to keep it in sync. - kge's :syntax off was silently undone the next frame because apply_syntax_to_buffer() re-applied the config default unconditionally every frame; added a user-override flag mirroring the existing edit-mode-detection guard. Added regression tests for all of the above (test_undo.cc, test_kkeymap.cc, test_command_semantics.cc, test_search_replace_flow.cc, and a new test_syntax_highlighting.cc). Full suite (163 tests) passes, including under AddressSanitizer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
244 lines
5.1 KiB
C++
244 lines
5.1 KiB
C++
#include <ncurses.h>
|
|
|
|
#include "KKeymap.h"
|
|
|
|
|
|
auto
|
|
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':
|
|
out = CommandId::KillLine;
|
|
return true;
|
|
case 's':
|
|
out = CommandId::Save;
|
|
return true;
|
|
case 'q':
|
|
out = CommandId::QuitNow;
|
|
return true;
|
|
case 'x':
|
|
out = CommandId::SaveAndQuit;
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 2) Case-sensitive bindings must be checked before case-insensitive table.
|
|
if (ascii_key == 'r') {
|
|
out = CommandId::Redo; // C-k r (redo)
|
|
return true;
|
|
}
|
|
if (ascii_key == '\'') {
|
|
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
|
return true;
|
|
}
|
|
if (ascii_key == 'E') {
|
|
// Explicitly rejected and kept distinct from 'e' (OpenFileStart): the
|
|
// switch below operates on the lowercased key, so it can't otherwise
|
|
// tell 'E' and 'e' apart. Return false and let the caller show its
|
|
// normal "unknown k-command" status-line message - curses already
|
|
// owns the terminal here, so writing to stderr would corrupt the
|
|
// screen.
|
|
return false;
|
|
}
|
|
|
|
switch (k_lower) {
|
|
case 'a':
|
|
out = CommandId::MarkAllAndJumpEnd;
|
|
return true;
|
|
case ' ': // C-k SPACE
|
|
out = CommandId::ToggleMark;
|
|
return true;
|
|
case 'i':
|
|
out = CommandId::BufferNew; // C-k i new empty buffer
|
|
return true;
|
|
case 'k':
|
|
out = CommandId::CenterOnCursor; // C-k k center current line
|
|
return true;
|
|
case 'b':
|
|
out = CommandId::BufferSwitchStart;
|
|
return true;
|
|
case 'c':
|
|
out = CommandId::BufferClose;
|
|
return true;
|
|
case 'd':
|
|
out = CommandId::KillToEOL;
|
|
return true;
|
|
case 'e':
|
|
out = CommandId::OpenFileStart;
|
|
return true;
|
|
case 'f':
|
|
out = CommandId::FlushKillRing;
|
|
return true;
|
|
case 'g':
|
|
out = CommandId::JumpToLine;
|
|
return true;
|
|
case 'h':
|
|
out = CommandId::ShowHelp;
|
|
return true;
|
|
case 'j':
|
|
out = CommandId::JumpToMark;
|
|
return true;
|
|
case 'l':
|
|
out = CommandId::ReloadBuffer;
|
|
return true;
|
|
case 'm':
|
|
out = CommandId::ToggleEditMode;
|
|
return true;
|
|
case 'n':
|
|
out = CommandId::BufferPrev;
|
|
return true;
|
|
case 'o':
|
|
out = CommandId::ChangeWorkingDirectory;
|
|
return true;
|
|
case 'p':
|
|
out = CommandId::BufferNext;
|
|
return true;
|
|
case 'q':
|
|
out = CommandId::Quit;
|
|
return true;
|
|
case 's':
|
|
out = CommandId::Save;
|
|
return true;
|
|
case 'u':
|
|
out = CommandId::Undo;
|
|
return true;
|
|
case 'v':
|
|
out = CommandId::VisualFilePickerToggle;
|
|
return true;
|
|
case 'w':
|
|
out = CommandId::ShowWorkingDirectory;
|
|
return true;
|
|
case 'x':
|
|
out = CommandId::SaveAndQuit;
|
|
return true;
|
|
case 'y':
|
|
out = CommandId::Yank;
|
|
return true;
|
|
case '-':
|
|
out = CommandId::UnindentRegion;
|
|
return true;
|
|
case '=':
|
|
out = CommandId::IndentRegion;
|
|
return true;
|
|
case '/':
|
|
out = CommandId::VisualLineModeToggle;
|
|
return true;
|
|
case ';':
|
|
out = CommandId::CommandPromptStart; // C-k ; : generic command prompt
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// 3) Non-control k-table (lowercased)
|
|
return false;
|
|
}
|
|
|
|
|
|
auto
|
|
KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
|
{
|
|
const int k = KLowerAscii(ascii_key);
|
|
switch (k) {
|
|
case 'w':
|
|
out = CommandId::KillRegion; // C-w
|
|
return true;
|
|
case 'y':
|
|
out = CommandId::Yank; // C-y
|
|
return true;
|
|
case 'd':
|
|
out = CommandId::DeleteChar; // C-d
|
|
return true;
|
|
case 'n':
|
|
out = CommandId::MoveDown;
|
|
return true;
|
|
case 'p':
|
|
out = CommandId::MoveUp;
|
|
return true;
|
|
case 'f':
|
|
out = CommandId::MoveRight;
|
|
return true;
|
|
case 'b':
|
|
out = CommandId::MoveLeft;
|
|
return true;
|
|
case 'a':
|
|
out = CommandId::MoveHome;
|
|
return true;
|
|
case 'e':
|
|
out = CommandId::MoveEnd;
|
|
return true;
|
|
case 's':
|
|
out = CommandId::FindStart;
|
|
return true;
|
|
case 'r':
|
|
out = CommandId::RegexFindStart; // C-r regex search
|
|
return true;
|
|
case 't':
|
|
out = CommandId::RegexpReplace; // C-t regex search & replace
|
|
return true;
|
|
case 'h':
|
|
out = CommandId::SearchReplace; // C-h: search & replace
|
|
return true;
|
|
case 'l':
|
|
out = CommandId::Refresh;
|
|
return true;
|
|
case 'g':
|
|
out = CommandId::Refresh;
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
auto
|
|
KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
|
{
|
|
const int k = KLowerAscii(ascii_key);
|
|
// Handle KEY_BACKSPACE (ESC BACKSPACE, Alt-Backspace)
|
|
if (ascii_key == KEY_BACKSPACE) {
|
|
out = CommandId::DeleteWordPrev;
|
|
return true;
|
|
}
|
|
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':
|
|
out = CommandId::WordPrev;
|
|
return true;
|
|
case 'f':
|
|
out = CommandId::WordNext;
|
|
return true;
|
|
case 'd':
|
|
out = CommandId::DeleteWordNext; // Esc d (Alt-d)
|
|
return true;
|
|
case 'q':
|
|
out = CommandId::ReflowParagraph; // Esc q (reflow paragraph)
|
|
return true;
|
|
case '\n':
|
|
case '\r':
|
|
out = CommandId::SmartNewline; // Shift+Enter (some terminals send this as Alt+Enter sequences)
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
} |