commit cdb68f6

Hubert Hirtz  ·  2020-08-19 19:53:01 +0000 UTC
parent 7a1312b
ui: Autocompletion of nicknames
3 files changed,  +126, -7
M app.go
M app.go
+54, -0
 1@@ -34,6 +34,9 @@ func NewApp(cfg Config) (app *App, err error) {
 2 
 3 	app.win, err = ui.New(ui.Config{
 4 		NickColWidth: cfg.NickColWidth,
 5+		AutoComplete: func(cursorIdx int, text []rune) []ui.Completion {
 6+			return app.completions(cursorIdx, text)
 7+		},
 8 	})
 9 	if err != nil {
10 		return
11@@ -229,6 +232,11 @@ func (app *App) handleUIEvent(ev tcell.Event) {
12 			if ok {
13 				app.typing()
14 			}
15+		case tcell.KeyTab:
16+			ok := app.win.InputAutoComplete()
17+			if ok {
18+				app.typing()
19+			}
20 		case tcell.KeyCR, tcell.KeyLF:
21 			buffer := app.win.CurrentBuffer()
22 			input := app.win.InputEnter()
23@@ -404,3 +412,49 @@ func (app *App) handleInput(buffer, content string) {
24 		}
25 	}
26 }
27+
28+func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
29+	var cs []ui.Completion
30+
31+	if len(text) == 0 {
32+		return cs
33+	}
34+
35+	var start int
36+	for start = cursorIdx - 1; 0 <= start; start-- {
37+		if text[start] == ' ' {
38+			break
39+		}
40+	}
41+	start++
42+	word := string(text[start:cursorIdx])
43+	wordCf := strings.ToLower(word)
44+	for _, name := range app.s.Names(app.win.CurrentBuffer()) {
45+		if strings.HasPrefix(strings.ToLower(name.Nick), wordCf) {
46+			nickComp := []rune(name.Nick)
47+			if start == 0 {
48+				nickComp = append(nickComp, ':')
49+			}
50+			nickComp = append(nickComp, ' ')
51+			c := make([]rune, len(text)+len(nickComp)-len(word))
52+			copy(c[:start], text[:start])
53+			if cursorIdx < len(text) {
54+				copy(c[start+len(nickComp):], text[cursorIdx:])
55+			}
56+			copy(c[start:], nickComp)
57+			cs = append(cs, ui.Completion{
58+				Text:      c,
59+				CursorIdx: start + len(nickComp),
60+			})
61+		}
62+	}
63+
64+	if cs != nil {
65+		cs = append(cs, ui.Completion{
66+			Text:      text,
67+			CursorIdx: cursorIdx,
68+		})
69+	}
70+
71+	return cs
72+}
+62, -6
  1@@ -4,6 +4,11 @@ import (
  2 	"github.com/gdamore/tcell"
  3 )
  4 
  5+type Completion struct {
  6+	Text      []rune
  7+	CursorIdx int
  8+}
  9+
 10 // editor is the text field where the user writes messages and commands.
 11 type editor struct {
 12 	// text contains the written runes. An empty slice means no text is written.
 13@@ -26,13 +31,19 @@ type editor struct {
 14 
 15 	// width is the width of the screen.
 16 	width int
 17+
 18+	autoComplete func(cursorIdx int, text []rune) []Completion
 19+	autoCache    []Completion
 20+	autoCacheIdx int
 21+	completeIdx  int
 22 }
 23 
 24-func newEditor(width int) editor {
 25+func newEditor(width int, autoComplete func(cursorIdx int, text []rune) []Completion) editor {
 26 	return editor{
 27-		text:      [][]rune{{}},
 28-		textWidth: []int{0},
 29-		width:     width,
 30+		text:         [][]rune{{}},
 31+		textWidth:    []int{0},
 32+		width:        width,
 33+		autoComplete: autoComplete,
 34 	}
 35 }
 36 
 37@@ -40,6 +51,7 @@ func (e *editor) Resize(width int) {
 38 	if width < e.width {
 39 		e.cursorIdx = 0
 40 		e.offsetIdx = 0
 41+		e.autoCache = nil
 42 	}
 43 	e.width = width
 44 }
 45@@ -53,6 +65,12 @@ func (e *editor) TextLen() int {
 46 }
 47 
 48 func (e *editor) PutRune(r rune) {
 49+	e.putRune(r)
 50+	e.Right()
 51+	e.autoCache = nil
 52+}
 53+
 54+func (e *editor) putRune(r rune) {
 55 	e.text[e.lineIdx] = append(e.text[e.lineIdx], ' ')
 56 	copy(e.text[e.lineIdx][e.cursorIdx+1:], e.text[e.lineIdx][e.cursorIdx:])
 57 	e.text[e.lineIdx][e.cursorIdx] = r
 58@@ -63,8 +81,6 @@ func (e *editor) PutRune(r rune) {
 59 	for i := e.cursorIdx + 1; i < len(e.textWidth); i++ {
 60 		e.textWidth[i] = rw + e.textWidth[i-1]
 61 	}
 62-
 63-	e.Right()
 64 }
 65 
 66 func (e *editor) RemRune() (ok bool) {
 67@@ -73,6 +89,7 @@ func (e *editor) RemRune() (ok bool) {
 68 		return
 69 	}
 70 	e.remRuneAt(e.cursorIdx - 1)
 71+	e.autoCache = nil
 72 	e.Left()
 73 	return
 74 }
 75@@ -83,6 +100,7 @@ func (e *editor) RemRuneForward() (ok bool) {
 76 		return
 77 	}
 78 	e.remRuneAt(e.cursorIdx)
 79+	e.autoCache = nil
 80 	return
 81 }
 82 
 83@@ -110,10 +128,16 @@ func (e *editor) Flush() (content string) {
 84 	e.textWidth = e.textWidth[:1]
 85 	e.cursorIdx = 0
 86 	e.offsetIdx = 0
 87+	e.autoCache = nil
 88 	return
 89 }
 90 
 91 func (e *editor) Right() {
 92+	e.right()
 93+	e.autoCache = nil
 94+}
 95+
 96+func (e *editor) right() {
 97 	if e.cursorIdx == len(e.text[e.lineIdx]) {
 98 		return
 99 	}
100@@ -138,18 +162,27 @@ func (e *editor) Left() {
101 			e.offsetIdx = 0
102 		}
103 	}
104+	e.autoCache = nil
105 }
106 
107 func (e *editor) Home() {
108+	if e.cursorIdx == 0 {
109+		return
110+	}
111 	e.cursorIdx = 0
112 	e.offsetIdx = 0
113+	e.autoCache = nil
114 }
115 
116 func (e *editor) End() {
117+	if e.cursorIdx == len(e.text[e.lineIdx]) {
118+		return
119+	}
120 	e.cursorIdx = len(e.text[e.lineIdx])
121 	for e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
122 		e.offsetIdx++
123 	}
124+	e.autoCache = nil
125 }
126 
127 func (e *editor) Up() {
128@@ -160,6 +193,7 @@ func (e *editor) Up() {
129 	e.computeTextWidth()
130 	e.cursorIdx = 0
131 	e.offsetIdx = 0
132+	e.autoCache = nil
133 	e.End()
134 }
135 
136@@ -175,9 +209,31 @@ func (e *editor) Down() {
137 	e.computeTextWidth()
138 	e.cursorIdx = 0
139 	e.offsetIdx = 0
140+	e.autoCache = nil
141 	e.End()
142 }
143 
144+func (e *editor) AutoComplete() (ok bool) {
145+	if e.autoCache == nil {
146+		e.autoCache = e.autoComplete(e.cursorIdx, e.text[e.lineIdx])
147+		if e.autoCache == nil {
148+			return false
149+		}
150+		if len(e.autoCache) == 0 {
151+			e.autoCache = nil
152+			return false
153+		}
154+		e.autoCacheIdx = 0
155+	}
156+
157+	e.text[e.lineIdx] = e.autoCache[e.autoCacheIdx].Text
158+	e.cursorIdx = e.autoCache[e.autoCacheIdx].CursorIdx
159+	e.computeTextWidth()
160+	e.autoCacheIdx = (e.autoCacheIdx + 1) % len(e.autoCache)
161+
162+	return true
163+}
164+
165 func (e *editor) computeTextWidth() {
166 	e.textWidth = e.textWidth[:1]
167 	rw := 0
+10, -1
 1@@ -10,6 +10,7 @@ import (
 2 
 3 type Config struct {
 4 	NickColWidth int
 5+	AutoComplete func(cursorIdx int, text []rune) []Completion
 6 }
 7 
 8 type UI struct {
 9@@ -55,7 +56,7 @@ func New(config Config) (ui *UI, err error) {
10 	ui.bs.Add(Home)
11 	ui.bs.AddLine("", NewLineNow("--", homeMessages[hmIdx]))
12 
13-	ui.e = newEditor(w)
14+	ui.e = newEditor(w, ui.config.AutoComplete)
15 
16 	ui.Resize()
17 
18@@ -199,6 +200,14 @@ func (ui *UI) InputDelete() (ok bool) {
19 	return
20 }
21 
22+func (ui *UI) InputAutoComplete() (ok bool) {
23+	ok = ui.e.AutoComplete()
24+	if ok {
25+		ui.draw()
26+	}
27+	return
28+}
29+
30 func (ui *UI) InputEnter() (content string) {
31 	content = ui.e.Flush()
32 	ui.draw()