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>
100 lines
2.6 KiB
C++
100 lines
2.6 KiB
C++
#include "Test.h"
|
|
|
|
#include "KKeymap.h"
|
|
|
|
#include <ncurses.h>
|
|
|
|
|
|
TEST(KKeymap_KPrefix_CanonicalChords)
|
|
{
|
|
CommandId id{};
|
|
|
|
// From docs/ke.md (K-commands)
|
|
ASSERT_TRUE(KLookupKCommand('s', false, id));
|
|
ASSERT_EQ(id, CommandId::Save);
|
|
ASSERT_TRUE(KLookupKCommand('s', true, id)); // C-k C-s
|
|
ASSERT_EQ(id, CommandId::Save);
|
|
|
|
ASSERT_TRUE(KLookupKCommand('d', false, id));
|
|
ASSERT_EQ(id, CommandId::KillToEOL);
|
|
ASSERT_TRUE(KLookupKCommand('d', true, id)); // C-k C-d
|
|
ASSERT_EQ(id, CommandId::KillLine);
|
|
|
|
ASSERT_TRUE(KLookupKCommand(' ', false, id)); // C-k SPACE
|
|
ASSERT_EQ(id, CommandId::ToggleMark);
|
|
|
|
ASSERT_TRUE(KLookupKCommand('j', false, id));
|
|
ASSERT_EQ(id, CommandId::JumpToMark);
|
|
|
|
ASSERT_TRUE(KLookupKCommand('f', false, id));
|
|
ASSERT_EQ(id, CommandId::FlushKillRing);
|
|
|
|
ASSERT_TRUE(KLookupKCommand('y', false, id));
|
|
ASSERT_EQ(id, CommandId::Yank);
|
|
|
|
// Unknown should not map
|
|
ASSERT_EQ(KLookupKCommand('Z', false, id), false);
|
|
}
|
|
|
|
|
|
TEST(KKeymap_KPrefix_UppercaseE_RejectedAndDistinctFromLowercaseE)
|
|
{
|
|
CommandId id{};
|
|
|
|
// 'e' is a real binding (OpenFileStart).
|
|
ASSERT_TRUE(KLookupKCommand('e', false, id));
|
|
ASSERT_EQ(id, CommandId::OpenFileStart);
|
|
|
|
// 'E' must be rejected, not silently aliased to 'e' via case-insensitive
|
|
// lookup (the switch normalizes to lowercase, so this has to be special-
|
|
// cased before it).
|
|
ASSERT_EQ(KLookupKCommand('E', false, id), false);
|
|
}
|
|
|
|
|
|
TEST(KKeymap_CtrlChords_CanonicalChords)
|
|
{
|
|
CommandId id{};
|
|
|
|
// From docs/ke.md (other keybindings)
|
|
ASSERT_TRUE(KLookupCtrlCommand('n', id));
|
|
ASSERT_EQ(id, CommandId::MoveDown);
|
|
ASSERT_TRUE(KLookupCtrlCommand('p', id));
|
|
ASSERT_EQ(id, CommandId::MoveUp);
|
|
ASSERT_TRUE(KLookupCtrlCommand('f', id));
|
|
ASSERT_EQ(id, CommandId::MoveRight);
|
|
ASSERT_TRUE(KLookupCtrlCommand('b', id));
|
|
ASSERT_EQ(id, CommandId::MoveLeft);
|
|
|
|
ASSERT_TRUE(KLookupCtrlCommand('w', id));
|
|
ASSERT_EQ(id, CommandId::KillRegion);
|
|
ASSERT_TRUE(KLookupCtrlCommand('y', id));
|
|
ASSERT_EQ(id, CommandId::Yank);
|
|
|
|
ASSERT_EQ(KLookupCtrlCommand('z', id), false);
|
|
}
|
|
|
|
|
|
TEST(KKeymap_EscChords_CanonicalChords)
|
|
{
|
|
CommandId id{};
|
|
|
|
// From docs/ke.md (ESC bindings)
|
|
ASSERT_TRUE(KLookupEscCommand('b', id));
|
|
ASSERT_EQ(id, CommandId::WordPrev);
|
|
ASSERT_TRUE(KLookupEscCommand('f', id));
|
|
ASSERT_EQ(id, CommandId::WordNext);
|
|
ASSERT_TRUE(KLookupEscCommand('d', id));
|
|
ASSERT_EQ(id, CommandId::DeleteWordNext);
|
|
ASSERT_TRUE(KLookupEscCommand('q', id));
|
|
ASSERT_EQ(id, CommandId::ReflowParagraph);
|
|
ASSERT_TRUE(KLookupEscCommand('w', id));
|
|
ASSERT_EQ(id, CommandId::CopyRegion);
|
|
|
|
// ESC BACKSPACE
|
|
ASSERT_TRUE(KLookupEscCommand(KEY_BACKSPACE, id));
|
|
ASSERT_EQ(id, CommandId::DeleteWordPrev);
|
|
|
|
ASSERT_EQ(KLookupEscCommand('z', id), false);
|
|
}
|