enabled multi-line paste
This commit is contained in:
@@ -55,7 +55,7 @@ All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority if
|
|||||||
./mb # MicroBantam (Perl 5)
|
./mb # MicroBantam (Perl 5)
|
||||||
```
|
```
|
||||||
|
|
||||||
In interactive mode, prompts can span multiple lines: press **Ctrl+J** to insert a real line break (the cursor moves to the next line), then **Enter** to submit the whole multi-line prompt. The Go port ships its own raw-mode line editor, working everywhere without third-party dependencies. It supports arrow keys and Up/Down for history, Backspace and Ctrl+D (non-empty line) to delete, and Emacs-style editing combos: **Ctrl+A** (start of line), **Ctrl+E** (end of line), **Ctrl+B** / **Ctrl+F** (move by character), **Ctrl+W** (delete previous word), **Ctrl+K** (kill to end of line), **Ctrl+U** (kill to start of line), **Ctrl+Left** / **Ctrl+Right** (move by word), and **Home** / **End** keys. **Ctrl+C** clears the line / interrupts an in-flight run, and **Ctrl+D** on an empty line exits.
|
In interactive mode, prompts can span multiple lines: press **Ctrl+J** to insert a real line break (the cursor moves to the next line), then **Enter** to submit the whole multi-line prompt. Bracketed paste is enabled (`ESC[?2004h`) while editing, so a multi-line block pasted from the clipboard is inserted as literal line breaks and only the final **Enter** submits it. The Go port ships its own raw-mode line editor, working everywhere without third-party dependencies. It supports arrow keys and Up/Down for history, Backspace and Ctrl+D (non-empty line) to delete, and Emacs-style editing combos: **Ctrl+A** (start of line), **Ctrl+E** (end of line), **Ctrl+B** / **Ctrl+F** (move by character), **Ctrl+W** (delete previous word), **Ctrl+K** (kill to end of line), **Ctrl+U** (kill to start of line), **Ctrl+Left** / **Ctrl+Right** (move by word), and **Home** / **End** keys. **Ctrl+C** clears the line / interrupts an in-flight run, and **Ctrl+D** on an empty line exits.
|
||||||
|
|
||||||
After every interaction, Bantam displays token usage (prompt tokens, cached/uncached breakdown when supported by the provider, completion tokens, and context window utilization):
|
After every interaction, Bantam displays token usage (prompt tokens, cached/uncached breakdown when supported by the provider, completion tokens, and context window utilization):
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -2230,13 +2230,14 @@ func visibleLen(s string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type editor struct {
|
type editor struct {
|
||||||
prompt string
|
prompt string
|
||||||
buf []rune
|
buf []rune
|
||||||
pos int
|
pos int
|
||||||
hist []string
|
hist []string
|
||||||
hpos int
|
hpos int
|
||||||
draft string
|
draft string
|
||||||
crow int
|
crow int
|
||||||
|
pasting bool // true while inside a bracketed paste (ESC[200~ .. ESC[201~)
|
||||||
}
|
}
|
||||||
|
|
||||||
func textPos(promptLen, W int, s string, pos int) (row, col int) {
|
func textPos(promptLen, W int, s string, pos int) (row, col int) {
|
||||||
@@ -2367,6 +2368,181 @@ func readPlain(prompt string) (string, bool) {
|
|||||||
return strings.TrimRight(line, "\r\n"), true
|
return strings.TrimRight(line, "\r\n"), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keyKind classifies what readLine should do after handling one input rune.
|
||||||
|
type keyKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyNone keyKind = iota // keep editing
|
||||||
|
keySubmit // submit the current buffer
|
||||||
|
keyAbort // interrupt (Ctrl+C)
|
||||||
|
keyEOF // end of input (Ctrl+D on an empty buffer)
|
||||||
|
)
|
||||||
|
|
||||||
|
// insert places r at the cursor, shifting the tail right.
|
||||||
|
func (e *editor) insert(r rune) {
|
||||||
|
e.buf = append(e.buf, 0)
|
||||||
|
copy(e.buf[e.pos+1:], e.buf[e.pos:])
|
||||||
|
e.buf[e.pos] = r
|
||||||
|
e.pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCSI interprets a complete CSI sequence (the bytes between ESC[ and its
|
||||||
|
// final byte): cursor movement, history and bracketed-paste markers.
|
||||||
|
func (e *editor) handleCSI(seq []byte) {
|
||||||
|
if len(seq) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch seq[len(seq)-1] {
|
||||||
|
case 'A':
|
||||||
|
e.histNav(true)
|
||||||
|
case 'B':
|
||||||
|
e.histNav(false)
|
||||||
|
case 'C':
|
||||||
|
if strings.Contains(string(seq), "5") {
|
||||||
|
e.wordFwd() // Ctrl+Right
|
||||||
|
} else if e.pos < len(e.buf) {
|
||||||
|
e.pos++
|
||||||
|
}
|
||||||
|
case 'D':
|
||||||
|
if strings.Contains(string(seq), "5") {
|
||||||
|
e.wordBack() // Ctrl+Left
|
||||||
|
} else if e.pos > 0 {
|
||||||
|
e.pos--
|
||||||
|
}
|
||||||
|
case 'H':
|
||||||
|
e.pos = 0 // Home (ESC[H)
|
||||||
|
case 'F':
|
||||||
|
e.pos = len(e.buf) // End (ESC[F)
|
||||||
|
case '~':
|
||||||
|
s := string(seq)
|
||||||
|
if strings.HasSuffix(s, "200~") {
|
||||||
|
e.pasting = true // bracketed paste start: ESC[200~
|
||||||
|
} else if strings.HasSuffix(s, "201~") {
|
||||||
|
e.pasting = false // bracketed paste end: ESC[201~
|
||||||
|
} else if seq[0] == '1' {
|
||||||
|
e.pos = 0 // Home (ESC[1~)
|
||||||
|
} else if seq[0] == '4' {
|
||||||
|
e.pos = len(e.buf) // End (ESC[4~)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swallowLF consumes a single buffered LF so CRLF input is not counted as two
|
||||||
|
// separate line breaks.
|
||||||
|
func (e *editor) swallowLF(r *bufio.Reader) {
|
||||||
|
if r.Buffered() > 0 {
|
||||||
|
if b, _ := r.Peek(1); len(b) > 0 && b[0] == '\n' {
|
||||||
|
r.ReadByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// feed processes one decoded key. r supplies the remainder of an escape
|
||||||
|
// sequence. It returns the action the REPL loop must take next. Keeping the key
|
||||||
|
// logic here, rather than inline in readLine, makes multi-line paste handling
|
||||||
|
// unit-testable without a real terminal.
|
||||||
|
func (e *editor) feed(rn rune, r *bufio.Reader) keyKind {
|
||||||
|
switch rn {
|
||||||
|
case '\r':
|
||||||
|
// Inside a bracketed paste a CR is a literal line break; only a real
|
||||||
|
// Enter (outside a paste) submits the prompt.
|
||||||
|
e.swallowLF(r)
|
||||||
|
if e.pasting {
|
||||||
|
e.insert('\n')
|
||||||
|
return keyNone
|
||||||
|
}
|
||||||
|
return keySubmit
|
||||||
|
case '\n':
|
||||||
|
e.insert('\n')
|
||||||
|
case 0x03:
|
||||||
|
if e.pasting {
|
||||||
|
return keyNone // ignore a stray ^C inside a pasted block
|
||||||
|
}
|
||||||
|
return keyAbort
|
||||||
|
case 0x04:
|
||||||
|
if len(e.buf) == 0 {
|
||||||
|
return keyEOF
|
||||||
|
}
|
||||||
|
// Ctrl+D with non-empty buffer: delete the character under the cursor
|
||||||
|
if e.pos < len(e.buf) {
|
||||||
|
e.buf = append(e.buf[:e.pos], e.buf[e.pos+1:]...)
|
||||||
|
}
|
||||||
|
case 0x01:
|
||||||
|
e.pos = 0 // Ctrl+A: beginning of line
|
||||||
|
case 0x05:
|
||||||
|
e.pos = len(e.buf) // Ctrl+E: end of line
|
||||||
|
case 0x02:
|
||||||
|
if e.pos > 0 {
|
||||||
|
e.pos--
|
||||||
|
} // Ctrl+B: backward char
|
||||||
|
case 0x06:
|
||||||
|
if e.pos < len(e.buf) {
|
||||||
|
e.pos++
|
||||||
|
} // Ctrl+F: forward char
|
||||||
|
case 0x17:
|
||||||
|
e.delWordBack() // Ctrl+W: delete previous word
|
||||||
|
case 0x0b:
|
||||||
|
e.buf = e.buf[:e.pos] // Ctrl+K: kill to end of line
|
||||||
|
case 0x15:
|
||||||
|
e.buf = e.buf[e.pos:] // Ctrl+U: kill to start of line
|
||||||
|
e.pos = 0
|
||||||
|
case 0x7f, 0x08:
|
||||||
|
if e.pos > 0 {
|
||||||
|
e.buf = append(e.buf[:e.pos-1], e.buf[e.pos:]...)
|
||||||
|
e.pos--
|
||||||
|
}
|
||||||
|
case 0x1b:
|
||||||
|
b1, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return keyNone
|
||||||
|
}
|
||||||
|
if b1 == '[' {
|
||||||
|
// Read the full CSI sequence (terminated by a byte in 0x40-0x7e).
|
||||||
|
var seq []byte
|
||||||
|
for {
|
||||||
|
b, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
seq = append(seq, b)
|
||||||
|
if b >= 0x40 && b <= 0x7e {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.handleCSI(seq)
|
||||||
|
} else if b1 == 'O' {
|
||||||
|
// xterm application cursor keys: ESC O H/F (Home/End), A-D (arrows)
|
||||||
|
b2, err2 := r.ReadByte()
|
||||||
|
if err2 != nil {
|
||||||
|
return keyNone
|
||||||
|
}
|
||||||
|
switch b2 {
|
||||||
|
case 'H':
|
||||||
|
e.pos = 0
|
||||||
|
case 'F':
|
||||||
|
e.pos = len(e.buf)
|
||||||
|
case 'A':
|
||||||
|
e.histNav(true)
|
||||||
|
case 'B':
|
||||||
|
e.histNav(false)
|
||||||
|
case 'C':
|
||||||
|
if e.pos < len(e.buf) {
|
||||||
|
e.pos++
|
||||||
|
}
|
||||||
|
case 'D':
|
||||||
|
if e.pos > 0 {
|
||||||
|
e.pos--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if rn >= 32 || rn == '\t' {
|
||||||
|
e.insert(rn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keyNone
|
||||||
|
}
|
||||||
|
|
||||||
func readLine(prompt string) (string, bool) {
|
func readLine(prompt string) (string, bool) {
|
||||||
if !isTerminal(int(os.Stdin.Fd())) {
|
if !isTerminal(int(os.Stdin.Fd())) {
|
||||||
return readPlain(prompt)
|
return readPlain(prompt)
|
||||||
@@ -2376,6 +2552,11 @@ func readLine(prompt string) (string, bool) {
|
|||||||
return readPlain(prompt)
|
return readPlain(prompt)
|
||||||
}
|
}
|
||||||
defer restore()
|
defer restore()
|
||||||
|
// Enable bracketed paste for the duration of editing: pasted blocks then
|
||||||
|
// arrive wrapped in ESC[200~ .. ESC[201~, letting feed() treat their
|
||||||
|
// embedded newlines as literal line breaks instead of submitting mid-paste.
|
||||||
|
fmt.Print("\033[?2004h")
|
||||||
|
defer fmt.Print("\033[?2004l")
|
||||||
e := &editor{prompt: prompt, hpos: -1, hist: hist}
|
e := &editor{prompt: prompt, hpos: -1, hist: hist}
|
||||||
for {
|
for {
|
||||||
e.draw()
|
e.draw()
|
||||||
@@ -2384,13 +2565,8 @@ func readLine(prompt string) (string, bool) {
|
|||||||
fmt.Print("\r\n")
|
fmt.Print("\r\n")
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
switch rn {
|
switch e.feed(rn, stdin) {
|
||||||
case '\r':
|
case keySubmit:
|
||||||
if stdin.Buffered() > 0 {
|
|
||||||
if b, _ := stdin.Peek(1); len(b) > 0 && b[0] == '\n' {
|
|
||||||
stdin.ReadByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
W := termWidth()
|
W := termWidth()
|
||||||
s := string(e.buf)
|
s := string(e.buf)
|
||||||
P := visibleLen(e.prompt)
|
P := visibleLen(e.prompt)
|
||||||
@@ -2401,130 +2577,12 @@ func readLine(prompt string) (string, bool) {
|
|||||||
}
|
}
|
||||||
fmt.Print("\r\n")
|
fmt.Print("\r\n")
|
||||||
return string(e.buf), true
|
return string(e.buf), true
|
||||||
case '\n':
|
case keyAbort:
|
||||||
e.buf = append(e.buf, 0)
|
|
||||||
copy(e.buf[e.pos+1:], e.buf[e.pos:])
|
|
||||||
e.buf[e.pos] = '\n'
|
|
||||||
e.pos++
|
|
||||||
case 0x03:
|
|
||||||
fmt.Print("^C\r\n")
|
fmt.Print("^C\r\n")
|
||||||
return "", true
|
return "", true
|
||||||
case 0x04:
|
case keyEOF:
|
||||||
if len(e.buf) == 0 {
|
fmt.Print("\r\n")
|
||||||
fmt.Print("\r\n")
|
return "", false
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
// Ctrl+D with non-empty buffer: delete character under cursor
|
|
||||||
if e.pos < len(e.buf) {
|
|
||||||
e.buf = append(e.buf[:e.pos], e.buf[e.pos+1:]...)
|
|
||||||
}
|
|
||||||
case 0x01:
|
|
||||||
e.pos = 0 // Ctrl+A: beginning of line
|
|
||||||
case 0x05:
|
|
||||||
e.pos = len(e.buf) // Ctrl+E: end of line
|
|
||||||
case 0x02:
|
|
||||||
if e.pos > 0 {
|
|
||||||
e.pos--
|
|
||||||
} // Ctrl+B: backward char
|
|
||||||
case 0x06:
|
|
||||||
if e.pos < len(e.buf) {
|
|
||||||
e.pos++
|
|
||||||
} // Ctrl+F: forward char
|
|
||||||
case 0x17:
|
|
||||||
e.delWordBack() // Ctrl+W: delete previous word
|
|
||||||
case 0x0b:
|
|
||||||
e.buf = e.buf[:e.pos] // Ctrl+K: kill to end of line
|
|
||||||
case 0x15:
|
|
||||||
e.buf = e.buf[e.pos:] // Ctrl+U: kill to start of line
|
|
||||||
e.pos = 0
|
|
||||||
case 0x7f, 0x08:
|
|
||||||
if e.pos > 0 {
|
|
||||||
e.buf = append(e.buf[:e.pos-1], e.buf[e.pos:]...)
|
|
||||||
e.pos--
|
|
||||||
}
|
|
||||||
case 0x1b:
|
|
||||||
b1, err1 := stdin.ReadByte()
|
|
||||||
if err1 != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if b1 == '[' {
|
|
||||||
// Read the full CSI sequence (terminated by a byte in 0x40-0x7e).
|
|
||||||
var seq []byte
|
|
||||||
for {
|
|
||||||
b, err := stdin.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
seq = append(seq, b)
|
|
||||||
if b >= 0x40 && b <= 0x7e {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(seq) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
last := seq[len(seq)-1]
|
|
||||||
switch last {
|
|
||||||
case 'A':
|
|
||||||
e.histNav(true)
|
|
||||||
case 'B':
|
|
||||||
e.histNav(false)
|
|
||||||
case 'C':
|
|
||||||
if strings.Contains(string(seq), "5") {
|
|
||||||
e.wordFwd() // Ctrl+Right
|
|
||||||
} else if e.pos < len(e.buf) {
|
|
||||||
e.pos++
|
|
||||||
}
|
|
||||||
case 'D':
|
|
||||||
if strings.Contains(string(seq), "5") {
|
|
||||||
e.wordBack() // Ctrl+Left
|
|
||||||
} else if e.pos > 0 {
|
|
||||||
e.pos--
|
|
||||||
}
|
|
||||||
case 'H':
|
|
||||||
e.pos = 0 // Home (ESC[H)
|
|
||||||
case 'F':
|
|
||||||
e.pos = len(e.buf) // End (ESC[F)
|
|
||||||
case '~':
|
|
||||||
// Home/End on some terminals: ESC[1~ / ESC[4~
|
|
||||||
if len(seq) >= 1 && seq[0] == '1' {
|
|
||||||
e.pos = 0
|
|
||||||
} else if len(seq) >= 1 && seq[0] == '4' {
|
|
||||||
e.pos = len(e.buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if b1 == 'O' {
|
|
||||||
// xterm application cursor keys: ESC O H/F (Home/End), A-D (arrows)
|
|
||||||
b2, err2 := stdin.ReadByte()
|
|
||||||
if err2 != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch b2 {
|
|
||||||
case 'H':
|
|
||||||
e.pos = 0
|
|
||||||
case 'F':
|
|
||||||
e.pos = len(e.buf)
|
|
||||||
case 'A':
|
|
||||||
e.histNav(true)
|
|
||||||
case 'B':
|
|
||||||
e.histNav(false)
|
|
||||||
case 'C':
|
|
||||||
if e.pos < len(e.buf) {
|
|
||||||
e.pos++
|
|
||||||
}
|
|
||||||
case 'D':
|
|
||||||
if e.pos > 0 {
|
|
||||||
e.pos--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if rn >= 32 {
|
|
||||||
e.buf = append(e.buf, 0)
|
|
||||||
copy(e.buf[e.pos+1:], e.buf[e.pos:])
|
|
||||||
e.buf[e.pos] = rn
|
|
||||||
e.pos++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2717,7 +2775,7 @@ func main() {
|
|||||||
if re == "" {
|
if re == "" {
|
||||||
re = "high"
|
re = "high"
|
||||||
}
|
}
|
||||||
fmt.Println(c("Bantam Agent ready", 1, 32) + c(" (Ctrl+J = new line)", 2))
|
fmt.Println(c("Bantam Agent ready", 1, 32) + c(" (Enter submits; Ctrl+J or paste = new line)", 2))
|
||||||
fmt.Println(c(fmt.Sprintf("endpoint: %s model: %s temp: %v reasoning: %s context: %d", cfg.Endpoint, cfg.Model, cfg.Temperature, re, cfg.ContextWindow), 2))
|
fmt.Println(c(fmt.Sprintf("endpoint: %s model: %s temp: %v reasoning: %s context: %d", cfg.Endpoint, cfg.Model, cfg.Temperature, re, cfg.ContextWindow), 2))
|
||||||
for {
|
for {
|
||||||
u, ok := readLine(c("> ", 1, 36))
|
u, ok := readLine(c("> ", 1, 36))
|
||||||
|
|||||||
+139
@@ -3477,3 +3477,142 @@ func TestOffloadToolResultUsesReadPrompt(t *testing.T) {
|
|||||||
cleanupTemps(tmps)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user