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