Commands: second-pass fixes from the follow-up review
- Motion and delete commands read rows through RowsView (on-demand, version-cached) instead of Buffer::Rows(), which rebuilt every line of the file after each edit. On a 500k-line buffer after an edit: backspace/delete/motions 80-90 ms -> ~0.1 ms; C-u 3000 C-d 273 s -> 0.25 s. Whole-buffer search scans keep Rows() (cached between edits). - While a prompt is open only prompt-aware and view-only commands run. Others edited the buffer behind the prompt (bypassing the read-only check) or switched/closed buffers under a pending confirmation, which then acted on the wrong buffer. The ':' command path now applies the read-only check too. - Pasting into a prompt (ImGui) flattens line breaks to spaces instead of emitting Newline, which accepted the prompt and sent the rest of the paste to the buffer (or ran replace-all). - The mark region clamps both ends; a stale mark past the end of the buffer is treated as end of buffer. This fixes kill-region deleting the wrong text and leaving the cursor past end-of-line. - Kill-line with a count stops at end of buffer instead of killing lines above the cursor (regression from the D3 change). - Undo groups nest, so SmartNewline in visual-line mode is one step. - Word motions and word deletion treat non-ASCII bytes as word characters and so never split a UTF-8 character. - The universal-argument count saturates at 1,000,000 instead of overflowing int. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
@@ -578,3 +578,120 @@ TEST(Audit_PieceTable_MovedFrom_LineIndexReset)
|
||||
ASSERT_EQ(a.LineCount(), (std::size_t) 1);
|
||||
ASSERT_EQ(b.LineCount(), (std::size_t) 4);
|
||||
}
|
||||
|
||||
|
||||
// Kill-line with a count stops at the end of the buffer instead of killing
|
||||
// lines above the starting line.
|
||||
TEST(Audit_KillLine_Count_StopsAtEndOfBuffer)
|
||||
{
|
||||
TestHarness h;
|
||||
Buffer &b = h.Buf();
|
||||
b.insert_text(0, 0, "a\nb\nc");
|
||||
b.SetCursor(0, 1);
|
||||
ASSERT_TRUE(h.Exec(CommandId::KillLine, std::string(), 3));
|
||||
ASSERT_EQ(h.Text(), std::string("a"));
|
||||
}
|
||||
|
||||
|
||||
// A mark left past the end of the buffer by edits is clamped before use:
|
||||
// kill-region deletes the text between the clamped mark and the cursor.
|
||||
TEST(Audit_KillRegion_StaleMarkIsClamped)
|
||||
{
|
||||
TestHarness h;
|
||||
Buffer &b = h.Buf();
|
||||
b.insert_text(0, 0, "abcdefgh");
|
||||
b.SetMark(1, 3); // no such row
|
||||
b.SetCursor(5, 0);
|
||||
ASSERT_TRUE(h.Exec(CommandId::KillRegion));
|
||||
ASSERT_EQ(h.Text(), std::string("abcde"));
|
||||
ASSERT_EQ(h.EditorRef().KillRingHead(), std::string("fgh"));
|
||||
}
|
||||
|
||||
|
||||
// After kill-region the cursor is on a real column, so typing then undoing
|
||||
// removes exactly what was typed.
|
||||
TEST(Audit_KillRegion_CursorClampedForUndo)
|
||||
{
|
||||
TestHarness h;
|
||||
Buffer &b = h.Buf();
|
||||
b.insert_text(0, 0, "worldond\nsecond\n");
|
||||
b.SetMark(11, 0); // past end of line 0 (the line was shortened)
|
||||
b.SetCursor(3, 1);
|
||||
ASSERT_TRUE(h.Exec(CommandId::KillRegion));
|
||||
ASSERT_EQ(h.Text(), std::string("worldondond\n"));
|
||||
ASSERT_EQ(b.Curx(), (std::size_t) 8);
|
||||
ASSERT_TRUE(h.Exec(CommandId::InsertText, "Z"));
|
||||
ASSERT_TRUE(h.Undo());
|
||||
ASSERT_EQ(h.Text(), std::string("worldondond\n"));
|
||||
}
|
||||
|
||||
|
||||
// Undo groups nest: SmartNewline in visual-line mode (which groups the
|
||||
// visual newline inside its own group) is a single undo step.
|
||||
TEST(Audit_UndoGroups_Nest_SmartNewlineVisual)
|
||||
{
|
||||
TestHarness h;
|
||||
Buffer &b = h.Buf();
|
||||
b.insert_text(0, 0, " ab\n cd");
|
||||
b.SetCursor(3, 0);
|
||||
b.VisualLineStart();
|
||||
b.VisualLineSetActiveY(1);
|
||||
b.SetCursor(3, 1);
|
||||
ASSERT_TRUE(h.Exec(CommandId::SmartNewline));
|
||||
b.VisualLineClear();
|
||||
ASSERT_TRUE(h.Undo());
|
||||
ASSERT_EQ(h.Text(), std::string(" ab\n cd"));
|
||||
}
|
||||
|
||||
|
||||
// Word motion and word deletion never stop inside a UTF-8 character.
|
||||
TEST(Audit_WordMotion_Utf8)
|
||||
{
|
||||
TestHarness h;
|
||||
Buffer &b = h.Buf();
|
||||
b.insert_text(0, 0, "ab \xC3\xA9");
|
||||
b.SetCursor(5, 0);
|
||||
ASSERT_TRUE(h.Exec(CommandId::WordPrev));
|
||||
ASSERT_EQ(b.Curx(), (std::size_t) 3);
|
||||
b.SetCursor(5, 0);
|
||||
ASSERT_TRUE(h.Exec(CommandId::DeleteWordPrev));
|
||||
ASSERT_EQ(h.Text(), std::string("ab "));
|
||||
}
|
||||
|
||||
|
||||
// With a prompt open, buffer-editing commands do not run (they used to edit
|
||||
// the buffer behind the prompt, skipping the read-only check).
|
||||
TEST(Audit_Prompt_BlocksBufferCommands)
|
||||
{
|
||||
TestHarness h;
|
||||
Editor &ed = h.EditorRef();
|
||||
Buffer &b = h.Buf();
|
||||
b.insert_text(0, 0, "one\ntwo\n");
|
||||
b.SetCursor(0, 0);
|
||||
ASSERT_TRUE(h.Exec(CommandId::FindStart));
|
||||
ASSERT_TRUE(ed.PromptActive());
|
||||
ASSERT_TRUE(h.Exec(CommandId::KillLine));
|
||||
ASSERT_TRUE(h.Exec(CommandId::DeleteChar));
|
||||
ASSERT_TRUE(h.Exec(CommandId::BufferNext));
|
||||
ASSERT_TRUE(ed.PromptActive());
|
||||
ASSERT_TRUE(ed.CurrentBuffer() == &b);
|
||||
ASSERT_TRUE(h.Exec(CommandId::Refresh)); // C-g still cancels
|
||||
ASSERT_TRUE(!ed.PromptActive());
|
||||
ASSERT_EQ(h.Text(), std::string("one\ntwo\n"));
|
||||
}
|
||||
|
||||
|
||||
// The universal-argument count saturates instead of overflowing int.
|
||||
TEST(Audit_UArg_DoesNotOverflow)
|
||||
{
|
||||
TestHarness h;
|
||||
Editor &ed = h.EditorRef();
|
||||
ed.UArgStart();
|
||||
for (int i = 0; i < 12; ++i)
|
||||
ed.UArgDigit(9);
|
||||
ASSERT_EQ(ed.UArgGet(), 1000000);
|
||||
ed.UArgStart();
|
||||
for (int i = 0; i < 40; ++i)
|
||||
ed.UArgStart();
|
||||
ASSERT_EQ(ed.UArgGet(), 1000000);
|
||||
}
|
||||
|
||||
@@ -102,3 +102,15 @@ TEST(PasteSplit_MultilineCRLF)
|
||||
ASSERT_TRUE(cmds[3].id == CommandId::Newline);
|
||||
ASSERT_TRUE(cmds[4].arg == "line3");
|
||||
}
|
||||
|
||||
|
||||
// Pasting into a prompt must not produce Newline (which accepts the prompt):
|
||||
// line breaks become spaces in a single InsertText.
|
||||
TEST(PasteSplit_ForPrompt_FlattensLineBreaks)
|
||||
{
|
||||
const auto cmds = SplitPasteForPrompt("foo\nbar\r\nbaz\rqux");
|
||||
ASSERT_EQ(cmds.size(), (std::size_t) 1);
|
||||
ASSERT_TRUE(cmds[0].id == CommandId::InsertText);
|
||||
ASSERT_EQ(cmds[0].arg, std::string("foo bar baz qux"));
|
||||
ASSERT_EQ(SplitPasteForPrompt("").size(), (std::size_t) 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user