commit edb9412
delthas
·
2021-07-11 21:40:36 +0000 UTC
parent e9a2fc4
Introduce backsearch message support with ctrl+R Fixes: #47
3 files changed,
+125,
-8
M
app.go
+2,
-0
1@@ -401,6 +401,8 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
2 if ok {
3 app.typing()
4 }
5+ case tcell.KeyCtrlR:
6+ app.win.InputBackSearch()
7 case tcell.KeyTab:
8 ok := app.win.InputAutoComplete(1)
9 if ok {
+119,
-8
1@@ -1,6 +1,8 @@
2 package ui
3
4 import (
5+ "strings"
6+
7 "github.com/gdamore/tcell/v2"
8 )
9
10@@ -35,6 +37,10 @@ type Editor struct {
11 autoComplete func(cursorIdx int, text []rune) []Completion
12 autoCache []Completion
13 autoCacheIdx int
14+
15+ backsearch bool
16+ backsearchPattern []rune // pre-lowercased
17+ backsearchIdx int
18 }
19
20 // NewEditor returns a new Editor.
21@@ -52,6 +58,7 @@ func (e *Editor) Resize(width int) {
22 e.cursorIdx = 0
23 e.offsetIdx = 0
24 e.autoCache = nil
25+ e.backsearchEnd()
26 }
27 e.width = width
28 }
29@@ -69,9 +76,31 @@ func (e *Editor) TextLen() int {
30 }
31
32 func (e *Editor) PutRune(r rune) {
33- e.putRune(r)
34- e.Right()
35 e.autoCache = nil
36+ lowerRune := runeToLower(r)
37+ if e.backsearch && e.cursorIdx < e.TextLen() {
38+ lowerNext := runeToLower(e.text[e.lineIdx][e.cursorIdx])
39+ if lowerRune == lowerNext {
40+ e.right()
41+ e.backsearchPattern = append(e.backsearchPattern, lowerRune)
42+ return
43+ }
44+ }
45+ e.putRune(r)
46+ e.right()
47+ if e.backsearch {
48+ wasEmpty := len(e.backsearchPattern) == 0
49+ e.backsearchPattern = append(e.backsearchPattern, lowerRune)
50+ if wasEmpty {
51+ clearLine := e.lineIdx == len(e.text)-1
52+ e.backsearchUpdate(e.lineIdx - 1)
53+ if clearLine && e.lineIdx < len(e.text)-1 {
54+ e.text = e.text[:len(e.text)-1]
55+ }
56+ } else {
57+ e.backsearchUpdate(e.lineIdx)
58+ }
59+ }
60 }
61
62 func (e *Editor) putRune(r rune) {
63@@ -93,8 +122,16 @@ func (e *Editor) RemRune() (ok bool) {
64 return
65 }
66 e.remRuneAt(e.cursorIdx - 1)
67+ e.left()
68 e.autoCache = nil
69- e.Left()
70+ if e.backsearch {
71+ if e.TextLen() == 0 {
72+ e.backsearchEnd()
73+ } else {
74+ e.backsearchPattern = e.backsearchPattern[:len(e.backsearchPattern)-1]
75+ e.backsearchUpdate(e.lineIdx)
76+ }
77+ }
78 return
79 }
80
81@@ -105,6 +142,7 @@ func (e *Editor) RemRuneForward() (ok bool) {
82 }
83 e.remRuneAt(e.cursorIdx)
84 e.autoCache = nil
85+ e.backsearchEnd()
86 return
87 }
88
89@@ -135,7 +173,7 @@ func (e *Editor) RemWord() (ok bool) {
90 // |
91 for e.cursorIdx > 0 && line[e.cursorIdx-1] == ' ' {
92 e.remRuneAt(e.cursorIdx - 1)
93- e.Left()
94+ e.left()
95 }
96
97 for i := e.cursorIdx - 1; i >= 0; i -= 1 {
98@@ -143,10 +181,11 @@ func (e *Editor) RemWord() (ok bool) {
99 break
100 }
101 e.remRuneAt(i)
102- e.Left()
103+ e.left()
104 }
105
106 e.autoCache = nil
107+ e.backsearchEnd()
108 return
109 }
110
111@@ -162,6 +201,7 @@ func (e *Editor) Flush() (content string) {
112 e.cursorIdx = 0
113 e.offsetIdx = 0
114 e.autoCache = nil
115+ e.backsearchEnd()
116 return
117 }
118
119@@ -180,6 +220,7 @@ func (e *Editor) Clear() bool {
120 func (e *Editor) Right() {
121 e.right()
122 e.autoCache = nil
123+ e.backsearchEnd()
124 }
125
126 func (e *Editor) right() {
127@@ -212,6 +253,11 @@ func (e *Editor) RightWord() {
128 }
129
130 func (e *Editor) Left() {
131+ e.left()
132+ e.backsearchEnd()
133+}
134+
135+func (e *Editor) left() {
136 if e.cursorIdx == 0 {
137 return
138 }
139@@ -232,13 +278,14 @@ func (e *Editor) LeftWord() {
140 line := e.text[e.lineIdx]
141
142 for e.cursorIdx > 0 && line[e.cursorIdx-1] == ' ' {
143- e.Left()
144+ e.left()
145 }
146 for i := e.cursorIdx - 1; i >= 0 && line[i] != ' '; i -= 1 {
147- e.Left()
148+ e.left()
149 }
150
151 e.autoCache = nil
152+ e.backsearchEnd()
153 }
154
155 func (e *Editor) Home() {
156@@ -248,6 +295,7 @@ func (e *Editor) Home() {
157 e.cursorIdx = 0
158 e.offsetIdx = 0
159 e.autoCache = nil
160+ e.backsearchEnd()
161 }
162
163 func (e *Editor) End() {
164@@ -259,6 +307,7 @@ func (e *Editor) End() {
165 e.offsetIdx++
166 }
167 e.autoCache = nil
168+ e.backsearchEnd()
169 }
170
171 func (e *Editor) Up() {
172@@ -270,6 +319,7 @@ func (e *Editor) Up() {
173 e.cursorIdx = 0
174 e.offsetIdx = 0
175 e.autoCache = nil
176+ e.backsearchEnd()
177 e.End()
178 }
179
180@@ -286,6 +336,7 @@ func (e *Editor) Down() {
181 e.cursorIdx = 0
182 e.offsetIdx = 0
183 e.autoCache = nil
184+ e.backsearchEnd()
185 e.End()
186 }
187
188@@ -311,9 +362,47 @@ func (e *Editor) AutoComplete(offset int) (ok bool) {
189 e.offsetIdx++
190 }
191
192+ e.backsearchEnd()
193 return true
194 }
195
196+func (e *Editor) BackSearch() {
197+ clearLine := false
198+ if !e.backsearch {
199+ e.backsearch = true
200+ e.backsearchPattern = []rune(strings.ToLower(string(e.text[e.lineIdx])))
201+ clearLine = e.lineIdx == len(e.text)-1
202+ }
203+ e.backsearchUpdate(e.lineIdx - 1)
204+ if clearLine && e.lineIdx < len(e.text)-1 {
205+ e.text = e.text[:len(e.text)-1]
206+ }
207+}
208+
209+func (e *Editor) backsearchUpdate(start int) {
210+ if len(e.backsearchPattern) == 0 {
211+ return
212+ }
213+ pattern := string(e.backsearchPattern)
214+ for i := start; i >= 0; i-- {
215+ if match := strings.Index(strings.ToLower(string(e.text[i])), pattern); match >= 0 {
216+ e.lineIdx = i
217+ e.computeTextWidth()
218+ e.cursorIdx = runeOffset(string(e.text[i]), match) + len(e.backsearchPattern)
219+ e.offsetIdx = 0
220+ for e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
221+ e.offsetIdx++
222+ }
223+ e.autoCache = nil
224+ break
225+ }
226+ }
227+}
228+
229+func (e *Editor) backsearchEnd() {
230+ e.backsearch = false
231+}
232+
233 func (e *Editor) computeTextWidth() {
234 e.textWidth = e.textWidth[:1]
235 rw := 0
236@@ -331,7 +420,11 @@ func (e *Editor) Draw(screen tcell.Screen, x0, y int) {
237
238 for i < len(e.text[e.lineIdx]) && x < x0+e.width {
239 r := e.text[e.lineIdx][i]
240- screen.SetContent(x, y, r, nil, st)
241+ s := st
242+ if e.backsearch && i < e.cursorIdx && i >= e.cursorIdx-len(e.backsearchPattern) {
243+ s = s.Underline(true)
244+ }
245+ screen.SetContent(x, y, r, nil, s)
246 x += runeWidth(r)
247 i++
248 }
249@@ -344,3 +437,21 @@ func (e *Editor) Draw(screen tcell.Screen, x0, y int) {
250 cursorX := x0 + e.textWidth[e.cursorIdx] - e.textWidth[e.offsetIdx]
251 screen.ShowCursor(cursorX, y)
252 }
253+
254+// runeOffset returns the lowercase version of a rune
255+// TODO: len(strings.ToLower(string(r))) == len(strings.ToUpper(string(r))) for all x?
256+func runeToLower(r rune) rune {
257+ return []rune(strings.ToLower(string(r)))[0]
258+}
259+
260+// runeOffset returns the rune index of the rune starting at byte i in string s
261+func runeOffset(s string, pos int) int {
262+ n := 0
263+ for i, _ := range s {
264+ if i >= pos {
265+ return n
266+ }
267+ n++
268+ }
269+ return n
270+}
M
ui/ui.go
+4,
-0
1@@ -265,6 +265,10 @@ func (ui *UI) InputClear() bool {
2 return ui.e.Clear()
3 }
4
5+func (ui *UI) InputBackSearch() {
6+ ui.e.BackSearch()
7+}
8+
9 func (ui *UI) Resize() {
10 w, h := ui.screen.Size()
11 innerWidth := w - 9 - ui.config.ChanColWidth - ui.config.NickColWidth - ui.config.MemberColWidth