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:
@@ -65,6 +65,49 @@ TEST (VisualLineMode_BroadcastInsert)
|
||||
}
|
||||
|
||||
|
||||
TEST (VisualLineMode_BroadcastInsert_UndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
b.insert_text(0, 0, "foo\nfoo\nfoo\n");
|
||||
b.SetCursor(1, 0); // fo|o
|
||||
ed.AddBuffer(std::move(b));
|
||||
|
||||
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, std::string("visual-line-toggle")));
|
||||
ASSERT_TRUE(Execute(ed, std::string("down"), std::string(), 2));
|
||||
|
||||
// Broadcast insert to all selected lines.
|
||||
ASSERT_TRUE(Execute(ed, std::string("insert"), std::string("X")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
const std::string exp = "fXoo\nfXoo\nfXoo\n\n";
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
|
||||
// Undo should restore all affected lines in a single step.
|
||||
ASSERT_TRUE(Execute(ed, std::string("undo")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
const std::string exp = "foo\nfoo\nfoo\n\n";
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
|
||||
// Redo should re-apply the whole insert.
|
||||
ASSERT_TRUE(Execute(ed, std::string("redo")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
const std::string exp = "fXoo\nfXoo\nfXoo\n\n";
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST (VisualLineMode_BroadcastBackspace)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
@@ -92,6 +135,46 @@ TEST (VisualLineMode_BroadcastBackspace)
|
||||
}
|
||||
|
||||
|
||||
TEST (VisualLineMode_BroadcastBackspace_UndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
b.insert_text(0, 0, "abcd\nabcd\nabcd\n");
|
||||
b.SetCursor(2, 0); // ab|cd
|
||||
ed.AddBuffer(std::move(b));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, std::string("visual-line-toggle")));
|
||||
ASSERT_TRUE(Execute(ed, std::string("down"), std::string(), 2));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, std::string("backspace")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
const std::string exp = "acd\nacd\nacd\n\n";
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
|
||||
// Undo should restore all affected lines.
|
||||
ASSERT_TRUE(Execute(ed, std::string("undo")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
const std::string exp = "abcd\nabcd\nabcd\n\n";
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
|
||||
// Redo should re-apply.
|
||||
ASSERT_TRUE(Execute(ed, std::string("redo")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
const std::string exp = "acd\nacd\nacd\n\n";
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST (VisualLineMode_CancelWithCtrlG)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
@@ -155,4 +238,95 @@ TEST (Yank_ClearsMarkAndVisualLine)
|
||||
|
||||
ASSERT_TRUE(!buf->MarkSet());
|
||||
ASSERT_TRUE(!buf->VisualLineActive());
|
||||
}
|
||||
|
||||
|
||||
TEST (VisualLineMode_Yank_BroadcastsToBOL_AndUndo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
b.insert_text(0, 0, "aa\nbb\ncc\n");
|
||||
b.SetCursor(1, 0); // a|a
|
||||
ed.AddBuffer(std::move(b));
|
||||
|
||||
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
||||
|
||||
// Enter visual-line mode and extend selection to 3 lines.
|
||||
ASSERT_TRUE(Execute(ed, std::string("visual-line-toggle")));
|
||||
ASSERT_TRUE(Execute(ed, std::string("down"), std::string(), 2));
|
||||
ASSERT_TRUE(ed.CurrentBuffer()->VisualLineActive());
|
||||
|
||||
ed.KillRingClear();
|
||||
ed.KillRingPush("X");
|
||||
|
||||
// Yank in visual-line mode should paste at BOL on every affected line.
|
||||
ASSERT_TRUE(Execute(ed, std::string("yank")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
// Note: buffers that end with a trailing '\n' have an extra empty row.
|
||||
const std::string exp = "Xaa\nXbb\nXcc\n\n";
|
||||
if (got != exp) {
|
||||
std::cerr << "Expected (len=" << exp.size() << ") bytes: " << dump_bytes(exp) << "\n";
|
||||
std::cerr << "Got (len=" << got.size() << ") bytes: " << dump_bytes(got) << "\n";
|
||||
}
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
|
||||
// Undo should restore all affected lines in a single step.
|
||||
ASSERT_TRUE(Execute(ed, std::string("undo")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
const std::string exp = "aa\nbb\ncc\n\n";
|
||||
if (got != exp) {
|
||||
std::cerr << "Expected (len=" << exp.size() << ") bytes: " << dump_bytes(exp) << "\n";
|
||||
std::cerr << "Got (len=" << got.size() << ") bytes: " << dump_bytes(got) << "\n";
|
||||
}
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
|
||||
// Redo should re-apply the whole yank.
|
||||
ASSERT_TRUE(Execute(ed, std::string("redo")));
|
||||
{
|
||||
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||
const std::string exp = "Xaa\nXbb\nXcc\n\n";
|
||||
ASSERT_TRUE(got == exp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST (VisualLineMode_Highlight_IsPerLineCursorSpot)
|
||||
{
|
||||
Buffer b;
|
||||
// Note: buffers that end with a trailing '\n' have an extra empty row.
|
||||
b.insert_text(0, 0, "abcd\nx\nhi\n");
|
||||
// Place primary cursor on line 0 at column 3 (abc|d).
|
||||
b.SetCursor(3, 0);
|
||||
|
||||
// Select lines 0..2 in visual-line mode.
|
||||
b.VisualLineStart();
|
||||
b.VisualLineSetActiveY(2);
|
||||
ASSERT_TRUE(b.VisualLineActive());
|
||||
ASSERT_TRUE(b.VisualLineStartY() == 0);
|
||||
ASSERT_TRUE(b.VisualLineEndY() == 2);
|
||||
|
||||
// Line 0: "abcd" (len=4) => spot is 3
|
||||
ASSERT_TRUE(b.VisualLineSpotSelected(0, 3));
|
||||
ASSERT_TRUE(!b.VisualLineSpotSelected(0, 0));
|
||||
ASSERT_TRUE(!b.VisualLineSpotSelected(0, 2));
|
||||
ASSERT_TRUE(!b.VisualLineSpotSelected(0, 4));
|
||||
|
||||
// Line 1: "x" (len=1) => spot clamps to EOL (1)
|
||||
ASSERT_TRUE(b.VisualLineSpotSelected(1, 1));
|
||||
ASSERT_TRUE(!b.VisualLineSpotSelected(1, 0));
|
||||
|
||||
// Line 2: "hi" (len=2) => spot clamps to EOL (2)
|
||||
ASSERT_TRUE(b.VisualLineSpotSelected(2, 2));
|
||||
ASSERT_TRUE(!b.VisualLineSpotSelected(2, 0));
|
||||
|
||||
// Outside the selected line range should never be highlighted.
|
||||
ASSERT_TRUE(!b.VisualLineSpotSelected(3, 0));
|
||||
}
|
||||
Reference in New Issue
Block a user