commit 22f5b82

Hubert Hirtz  ·  2020-08-02 13:59:27 +0000 UTC
parent f27f70d
Improve line editing

Correctly support moving around the text and placing glyphs at any
position.
4 files changed,  +289, -68
+1, -1
1@@ -199,7 +199,7 @@ func handleUIEvent(app *ui.UI, s *irc.Session, ev tcell.Event) {
2 			handleInput(app, s, buffer, input)
3 		case tcell.KeyRune:
4 			app.InputRune(ev.Rune())
5-			if app.CurrentBuffer() != "home" && !strings.HasPrefix(app.Input(), "/") {
6+			if app.CurrentBuffer() != "home" && !app.InputIsCommand() {
7 				s.Typing(app.CurrentBuffer())
8 			}
9 		}
+151, -0
  1@@ -0,0 +1,151 @@
  2+package ui
  3+
  4+import (
  5+	"github.com/gdamore/tcell"
  6+	"github.com/mattn/go-runewidth"
  7+)
  8+
  9+// editor is the text field where the user writes messages and commands.
 10+type editor struct {
 11+	// text contains the written runes. An empty slice means no text is written.
 12+	text []rune
 13+
 14+	// textWidth[i] contains the width of string(text[:i]). Therefore
 15+	// len(textWidth) is always strictly greater than 0 and textWidth[0] is
 16+	// always 0.
 17+	textWidth []int
 18+
 19+	// cursorIdx is the index in text of the placement of the cursor, or is
 20+	// equal to len(text) if the cursor is at the end.
 21+	cursorIdx int
 22+
 23+	// offsetIdx is the number of elements of text that are skipped when
 24+	// rendering.
 25+	offsetIdx int
 26+
 27+	// width is the width of the screen.
 28+	width int
 29+}
 30+
 31+func newEditor(width int) editor {
 32+	return editor{
 33+		text:      []rune{},
 34+		textWidth: []int{0},
 35+		width:     width,
 36+	}
 37+}
 38+
 39+func (e *editor) Resize(width int) {
 40+	if width < e.width {
 41+		e.cursorIdx = 0
 42+		e.offsetIdx = 0
 43+	}
 44+	e.width = width
 45+}
 46+
 47+func (e *editor) IsCommand() bool {
 48+	return len(e.text) != 0 && e.text[0] == '/'
 49+}
 50+
 51+func (e *editor) TextLen() int {
 52+	return len(e.text)
 53+}
 54+
 55+func (e *editor) PutRune(r rune) {
 56+	e.text = append(e.text, ' ')
 57+	copy(e.text[e.cursorIdx+1:], e.text[e.cursorIdx:])
 58+	e.text[e.cursorIdx] = r
 59+
 60+	rw := runewidth.RuneWidth(r)
 61+	tw := e.textWidth[len(e.textWidth)-1]
 62+	e.textWidth = append(e.textWidth, tw+rw)
 63+	for i := e.cursorIdx + 1; i < len(e.textWidth); i++ {
 64+		e.textWidth[i] = rw + e.textWidth[i-1]
 65+	}
 66+
 67+	e.Right()
 68+}
 69+
 70+func (e *editor) RemRune() (ok bool) {
 71+	ok = 0 < e.cursorIdx
 72+	if !ok {
 73+		return
 74+	}
 75+
 76+	// TODO avoid looping twice
 77+	rw := e.textWidth[e.cursorIdx] - e.textWidth[e.cursorIdx-1]
 78+	for i := e.cursorIdx; i < len(e.textWidth); i++ {
 79+		e.textWidth[i] -= rw
 80+	}
 81+	copy(e.textWidth[e.cursorIdx:], e.textWidth[e.cursorIdx+1:])
 82+	e.textWidth = e.textWidth[:len(e.textWidth)-1]
 83+
 84+	copy(e.text[e.cursorIdx-1:], e.text[e.cursorIdx:])
 85+	e.text = e.text[:len(e.text)-1]
 86+	e.Left()
 87+	return
 88+}
 89+
 90+func (e *editor) Flush() (content string) {
 91+	content = string(e.text)
 92+	e.text = e.text[:0]
 93+	e.textWidth = e.textWidth[:1]
 94+	e.cursorIdx = 0
 95+	e.offsetIdx = 0
 96+	return
 97+}
 98+
 99+func (e *editor) Right() {
100+	if e.cursorIdx == len(e.text) {
101+		return
102+	}
103+	e.cursorIdx++
104+	if e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx] {
105+		e.offsetIdx += 16
106+		max := len(e.text) - 1
107+		if max < e.offsetIdx {
108+			e.offsetIdx = max
109+		}
110+	}
111+}
112+
113+func (e *editor) Left() {
114+	if e.cursorIdx == 0 {
115+		return
116+	}
117+	e.cursorIdx--
118+	if e.cursorIdx <= e.offsetIdx {
119+		e.offsetIdx -= 16
120+		if e.offsetIdx < 0 {
121+			e.offsetIdx = 0
122+		}
123+	}
124+}
125+
126+func (e *editor) Draw(screen tcell.Screen, y int) {
127+	st := tcell.StyleDefault
128+
129+	x := 0
130+	i := e.offsetIdx
131+
132+	for i < len(e.text) && x < e.width {
133+		r := e.text[i]
134+		screen.SetContent(x, y, r, nil, st)
135+		x += runewidth.RuneWidth(r)
136+		i++
137+	}
138+
139+	for x < e.width {
140+		screen.SetContent(x, y, ' ', nil, st)
141+		x++
142+	}
143+
144+	curStart := e.textWidth[e.cursorIdx] - e.textWidth[e.offsetIdx]
145+	curEnd := curStart + 1
146+	if e.cursorIdx+1 < len(e.textWidth) {
147+		curEnd = e.textWidth[e.cursorIdx+1] - e.textWidth[e.offsetIdx]
148+	}
149+	for x := curStart; x < curEnd; x++ {
150+		screen.ShowCursor(x, y)
151+	}
152+}
+107, -0
  1@@ -0,0 +1,107 @@
  2+package ui
  3+
  4+import "testing"
  5+
  6+var hell editor = editor{
  7+	text:      []rune{'h', 'e', 'l', 'l'},
  8+	textWidth: []int{0, 1, 2, 3, 4},
  9+	cursorIdx: 4,
 10+	offsetIdx: 0,
 11+	width:     5,
 12+}
 13+
 14+func assertEditorEq(t *testing.T, actual, expected editor) {
 15+	if len(actual.text) != len(expected.text) {
 16+		t.Errorf("expected text len to be %d, got %d\n", len(expected.text), len(actual.text))
 17+	} else {
 18+		for i := 0; i < len(actual.text); i++ {
 19+			a := actual.text[i]
 20+			e := expected.text[i]
 21+
 22+			if a != e {
 23+				t.Errorf("expected rune #%d to be '%c', got '%c'\n", i, e, a)
 24+			}
 25+		}
 26+	}
 27+
 28+	if len(actual.textWidth) != len(expected.textWidth) {
 29+		t.Errorf("expected textWidth len to be %d, got %d\n", len(expected.textWidth), len(actual.textWidth))
 30+	} else {
 31+		for i := 0; i < len(actual.textWidth); i++ {
 32+			a := actual.textWidth[i]
 33+			e := expected.textWidth[i]
 34+
 35+			if a != e {
 36+				t.Errorf("expected width #%d to be %d, got %d\n", i, e, a)
 37+			}
 38+		}
 39+	}
 40+
 41+	if actual.cursorIdx != expected.cursorIdx {
 42+		t.Errorf("expected cursorIdx to be %d, got %d\n", expected.cursorIdx, actual.cursorIdx)
 43+	}
 44+
 45+	if actual.offsetIdx != expected.offsetIdx {
 46+		t.Errorf("expected offsetIdx to be %d, got %d\n", expected.offsetIdx, actual.offsetIdx)
 47+	}
 48+
 49+	if actual.width != expected.width {
 50+		t.Errorf("expected width to be %d, got %d\n", expected.width, actual.width)
 51+	}
 52+}
 53+
 54+func TestOneLetter(t *testing.T) {
 55+	e := newEditor(5)
 56+	e.PutRune('h')
 57+	assertEditorEq(t, e, editor{
 58+		text:      []rune{'h'},
 59+		textWidth: []int{0, 1},
 60+		cursorIdx: 1,
 61+		offsetIdx: 0,
 62+		width:     5,
 63+	})
 64+}
 65+
 66+func TestFourLetters(t *testing.T) {
 67+	e := newEditor(5)
 68+	e.PutRune('h')
 69+	e.PutRune('e')
 70+	e.PutRune('l')
 71+	e.PutRune('l')
 72+	assertEditorEq(t, e, hell)
 73+}
 74+
 75+func TestOneLeft(t *testing.T) {
 76+	e := newEditor(5)
 77+	e.PutRune('h')
 78+	e.PutRune('l')
 79+	e.Left()
 80+	e.PutRune('e')
 81+	e.PutRune('l')
 82+	e.Right()
 83+	assertEditorEq(t, e, hell)
 84+}
 85+
 86+func TestOneRem(t *testing.T) {
 87+	e := newEditor(5)
 88+	e.PutRune('h')
 89+	e.PutRune('l')
 90+	e.RemRune()
 91+	e.PutRune('e')
 92+	e.PutRune('l')
 93+	e.PutRune('l')
 94+	assertEditorEq(t, e, hell)
 95+}
 96+
 97+func TestLeftAndRem(t *testing.T) {
 98+	e := newEditor(5)
 99+	e.PutRune('h')
100+	e.PutRune('l')
101+	e.PutRune('e')
102+	e.Left()
103+	e.RemRune()
104+	e.Right()
105+	e.PutRune('l')
106+	e.PutRune('l')
107+	assertEditorEq(t, e, hell)
108+}
+30, -67
  1@@ -17,8 +17,7 @@ type UI struct {
  2 	scrollAmt   int
  3 	scrollAtTop bool
  4 
  5-	textInput  []rune
  6-	textCursor int
  7+	e editor
  8 }
  9 
 10 func New() (ui *UI, err error) {
 11@@ -34,7 +33,7 @@ func New() (ui *UI, err error) {
 12 		return
 13 	}
 14 
 15-	_, h := ui.screen.Size()
 16+	w, h := ui.screen.Size()
 17 	ui.screen.Clear()
 18 	ui.screen.ShowCursor(0, h-2)
 19 
 20@@ -58,7 +57,7 @@ func New() (ui *UI, err error) {
 21 		},
 22 	}
 23 
 24-	ui.textInput = []rune{}
 25+	ui.e = newEditor(w)
 26 
 27 	ui.Resize()
 28 
 29@@ -220,105 +219,69 @@ func (ui *UI) AddHistoryLines(buffer string, lines []Line) {
 30 	}
 31 }
 32 
 33-func (ui *UI) Input() string {
 34-	return string(ui.textInput)
 35+func (ui *UI) InputIsCommand() bool {
 36+	return ui.e.IsCommand()
 37 }
 38 
 39 func (ui *UI) InputLen() int {
 40-	return len(ui.textInput)
 41+	return ui.e.TextLen()
 42 }
 43 
 44 func (ui *UI) InputRune(r rune) {
 45-	ui.textInput = append(ui.textInput, r)
 46-	ui.textCursor++
 47-	ui.drawEditor()
 48+	ui.e.PutRune(r)
 49+	_, h := ui.screen.Size()
 50+	ui.e.Draw(ui.screen, h-2)
 51+	ui.screen.Show()
 52 }
 53 
 54 func (ui *UI) InputRight() {
 55-	if ui.textCursor < len(ui.textInput) {
 56-		ui.textCursor++
 57-		ui.drawEditor()
 58-	}
 59+	ui.e.Right()
 60+	_, h := ui.screen.Size()
 61+	ui.e.Draw(ui.screen, h-2)
 62+	ui.screen.Show()
 63 }
 64 
 65 func (ui *UI) InputLeft() {
 66-	if 0 < ui.textCursor {
 67-		ui.textCursor--
 68-		ui.drawEditor()
 69-	}
 70+	ui.e.Left()
 71+	_, h := ui.screen.Size()
 72+	ui.e.Draw(ui.screen, h-2)
 73+	ui.screen.Show()
 74 }
 75 
 76 func (ui *UI) InputBackspace() (ok bool) {
 77-	ok = 0 < len(ui.textInput)
 78-
 79+	ok = ui.e.RemRune()
 80 	if ok {
 81-		ui.textInput = ui.textInput[:len(ui.textInput)-1]
 82-		if len(ui.textInput) < ui.textCursor {
 83-			ui.textCursor = len(ui.textInput)
 84-		}
 85-		ui.drawEditor()
 86+		_, h := ui.screen.Size()
 87+		ui.e.Draw(ui.screen, h-2)
 88+		ui.screen.Show()
 89 	}
 90-
 91 	return
 92 }
 93 
 94 func (ui *UI) InputEnter() (content string) {
 95-	content = string(ui.textInput)
 96-
 97-	ui.textInput = []rune{}
 98-	ui.textCursor = 0
 99-	ui.drawEditor()
100-
101+	content = ui.e.Flush()
102+	_, h := ui.screen.Size()
103+	ui.e.Draw(ui.screen, h-2)
104+	ui.screen.Show()
105 	return
106 }
107 
108 func (ui *UI) Resize() {
109+	w, _ := ui.screen.Size()
110+	ui.e.Resize(w)
111 	ui.bufferList.Invalidate()
112 	ui.scrollAmt = 0
113 	ui.draw()
114 }
115 
116 func (ui *UI) draw() {
117+	_, h := ui.screen.Size()
118 	ui.drawStatus()
119-	ui.drawEditor()
120+	ui.e.Draw(ui.screen, h-2)
121 	ui.drawTyping()
122 	ui.drawBuffer()
123 }
124 
125-func (ui *UI) drawEditor() {
126-	st := tcell.StyleDefault
127-	w, h := ui.screen.Size()
128-	if w == 0 {
129-		return
130-	}
131-
132-	s := string(ui.textInput)
133-	sw := runewidth.StringWidth(s)
134-
135-	x := 0
136-	y := h - 2
137-	i := 0
138-
139-	for ; w < sw+1 && i < len(ui.textInput); i++ {
140-		r := ui.textInput[i]
141-		rw := runewidth.RuneWidth(r)
142-		sw -= rw
143-	}
144-
145-	for ; i < len(ui.textInput); i++ {
146-		r := ui.textInput[i]
147-		ui.screen.SetContent(x, y, r, nil, st)
148-		x += runewidth.RuneWidth(r)
149-	}
150-
151-	for ; x < w; x++ {
152-		ui.screen.SetContent(x, y, ' ', nil, st)
153-	}
154-
155-	ui.screen.ShowCursor(ui.textCursor, y)
156-	ui.screen.Show()
157-}
158-
159 func (ui *UI) drawTyping() {
160 	st := tcell.StyleDefault.Dim(true)
161 	w, h := ui.screen.Size()