commit a601d8e
delthas
·
2024-07-24 15:17:20 +0000 UTC
parent e193ae4
commands: Add auto-completion to /join This also adds a small "framework" to support async completions.
6 files changed,
+182,
-16
M
app.go
+68,
-5
1@@ -96,6 +96,12 @@ type boundKey struct {
2 target string
3 }
4
5+type pendingCompletion struct {
6+ id int
7+ f completionAsync
8+ deadline time.Time
9+}
10+
11 type App struct {
12 win *ui.UI
13 sessions map[string]*irc.Session // map of network IDs to their current session
14@@ -117,6 +123,9 @@ type App struct {
15 networkLock sync.RWMutex // locks networks
16 networks map[string]struct{} // set of network IDs we want to connect to; to be locked with networkLock
17
18+ pendingCompletions map[string][]pendingCompletion
19+ pendingCompletionsOff int
20+
21 lastMessageTime time.Time
22 lastCloseTime time.Time
23
24@@ -146,11 +155,12 @@ func NewApp(cfg Config) (app *App, err error) {
25 networks: map[string]struct{}{
26 "": {}, // add the master network by default
27 },
28- sessions: map[string]*irc.Session{},
29- events: make(chan event, eventChanSize),
30- cfg: cfg,
31- messageBounds: map[boundKey]bound{},
32- monitor: make(map[string]map[string]struct{}),
33+ pendingCompletions: make(map[string][]pendingCompletion),
34+ sessions: map[string]*irc.Session{},
35+ events: make(chan event, eventChanSize),
36+ cfg: cfg,
37+ messageBounds: map[boundKey]bound{},
38+ monitor: make(map[string]map[string]struct{}),
39 }
40
41 if cfg.Highlights != nil {
42@@ -1099,6 +1109,29 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
43 app.lastMessageTime = t
44 }
45
46+ if cs, ok := app.pendingCompletions[netID]; ok {
47+ now := time.Now()
48+ for i := 0; i < len(cs); i++ {
49+ c := &cs[i]
50+ var r []ui.Completion
51+ eat := false
52+ if c.deadline.After(now) {
53+ r = c.f(ev)
54+ if r == nil {
55+ continue
56+ }
57+ eat = true
58+ }
59+ app.win.AsyncCompletions(c.id, r)
60+ copy(cs[i:], cs[i+1:])
61+ app.pendingCompletions[netID] = app.pendingCompletions[netID][:len(cs)-1]
62+ i--
63+ if eat {
64+ return
65+ }
66+ }
67+ }
68+
69 // Mutate UI state
70 switch ev := ev.(type) {
71 case irc.RegisteredEvent:
72@@ -1397,6 +1430,21 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
73 }
74 app.win.RemoveNetworkBuffers(ev.ID)
75 }
76+ case irc.ListEvent:
77+ for _, item := range ev {
78+ text := fmt.Sprintf("There are %4s users on channel %s", item.Count, item.Channel)
79+ if item.Topic != "" {
80+ text += " -- " + item.Topic
81+ }
82+ app.addStatusLine(netID, ui.Line{
83+ At: msg.TimeOrNow(),
84+ Head: "List --",
85+ HeadColor: app.cfg.Colors.Status,
86+ Body: ui.Styled(text, vaxis.Style{
87+ Foreground: app.cfg.Colors.Status,
88+ }),
89+ })
90+ }
91 case irc.InfoEvent:
92 var head string
93 if ev.Prefix != "" {
94@@ -1578,11 +1626,26 @@ func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
95 cs = app.completionsChannelTopic(cs, cursorIdx, text)
96 cs = app.completionsChannelMembers(cs, cursorIdx, text)
97 }
98+ cs = app.completionsJoin(cs, cursorIdx, text)
99 cs = app.completionsUpload(cs, cursorIdx, text)
100 cs = app.completionsMsg(cs, cursorIdx, text)
101 cs = app.completionsCommands(cs, cursorIdx, text)
102 cs = app.completionsEmoji(cs, cursorIdx, text)
103
104+ for i := 0; i < len(cs); i++ {
105+ c := &cs[i]
106+ if c.Async == nil {
107+ continue
108+ }
109+ c.AsyncID = app.pendingCompletionsOff
110+ app.pendingCompletionsOff++
111+ app.pendingCompletions[netID] = append(app.pendingCompletions[netID], pendingCompletion{
112+ id: c.AsyncID,
113+ f: c.Async.(completionAsync),
114+ deadline: time.Now().Add(4 * time.Second),
115+ })
116+ }
117+
118 return cs
119 }
120
+49,
-0
1@@ -6,9 +6,12 @@ import (
2 "path/filepath"
3 "strings"
4
5+ "git.sr.ht/~delthas/senpai/irc"
6 "git.sr.ht/~delthas/senpai/ui"
7 )
8
9+type completionAsync func(e irc.Event) []ui.Completion
10+
11 func (app *App) completionsChannelMembers(cs []ui.Completion, cursorIdx int, text []rune) []ui.Completion {
12 var start int
13 for start = cursorIdx - 1; 0 <= start; start-- {
14@@ -49,6 +52,52 @@ func (app *App) completionsChannelMembers(cs []ui.Completion, cursorIdx int, tex
15 return cs
16 }
17
18+func (app *App) completionsJoin(cs []ui.Completion, cursorIdx int, text []rune) []ui.Completion {
19+ if !hasPrefix(text[:cursorIdx], []rune("/join #")) {
20+ return cs
21+ }
22+ netID, _ := app.win.CurrentBuffer()
23+ s := app.sessions[netID] // is not nil
24+ if s == nil {
25+ return cs
26+ }
27+ if !s.HasListMask() {
28+ return cs
29+ }
30+
31+ post := append([]rune{}, text[cursorIdx:]...)
32+ channel := append([]rune{}, text[6:]...)
33+ if len(channel) < 3 {
34+ // Require at least 2 characters for a search, to avoid triggering large LISTs in completions
35+ return cs
36+ }
37+
38+ s.List(string(channel) + "*")
39+
40+ cs = append(cs, ui.Completion{
41+ Async: completionAsync(func(e irc.Event) []ui.Completion {
42+ l, ok := e.(irc.ListEvent)
43+ if !ok {
44+ return nil
45+ }
46+ cs := make([]ui.Completion, len(l))
47+ for i, e := range l {
48+ text := []rune("/join ")
49+ text = append(text, []rune(e.Channel)...)
50+ text = append(text, post...)
51+ cs[i] = ui.Completion{
52+ StartIdx: 6,
53+ EndIdx: 6 + len([]rune(e.Channel)),
54+ Text: text,
55+ CursorIdx: cursorIdx + len([]rune(e.Channel)) - len(channel),
56+ }
57+ }
58+ return cs
59+ }),
60+ })
61+ return cs
62+}
63+
64 func (app *App) completionsChannelTopic(cs []ui.Completion, cursorIdx int, text []rune) []ui.Completion {
65 if !hasPrefix(text, []rune("/topic ")) {
66 return cs
+9,
-1
1@@ -68,7 +68,7 @@ type TopicChangeEvent struct {
2 Channel string
3 Topic string
4 Time time.Time
5- Who string
6+ Who string
7 }
8
9 type ModeChangeEvent struct {
10@@ -92,6 +92,14 @@ type MessageEvent struct {
11 Time time.Time
12 }
13
14+type ListItem struct {
15+ Channel string
16+ Count string
17+ Topic string
18+}
19+
20+type ListEvent []ListItem
21+
22 type HistoryEvent struct {
23 Target string
24 Messages []Event
+19,
-9
1@@ -139,6 +139,7 @@ type Session struct {
2 prefixModes string
3 monitor bool
4 whox bool
5+ listMask bool
6 upload string
7
8 users map[string]*User // known users.
9@@ -150,6 +151,7 @@ type Session struct {
10 searchBatchID string // ID of the search targets batch being processed.
11 searchBatch SearchEvent // search batch being processed.
12 monitors map[string]struct{} // set of users we want to monitor (and keep even if they are disconnected).
13+ pendingList ListEvent // current list response being received (flushed on list end).
14
15 pendingChannels map[string]time.Time // set of join requests stamps for channels.
16
17@@ -239,6 +241,10 @@ func (s *Session) UploadURL() string {
18 return s.upload
19 }
20
21+func (s *Session) HasListMask() bool {
22+ return s.listMask
23+}
24+
25 func (s *Session) Nick() string {
26 return s.nick
27 }
28@@ -1494,7 +1500,7 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
29 // useless stats delimiter
30 case rplEndofwhois:
31 // useless whois delimiter
32- case rplListstart, rplListend:
33+ case rplListstart:
34 // useless list delimiter
35 case rplEndofinvitelist, rplEndofinvexlist:
36 // useless invite list delimiter
37@@ -1722,14 +1728,16 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
38 if err := msg.ParseParams(nil, &channel, &count, &topic); err != nil {
39 return nil, err
40 }
41- text := fmt.Sprintf("There are %4s users on channel %s", count, channel)
42- if topic != "" {
43- text += " -- " + topic
44- }
45- return InfoEvent{
46- Prefix: "List",
47- Message: text,
48- }, nil
49+ s.pendingList = append(s.pendingList, ListItem{
50+ Channel: channel,
51+ Count: count,
52+ Topic: topic,
53+ })
54+ return nil, nil
55+ case rplListend:
56+ list := s.pendingList
57+ s.pendingList = nil
58+ return list, nil
59 case rplChannelmodeis:
60 var channel string
61 if err := msg.ParseParams(nil, &channel); err != nil {
62@@ -2059,6 +2067,8 @@ func (s *Session) updateFeatures(features []string) {
63 if err == nil {
64 s.historyLimit = historyLimit
65 }
66+ case "ELIST":
67+ s.listMask = strings.Contains(strings.ToUpper(value), "M")
68 case "LINELEN":
69 linelen, err := strconv.Atoi(value)
70 if err == nil && linelen != 0 {
+33,
-1
1@@ -7,6 +7,9 @@ import (
2 )
3
4 type Completion struct {
5+ Async any // not nil if this completion is asynchronously loading values
6+ AsyncID int
7+
8 StartIdx int
9 EndIdx int
10 Text []rune
11@@ -444,10 +447,13 @@ func (e *Editor) AutoComplete() (ok bool) {
12 return false
13 }
14 e.autoCacheIdx = 0
15- if len(e.autoCache) > 1 {
16+ if len(e.autoCache) > 1 || e.autoCache[0].Async != nil {
17 return false
18 }
19 }
20+ if e.autoCache[e.autoCacheIdx].Async != nil {
21+ return false
22+ }
23
24 e.text[e.lineIdx].runes = e.autoCache[e.autoCacheIdx].Text
25 e.recompute()
26@@ -465,6 +471,26 @@ func (e *Editor) AutoComplete() (ok bool) {
27 return true
28 }
29
30+func (e *Editor) AsyncCompletions(id int, cs []Completion) {
31+ for i := 0; i < len(e.autoCache); i++ {
32+ c := &e.autoCache[i]
33+ if c.AsyncID != id {
34+ continue
35+ }
36+ a := append([]Completion{}, e.autoCache[:i]...)
37+ a = append(a, cs...)
38+ a = append(a, e.autoCache[i+1:]...)
39+ e.autoCache = a
40+ break
41+ }
42+ if len(e.autoCache) == 0 {
43+ e.autoCache = nil
44+ e.autoCacheIdx = 0
45+ } else if e.autoCacheIdx >= len(e.autoCache) {
46+ e.autoCacheIdx = len(e.autoCache) - 1
47+ }
48+}
49+
50 func (e *Editor) BackSearch() {
51 if !e.backsearch {
52 e.backsearch = true
53@@ -584,6 +610,9 @@ func (e *Editor) Draw(vx *Vaxis, x0, y int, hint string) {
54 if display == nil {
55 display = completion.Text[completion.StartIdx:]
56 }
57+ if completion.Async != nil {
58+ display = []rune("Loading...")
59+ }
60
61 x := autoX
62 y := y - ci - 1
63@@ -598,6 +627,9 @@ func (e *Editor) Draw(vx *Vaxis, x0, y int, hint string) {
64 } else {
65 s.Attribute |= vaxis.AttrDim
66 }
67+ if completion.Async != nil {
68+ s.Attribute |= vaxis.AttrItalic
69+ }
70 dx, di := printCluster(vx, x, y, x0+e.width, display[i:], s)
71 if di == 0 {
72 break
M
ui/ui.go
+4,
-0
1@@ -640,6 +640,10 @@ func (ui *UI) ShowImage(img image.Image) bool {
2 return true
3 }
4
5+func (ui *UI) AsyncCompletions(id int, cs []Completion) {
6+ ui.e.AsyncCompletions(id, cs)
7+}
8+
9 func (ui *UI) Draw(members []irc.Member) {
10 ui.clickEvents = ui.clickEvents[:0]
11