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
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+}
+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