Files
kte/tests/test_command_semantics.cc
T
Claude 7039c14507 Command.cc: share the word scans, search reset and completion prefix
Word motion (M-b/M-f) and word deletion (M-Backspace/M-d) each carried a
copy of the same backward and forward scans; word_back and word_forward
now serve both. Deleting N words backwards built the kill text by
prepending each piece (quadratic in N); the pieces are joined once. The
five-call search reset appeared nine times (end_search), and the
completion longest-common-prefix loop twice (keep_common_prefix).

A new test pins word motion and deletion with counts across line ends
and punctuation; it gives the same results on the code before this
change.

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

186 lines
5.3 KiB
C++

#include "Test.h"
#include "TestHarness.h"
using ktet::TestHarness;
TEST(CommandSemantics_KillToEOL_KillChain_And_Yank)
{
TestHarness h;
Editor &ed = h.EditorRef();
Buffer &b = h.Buf();
b.insert_text(0, 0, std::string("abc\ndef"));
b.SetCursor(1, 0); // a|bc
ed.KillRingClear();
ed.SetKillChain(false);
ASSERT_TRUE(h.Exec(CommandId::KillToEOL));
ASSERT_EQ(h.Text(), std::string("a\ndef"));
ASSERT_EQ(ed.KillRingHead(), std::string("bc"));
// At EOL, KillToEOL kills the newline (join).
ASSERT_TRUE(h.Exec(CommandId::KillToEOL));
ASSERT_EQ(h.Text(), std::string("adef"));
ASSERT_EQ(ed.KillRingHead(), std::string("bc\n"));
// Yank pastes the kill ring head and breaks the kill chain.
ASSERT_TRUE(h.Exec(CommandId::Yank));
ASSERT_EQ(h.Text(), std::string("abc\ndef"));
ASSERT_EQ(ed.KillRingHead(), std::string("bc\n"));
ASSERT_EQ(ed.KillChain(), false);
}
TEST(CommandSemantics_ToggleMark_JumpToMark)
{
TestHarness h;
Buffer &b = h.Buf();
b.insert_text(0, 0, std::string("hello"));
b.SetCursor(2, 0);
ASSERT_EQ(b.MarkSet(), false);
ASSERT_TRUE(h.Exec(CommandId::ToggleMark));
ASSERT_EQ(b.MarkSet(), true);
ASSERT_EQ(b.MarkCurx(), (std::size_t) 2);
ASSERT_EQ(b.MarkCury(), (std::size_t) 0);
b.SetCursor(4, 0);
ASSERT_TRUE(h.Exec(CommandId::JumpToMark));
ASSERT_EQ(b.Curx(), (std::size_t) 2);
ASSERT_EQ(b.Cury(), (std::size_t) 0);
// Jump-to-mark swaps: mark becomes previous cursor.
ASSERT_EQ(b.MarkSet(), true);
ASSERT_EQ(b.MarkCurx(), (std::size_t) 4);
ASSERT_EQ(b.MarkCury(), (std::size_t) 0);
}
TEST(CommandSemantics_CtrlGRefresh_ClearsMark_WhenNothingElseToCancel)
{
TestHarness h;
Buffer &b = h.Buf();
b.insert_text(0, 0, std::string("hello"));
b.SetCursor(2, 0);
ASSERT_EQ(b.MarkSet(), false);
ASSERT_TRUE(h.Exec(CommandId::ToggleMark));
ASSERT_EQ(b.MarkSet(), true);
// C-g is mapped to Refresh; when there's no prompt/search/visual-line mode to cancel,
// it should clear the mark.
ASSERT_TRUE(h.Exec(CommandId::Refresh));
ASSERT_EQ(b.MarkSet(), false);
}
TEST(CommandSemantics_CopyRegion_And_KillRegion)
{
TestHarness h;
Editor &ed = h.EditorRef();
Buffer &b = h.Buf();
b.insert_text(0, 0, std::string("hello world"));
b.SetCursor(0, 0);
ed.KillRingClear();
ed.SetKillChain(false);
// Copy "hello" (region [0,5)).
ASSERT_TRUE(h.Exec(CommandId::ToggleMark));
b.SetCursor(5, 0);
ASSERT_TRUE(h.Exec(CommandId::CopyRegion));
ASSERT_EQ(ed.KillRingHead(), std::string("hello"));
ASSERT_EQ(b.MarkSet(), false);
ASSERT_EQ(h.Text(), std::string("hello world"));
// Kill "world" (region [6,11)).
ed.SetKillChain(false);
b.SetCursor(6, 0);
ASSERT_TRUE(h.Exec(CommandId::ToggleMark));
b.SetCursor(11, 0);
ASSERT_TRUE(h.Exec(CommandId::KillRegion));
ASSERT_EQ(ed.KillRingHead(), std::string("world"));
ASSERT_EQ(b.MarkSet(), false);
ASSERT_EQ(h.Text(), std::string("hello "));
}
TEST(CommandSemantics_Syntax_OnOff_SetsUserOverride)
{
TestHarness h;
Editor &ed = h.EditorRef();
Buffer &b = h.Buf();
// Before any explicit :syntax command, nothing has overridden the
// frontend's config-driven default.
ASSERT_EQ(b.SyntaxUserOverride(), false);
ASSERT_TRUE(Execute(ed, CommandId::Syntax, "off"));
ASSERT_EQ(b.SyntaxEnabled(), false);
// This flag is what a frontend's per-frame "apply config default" pass
// must check before re-enabling syntax, or a manual :syntax off gets
// silently undone on the next frame.
ASSERT_EQ(b.SyntaxUserOverride(), true);
ASSERT_TRUE(Execute(ed, CommandId::Syntax, "on"));
ASSERT_EQ(b.SyntaxEnabled(), true);
ASSERT_EQ(b.SyntaxUserOverride(), true);
}
// Word motion and word deletion with counts, across line ends and
// punctuation; deleting backwards N words kills them in text order.
TEST(CommandSemantics_WordMotionAndDelete_WithCounts)
{
const std::string text = "alpha beta, gamma\n\n delta.epsilon zeta\nend";
struct Pos {
std::size_t x, y;
};
auto run_motion = [&](CommandId id, Pos start, int count) {
TestHarness h;
h.Buf().insert_text(0, 0, text);
h.Buf().SetCursor(start.x, start.y);
(void) h.Exec(id, "", count);
return Pos{h.Buf().Curx(), h.Buf().Cury()};
};
Pos p = run_motion(CommandId::WordNext, {0, 0}, 1);
ASSERT_EQ(p.x, (std::size_t) 6);
p = run_motion(CommandId::WordNext, {0, 0}, 3);
ASSERT_EQ(p.y, (std::size_t) 2);
ASSERT_EQ(p.x, (std::size_t) 2);
p = run_motion(CommandId::WordNext, {0, 0}, 5);
ASSERT_EQ(p.y, (std::size_t) 2);
ASSERT_EQ(p.x, (std::size_t) 16);
p = run_motion(CommandId::WordPrev, {3, 3}, 1);
ASSERT_EQ(p.y, (std::size_t) 3);
ASSERT_EQ(p.x, (std::size_t) 0);
p = run_motion(CommandId::WordPrev, {3, 3}, 3);
ASSERT_EQ(p.y, (std::size_t) 2);
ASSERT_EQ(p.x, (std::size_t) 8);
p = run_motion(CommandId::WordPrev, {3, 3}, 50);
ASSERT_EQ(p.y, (std::size_t) 0);
ASSERT_EQ(p.x, (std::size_t) 0);
{
TestHarness h;
h.Buf().insert_text(0, 0, text);
h.Buf().SetCursor(3, 3);
ASSERT_TRUE(h.Exec(CommandId::DeleteWordPrev, "", 3));
ASSERT_EQ(h.Text(), std::string("alpha beta, gamma\n\n delta."));
ASSERT_EQ(h.EditorRef().KillRingHead(), std::string("epsilon zeta\nend"));
}
{
TestHarness h;
h.Buf().insert_text(0, 0, text);
h.Buf().SetCursor(6, 0);
ASSERT_TRUE(h.Exec(CommandId::DeleteWordNext, "", 2));
ASSERT_EQ(h.Text(), std::string("alpha delta.epsilon zeta\nend"));
ASSERT_EQ(h.EditorRef().KillRingHead(), std::string("beta, gamma\n\n "));
}
}