threading: enable filtering of server-side threads

This patch enables the filtering of a threaded view which uses
server-built threads. Filtering is done server-side, in order to
preserve the use of server-built threads.

In adding this feature, the filtering of notmuch folders was brought up
to feature parity with the other workers. The filters function the same
(ie: they can be stacked). The notmuch filters, however, still use
notmuch syntax for the filtering.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
Tim Culverhouse 2022-07-05 14:48:40 -05:00 committed by Robin Jarry
parent ccd042889f
commit c2f4404fca
10 changed files with 102 additions and 76 deletions

View file

@ -6,6 +6,7 @@ import (
"git.sr.ht/~rjarry/aerc/lib/statusline" "git.sr.ht/~rjarry/aerc/lib/statusline"
"git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/widgets"
"git.sr.ht/~rjarry/aerc/worker/types"
) )
type SearchFilter struct{} type SearchFilter struct{}
@ -32,27 +33,29 @@ func (SearchFilter) Execute(aerc *widgets.Aerc, args []string) error {
return errors.New("Cannot perform action. Messages still loading") return errors.New("Cannot perform action. Messages still loading")
} }
var cb func([]uint32)
if args[0] == "filter" { if args[0] == "filter" {
if len(args[1:]) == 0 { if len(args[1:]) == 0 {
return Clear{}.Execute(aerc, []string{"clear"}) return Clear{}.Execute(aerc, []string{"clear"})
} }
acct.SetStatus(statusline.FilterActivity("Filtering..."), statusline.Search("")) acct.SetStatus(statusline.FilterActivity("Filtering..."), statusline.Search(""))
cb = func(uids []uint32) { store.SetFilter(args[1:])
acct.SetStatus(statusline.FilterResult(strings.Join(args, " "))) cb := func(msg types.WorkerMessage) {
acct.Logger().Printf("Filter results: %v", uids) if _, ok := msg.(*types.Done); ok {
store.ApplyFilter(uids) acct.SetStatus(statusline.FilterResult(strings.Join(args, " ")))
acct.Logger().Printf("Filter results: %v", store.Uids())
}
} }
store.Sort(nil, cb)
} else { } else {
acct.SetStatus(statusline.Search("Searching...")) acct.SetStatus(statusline.Search("Searching..."))
cb = func(uids []uint32) { cb := func(uids []uint32) {
acct.SetStatus(statusline.Search(strings.Join(args, " "))) acct.SetStatus(statusline.Search(strings.Join(args, " ")))
acct.Logger().Printf("Search results: %v", uids) acct.Logger().Printf("Search results: %v", uids)
store.ApplySearch(uids) store.ApplySearch(uids)
// TODO: Remove when stores have multiple OnUpdate handlers // TODO: Remove when stores have multiple OnUpdate handlers
acct.Messages().Invalidate() acct.Messages().Invalidate()
} }
store.Search(args, cb)
} }
store.Search(args, cb)
return nil return nil
} }

View file

@ -84,8 +84,10 @@ func (Sort) Execute(aerc *widgets.Aerc, args []string) error {
} }
acct.SetStatus(statusline.Sorting(true)) acct.SetStatus(statusline.Sorting(true))
store.Sort(sortCriteria, func() { store.Sort(sortCriteria, func(msg types.WorkerMessage) {
acct.SetStatus(statusline.Sorting(false)) if _, ok := msg.(*types.Done); ok {
acct.SetStatus(statusline.Sorting(false))
}
}) })
return nil return nil
} }

View file

@ -74,10 +74,14 @@ in notmuch, like :delete and :archive.++
Others are slightly different in semantics and mentioned below: Others are slightly different in semantics and mentioned below:
*cf* <notmuch query> *cf* <notmuch query>
The change folder command allows for arbitrary notmuch queries and should The change folder command allows for arbitrary notmuch queries. Performing a
usually be preferred over *:filter* as it will be much faster if you use cf command will perform a new top-level notmuch query
the notmuch database to do the filtering
*filter* <notmuch query>
The filter command for notmuch backends takes in arbitrary notmuch queries.
It applies the query on the set of messages shown in the message list. This
can be used to perform successive filters/queries. It is equivalent to
performing a set of queries concatenated with "and"
# SEE ALSO # SEE ALSO

