go.crypto/ssh/terminal: better handling of window resizing.

There doesn't appear to be perfect behaviour for line editing
code in the face of terminal resizing. But this change works
pretty well on xterm and gnome-terminal and certainly a lot
better than it used to.

LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/105580043
diff --git a/terminal.go b/terminal.go
index 18ac2ba..123de5e 100644
--- a/terminal.go
+++ b/terminal.go
@@ -53,7 +53,7 @@
 	lock sync.Mutex
 
 	c      io.ReadWriter
-	prompt string
+	prompt []rune
 
 	// line is the current line being entered.
 	line []rune
@@ -98,7 +98,7 @@
 	return &Terminal{
 		Escape:       &vt100EscapeCodes,
 		c:            c,
-		prompt:       prompt,
+		prompt:       []rune(prompt),
 		termWidth:    80,
 		termHeight:   24,
 		echo:         true,
@@ -220,7 +220,7 @@
 		return
 	}
 
-	x := len(t.prompt) + pos
+	x := visualLength(t.prompt) + pos
 	y := x / t.termWidth
 	x = x % t.termWidth
 
@@ -300,6 +300,29 @@
 	t.pos = newPos
 }
 
+func (t *Terminal) advanceCursor(places int) {
+	t.cursorX += places
+	t.cursorY += t.cursorX / t.termWidth
+	if t.cursorY > t.maxLine {
+		t.maxLine = t.cursorY
+	}
+	t.cursorX = t.cursorX % t.termWidth
+
+	if places > 0 && t.cursorX == 0 {
+		// Normally terminals will advance the current position
+		// when writing a character. But that doesn't happen
+		// for the last character in a line. However, when
+		// writing a character (except a new line) that causes
+		// a line wrap, the position will be advanced two
+		// places.
+		//
+		// So, if we are stopping at the end of a line, we
+		// need to write a newline so that our cursor can be
+		// advanced to the next line.
+		t.outBuf = append(t.outBuf, '\n')
+	}
+}
+
 func (t *Terminal) eraseNPreviousChars(n int) {
 	if n == 0 {
 		return
@@ -318,7 +341,7 @@
 		for i := 0; i < n; i++ {
 			t.queue(space)
 		}
-		t.cursorX += n
+		t.advanceCursor(n)
 		t.moveCursorToPos(t.pos)
 	}
 }
@@ -367,6 +390,27 @@
 	return pos - t.pos
 }
 
+// visualLength returns the number of visible glyphs in s.
+func visualLength(runes []rune) int {
+	inEscapeSeq := false
+	length := 0
+
+	for _, r := range runes {
+		switch {
+		case inEscapeSeq:
+			if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
+				inEscapeSeq = false
+			}
+		case r == '\x1b':
+			inEscapeSeq = true
+		default:
+			length++
+		}
+	}
+
+	return length
+}
+
 // 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) {
@@ -453,7 +497,7 @@
 		// end of line.
 		for i := t.pos; i < len(t.line); i++ {
 			t.queue(space)
-			t.cursorX++
+			t.advanceCursor(1)
 		}
 		t.line = t.line[:t.pos]
 		t.moveCursorToPos(t.pos)
@@ -470,9 +514,9 @@
 	case keyClearScreen:
 		// Erases the screen and moves the cursor to the home position.
 		t.queue([]rune("\x1b[2J\x1b[H"))
-		t.queue([]rune(t.prompt))
-		t.cursorX = len(t.prompt)
-		t.cursorY = 0
+		t.queue(t.prompt)
+		t.cursorX, t.cursorY = 0, 0
+		t.advanceCursor(visualLength(t.prompt))
 		t.setLine(t.line, t.pos)
 	default:
 		if t.AutoCompleteCallback != nil {
@@ -519,16 +563,8 @@
 			todo = remainingOnLine
 		}
 		t.queue(line[:todo])
-		t.cursorX += todo
+		t.advanceCursor(visualLength(line[:todo]))
 		line = line[todo:]
