msgstore: implement a uid-based architecture

Change the message store architecture from an index-based to a uid-based
one. Key advantage of this design approach is that no reselect mechanism
is required anymore since it comes with the design for free.

Fixes: https://todo.sr.ht/~rjarry/aerc/43
Signed-off-by: Koni Marti <koni.marti@gmail.com>
Tested-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
Koni Marti 2022-07-26 11:30:26 +02:00 committed by Robin Jarry
parent 3b90b3b0dd
commit 8f7695fde5
5 changed files with 73 additions and 86 deletions

View File

@ -51,8 +51,6 @@ func (Clear) Execute(aerc *widgets.Aerc, args []string) error {
if clearSelected { if clearSelected {
defer store.Select(0) defer store.Select(0)
} else {
store.SetReselect(store.Selected())
} }
store.ApplyClear() store.ApplyClear()
acct.SetStatus(statusline.SearchFilterClear()) acct.SetStatus(statusline.SearchFilterClear())

View File

@ -56,7 +56,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
// no more messages in the list // no more messages in the list
if next == nil { if next == nil {
aerc.RemoveTab(h.msgProvider) aerc.RemoveTab(h.msgProvider)
store.Select(len(store.Uids())) acct.Messages().Select(0)
acct.Messages().Invalidate() acct.Messages().Invalidate()
return return
} }
@ -74,7 +74,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
if next == nil { if next == nil {
// We deleted the last message, select the new last message // We deleted the last message, select the new last message
// instead of the first message // instead of the first message
store.Select(len(store.Uids())) acct.Messages().Select(0)
} }
} }
acct.Messages().Invalidate() acct.Messages().Invalidate()

View File

