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>
134 lines
3.5 KiB
C++
134 lines
3.5 KiB
C++
#include "Test.h"
|
|
|
|
#include "TestHarness.h"
|
|
|
|
using ktet::TestHarness;
|
|
|
|
|
|
TEST(CommandSemantics_KillToEOL_KillChain_And_Yank)
|
|
{
|
|
TestHarness h;
|
|
Editor &ed = h.EditorRef();
|
|
Buffer &b = h.Buf();
|
|
|
|
b.insert_text(0, 0, std::string("abc\ndef"));
|
|
b.SetCursor(1, 0); // a|bc
|
|
|
|
ed.KillRingClear();
|
|
ed.SetKillChain(false);
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::KillToEOL));
|
|
ASSERT_EQ(h.Text(), std::string("a\ndef"));
|
|
ASSERT_EQ(ed.KillRingHead(), std::string("bc"));
|
|
|
|
// At EOL, KillToEOL kills the newline (join).
|
|
ASSERT_TRUE(h.Exec(CommandId::KillToEOL));
|
|
ASSERT_EQ(h.Text(), std::string("adef"));
|
|
ASSERT_EQ(ed.KillRingHead(), std::string("bc\n"));
|
|
|
|
// Yank pastes the kill ring head and breaks the kill chain.
|
|
ASSERT_TRUE(h.Exec(CommandId::Yank));
|
|
ASSERT_EQ(h.Text(), std::string("abc\ndef"));
|
|
ASSERT_EQ(ed.KillRingHead(), std::string("bc\n"));
|
|
ASSERT_EQ(ed.KillChain(), false);
|
|
}
|
|
|
|
|
|
TEST(CommandSemantics_ToggleMark_JumpToMark)
|
|
{
|
|
TestHarness h;
|
|
Buffer &b = h.Buf();
|
|
|
|
b.insert_text(0, 0, std::string("hello"));
|
|
b.SetCursor(2, 0);
|
|
ASSERT_EQ(b.MarkSet(), false);
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::ToggleMark));
|
|
ASSERT_EQ(b.MarkSet(), true);
|
|
ASSERT_EQ(b.MarkCurx(), (std::size_t) 2);
|
|
ASSERT_EQ(b.MarkCury(), (std::size_t) 0);
|
|
|
|
b.SetCursor(4, 0);
|
|
ASSERT_TRUE(h.Exec(CommandId::JumpToMark));
|
|
ASSERT_EQ(b.Curx(), (std::size_t) 2);
|
|
ASSERT_EQ(b.Cury(), (std::size_t) 0);
|
|
// Jump-to-mark swaps: mark becomes previous cursor.
|
|
ASSERT_EQ(b.MarkSet(), true);
|
|
ASSERT_EQ(b.MarkCurx(), (std::size_t) 4);
|
|
ASSERT_EQ(b.MarkCury(), (std::size_t) 0);
|
|
}
|
|
|
|
|
|
TEST(CommandSemantics_CtrlGRefresh_ClearsMark_WhenNothingElseToCancel)
|
|
{
|
|
TestHarness h;
|
|
Buffer &b = h.Buf();
|
|
|
|
b.insert_text(0, 0, std::string("hello"));
|
|
b.SetCursor(2, 0);
|
|
ASSERT_EQ(b.MarkSet(), false);
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::ToggleMark));
|
|
ASSERT_EQ(b.MarkSet(), true);
|
|
|
|
// C-g is mapped to Refresh; when there's no prompt/search/visual-line mode to cancel,
|
|
// it should clear the mark.
|
|
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
|
ASSERT_EQ(b.MarkSet(), false);
|
|
}
|
|
|
|
|
|
TEST(CommandSemantics_CopyRegion_And_KillRegion)
|
|
{
|
|
TestHarness h;
|
|
Editor &ed = h.EditorRef();
|
|
Buffer &b = h.Buf();
|
|
|
|
b.insert_text(0, 0, std::string("hello world"));
|
|
b.SetCursor(0, 0);
|
|
|
|
ed.KillRingClear();
|
|
ed.SetKillChain(false);
|
|
|
|
// Copy "hello" (region [0,5)).
|
|
ASSERT_TRUE(h.Exec(CommandId::ToggleMark));
|
|
b.SetCursor(5, 0);
|
|
ASSERT_TRUE(h.Exec(CommandId::CopyRegion));
|
|
ASSERT_EQ(ed.KillRingHead(), std::string("hello"));
|
|
ASSERT_EQ(b.MarkSet(), false);
|
|
ASSERT_EQ(h.Text(), std::string("hello world"));
|
|
|
|
// Kill "world" (region [6,11)).
|
|
ed.SetKillChain(false);
|
|
b.SetCursor(6, 0);
|
|
ASSERT_TRUE(h.Exec(CommandId::ToggleMark));
|
|
b.SetCursor(11, 0);
|
|
ASSERT_TRUE(h.Exec(CommandId::KillRegion));
|
|
ASSERT_EQ(ed.KillRingHead(), std::string("world"));
|
|
ASSERT_EQ(b.MarkSet(), false);
|
|
ASSERT_EQ(h.Text(), std::string("hello "));
|
|
}
|
|
|
|
|
|
TEST(CommandSemantics_Syntax_OnOff_SetsUserOverride)
|
|
{
|
|
TestHarness h;
|
|
Editor &ed = h.EditorRef();
|
|
Buffer &b = h.Buf();
|
|
|
|
// Before any explicit :syntax command, nothing has overridden the
|
|
// frontend's config-driven default.
|
|
ASSERT_EQ(b.SyntaxUserOverride(), false);
|
|
|
|
ASSERT_TRUE(Execute(ed, CommandId::Syntax, "off"));
|
|
ASSERT_EQ(b.SyntaxEnabled(), false);
|
|
// This flag is what a frontend's per-frame "apply config default" pass
|
|
// must check before re-enabling syntax, or a manual :syntax off gets
|
|
// silently undone on the next frame.
|
|
ASSERT_EQ(b.SyntaxUserOverride(), true);
|
|
|
|
ASSERT_TRUE(Execute(ed, CommandId::Syntax, "on"));
|
|
ASSERT_EQ(b.SyntaxEnabled(), true);
|
|
ASSERT_EQ(b.SyntaxUserOverride(), true);
|
|
}
|