term: allow multi-line bracketed paste to not create single line with verbatim LFs

Treat "\n" (LF) like "Enter" (CR)

Avoids that when pasting 3 lines
(with a terminal like kitty, ghostty, alacritty that do not change the clipboard
in bracketed paste mode)
it turns into 1 prompt looking like:

Test> line one
..............line.two
......................line.three

Fixes golang/go#74600

Change-Id: I4a86044a4a175eccb3a96dbf7021fee97a5940ce
GitHub-Last-Rev: 0cf26df9aec994dfc61392e98b9034fe7133fb7f
GitHub-Pull-Request: golang/term#21
Reviewed-on: https://go-review.googlesource.com/c/term/+/687755
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/terminal.go b/terminal.go
index 13e9a64..bddb2e2 100644
--- a/terminal.go
+++ b/terminal.go
@@ -146,6 +146,7 @@
 	keyCtrlD     = 4
 	keyCtrlU     = 21
 	keyEnter     = '\r'
+	keyLF        = '\n'
 	keyEscape    = 27
 	keyBackspace = 127
 	keyUnknown   = 0xd800 /* UTF-16 surrogate area */ + iota
@@ -497,7 +498,7 @@
 // handleKey processes the given key and, optionally, returns a line of text
 // that the user has entered.
 func (t *Terminal) handleKey(key rune) (line string, ok bool) {
-	if t.pasteActive && key != keyEnter {
+	if t.pasteActive && key != keyEnter && key != keyLF {
 		t.addKeyToLine(key)
 		return
 	}
@@ -567,7 +568,7 @@
 				t.setLine(runes, len(runes))
 			}
 		}
-	case keyEnter:
+	case keyEnter, keyLF:
 		t.moveCursorToPos(len(t.line))
 		t.queue([]rune("\r\n"))
 		line = string(t.line)
@@ -812,6 +813,10 @@
 			if !t.pasteActive {
 				lineIsPasted = false
 			}
+			// If we have CR, consume LF if present (CRLF sequence) to avoid returning an extra empty line.
+			if key == keyEnter && len(rest) > 0 && rest[0] == keyLF {
+				rest = rest[1:]
+			}
 			line, lineOk = t.handleKey(key)
 		}
 		if len(rest) > 0 {
diff --git a/terminal_test.go b/terminal_test.go
index 29dd874..5d35cc5 100644
--- a/terminal_test.go
+++ b/terminal_test.go
@@ -6,6 +6,8 @@
 
 import (
 	"bytes"
+	"errors"
+	"fmt"
 	"io"
 	"os"
 	"runtime"
@@ -209,12 +211,24 @@
 		throwAwayLines: 1,
 	},
 	{
+		// Newline in bracketed paste mode should still work.
+		in:             "abc\x1b[200~d\nefg\x1b[201~h\r",
+		line:           "efgh",
+		throwAwayLines: 1,
+	},
+	{
 		// Lines consisting entirely of pasted data should be indicated as such.
 		in:   "\x1b[200~a\r",
 		line: "a",
 		err:  ErrPasteIndicator,
 	},
 	{
+		// Lines consisting entirely of pasted data should be indicated as such (\n paste).
+		in:   "\x1b[200~a\n",
+		line: "a",
+		err:  ErrPasteIndicator,
+	},
+	{
 		// Ctrl-C terminates readline
 		in:  "\003",
 		err: io.EOF,
@@ -296,6 +310,36 @@
 	}
 }
 
+func TestCRLF(t *testing.T) {
+	c := &MockTerminal{
+		toSend: []byte("line1\rline2\r\nline3\n"),
+		// bytesPerRead 0 in this test means read all at once
+		// CR+LF need to be in same read for ReadLine to not produce an extra empty line
+		// which is what terminals do for reasonably small paste. if way many lines are pasted
+		// and going over say 1k-16k buffer, readline current implementation will possibly generate 1
+		// extra empty line, if the CR is in chunk1 and LF in chunk2 (and that's fine).
+	}
+
+	ss := NewTerminal(c, "> ")
+	for i := range 3 {
+		line, err := ss.ReadLine()
+		if err != nil {
+			t.Fatalf("failed to read line %d: %v", i+1, err)
+		}
+		expected := fmt.Sprintf("line%d", i+1)
+		if line != expected {
+			t.Fatalf("expected '%s', got '%s'", expected, line)
+		}
+	}
+	line, err := ss.ReadLine()
+	if !errors.Is(err, io.EOF) {
+		t.Fatalf("expected EOF after 3 lines, got '%s' with error %v", line, err)
+	}
+	if line != "" {
+		t.Fatalf("expected empty line after EOF, got '%s'", line)
+	}
+}
+
 func TestPasswordNotSaved(t *testing.T) {
 	c := &MockTerminal{
 		toSend:       []byte("password\r\x1b[A\r"),