Add swap journaling and group undo/redo with extensive tests.
- Introduced SwapManager for sidecar journaling of buffer mutations, with a safe recovery mechanism. - Added group undo/redo functionality, allowing atomic grouping of related edits. - Implemented `SwapRecorder` and integrated it as a callback interface for mutations. - Added unit tests for swap journaling (save/load/replay) and undo grouping. - Refactored undo to support group tracking and ID management. - Updated CMake to include the new tests and swap journaling logic.
This commit is contained in:
129
tests/test_search_replace_flow.cc
Normal file
129
tests/test_search_replace_flow.cc
Normal file
@@ -0,0 +1,129 @@
|
||||
#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_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());
|
||||
}
|
||||
Reference in New Issue
Block a user