Files
kte/tests/test_visual_line_mode.cc
T
Claude 29d0ee8244 Remove Buffer::Rows() and the Line wrapper
The row cache rebuilt a string for every line of the file after each edit
and had one production caller left (filetype detection in the ImGui
frontend, which needs only the first line). Removed with it: rows_,
nrows_, the dirty flag set on every edit, ensure_rows_cache and a mutex
guarding only that cache. Tests read lines with GetLineString, Nrows or,
where they compare every line, a test-only LinesForTests().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
2026-09-24 12:19:59 -07:00

333 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.LinesForTests()) {
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));
}