Terminal: decode UTF-8 input in kte; visual-line edits keep characters whole

- TerminalInputHandler reads bytes with getch() and assembles UTF-8
  itself, waiting up to a second for a character's continuation bytes.
  The frontend reads the rest of an input batch with a zero timeout, and
  get_wch() then gave up on a character whose bytes had not all arrived;
  ncurses discarded the rest as an invalid sequence, so text pasted over
  a slow link silently lost characters and what followed them (typing
  'ab\xc3', then '\xa9cd' 50 ms later saved 'ab').
- Visual-line insert, delete, backspace and newline carried the cursor's
  byte column to the other selected lines, splitting multibyte
  characters there (the saved file became invalid UTF-8). The column is
  now carried as a character count.
- Visual-line delete-char left the cursor on the last selected line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent 8cb688c574
commit 868ee3fe2f
3 changed files with 145 additions and 16 deletions
+41
View File
@@ -1400,3 +1400,44 @@ TEST(Audit_VisualYank_CursorStaysInBuffer)
(void) h.Exec(CommandId::Undo);
ASSERT_EQ(h.Text(), std::string("abc def\n"));
}
// Visual-line edits apply at the cursor's column on every selected line; a
// byte column taken from an ASCII line fell inside the CJK characters of
// the next line and split them. The column is now carried in characters.
TEST(Audit_VisualLine_EditsKeepUtf8Whole)
{
const std::string cjk = "\xE4\xB8\x96\xE7\x95\x8C"; // two 3-byte characters
{
TestHarness h;
h.Buf().insert_text(0, 0, "abcdef\n" + cjk + "\n");
h.Buf().SetCursor(0, 1);
ASSERT_TRUE(h.Exec(CommandId::VisualLineModeToggle));
ASSERT_TRUE(h.Exec(CommandId::MoveUp));
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
ASSERT_TRUE(h.Exec(CommandId::InsertText, "X"));
ASSERT_EQ(h.Text(), std::string("aXbcdef\n\xE4\xB8\x96X\xE7\x95\x8C\n"));
}
{
TestHarness h;
h.Buf().insert_text(0, 0, "abcdef\n" + cjk + "\n");
h.Buf().SetCursor(0, 1);
ASSERT_TRUE(h.Exec(CommandId::VisualLineModeToggle));
ASSERT_TRUE(h.Exec(CommandId::MoveUp));
ASSERT_TRUE(h.Exec(CommandId::DeleteChar));
ASSERT_EQ(h.Text(), std::string("bcdef\n\xE7\x95\x8C\n"));
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
ASSERT_TRUE(h.Exec(CommandId::Backspace));
ASSERT_EQ(h.Text(), std::string("cdef\n\n"));
}
{
TestHarness h;
h.Buf().insert_text(0, 0, "abcdef\n" + cjk + "\n");
h.Buf().SetCursor(0, 1);
ASSERT_TRUE(h.Exec(CommandId::VisualLineModeToggle));
ASSERT_TRUE(h.Exec(CommandId::MoveUp));
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
ASSERT_TRUE(h.Exec(CommandId::Newline));
ASSERT_EQ(h.Text(), std::string("a\nbcdef\n\xE4\xB8\x96\n\xE7\x95\x8C\n"));
}
}