-
-		if t.cursorX == t.termWidth {
-			t.cursorX = 0
-			t.cursorY++
-			if t.cursorY > t.maxLine {
-				t.maxLine = t.cursorY
-			}
-		}
 	}
 }
 
@@ -563,14 +599,11 @@
 		return
 	}
 
-	t.queue([]rune(t.prompt))
-	chars := len(t.prompt)
+	t.writeLine(t.prompt)
 	if t.echo {
-		t.queue(t.line)
-		chars += len(t.line)
+		t.writeLine(t.line)
 	}
-	t.cursorX = chars % t.termWidth
-	t.cursorY = chars / t.termWidth
+
 	t.moveCursorToPos(t.pos)
 
 	if _, err = t.c.Write(t.outBuf); err != nil {
@@ -587,7 +620,7 @@
 	defer t.lock.Unlock()
 
 	oldPrompt := t.prompt
-	t.prompt = prompt
+	t.prompt = []rune(prompt)
 	t.echo = false
 
 	line, err = t.readLine()
@@ -610,7 +643,7 @@
 	// t.lock must be held at this point
 
 	if t.cursorX == 0 && t.cursorY == 0 {
-		t.writeLine([]rune(t.prompt))
+		t.writeLine(t.prompt)
 		t.c.Write(t.outBuf)
 		t.outBuf = t.outBuf[:0]
 	}
@@ -671,14 +704,70 @@
 	t.lock.Lock()
 	defer t.lock.Unlock()
 
-	t.prompt = prompt
+	t.prompt = []rune(prompt)
 }
 
-func (t *Terminal) SetSize(width, height int) {
+func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) {
+	// Move cursor to column zero at the start of the line.
+	t.move(t.cursorY, 0, t.cursorX, 0)
+	t.cursorX, t.cursorY = 0, 0
+	t.clearLineToRight()
+	for t.cursorY < numPrevLines {
+		// Move down a line
+		t.move(0, 1, 0, 0)
+		t.cursorY++
+		t.clearLineToRight()
+	}
+	// Move back to beginning.
+	t.move(t.cursorY, 0, 0, 0)
+	t.cursorX, t.cursorY = 0, 0
+
+	t.queue(t.prompt)
+	t.advanceCursor(visualLength(t.prompt))
+	t.writeLine(t.line)
+	t.moveCursorToPos(t.pos)
+}
+
+func (t *Terminal) SetSize(width, height int) error {
 	t.lock.Lock()
 	defer t.lock.Unlock()
 
+	oldWidth := t.termWidth
 	t.termWidth, t.termHeight = width, height
+
+	switch {
+	case width == oldWidth || len(t.line) == 0:
+		// If the width didn't change then nothing else needs to be
+		// done.
+		return nil
+	case width < oldWidth:
+		// Some terminals (e.g. xterm) will truncate lines that were
+		// too long when shinking. Others, (e.g. gnome-terminal) will
+		// attempt to wrap them. For the former, repainting t.maxLine
+		// works great, but that behaviour goes badly wrong in the case
+		// of the latter because they have doubled every full line.
+
+		// We assume that we are working on a terminal that wraps lines
+		// and adjust the cursor position based on every previous line
+		// wrapping and turning into two. This causes the prompt on
+		// xterms to move upwards, which isn't great, but it avoids a
+		// huge mess with gnome-terminal.
+		t.cursorY *= 2
+		t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2)
+	case width > oldWidth:
+		// If the terminal expands then our position calculations will
+		// be wrong in the future because we think the cursor is
+		// |t.pos| chars into the string, but there will be a gap at
+		// the end of any wrapped line.
+		//
+		// But the position will actually be correct until we move, so
+		// we can move back to the beginning and repaint everything.
+		t.clearAndRepaintLinePlusNPrevious(t.maxLine)
+	}
+
+	_, err := t.c.Write(t.outBuf)
+	t.outBuf = t.outBuf[:0]
+	return err
 }
 
 // stRingBuffer is a ring buffer of strings.