Files
kte/tests/test_paste_split.cc
T
Claude 812cde090f 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
2026-09-24 09:16:04 -07:00

117 lines
3.6 KiB
C++

// Tests for SplitPasteIntoCommands: pasted text must turn embedded line
// breaks into Newline commands so InsertText never receives a '\r' or '\n'.
#include "Test.h"
#include "PasteSplit.h"
namespace {
// Assert that no InsertText command carries an embedded newline/carriage
// return, mirroring the rejection in Command.cc's InsertText handler.
void
assert_no_newlines_in_inserts(const std::vector<MappedInput> &cmds)
{
for (const auto &c : cmds) {
if (c.id == CommandId::InsertText) {
ASSERT_TRUE(c.arg.find('\n') == std::string::npos);
ASSERT_TRUE(c.arg.find('\r') == std::string::npos);
}
}
}
} // namespace
TEST(PasteSplit_PlainTextNoNewline)
{
auto cmds = SplitPasteIntoCommands("hello world");
ASSERT_EQ(cmds.size(), 1u);
ASSERT_TRUE(cmds[0].id == CommandId::InsertText);
ASSERT_TRUE(cmds[0].arg == "hello world");
}
TEST(PasteSplit_UnixNewlines)
{
auto cmds = SplitPasteIntoCommands("a\nb");
assert_no_newlines_in_inserts(cmds);
ASSERT_EQ(cmds.size(), 3u);
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
}
// Regression: CRLF clipboards left a '\r' in the InsertText segment, which
// InsertText rejected with "InsertText arg must not contain newlines".
TEST(PasteSplit_WindowsCRLF)
{
auto cmds = SplitPasteIntoCommands("a\r\nb");
assert_no_newlines_in_inserts(cmds);
ASSERT_EQ(cmds.size(), 3u);
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
}
// Regression: a bare '\r' (classic Mac / some macOS apps) must also break lines.
TEST(PasteSplit_BareCR)
{
auto cmds = SplitPasteIntoCommands("a\rb");
assert_no_newlines_in_inserts(cmds);
ASSERT_EQ(cmds.size(), 3u);
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
}
TEST(PasteSplit_BlankLinesPreserved)
{
// Two line breaks between "a" and "b" => one empty line.
auto cmds = SplitPasteIntoCommands("a\n\nb");
assert_no_newlines_in_inserts(cmds);
ASSERT_EQ(cmds.size(), 4u);
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
ASSERT_TRUE(cmds[2].id == CommandId::Newline);
ASSERT_TRUE(cmds[3].id == CommandId::InsertText && cmds[3].arg == "b");
}
TEST(PasteSplit_TrailingNewline)
{
auto cmds = SplitPasteIntoCommands("abc\n");
assert_no_newlines_in_inserts(cmds);
ASSERT_EQ(cmds.size(), 2u);
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "abc");
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
}
TEST(PasteSplit_MultilineCRLF)
{
auto cmds = SplitPasteIntoCommands("line1\r\nline2\r\nline3");
assert_no_newlines_in_inserts(cmds);
ASSERT_EQ(cmds.size(), 5u);
ASSERT_TRUE(cmds[0].arg == "line1");
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
ASSERT_TRUE(cmds[2].arg == "line2");
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);
}