enabled multi-line paste

This commit is contained in:
Luxferre
2026-09-24 14:38:57 +03:00
parent 3b5a8a5bfd
commit 4eee5d6ef9
3 changed files with 335 additions and 138 deletions
+139
View File
@@ -3477,3 +3477,142 @@ func TestOffloadToolResultUsesReadPrompt(t *testing.T) {
cleanupTemps(tmps)
}
// ---------- editor.feed (bracketed paste / key handling) ----------
// feedAll decodes every rune in s through e.feed, returning the resulting
// actions. It uses a real bufio.Reader so escape sequences are consumed
// exactly as they would be in the REPL loop.
func feedAll(e *editor, s string) []keyKind {
r := bufio.NewReader(strings.NewReader(s))
var kinds []keyKind
for {
rn, _, err := r.ReadRune()
if err != nil {
break
}
kinds = append(kinds, e.feed(rn, r))
}
return kinds
}
// TestFeedBracketedPasteInsertsNewlines verifies that a block pasted between
// ESC[200~ and ESC[201~ becomes literal line breaks in the buffer and that only
// the final Enter (outside the paste) submits the prompt.
func TestFeedBracketedPasteInsertsNewlines(t *testing.T) {
e := &editor{hpos: -1}
kinds := feedAll(e, "\x1b[200~line1\nline2\nline3\x1b[201~\r")
if got := string(e.buf); got != "line1\nline2\nline3" {
t.Fatalf("buf = %q, want %q", got, "line1\nline2\nline3")
}
subs := 0
for _, k := range kinds {
if k == keySubmit {
subs++
}
}
if subs != 1 {
t.Fatalf("keySubmit count = %d, want 1 (paste must not trigger submission)", subs)
}
if kinds[len(kinds)-1] != keySubmit {
t.Fatalf("last action = %v, want keySubmit", kinds[len(kinds)-1])
}
}
// TestFeedCRLFInsidePasteCountsOnce verifies a CRLF inside a pasted block is
// swallowed as a single line break (not two).
func TestFeedCRLFInsidePasteCountsOnce(t *testing.T) {
e := &editor{hpos: -1}
feedAll(e, "\x1b[200~a\r\nb\x1b[201~")
if got := string(e.buf); got != "a\nb" {
t.Fatalf("buf = %q, want %q", got, "a\nb")
}
}
// TestFeedEnterSubmitsOutsidePaste verifies a plain Enter outside a paste
// submits the buffer.
func TestFeedEnterSubmitsOutsidePaste(t *testing.T) {
e := &editor{hpos: -1, buf: []rune("hi"), pos: 2}
if k := e.feed('\r', bufio.NewReader(strings.NewReader(""))); k != keySubmit {
t.Fatalf("enter action = %v, want keySubmit", k)
}
if got := string(e.buf); got != "hi" {
t.Fatalf("buf = %q, want %q", got, "hi")
}
}
// TestFeedCtrlCAbortsOutsidePaste verifies Ctrl+C interrupts when not pasting.
func TestFeedCtrlCAbortsOutsidePaste(t *testing.T) {
e := &editor{hpos: -1}
if k := e.feed(0x03, bufio.NewReader(strings.NewReader(""))); k != keyAbort {
t.Fatalf("ctrl-c action = %v, want keyAbort", k)
}
}
// TestFeedCtrlCIgnoredInsidePaste verifies a stray ^C byte inside a pasted block
// is ignored instead of interrupting.
func TestFeedCtrlCIgnoredInsidePaste(t *testing.T) {
e := &editor{hpos: -1, pasting: true}
if k := e.feed(0x03, bufio.NewReader(strings.NewReader(""))); k != keyNone {
t.Fatalf("ctrl-c in paste action = %v, want keyNone", k)
}
if len(e.buf) != 0 {
t.Fatalf("buf = %q, want empty", string(e.buf))
}
}
// TestFeedCtrlDEOFEmptyBuffer verifies Ctrl+D exits only on an empty buffer.
func TestFeedCtrlDEOFEmptyBuffer(t *testing.T) {
e := &editor{hpos: -1}
if k := e.feed(0x04, bufio.NewReader(strings.NewReader(""))); k != keyEOF {
t.Fatalf("ctrl-d action = %v, want keyEOF", k)
}
e2 := &editor{hpos: -1, buf: []rune("x"), pos: 1}
if k := e2.feed(0x04, bufio.NewReader(strings.NewReader(""))); k != keyNone {
t.Fatalf("ctrl-d non-empty action = %v, want keyNone", k)
}
}
// TestFeedArrowKeysAndHomeEnd verifies the CSI/SS3 cursor keys still move the
// cursor after the refactor.
func TestFeedArrowKeysAndHomeEnd(t *testing.T) {
e := &editor{hpos: -1, buf: []rune("abc"), pos: 3}
e.feed(0x1b, bufio.NewReader(strings.NewReader("[D")))
if e.pos != 2 {
t.Fatalf("after left arrow pos = %d, want 2", e.pos)
}
e.feed(0x1b, bufio.NewReader(strings.NewReader("[H")))
if e.pos != 0 {
t.Fatalf("after home pos = %d, want 0", e.pos)
}
e.feed(0x1b, bufio.NewReader(strings.NewReader("[F")))
if e.pos != 3 {
t.Fatalf("after end pos = %d, want 3", e.pos)
}
// Up arrow with history should restore the newest entry.
e.hist = []string{"first", "second"}
e.feed(0x1b, bufio.NewReader(strings.NewReader("[A")))
if got := string(e.buf); got != "second" {
t.Fatalf("after up arrow buf = %q, want %q", got, "second")
}
}
// TestFeedTabInserted verifies Tab is inserted literally (not a control no-op).
func TestFeedTabInserted(t *testing.T) {
e := &editor{hpos: -1}
e.feed('\t', bufio.NewReader(strings.NewReader("")))
if got := string(e.buf); got != "\t" {
t.Fatalf("buf = %q, want a tab", got)
}
}
// TestFeedInsertAtCursor verifies insert() splices at the cursor.
func TestFeedInsertAtCursor(t *testing.T) {
e := &editor{hpos: -1, buf: []rune("ac"), pos: 1}
e.feed('b', bufio.NewReader(strings.NewReader("")))
if got := string(e.buf); got != "abc" {
t.Fatalf("buf = %q, want %q", got, "abc")
}
if e.pos != 2 {
t.Fatalf("pos = %d, want 2", e.pos)
}
}