[PATCH RFC] Emacs: Add address completion mechanism implemented in elisp

Michal Sojka sojkam1 at fel.cvut.cz
Tue Jul 29 09:57:50 PDT 2014


Notmuch currently has an address completion mechanism that requires
external script to provide completion candidates. This patch adds a
completion mechanism found in https://github.com/tjim/nevermore, which
is implemented in elisp only.

notmuch-lib.el is extended with function notmuch-async-harvest that
collects the completion candidates from notmuch database and stores
them in notmuch-completion-addresses.

notmuch-company.el hooks itself into message-mode and uses
company-mode to offer the completion to the user. The file is put into
the contrib directory which means that the use has to install it
himself. This is because company-mode is not a part of Emacs and
bytecompiling notmuch-company.el fails due to used --quick option that
causes user installed packages to be ignored. Moreover, Debian
bytecompiles elisp files during installation which would require
having company-mode packaged for Debian. This would be possible but
company-mode requires emacs24 which would complicate notmuch Debian
maintainer scripts.

It would probably make sense to implement another completion frontend
based only on Emacs built-in functionality and integrate it with
notmuch-addresses.el.

The original  nevermore code was modified in the following ways:
1) Prefix was changes from nm- to notmuch-.
2) A few docstrings and comments were added.
3) notmuch-flatten-* functions were renamed to match match
   devel/schemata.
---
 debian/notmuch-emacs.examples    |  1 +
 emacs/contrib/notmuch-company.el | 62 ++++++++++++++++++++++++++++
 emacs/notmuch-lib.el             | 87 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 150 insertions(+)
 create mode 100644 debian/notmuch-emacs.examples
 create mode 100644 emacs/contrib/notmuch-company.el

