mu4e: Contact completion - better sorting / display

Improve the contact-sorting algorithm, and make it better cooperate with
completion-at-point functions.

Also deal better with broken rewritten contacts, and document how
rewriting should done (docstrings and reference doc).
This commit is contained in:
djcb 2016-01-09 20:02:49 +02:00
parent 25da1fdc7b
commit 144e2a8f1b
4 changed files with 81 additions and 60 deletions

View File

@ -1,7 +1,7 @@
;; -*-mode: emacs-lisp; tab-width: 8; indent-tabs-mode: t -*-
;; mu4e-compose.el -- part of mu4e, the mu mail user agent for emacs
;;
;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema
;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
@ -260,15 +260,15 @@ appear on disk."
(defun mu4e~compose-complete-handler (str pred action)
(cond
((eq action nil)
(try-completion str mu4e~contacts-for-completion pred))
(try-completion str mu4e~contacts pred))
((eq action t)
(all-completions str mu4e~contacts-for-completion pred))
(all-completions str mu4e~contacts pred))
((eq action 'metadata)
;; our contacts are already sorted - just need to tell the
;; completion machinery not to try to undo that...
'(metadata
(display-sort-function . identity) ;; i.e., alphabetically
(cycle-sort-function . identity)))))
(display-sort-function . mu4e~sort-contacts-for-completion)
(cycle-sort-function . mu4e~sort-contacts-for-completion)))))
(defun mu4e~compose-complete-contact (&optional start)
"Complete the text at START with a contact.
@ -672,8 +672,6 @@ end of the buffer."
(define-key mu4e-compose-mode-map
(vector 'remap 'end-of-buffer) 'mu4e-compose-goto-bottom)
(provide 'mu4e-compose)
;; Load mu4e completely even when this file was loaded through

View File

@ -1,6 +1,6 @@
;;; mu4e-utils.el -- part of mu4e, the mu mail user agent
;;
;; Copyright (C) 2011-2014 Dirk-Jan C. Binnema
;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema
;; Copyright (C) 2013 Tibor Simko
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
@ -347,7 +347,7 @@ maildirs under `mu4e-maildir'."
(mapconcat
(lambda (item)
(concat
"["
q "["
(propertize (make-string 1 (cdr item))
'face 'mu4e-highlight-face)
"]"
@ -657,53 +657,66 @@ or (rfc822-string . CONTACT) otherwise."
(if name (format "%s <%s>" (mu4e~rfc822-quoteit name) mail) mail)
contact)))))
(defsubst mu4e~sort-contacts (contacts)
"Destructively sort contacts (only for cycling). Sort by
last-use when that is at most 10 days old. Otherwise, sort by
frequency."
(defun mu4e~sort-contacts (contacts)
"Destructively sort contacts (only for cycling) in order of
'mostly likely contact'.t See the code for the detail"
(let* ((now (+ (float-time) 3600)) ;; allow for clock diffs
(recent (- (float-time) (* 30 24 3600))))
(recent (- (float-time) (* 15 24 3600))))
(sort* contacts
(lambda (c1 c2)
(let* ( (c1 (cdr c1)) (c2 (cdr c2))
(personal1 (plist-get c1 :personal))
(personal2 (plist-get c2 :personal))
(freq1 (plist-get c1 :freq))
(freq2 (plist-get c2 :freq))
(tstamp1 (plist-get c1 :tstamp))
(tstamp2 (plist-get c2 :tstamp)))
;; personal contacts come first
(if (or personal1 personal2)
(if (not (and personal1 personal2))
;; if only one is personal, that one comes first
(if personal1 t nil)
;; then come recently seen ones; but only if they're not in
;; the future (as seen in spams)
(if (and (<= tstamp1 now) (<= tstamp2 now)
(or (> tstamp1 recent) (> tstamp2 recent)))
(> tstamp1 tstamp2)
;; otherwise, use the frequency
(> freq1 freq2)))))))))
;; note: freq, tstamp can only be missing if the rewrite
;; function removed them. If the rewrite function changed the
;; contact somehow, we guess it's important.
(freq1 (or (plist-get c1 :freq) 500))
(freq2 (or (plist-get c2 :freq) 500))
(tstamp1 (or (plist-get c1 :tstamp) now))
(tstamp2 (or (plist-get c2 :tstamp) now)))
;; only one is personal? if so, that one comes first
(if (not (equal personal1 personal2))
(if personal1 t nil)
;; only one is recent? that one comes first
(if (not (equal (> tstamp1 recent) (> tstamp2 recent)))
(> tstamp1 tstamp2)
;; otherwise, use the frequency
(> freq1 freq2))))))))
(defun mu4e~sort-contacts-for-completion (contacts)
"Takes CONTACTS, which is a list of RFC-822 addresses, and sort them based
on the ranking in `mu4e~contacts.'"
(sort* contacts
(lambda (c1 c2)
(let ((rank1 (gethash c1 mu4e~contacts))
(rank2 (gethash c2 mu4e~contacts)))
(< rank1 rank2)))))
;; start and stopping
(defun mu4e~fill-contacts (contacts)
(defun mu4e~fill-contacts (contact-data)
"We receive a list of contacts, which each contact of the form
(:name NAME :mail EMAIL :tstamp TIMESTAMP :freq FREQUENCY)
and fill the list `mu4e~contacts-for-completion' with it, with
each element looking like
name <email>
This is used by the completion function in mu4e-compose."
(setq mu4e~contacts-for-completion nil)
(dolist (contact contacts)
(let ((contact (mu4e~process-contact contact)))
;; note, this gives cells (rfc822-address . contact)
(when contact (push contact mu4e~contacts-for-completion))))
(setq mu4e~contacts-for-completion
(mapcar 'car ;; strip off the other stuff again
(mu4e~sort-contacts mu4e~contacts-for-completion)))
(mu4e-index-message "Contacts received: %d"
(length mu4e~contacts-for-completion)))
(:me NAME :mail EMAIL :tstamp TIMESTAMP :freq FREQUENCY)
and fill the hash `mu4e~contacts-for-completion' with it, with
each contact mapped to an integer for their ranking.
This is used by the completion function in mu4e-compose."
(let ((contacts) (rank 0))
(dolist (contact contact-data)
(let ((contact-maybe (mu4e~process-contact contact)))
;; note, this gives cells (rfc822-address . contact)
(when contact-maybe (push contact-maybe contacts))))
(setq contacts (mu4e~sort-contacts contacts))
;; now, we have our nicely sorted list, map them to a list
;; of increasing integers. We use that map in the composer
;; to sort them there. It would have been so much easier if emacs
;; allowed us to use the sorted-list as-is, but no such luck.
(setq mu4e~contacts (make-hash-table :test 'equal :weakness nil
:size (length contacts)))
(dolist (contact contacts)
(puthash (car contact) rank mu4e~contacts)
(incf rank))
(mu4e-index-message "Contacts received: %d"
(hash-table-count mu4e~contacts))))
(defun mu4e~check-requirements ()
"Check for the settings required for running mu4e."
@ -812,7 +825,7 @@ successful, call FUNC (if non-nil) afterwards."
"Clear any cached resources."
(setq
mu4e-maildir-list nil
mu4e~contacts-for-completion nil))
mu4e~contacts nil))
(defun mu4e~stop ()
"Stop the mu4e session."
@ -1194,6 +1207,5 @@ the view and compose modes."
(add-text-properties p (point-max) '(face mu4e-footer-face)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(provide 'mu4e-utils)
;;; End of mu4e-utils.el

View File

@ -1,6 +1,6 @@
;;; mu4e-vars.el -- part of mu4e, the mu mail user agent
;;
;; Copyright (C) 2011-2015 Dirk-Jan C. Binnema
;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
@ -312,13 +312,17 @@ their canonical counterpart; useful as an example."
contacts and rewrite them or remove them altogether.
If the function receives the contact as a list of the form
(:name NAME :mail EMAIL)
(:name NAME :mail EMAIL ... other properties ... )
(other properties may be there as well)
The function should return either:
- nil: remove this contact
- a possible rewritten cell (:name NAME :mail EMAIL), or simply return
the functions parameter."
- nil: remove this contact, or
- the rewritten cell, or
- the existing cell as-is
For rewriting, it is recommended to use `plist-put' to set the
changed parameters, so the other properties stay in place. Those
are needed for sorting the contacts."
:type 'function
:group 'mu4e-compose)
@ -769,10 +773,11 @@ for an example.")
(defvar mu4e~view-headers-buffer nil
"The headers buffer connected to this view.")
(defvar mu4e~contacts-for-completion nil
"List of contacts (ie. 'name <e-mail>').
This is used by the completion functions in mu4e-compose, filled
when mu4e starts.")
(defvar mu4e~contacts nil
"Hash of that maps contacts (ie. 'name <e-mail>') to an integer
with their sort order. We need to keep this information around to
quickly re-sort subsets of the contacts in the completions function in
mu4e-compose.")
(defvar mu4e~server-props nil
"Properties we receive from the mu4e server process.

View File

@ -12,7 +12,7 @@
@c %**end of header
@copying
Copyright @copyright{} 2012-2015 Dirk-Jan C. Binnema
Copyright @copyright{} 2012-2016 Dirk-Jan C. Binnema
@quotation
Permission is granted to copy, distribute and/or modify this document
@ -2885,7 +2885,11 @@ return either the possibly rewritten contact or @code{nil} to remove the
contact from the list - note that the latter can also be achieved using
@code{mu4e-compose-complete-ignore-address-regexp}.
Let's look at an example.
Each of the contacts are property-lists ('plists'), with properties
@code{:name} (which may be @code{nil}), and @code{:mail}, and a number
of other properties which you should return unchanged.
Let's look at an example:
@lisp
(defun my-rewrite-function (contact)
@ -2893,7 +2897,9 @@ Let's look at an example.
(mail (plist-get contact :mail)))
(cond
;; jonh smiht --> John Smith
((string= "jonh smiht" name) (list :name "John Smith" :mail mail))
((string= "jonh smiht" name)
(plist-put contact :name "John C. Smith")
contact)
;; remove evilspammer from the contacts list
((string= "evilspammer@@example.com" mail) nil)
;; others stay as the are