View file

@ -33,8 +33,7 @@ type MessageStore struct {
// Search/filter results // Search/filter results
results []uint32 results []uint32
resultIndex int resultIndex int
filtered []uint32 filter []string
filter bool
sortCriteria []*types.SortCriterion sortCriteria []*types.SortCriterion
@ -83,6 +82,7 @@ func NewMessageStore(worker *types.Worker,
threadedView: thread, threadedView: thread,
buildThreads: clientThreads, buildThreads: clientThreads,
filter: []string{"filter"},
sortCriteria: defaultSortCriteria, sortCriteria: defaultSortCriteria,
pendingBodies: make(map[uint32]interface{}), pendingBodies: make(map[uint32]interface{}),
@ -215,7 +215,6 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
} }
store.Messages = newMap store.Messages = newMap
store.uids = msg.Uids store.uids = msg.Uids
sort.SortBy(store.filtered, store.uids)
store.checkMark() store.checkMark()
update = true update = true
case *types.DirectoryThreaded: case *types.DirectoryThreaded:
@ -313,14 +312,6 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
} }
store.results = newResults store.results = newResults
var newFiltered []uint32
for _, res := range store.filtered {
if _, deleted := toDelete[res]; !deleted {
newFiltered = append(newFiltered, res)
}
}
store.filtered = newFiltered
for _, thread := range store.Threads { for _, thread := range store.Threads {
thread.Walk(func(t *types.Thread, _ int, _ error) error { thread.Walk(func(t *types.Thread, _ int, _ error) error {
if _, deleted := toDelete[t.Uid]; deleted { if _, deleted := toDelete[t.Uid]; deleted {
@ -392,13 +383,7 @@ func (store *MessageStore) runThreadBuilder() {
store.builder.Update(msg) store.builder.Update(msg)
} }
} }
var uids []uint32 store.Threads = store.builder.Threads(store.uids)
if store.filter {
uids = store.filtered
} else {
uids = store.uids
}
store.Threads = store.builder.Threads(uids)
} }
func (store *MessageStore) Delete(uids []uint32, func (store *MessageStore) Delete(uids []uint32,
@ -496,10 +481,6 @@ func (store *MessageStore) Uids() []uint32 {
return uids return uids
} }
} }
if store.filter {
return store.filtered
}
return store.uids return store.uids
} }
@ -732,30 +713,16 @@ func (store *MessageStore) ApplySearch(results []uint32) {
store.NextResult() store.NextResult()
} }
func (store *MessageStore) ApplyFilter(results []uint32) { func (store *MessageStore) SetFilter(args []string) {
defer store.Reselect(store.Selected()) store.filter = append(store.filter, args...)
store.results = nil
store.filtered = results
store.filter = true
if store.onFilterChange != nil {
store.onFilterChange(store)
}
store.update()
// any marking is now invalid
// TODO: could save that probably
store.ClearVisualMark()
} }
func (store *MessageStore) ApplyClear() { func (store *MessageStore) ApplyClear() {
store.results = nil store.filter = []string{"filter"}
store.filtered = nil
store.filter = false
if store.BuildThreads() {
store.runThreadBuilder()
}
if store.onFilterChange != nil { if store.onFilterChange != nil {
store.onFilterChange(store) store.onFilterChange(store)
} }
store.Sort(nil, nil)
} }
func (store *MessageStore) nextPrevResult(delta int) { func (store *MessageStore) nextPrevResult(delta int) {
@ -796,24 +763,30 @@ func (store *MessageStore) ModifyLabels(uids []uint32, add, remove []string,
}, cb) }, cb)
} }
func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func()) { func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func(types.WorkerMessage)) {
if criteria == nil {
criteria = store.sortCriteria
} else {
store.sortCriteria = criteria
}
store.Sorting = true store.Sorting = true
store.sortCriteria = criteria
handle_return := func(msg types.WorkerMessage) { handle_return := func(msg types.WorkerMessage) {
store.Sorting = false store.Sorting = false
if cb != nil { if cb != nil {
cb() cb(msg)
} }
} }
if store.threadedView && !store.buildThreads { if store.threadedView && !store.buildThreads {
store.worker.PostAction(&types.FetchDirectoryThreaded{ store.worker.PostAction(&types.FetchDirectoryThreaded{
SortCriteria: criteria, SortCriteria: criteria,
FilterCriteria: store.filter,
}, handle_return) }, handle_return)
} else { } else {
store.worker.PostAction(&types.FetchDirectoryContents{ store.worker.PostAction(&types.FetchDirectoryContents{
SortCriteria: criteria, SortCriteria: criteria,
FilterCriteria: store.filter,
}, handle_return) }, handle_return)
} }
} }

