store: improve cursor position

Improve cursor re-positioning while filtering with and without threads.
Reposition cursor in client-side threading mode with a callback that is
set during store.NextPrev(). Run callback when the threads are
constructed in order to reposition the cursor correctly. The callback is
deactivated when store.Select() is called.

Steps to reproduce two issues:

* Reproduce issue 1:

1. Activate client-side threading
2. Apply a filter, e.g. :filter -f Koni
3. Move cursor around so that a message is highlighted
4. clear filter with :clear
5. The cursor is expected to remain on the selected message but is
   actually not

* Reproduce issue 2:

1. Activate client-side threading
2. Go the end of the message list
2. Apply a filter, e.g. :filter -f Koni
5. The cursor is now at the end of the filtered results instead of at
   the beginning

This patch fixes both of those issues. Tested in regular and threaded
view according to the following check list (expected behavior in
parenthesis):

1. Apply filter from a message that remains in the filter (cursor on message,
   message selected)
2. Apply filter from a message that will not remain (cursor at the top,
   no message selected)
3. Clear filter (cursor remains on message, message selected)
4. Scroll line-by-line (threads: cursor remains on line, does not "jump"
   with message)
5. Search (cursor on first result)

Signed-off-by: Koni Marti <koni.marti@gmail.com>
Tested-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Koni Marti 2022-08-12 23:15:42 +02:00 committed by Robin Jarry
parent d138da0c9f
commit 588be1a284
2 changed files with 30 additions and 24 deletions

View File

@ -53,7 +53,9 @@ type MessageStore struct {
threadBuilderDebounce *time.Timer threadBuilderDebounce *time.Timer
threadBuilderDelay time.Duration threadBuilderDelay time.Duration
threadCallback func()
// threads mutex protects the store.threads and store.threadCallback
threadsMutex sync.Mutex threadsMutex sync.Mutex
} }
@ -343,6 +345,8 @@ func (store *MessageStore) SetThreadedView(thread bool) {
if store.buildThreads { if store.buildThreads {
if store.threadedView { if store.threadedView {
store.runThreadBuilder() store.runThreadBuilder()
} else if store.threadBuilderDebounce != nil {
store.threadBuilderDebounce.Stop()
} }
return return
} }
@ -376,35 +380,19 @@ func (store *MessageStore) runThreadBuilder() {
} }
} }
store.threadBuilderDebounce = time.AfterFunc(store.threadBuilderDelay, func() { store.threadBuilderDebounce = time.AfterFunc(store.threadBuilderDelay, func() {
// temporarily deactiviate the selector in the message list by
// setting SelectedUid to the MagicUid
oldUid := store.SelectedUid()
store.Select(MagicUid)
// Get the current index (we want to stay at that position in
// the updated uid list to provide a similar scrolling
// experience to the user as in the regular view
idx := store.FindIndexByUid(oldUid)
// build new threads // build new threads
th := store.builder.Threads(store.uids) th := store.builder.Threads(store.uids)
// try to select the same index in the updated uid list; if // save local threads to the message store variable and
// index is out of bound, stay at the selected message // run callback if defined (callback should reposition cursor)
rebuildUids := store.builder.Uids()
if idx >= 0 && idx < len(rebuildUids) {
store.Select(rebuildUids[idx])
} else {
store.Select(oldUid)
}
// save local threads to the message store variable
store.threadsMutex.Lock() store.threadsMutex.Lock()
store.threads = th store.threads = th
if store.threadCallback != nil {
store.threadCallback()
}
store.threadsMutex.Unlock() store.threadsMutex.Unlock()
// invalidate message list so that it is redrawn with the new // invalidate message list
// threads and selected message
if store.onUpdate != nil { if store.onUpdate != nil {
store.onUpdate(store) store.onUpdate(store)
} }
@ -543,6 +531,11 @@ func (store *MessageStore) SelectedUid() uint32 {
} }
func (store *MessageStore) Select(uid uint32) { func (store *MessageStore) Select(uid uint32) {
store.threadsMutex.Lock()
if store.threadCallback != nil {
store.threadCallback = nil
}
store.threadsMutex.Unlock()
store.selectedUid = uid store.selectedUid = uid
if store.marker != nil { if store.marker != nil {
store.marker.UpdateVisualMark() store.marker.UpdateVisualMark()
@ -572,6 +565,16 @@ func (store *MessageStore) NextPrev(delta int) {
store.Select(uids[newIdx]) store.Select(uids[newIdx])
if store.BuildThreads() && store.ThreadedView() {
store.threadsMutex.Lock()
store.threadCallback = func() {
if uids := store.Uids(); len(uids) > newIdx {
store.selectedUid = uids[newIdx]
}
}
store.threadsMutex.Unlock()
}
if store.marker != nil { if store.marker != nil {
store.marker.UpdateVisualMark() store.marker.UpdateVisualMark()
} }
@ -672,6 +675,7 @@ func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func(types.W
store.Sorting = true store.Sorting = true
handle_return := func(msg types.WorkerMessage) { handle_return := func(msg types.WorkerMessage) {
store.Select(store.SelectedUid())
store.Sorting = false store.Sorting = false
if cb != nil { if cb != nil {
cb(msg) cb(msg)

View File

@ -70,9 +70,11 @@ 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 {
if idx := store.FindIndexByUid(store.SelectedUid()); idx >= 0 { idx := store.FindIndexByUid(store.SelectedUid())
ml.EnsureScroll(len(store.Uids()) - idx - 1) if idx < 0 {
idx = len(store.Uids()) - 1
} }
ml.EnsureScroll(len(store.Uids()) - idx - 1)
} }
textWidth := ctx.Width() textWidth := ctx.Width()