@ -34,7 +34,6 @@ func (ToggleThreads) Execute(aerc *widgets.Aerc, args []string) error {
if err != nil { if err != nil {
return err return err
} }
store.SetReselect(store.Selected())
store.SetThreadedView(!store.ThreadedView()) store.SetThreadedView(!store.ThreadedView())
acct.SetStatus(statusline.Threading(store.ThreadedView())) acct.SetStatus(statusline.Threading(store.ThreadedView()))
acct.Messages().Invalidate() acct.Messages().Invalidate()

View File

@ -20,7 +20,7 @@ type MessageStore struct {
uids []uint32 uids []uint32
Threads []*types.Thread Threads []*types.Thread
selected int selectedUid uint32
reselect *models.MessageInfo reselect *models.MessageInfo
bodyCallbacks map[uint32][]func(*types.FullMessage) bodyCallbacks map[uint32][]func(*types.FullMessage)
headerCallbacks map[uint32][]func(*types.MessageInfo) headerCallbacks map[uint32][]func(*types.MessageInfo)
@ -57,6 +57,8 @@ type MessageStore struct {
dirInfoUpdateDelay time.Duration dirInfoUpdateDelay time.Duration
} }
const MagicUid = 0xFFFFFFFF
func NewMessageStore(worker *types.Worker, func NewMessageStore(worker *types.Worker,
dirInfo *models.DirectoryInfo, dirInfo *models.DirectoryInfo,
defaultSortCriteria []*types.SortCriterion, defaultSortCriteria []*types.SortCriterion,
@ -75,7 +77,7 @@ func NewMessageStore(worker *types.Worker,
DirInfo: *dirInfo, DirInfo: *dirInfo,
Messages: make(map[uint32]*models.MessageInfo), Messages: make(map[uint32]*models.MessageInfo),
selected: 0, selectedUid: MagicUid,
marked: make(map[uint32]struct{}), marked: make(map[uint32]struct{}),
bodyCallbacks: make(map[uint32][]func(*types.FullMessage)), bodyCallbacks: make(map[uint32][]func(*types.FullMessage)),
headerCallbacks: make(map[uint32][]func(*types.MessageInfo)), headerCallbacks: make(map[uint32][]func(*types.MessageInfo)),
@ -197,9 +199,6 @@ func merge(to *models.MessageInfo, from *models.MessageInfo) {
func (store *MessageStore) Update(msg types.WorkerMessage) { func (store *MessageStore) Update(msg types.WorkerMessage) {
update := false update := false
directoryChange := false directoryChange := false
if store.reselect == nil {
store.SetReselect(store.Selected())
}
switch msg := msg.(type) { switch msg := msg.(type) {
case *types.DirectoryInfo: case *types.DirectoryInfo:
store.DirInfo = *msg.Info store.DirInfo = *msg.Info
@ -367,7 +366,6 @@ func (store *MessageStore) SetThreadedView(thread bool) {
if store.threadedView { if store.threadedView {
store.runThreadBuilder() store.runThreadBuilder()
} }
store.Reselect()
return return
} }
store.Sort(store.sortCriteria, nil) store.Sort(store.sortCriteria, nil)
@ -490,56 +488,22 @@ func (store *MessageStore) Uids() []uint32 {
} }
func (store *MessageStore) Selected() *models.MessageInfo { func (store *MessageStore) Selected() *models.MessageInfo {
uids := store.Uids() return store.Messages[store.selectedUid]
idx := len(uids) - store.selected - 1
if len(uids) == 0 || idx < 0 || idx >= len(uids) {
return nil
}
return store.Messages[uids[idx]]
} }
func (store *MessageStore) SelectedIndex() int { func (store *MessageStore) SelectedUid() uint32 {
return store.selected if store.selectedUid == MagicUid && len(store.Uids()) > 0 {
uids := store.Uids()
store.selectedUid = uids[len(uids)-1]
}
return store.selectedUid
} }
func (store *MessageStore) Select(index int) { func (store *MessageStore) Select(uid uint32) {
l := len(store.Uids()) store.selectedUid = uid
switch {
case l+index < 0:
// negative index overruns length of list
store.selected = 0
case index < 0:
// negative index, select from bottom
store.selected = l + index
case index >= l:
// index greater than length, select last
store.selected = l - 1
default:
store.selected = index
}
store.updateVisual() store.updateVisual()
} }
func (store *MessageStore) Reselect() {
if store.reselect == nil {
return
}
uid := store.reselect.Uid
newIdx := 0
for idx, uidStore := range store.Uids() {
if uidStore == uid {
newIdx = len(store.Uids()) - idx - 1
break
}
}
store.reselect = nil
store.Select(newIdx)
}
func (store *MessageStore) SetReselect(info *models.MessageInfo) {
store.reselect = info
}
// Mark sets the marked state on a MessageInfo // Mark sets the marked state on a MessageInfo
func (store *MessageStore) Mark(uid uint32) { func (store *MessageStore) Mark(uid uint32) {
if store.visualMarkMode { if store.visualMarkMode {
@ -648,15 +612,20 @@ func (store *MessageStore) updateVisual() {
store.ClearVisualMark() store.ClearVisualMark()
return return
} }
uidLen := len(store.Uids())
// store.selected is the inverted form of the actual array selectedIdx := store.FindIndexByUid(store.SelectedUid())
selectedIdx := uidLen - store.selected - 1 if selectedIdx < 0 {
store.ClearVisualMark()
return
}
var visUids []uint32 var visUids []uint32
if selectedIdx > startIdx { if selectedIdx > startIdx {
visUids = store.Uids()[startIdx : selectedIdx+1] visUids = store.Uids()[startIdx : selectedIdx+1]
} else { } else {
visUids = store.Uids()[selectedIdx : startIdx+1] visUids = store.Uids()[selectedIdx : startIdx+1]
} }
store.resetMark() store.resetMark()
for _, uid := range visUids { for _, uid := range visUids {
store.marked[uid] = struct{}{} store.marked[uid] = struct{}{}
@ -675,20 +644,31 @@ func (store *MessageStore) NextPrev(delta int) {
if len(uids) == 0 { if len(uids) == 0 {
return return
} }
idx := store.SelectedIndex() + delta
if idx < 0 { uid := store.SelectedUid()
store.Select(0)
} else { newIdx := store.FindIndexByUid(uid)
store.Select(idx) if newIdx < 0 {
store.Select(uids[len(uids)-1])
} }
newIdx -= delta
if newIdx >= len(uids) {
newIdx = len(uids) - 1
} else if newIdx < 0 {
newIdx = 0
}
store.Select(uids[newIdx])
store.updateVisual() store.updateVisual()
nextResultIndex := len(store.results) - store.resultIndex - 2*delta nextResultIndex := len(store.results) - store.resultIndex - 2*delta
if nextResultIndex < 0 || nextResultIndex >= len(store.results) { if nextResultIndex < 0 || nextResultIndex >= len(store.results) {
return return
} }
nextResultUid := store.results[nextResultIndex] nextResultUid := store.results[nextResultIndex]
selectedUid := uids[len(uids)-store.selected-1] if nextResultUid == store.SelectedUid() {
if nextResultUid == selectedUid {
store.resultIndex += delta store.resultIndex += delta
} }
} }
@ -734,21 +714,12 @@ func (store *MessageStore) SetFilter(args []string) {
} }
func (store *MessageStore) ApplyClear() { func (store *MessageStore) ApplyClear() {
if store.reselect == nil {
store.SetReselect(store.Selected())
}
store.filter = []string{"filter"} store.filter = []string{"filter"}
store.results = nil store.results = nil
if store.onFilterChange != nil { if store.onFilterChange != nil {
store.onFilterChange(store) store.onFilterChange(store)
} }
cb := func(msg types.WorkerMessage) { store.Sort(nil, nil)
switch msg.(type) {
case *types.Done:
store.Reselect()
}
}
store.Sort(nil, cb)
} }
func (store *MessageStore) nextPrevResult(delta int) { func (store *MessageStore) nextPrevResult(delta int) {
@ -762,13 +733,7 @@ func (store *MessageStore) nextPrevResult(delta int) {
if store.resultIndex < 0 { if store.resultIndex < 0 {
store.resultIndex = len(store.results) - 1 store.resultIndex = len(store.results) - 1
} }
uids := store.Uids() store.Select(store.results[len(store.results)-store.resultIndex-1])
for i, uid := range uids {
if store.results[len(store.results)-store.resultIndex-1] == uid {
store.Select(len(uids) - i - 1)
break
}
}
store.update() store.update()
} }
@ -826,3 +791,13 @@ func (store *MessageStore) visualStartIdx() int {
} }
return -1 return -1
} }
// FindIndexByUid returns the index in store.Uids() or -1 if not found
func (store *MessageStore) FindIndexByUid(uid uint32) int {
for idx, u := range store.Uids() {
if u == uid {
return idx
}
}
return -1
}

View File

@ -69,7 +69,9 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
ml.UpdateScroller(ml.height, len(store.Uids())) ml.UpdateScroller(ml.height, len(store.Uids()))
if store := ml.Store(); store != nil && len(store.Uids()) > 0 { if store := ml.Store(); store != nil && len(store.Uids()) > 0 {
ml.EnsureScroll(store.SelectedIndex()) if idx := store.FindIndexByUid(store.SelectedUid()); idx >= 0 {
ml.EnsureScroll(len(store.Uids()) - idx - 1)
}
} }
textWidth := ctx.Width() textWidth := ctx.Width()
@ -244,7 +246,7 @@ func (ml *MessageList) drawRow(textWidth int, ctx *ui.Context, uid uint32, row i
var style tcell.Style var style tcell.Style
// current row // current row
if row == ml.store.SelectedIndex()-ml.Scroll() { if msg.Uid == ml.store.SelectedUid() {
style = uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, msg_styles) style = uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, msg_styles)
} else { } else {
style = uiConfig.GetComposedStyle(config.STYLE_MSGLIST_DEFAULT, msg_styles) style = uiConfig.GetComposedStyle(config.STYLE_MSGLIST_DEFAULT, msg_styles)
@ -342,7 +344,6 @@ func (ml *MessageList) storeUpdate(store *lib.MessageStore) {
if ml.Store() != store { if ml.Store() != store {
return return
} }
store.Reselect()
} }
func (ml *MessageList) SetStore(store *lib.MessageStore) { func (ml *MessageList) SetStore(store *lib.MessageStore) {
@ -352,7 +353,8 @@ func (ml *MessageList) SetStore(store *lib.MessageStore) {
ml.store = store ml.store = store
if store != nil { if store != nil {
ml.spinner.Stop() ml.spinner.Stop()
ml.nmsgs = len(store.Uids()) uids := store.Uids()
ml.nmsgs = len(uids)
store.OnUpdate(ml.storeUpdate) store.OnUpdate(ml.storeUpdate)
store.OnFilterChange(func(store *lib.MessageStore) { store.OnFilterChange(func(store *lib.MessageStore) {
if ml.Store() != store { if ml.Store() != store {
@ -384,8 +386,21 @@ func (ml *MessageList) Selected() *models.MessageInfo {
} }
func (ml *MessageList) Select(index int) { func (ml *MessageList) Select(index int) {
// Note that the msgstore.Select function expects a uid as argument
// whereas the msglist.Select expects the message number
store := ml.Store() store := ml.Store()
store.Select(index) uids := store.Uids()
if len(uids) == 0 {
return
}
uidIdx := len(uids) - index - 1
if uidIdx >= len(store.Uids()) {
uidIdx = 0
} else if uidIdx < 0 {
uidIdx = len(store.Uids()) - 1
}
store.Select(store.Uids()[uidIdx])
ml.Invalidate() ml.Invalidate()
} }