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>
160 lines
4.5 KiB
C++
160 lines
4.5 KiB
C++
#include "Test.h"
|
|
|
|
#include "tests/TestHarness.h"
|
|
|
|
using ktet::TestHarness;
|
|
|
|
// These tests intentionally drive the prompt-based search/replace UI headlessly
|
|
// via `Execute(Editor&, CommandId, ...)` to lock down behavior without ncurses.
|
|
|
|
TEST(SearchFlow_FindStart_Success_LeavesCursorOnMatch_And_ClearsSearchState)
|
|
{
|
|
TestHarness h;
|
|
Editor &ed = h.EditorRef();
|
|
Buffer &b = h.Buf();
|
|
|
|
b.insert_text(0, 0, "abc def abc");
|
|
b.SetCursor(0, 0);
|
|
b.SetOffsets(0, 0);
|
|
|
|
// Keep a mark set to ensure search doesn't clobber it.
|
|
b.SetMark(0, 0);
|
|
ASSERT_TRUE(b.MarkSet());
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::FindStart));
|
|
ASSERT_TRUE(ed.PromptActive());
|
|
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::Search);
|
|
ASSERT_TRUE(ed.SearchActive());
|
|
|
|
// Typing into the prompt uses InsertText and should jump to the first match.
|
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "def"));
|
|
ASSERT_EQ(b.Cury(), (std::size_t) 0);
|
|
ASSERT_EQ(b.Curx(), (std::size_t) 4);
|
|
|
|
// Enter (Newline) accepts the prompt and ends incremental search.
|
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
|
ASSERT_TRUE(!ed.PromptActive());
|
|
ASSERT_TRUE(!ed.SearchActive());
|
|
ASSERT_TRUE(b.MarkSet());
|
|
}
|
|
|
|
|
|
TEST(SearchFlow_FindStart_NotFound_RestoresOrigin_And_ClearsSearchState)
|
|
{
|
|
TestHarness h;
|
|
Editor &ed = h.EditorRef();
|
|
Buffer &b = h.Buf();
|
|
|
|
b.insert_text(0, 0, "hello world\nsecond line\n");
|
|
b.SetCursor(3, 0);
|
|
b.SetOffsets(1, 2);
|
|
|
|
const std::size_t ox = b.Curx();
|
|
const std::size_t oy = b.Cury();
|
|
const std::size_t orow = b.Rowoffs();
|
|
const std::size_t ocol = b.Coloffs();
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::FindStart));
|
|
ASSERT_TRUE(ed.PromptActive());
|
|
ASSERT_TRUE(ed.SearchActive());
|
|
|
|
// Not-found should restore cursor/viewport to the saved origin while still in prompt.
|
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "zzzz"));
|
|
ASSERT_EQ(b.Curx(), ox);
|
|
ASSERT_EQ(b.Cury(), oy);
|
|
ASSERT_EQ(b.Rowoffs(), orow);
|
|
ASSERT_EQ(b.Coloffs(), ocol);
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
|
ASSERT_TRUE(!ed.PromptActive());
|
|
ASSERT_TRUE(!ed.SearchActive());
|
|
}
|
|
|
|
|
|
TEST(SearchFlow_SearchReplace_EmptyFind_DoesNotMutateBuffer_And_ClearsState)
|
|
{
|
|
TestHarness h;
|
|
Editor &ed = h.EditorRef();
|
|
Buffer &b = h.Buf();
|
|
|
|
b.insert_text(0, 0, "abc abc\n");
|
|
b.SetCursor(0, 0);
|
|
|
|
const std::string before = h.Text();
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::SearchReplace));
|
|
ASSERT_TRUE(ed.PromptActive());
|
|
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::ReplaceFind);
|
|
|
|
// Accept empty find -> proceed to ReplaceWith.
|
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
|
ASSERT_TRUE(ed.PromptActive());
|
|
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::ReplaceWith);
|
|
|
|
// Provide replacement and accept -> should cancel due to empty find.
|
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "X"));
|
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
|
|
|
ASSERT_TRUE(!ed.PromptActive());
|
|
ASSERT_TRUE(!ed.SearchActive());
|
|
ASSERT_EQ(h.Text(), before);
|
|
}
|
|
|
|
|
|
TEST(SearchFlow_SearchReplace_EmptyWith_ReplacesAdjacentOverlappingMatches)
|
|
{
|
|
TestHarness h;
|
|
Editor &ed = h.EditorRef();
|
|
Buffer &b = h.Buf();
|
|
|
|
// "aaaa" with "aa" -> "" must remove both non-overlapping occurrences, not
|
|
// just the first: after deleting the match at column 0, the next "aa" now
|
|
// sits at column 0 too (not column 1), so the scan must resume at the
|
|
// deletion point rather than one character past it.
|
|
b.insert_text(0, 0, "aaaa\n");
|
|
b.SetCursor(0, 0);
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::SearchReplace));
|
|
ASSERT_TRUE(ed.PromptActive());
|
|
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::ReplaceFind);
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "aa"));
|
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
|
ASSERT_TRUE(ed.PromptActive());
|
|
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::ReplaceWith);
|
|
|
|
// Leave the replacement empty and accept.
|
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
|
|
|
ASSERT_TRUE(!ed.PromptActive());
|
|
ASSERT_EQ(std::string(b.Rows()[0]), std::string(""));
|
|
}
|
|
|
|
|
|
TEST(SearchFlow_RegexFind_InvalidPattern_FailsSafely_And_ClearsStateOnEnter)
|
|
{
|
|
TestHarness h;
|
|
Editor &ed = h.EditorRef();
|
|
Buffer &b = h.Buf();
|
|
|
|
b.insert_text(0, 0, "abc\ndef\n");
|
|
b.SetCursor(1, 0);
|
|
b.SetOffsets(0, 0);
|
|
|
|
const std::size_t ox = b.Curx();
|
|
const std::size_t oy = b.Cury();
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::RegexFindStart));
|
|
ASSERT_TRUE(ed.PromptActive());
|
|
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::RegexSearch);
|
|
|
|
// Invalid regex should not crash; cursor should remain at origin due to no matches.
|
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "("));
|
|
ASSERT_EQ(b.Curx(), ox);
|
|
ASSERT_EQ(b.Cury(), oy);
|
|
|
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
|
ASSERT_TRUE(!ed.PromptActive());
|
|
ASSERT_TRUE(!ed.SearchActive());
|
|
}
|