diff --git a/debian/notmuch-emacs.examples b/debian/notmuch-emacs.examples
new file mode 100644
index 0000000..4a42a47
--- /dev/null
+++ b/debian/notmuch-emacs.examples
@@ -0,0 +1 @@
+emacs/contrib/notmuch-company.el
diff --git a/emacs/contrib/notmuch-company.el b/emacs/contrib/notmuch-company.el
new file mode 100644
index 0000000..228de94
--- /dev/null
+++ b/emacs/contrib/notmuch-company.el
@@ -0,0 +1,62 @@
+;; notmuch-company.el --- Mail address completion for notmuch via company-mode
+
+;; Author: Trevor Jim <tjim at mac.com>
+;; Keywords: mail, completion
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; To enable this, install company mode (https://company-mode.github.io/)
+;; and add
+;;
+;;     (require 'notmuch-company)
+;;
+;; to your .emacs.
+;;
+;; NB company-minimum-prefix-length defaults to 3 so you don't get
+;; completion unless you type 3 characters
+
+
+;;; Code:
+
+(require 'company)
+(require 'message)
+(require 'notmuch-lib)
+
+(defvar-local notmuch-company-last-prefix nil)
+;;;###autoload
+(defun notmuch-company (command &optional arg &rest ignore)
+  "`company-mode' completion back-end for `nevermore (nm)'."
+  (interactive (list 'interactive))
+  (let ((case-fold-search t))
+    (pcase command
+      (`interactive (company-begin-backend 'notmuch-company))
+      (`prefix (and (eq major-mode 'message-mode)
+                    (looking-back "^\\(To\\|Cc\\|Bcc\\):.*"
+                                  (line-beginning-position))
+                    (setq notmuch-company-last-prefix (company-grab-symbol))))
+      (`candidates (let ((results (completion-substring--all-completions arg notmuch-completion-addresses nil 0)))
+                     (when results (car results))))
+      (`match (if (string-match notmuch-company-last-prefix arg)
+                  (match-end 0)
+                0))
+      (`no-cache t))))
+
+(add-hook 'message-mode-hook '(lambda ()
+                                (company-mode)
+                                (make-local-variable 'company-backends)
+                                (setq company-backends '(notmuch-company))
+				(when (not notmuch-completion-addresses) (notmuch-async-harvest))))
+(provide 'notmuch-company)
diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el
index 2941da3..c0f4ba0 100644
--- a/emacs/notmuch-lib.el
+++ b/emacs/notmuch-lib.el
@@ -216,6 +216,9 @@ on the command line, and then retry your notmuch command")))
   "Return the user.other_email value (as a list) from the notmuch configuration."
   (split-string (notmuch-config-get "user.other_email") "\n" t))
 
+(defun notmuch-user-emails ()
+  (cons (notmuch-user-primary-email) (notmuch-user-other-email)))
+
 (defun notmuch-poll ()
   "Run \"notmuch new\" or an external script to import mail.
 
@@ -845,6 +848,90 @@ status."
 (defvar notmuch-show-process-crypto nil)
 (make-variable-buffer-local 'notmuch-show-process-crypto)
 
+(defun notmuch-flatten-thread-set (thread-set)
+  "Convert the result of 'notmuch show' to the plain list of messages."
+;;  (display-message-or-buffer (format "Before: %S" thread-set))
+  (let ((result
+         (apply 'append
+                (mapcar 'notmuch-flatten-thread thread-set))))
+;;    (display-message-or-buffer (format "After: %S" result))
+    result))
+
+(defun notmuch-flatten-thread (thread)
+  (apply 'append
+         (mapcar 'notmuch-flatten-thread-node thread)))
+
+(defun notmuch-flatten-thread-node (thread-node)
+  (let ((msg (car thread-node))
+	(replies (cadr thread-node)))
+    (if msg
+        (cons msg (notmuch-flatten-thread replies))
+      (notmuch-flatten-thread replies))))
+
+;;; async address harvesting
+(defvar notmuch-completion-addresses nil
+  "Hash of email addresses for completion during email composition.
+  This variable is set by `notmuch-async-harvest'.")
+
+(defvar notmuch-async-harvest-pending-proc nil)   ; the process of a harvest underway
+(defvar notmuch-async-harvest-pending-output nil) ; holds the not-yet-processed part of the output of the harvest process
+(defun notmuch-async-harvest ()
+  "Collect possible addresses for completion. It queries the
+notmuch database for all emails sent by the user and collects the
+destination addresses from them in
+`notmuch-completion-addresses'. This takes some time so the
+address collection runs asynchronously."
+  (when notmuch-async-harvest-pending-proc
+      (ignore-errors (kill-process notmuch-async-harvest-pending-proc))
+      ; kill-process sends signal, actual process death is asynchronous, so indicate that we want the process dead
+      (setq notmuch-async-harvest-pending-proc nil))
+  (setq notmuch-completion-addresses (make-hash-table :test 'equal))
+  (setq notmuch-async-harvest-pending-output nil) ; indicate that we have not gotten any output yet
+  (setq notmuch-async-harvest-pending-proc
+        (notmuch-start-notmuch
+         "notmuch-async-harvest" ; process name
+         nil                ; process buffer
+         nil                ; process sentinel
+         "show"             ; notmuch command
+         "--format=sexp"
+         "--format-version=2"
+         "--body=false"
+         "--entire-thread=false"
+	 (mapconcat (lambda (x) (concat "from:" x)) (notmuch-user-emails) " or ")
+	 ))
+  (set-process-filter
+   notmuch-async-harvest-pending-proc
+   (lambda (proc string)
+     (when (and notmuch-async-harvest-pending-proc (equal (process-id proc) (process-id notmuch-async-harvest-pending-proc)))
+       (if notmuch-async-harvest-pending-output
+                                        ; This is not the first time we have seen output, add it to anything remaining from last time
+           (setq notmuch-async-harvest-pending-output (concat notmuch-async-harvest-pending-output string))
+                                        ; This is the first time we have seen output.  Skip the initial open paren
+         (setq notmuch-async-harvest-pending-output (substring string 1)))
+       (while
+           (let ((result (ignore-errors (read-from-string notmuch-async-harvest-pending-output))))
+             (and result
+                  (let ((obj (car result))
+                        (offset (cdr result)))
+                    (setq notmuch-async-harvest-pending-output (substring notmuch-async-harvest-pending-output offset))
+                    (let ((msgs (notmuch-flatten-thread-set (list obj))))
+                      (mapc
+                       (lambda (msg)
+                         (let* ((headers (plist-get msg :headers))
+                                (to (ignore-errors (mail-extract-address-components (plist-get headers :To) t)))
+                                (cc (ignore-errors (mail-extract-address-components (plist-get headers :Cc) t)))
+                                (bcc (ignore-errors (mail-extract-address-components (plist-get headers :Bcc) t))))
+                           (mapc (lambda (parts)
+                                   (let* ((name (car parts))
+                                          (email (cadr parts))
+                                          (entry (if name (format "%s <%s>" name email) email)))
+                                     (puthash entry t notmuch-completion-addresses)))
+                                 (append to cc bcc))))
+                       msgs)
+                      t))))))))
+  ; return value
+  nil)
+
 (provide 'notmuch-lib)
 
 ;; Local Variables:
-- 
2.0.1



More information about the notmuch mailing list