- 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.
332 lines
9.0 KiB
C++
332 lines
9.0 KiB
C++
#include "Test.h"
|
|
|
|
#include "Buffer.h"
|
|
#include "Command.h"
|
|
#include "Editor.h"
|
|
|
|
#include <string>
|
|
|
|
|
|
static std::string
|
|
dump_buf(const Buffer &buf)
|
|
{
|
|
std::string out;
|
|
for (const auto &r: buf.Rows()) {
|
|
out += static_cast<std::string>(r);
|
|
out.push_back('\n');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
static std::string
|
|
dump_bytes(const std::string &s)
|
|
{
|
|
static const char *hex = "0123456789abcdef";
|
|
std::string out;
|
|
for (unsigned char c: s) {
|
|
out.push_back(hex[(c >> 4) & 0xF]);
|
|
out.push_back(hex[c & 0xF]);
|
|
out.push_back(' ');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
TEST (VisualLineMode_BroadcastInsert)
|
|
{
|
|
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);
|
|
|
|
// 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));
|
|
|
|
// Broadcast insert to all selected lines
|
|
ASSERT_TRUE(Execute(ed, std::string("insert"), std::string("X")));
|
|
|
|
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 = "fXoo\nfXoo\nfXoo\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);
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
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());
|
|
// Note: buffers that end with a trailing '\n' have an extra empty row.
|
|
const std::string exp = "acd\nacd\nacd\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);
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
Editor ed;
|
|
ed.SetDimensions(24, 80);
|
|
|
|
Buffer b;
|
|
b.insert_text(0, 0, "foo\nfoo\nfoo\n");
|
|
b.SetCursor(1, 0);
|
|
ed.AddBuffer(std::move(b));
|
|
|
|
ASSERT_TRUE(Execute(ed, std::string("visual-line-toggle")));
|
|
ASSERT_TRUE(Execute(ed, std::string("down"), std::string(), 2));
|
|
|
|
// C-g is mapped to "refresh" and should cancel visual-line mode.
|
|
ASSERT_TRUE(Execute(ed, std::string("refresh")));
|
|
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
|
ASSERT_TRUE(!ed.CurrentBuffer()->VisualLineActive());
|
|
|
|
// After cancel, edits should only affect the primary cursor line.
|
|
ASSERT_TRUE(Execute(ed, std::string("insert"), std::string("X")));
|
|
const std::string got = dump_buf(*ed.CurrentBuffer());
|
|
// Cursor is still on the last line we moved to (down, down).
|
|
const std::string exp = "foo\nfoo\nfXoo\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);
|
|
}
|
|
|
|
|
|
TEST (Yank_ClearsMarkAndVisualLine)
|
|
{
|
|
InstallDefaultCommands();
|
|
|
|
Editor ed;
|
|
ed.SetDimensions(24, 80);
|
|
|
|
Buffer b;
|
|
b.insert_text(0, 0, "foo\nbar\n");
|
|
b.SetCursor(1, 0);
|
|
ed.AddBuffer(std::move(b));
|
|
|
|
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
|
Buffer *buf = ed.CurrentBuffer();
|
|
|
|
// Seed mark + visual-line highlighting.
|
|
buf->SetMark(buf->Curx(), buf->Cury());
|
|
ASSERT_TRUE(buf->MarkSet());
|
|
|
|
ASSERT_TRUE(Execute(ed, std::string("visual-line-toggle")));
|
|
ASSERT_TRUE(Execute(ed, std::string("down"), std::string(), 1));
|
|
ASSERT_TRUE(buf->VisualLineActive());
|
|
|
|
// Yank should clear mark and any highlighting.
|
|
ed.KillRingClear();
|
|
ed.KillRingPush("X");
|
|
ASSERT_TRUE(Execute(ed, std::string("yank")));
|
|
|
|
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));
|
|
} |