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+}
M
ui/ui.go
+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()