View file

@ -191,7 +191,11 @@ func (dirlist *DirectoryList) Select(name string) {
} }
dirlist.sortDirsByFoldersSortConfig() dirlist.sortDirsByFoldersSortConfig()
if newStore { if newStore {
dirlist.worker.PostAction(&types.FetchDirectoryContents{}, nil) store, ok := dirlist.MsgStore(name)
if ok {
// Fetch directory contents via store.Sort
store.Sort(nil, nil)
}
} }
} }
dirlist.Invalidate() dirlist.Invalidate()

View file

@ -3,7 +3,6 @@ package imap
import ( import (
"sort" "sort"
"github.com/emersion/go-imap"
sortthread "github.com/emersion/go-imap-sortthread" sortthread "github.com/emersion/go-imap-sortthread"
"git.sr.ht/~rjarry/aerc/worker/types" "git.sr.ht/~rjarry/aerc/worker/types"
@ -29,11 +28,13 @@ func (imapw *IMAPWorker) handleFetchDirectoryContents(
imapw.worker.Logger.Printf("Fetching UID list") imapw.worker.Logger.Printf("Fetching UID list")
seqSet := &imap.SeqSet{} searchCriteria, err := parseSearch(msg.FilterCriteria)
seqSet.AddRange(1, imapw.selected.Messages) if err != nil {
imapw.worker.PostMessage(&types.Error{
searchCriteria := &imap.SearchCriteria{ Message: types.RespondTo(msg),
SeqNum: seqSet, Error: err,
}, nil)
return
} }
sortCriteria := translateSortCriterions(msg.SortCriteria) sortCriteria := translateSortCriterions(msg.SortCriteria)
@ -98,10 +99,16 @@ func (imapw *IMAPWorker) handleDirectoryThreaded(
msg *types.FetchDirectoryThreaded) { msg *types.FetchDirectoryThreaded) {
imapw.worker.Logger.Printf("Fetching threaded UID list") imapw.worker.Logger.Printf("Fetching threaded UID list")
seqSet := &imap.SeqSet{} searchCriteria, err := parseSearch(msg.FilterCriteria)
seqSet.AddRange(1, imapw.selected.Messages) if err != nil {
imapw.worker.PostMessage(&types.Error{
Message: types.RespondTo(msg),
Error: err,
}, nil)
return
}
threads, err := imapw.client.thread.UidThread(sortthread.References, threads, err := imapw.client.thread.UidThread(sortthread.References,
&imap.SearchCriteria{SeqNum: seqSet}) searchCriteria)
if err != nil { if err != nil {
imapw.worker.PostMessage(&types.Error{ imapw.worker.PostMessage(&types.Error{
Message: types.RespondTo(msg), Message: types.RespondTo(msg),

View file

@ -11,6 +11,9 @@ import (
func parseSearch(args []string) (*imap.SearchCriteria, error) { func parseSearch(args []string) (*imap.SearchCriteria, error) {
criteria := imap.NewSearchCriteria() criteria := imap.NewSearchCriteria()
if len(args) == 0 {
return criteria, nil
}
opts, optind, err := getopt.Getopts(args, "rubax:X:t:H:f:c:") opts, optind, err := getopt.Getopts(args, "rubax:X:t:H:f:c:")
if err != nil { if err != nil {

View file

@ -406,10 +406,25 @@ func (w *Worker) handleOpenDirectory(msg *types.OpenDirectory) error {
func (w *Worker) handleFetchDirectoryContents( func (w *Worker) handleFetchDirectoryContents(
msg *types.FetchDirectoryContents) error { msg *types.FetchDirectoryContents) error {
uids, err := w.c.UIDs(*w.selected) var (
if err != nil { uids []uint32
w.worker.Logger.Printf("error scanning uids: %v", err) err error
return err )
if len(msg.FilterCriteria) > 0 {
filter, err := parseSearch(msg.FilterCriteria)
if err != nil {
return err
}
uids, err = w.search(filter)
if err != nil {
return err
}
} else {
uids, err = w.c.UIDs(*w.selected)
if err != nil {
w.worker.Logger.Printf("error scanning uids: %v", err)
return err
}
} }
sortedUids, err := w.sort(uids, msg.SortCriteria) sortedUids, err := w.sort(uids, msg.SortCriteria)
if err != nil { if err != nil {

View file

@ -550,7 +550,14 @@ func (w *worker) loadExcludeTags(
} }
func (w *worker) emitDirectoryContents(parent types.WorkerMessage) error { func (w *worker) emitDirectoryContents(parent types.WorkerMessage) error {
uids, err := w.uidsFromQuery(w.query) query := w.query
if msg, ok := parent.(*types.FetchDirectoryContents); ok {
s := strings.Join(msg.FilterCriteria[1:], " ")
if s != "" {
query = fmt.Sprintf("(%v) and (%v)", query, s)
}
}
uids, err := w.uidsFromQuery(query)
if err != nil { if err != nil {
return fmt.Errorf("could not fetch uids: %v", err) return fmt.Errorf("could not fetch uids: %v", err)
} }
@ -567,12 +574,18 @@ func (w *worker) emitDirectoryContents(parent types.WorkerMessage) error {
} }
func (w *worker) emitDirectoryThreaded(parent types.WorkerMessage) error { func (w *worker) emitDirectoryThreaded(parent types.WorkerMessage) error {
threads, err := w.db.ThreadsFromQuery(w.query) query := w.query
if msg, ok := parent.(*types.FetchDirectoryThreaded); ok {
s := strings.Join(msg.FilterCriteria[1:], " ")
if s != "" {
query = fmt.Sprintf("(%v) and (%v)", query, s)
}
}
threads, err := w.db.ThreadsFromQuery(query)
if err != nil { if err != nil {
return err return err
} }
w.w.PostMessage(&types.DirectoryThreaded{ w.w.PostMessage(&types.DirectoryThreaded{
Message: types.RespondTo(parent),
Threads: threads, Threads: threads,
}, nil) }, nil)
return nil return nil

View file

@ -87,12 +87,14 @@ type OpenDirectory struct {
type FetchDirectoryContents struct { type FetchDirectoryContents struct {
Message Message
SortCriteria []*SortCriterion SortCriteria []*SortCriterion
FilterCriteria []string
} }
type FetchDirectoryThreaded struct { type FetchDirectoryThreaded struct {
Message Message
SortCriteria []*SortCriterion SortCriteria []*SortCriterion
FilterCriteria []string
} }
type SearchDirectory struct { type SearchDirectory struct {