commit 31bdbef

delthas  ·  2023-12-25 01:26:25 +0000 UTC
parent 09129df
Add auto-completion suggestion popup

Inspired by Consolatis from the referenced GitHub PR.

See: https://github.com/Consolatis/senpai/pull/3/files
Fixes: https://todo.sr.ht/~delthas/senpai/130
5 files changed,  +97, -26
M app.go
M app.go
+1, -13
 1@@ -659,12 +659,7 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
 2 	case tcell.KeyCtrlR:
 3 		app.win.InputBackSearch()
 4 	case tcell.KeyTab:
 5-		ok := app.win.InputAutoComplete(1)
 6-		if ok {
 7-			app.typing()
 8-		}
 9-	case tcell.KeyBacktab:
10-		ok := app.win.InputAutoComplete(-1)
11+		ok := app.win.InputAutoComplete()
12 		if ok {
13 			app.typing()
14 		}
15@@ -1252,13 +1247,6 @@ func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
16 	cs = app.completionsCommands(cs, cursorIdx, text)
17 	cs = app.completionsEmoji(cs, cursorIdx, text)
18 
19-	if cs != nil {
20-		cs = append(cs, ui.Completion{
21-			Text:      text,
22-			CursorIdx: cursorIdx,
23-		})
24-	}
25-
26 	return cs
27 }
28 
+13, -0
 1@@ -1,6 +1,7 @@
 2 package senpai
 3 
 4 import (
 5+	"fmt"
 6 	"strings"
 7 
 8 	"git.sr.ht/~delthas/senpai/ui"
 9@@ -35,7 +36,10 @@ func (app *App) completionsChannelMembers(cs []ui.Completion, cursorIdx int, tex
10 			}
11 			copy(c[start:], nickComp)
12 			cs = append(cs, ui.Completion{
13+				StartIdx:  start,
14+				EndIdx:    cursorIdx,
15 				Text:      c,
16+				Display:   []rune(name.Name.Name),
17 				CursorIdx: start + len(nickComp),
18 			})
19 		}
20@@ -53,6 +57,8 @@ func (app *App) completionsChannelTopic(cs []ui.Completion, cursorIdx int, text
21 	if cursorIdx == len(text) {
22 		compText := append(text, []rune(topic)...)
23 		cs = append(cs, ui.Completion{
24+			StartIdx:  cursorIdx,
25+			EndIdx:    cursorIdx,
26 			Text:      compText,
27 			CursorIdx: len(compText),
28 		})
29@@ -91,6 +97,8 @@ func (app *App) completionsMsg(cs []ui.Completion, cursorIdx int, text []rune) [
30 				copy(c[5+len(nickComp):], text[cursorIdx:])
31 			}
32 			cs = append(cs, ui.Completion{
33+				StartIdx:  5,
34+				EndIdx:    cursorIdx,
35 				Text:      c,
36 				CursorIdx: 5 + len(nickComp),
37 			})
38@@ -121,6 +129,8 @@ func (app *App) completionsCommands(cs []ui.Completion, cursorIdx int, text []ru
39 			copy(c[1+len(name):], text[cursorIdx:])
40 
41 			cs = append(cs, ui.Completion{
42+				StartIdx:  0,
43+				EndIdx:    cursorIdx,
44 				Text:      c,
45 				CursorIdx: 1 + len(name),
46 			})
47@@ -157,7 +167,10 @@ func (app *App) completionsEmoji(cs []ui.Completion, cursorIdx int, text []rune)
48 			c = append(c, text[cursorIdx:]...)
49 		}
50 		cs = append(cs, ui.Completion{
51+			StartIdx:  start - 1,
52+			EndIdx:    cursorIdx,
53 			Text:      c,
54+			Display:   []rune(fmt.Sprintf("%v (%v)", emoji.Emoji, emoji.Alias)),
55 			CursorIdx: start - 1 + len([]rune(emoji.Emoji)),
56 		})
57 	}
+2, -2
 1@@ -129,8 +129,8 @@ senpai eats these events for eg selecting channels.*
 2 	Sends the contents of the input field.
 3 
 4 *TAB*
 5-	Trigger the auto-completion.  Press several times to cycle through
 6-	completions.
 7+	Open the auto-completion dialog. Choose auto-completion item with *UP* and
 8+	*DOWN*, then press *TAB* again to confirm.
 9 
10 *CTRL-L*
11 	Refresh the window.
+73, -3
  1@@ -7,7 +7,10 @@ import (
  2 )
  3 
  4 type Completion struct {
  5+	StartIdx  int
  6+	EndIdx    int
  7 	Text      []rune
  8+	Display   []rune
  9 	CursorIdx int
 10 }
 11 
 12@@ -340,6 +343,10 @@ func (e *Editor) End() {
 13 }
 14 
 15 func (e *Editor) Up() {
 16+	if e.autoCache != nil {
 17+		e.autoCacheIdx = (e.autoCacheIdx + 1) % len(e.autoCache)
 18+		return
 19+	}
 20 	if e.lineIdx == 0 {
 21 		return
 22 	}
 23@@ -353,6 +360,10 @@ func (e *Editor) Up() {
 24 }
 25 
 26 func (e *Editor) Down() {
 27+	if e.autoCache != nil {
 28+		e.autoCacheIdx = (e.autoCacheIdx + len(e.autoCache) - 1) % len(e.autoCache)
 29+		return
 30+	}
 31 	if e.lineIdx == len(e.text)-1 {
 32 		e.Flush()
 33 		return
 34@@ -366,7 +377,7 @@ func (e *Editor) Down() {
 35 	e.End()
 36 }
 37 
 38-func (e *Editor) AutoComplete(offset int) (ok bool) {
 39+func (e *Editor) AutoComplete() (ok bool) {
 40 	if e.autoCache == nil {
 41 		e.autoCache = e.autoComplete(e.cursorIdx, e.text[e.lineIdx])
 42 		if len(e.autoCache) == 0 {
 43@@ -374,8 +385,7 @@ func (e *Editor) AutoComplete(offset int) (ok bool) {
 44 			return false
 45 		}
 46 		e.autoCacheIdx = 0
 47-	} else {
 48-		e.autoCacheIdx = (e.autoCacheIdx + len(e.autoCache) + offset) % len(e.autoCache)
 49+		return
 50 	}
 51 
 52 	e.text[e.lineIdx] = e.autoCache[e.autoCacheIdx].Text
 53@@ -388,6 +398,7 @@ func (e *Editor) AutoComplete(offset int) (ok bool) {
 54 	for e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
 55 		e.offsetIdx++
 56 	}
 57+	e.autoCache = nil
 58 
 59 	e.backsearchEnd()
 60 	return true
 61@@ -447,22 +458,81 @@ func (e *Editor) Draw(screen tcell.Screen, x0, y int) {
 62 	x := x0
 63 	i := e.offsetIdx
 64 
 65+	autoStart := -1
 66+	autoEnd := -1
 67+	autoX := x0
 68+	if e.autoCache != nil {
 69+		autoStart = e.autoCache[e.autoCacheIdx].StartIdx
 70+		autoEnd = e.autoCache[e.autoCacheIdx].EndIdx
 71+	}
 72+
 73 	for i < len(e.text[e.lineIdx]) && x < x0+e.width {
 74 		r := e.text[e.lineIdx][i]
 75 		s := st
 76 		if e.backsearch && i < e.cursorIdx && i >= e.cursorIdx-len(e.backsearchPattern) {
 77 			s = s.Underline(true)
 78 		}
 79+		if i >= autoStart && i < autoEnd {
 80+			s = s.Underline(true)
 81+		}
 82+		if i == autoStart {
 83+			autoX = x
 84+		}
 85 		screen.SetContent(x, y, r, nil, s)
 86 		x += runeWidth(r)
 87 		i++
 88 	}
 89+	if i == autoStart {
 90+		autoX = x
 91+	}
 92 
 93 	for x < x0+e.width {
 94 		screen.SetContent(x, y, ' ', nil, st)
 95 		x++
 96 	}
 97 
 98+	autoCount := y - 2
 99+	if autoCount < 0 {
100+		autoCount = 0
101+	} else if autoCount > len(e.autoCache) {
102+		autoCount = len(e.autoCache)
103+	} else if autoCount > 10 {
104+		autoCount = 10
105+	}
106+	autoOff := 0
107+	if len(e.autoCache) > autoCount {
108+		autoOff = e.autoCacheIdx - autoCount/2
109+		if autoOff < 0 {
110+			autoOff = 0
111+		} else if autoOff > len(e.autoCache)-autoCount {
112+			autoOff = len(e.autoCache) - autoCount
113+		}
114+	}
115+
116+	for i, completion := range e.autoCache[autoOff : autoOff+autoCount] {
117+		display := completion.Display
118+		if display == nil {
119+			display = completion.Text[completion.StartIdx:]
120+		}
121+
122+		x := autoX
123+		y := y - i - 1
124+		for _, r := range display {
125+			if x >= x0+e.width {
126+				break
127+			}
128+			s := st.Background(tcell.ColorBlack)
129+			s = s.Reverse(true)
130+			if i+autoOff == e.autoCacheIdx {
131+				s = s.Bold(true)
132+			} else {
133+				s = s.Dim(true)
134+			}
135+			screen.SetContent(x, y, r, nil, s)
136+			x += runeWidth(r)
137+		}
138+	}
139+
140 	cursorX := x0 + e.textWidth[e.cursorIdx] - e.textWidth[e.offsetIdx]
141 	screen.ShowCursor(cursorX, y)
142 }
+8, -8
 1@@ -419,8 +419,8 @@ func (ui *UI) InputDeleteWord() (ok bool) {
 2 	return ui.e.RemWord()
 3 }
 4 
 5-func (ui *UI) InputAutoComplete(offset int) (ok bool) {
 6-	return ui.e.AutoComplete(offset)
 7+func (ui *UI) InputAutoComplete() (ok bool) {
 8+	return ui.e.AutoComplete()
 9 }
10 
11 func (ui *UI) InputFlush() (content string) {
12@@ -474,12 +474,6 @@ func (ui *UI) Notify(title string, body string) {
13 func (ui *UI) Draw(members []irc.Member) {
14 	w, h := ui.screen.Size()
15 
16-	if ui.channelWidth == 0 {
17-		ui.e.Draw(ui.screen, 9+ui.config.NickColWidth, h-2)
18-	} else {
19-		ui.e.Draw(ui.screen, 9+ui.channelWidth+ui.config.NickColWidth, h-1)
20-	}
21-
22 	ui.bs.DrawTimeline(ui.screen, ui.channelWidth, 0, ui.config.NickColWidth)
23 	if ui.channelWidth == 0 {
24 		ui.bs.DrawHorizontalBufferList(ui.screen, 0, h-1, w-ui.memberWidth, &ui.channelOffset)
25@@ -507,6 +501,12 @@ func (ui *UI) Draw(members []irc.Member) {
26 		printIdent(ui.screen, ui.channelWidth+7, h-1, ui.config.NickColWidth, ui.prompt)
27 	}
28 
29+	if ui.channelWidth == 0 {
30+		ui.e.Draw(ui.screen, 9+ui.config.NickColWidth, h-2)
31+	} else {
32+		ui.e.Draw(ui.screen, 9+ui.channelWidth+ui.config.NickColWidth, h-1)
33+	}
34+
35 	ui.screen.Show()
36 }
37