Re: [Patch v7 2/3] Emacs: Add address completion mechanism implemented in elisp
authorMark Walters <markwalters1009@gmail.com>
Sun, 25 Oct 2015 17:28:51 +0000 (17:28 +0000)
committerW. Trevor King <wking@tremily.us>
Sat, 20 Aug 2016 21:49:55 +0000 (14:49 -0700)
1c/c61b667b662a3b0d50d1fcb51562116d19e27d [new file with mode: 0644]

diff --git a/1c/c61b667b662a3b0d50d1fcb51562116d19e27d b/1c/c61b667b662a3b0d50d1fcb51562116d19e27d
new file mode 100644 (file)
index 0000000..4e6130c
--- /dev/null
@@ -0,0 +1,372 @@
+Return-Path: <markwalters1009@gmail.com>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+ by arlo.cworth.org (Postfix) with ESMTP id BF8A76DE1479\r
+ for <notmuch@notmuchmail.org>; Sun, 25 Oct 2015 10:28:58 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at cworth.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: 0.25\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=0.25 tagged_above=-999 required=5 tests=[AWL=0.076,\r
+ DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1,\r
+ FREEMAIL_ENVFROM_END_DIGIT=0.25, FREEMAIL_FROM=0.001,\r
+ RCVD_IN_DNSWL_LOW=-0.7, RCVD_IN_MSPIKE_H3=-0.01,\r
+ RCVD_IN_MSPIKE_WL=-0.01, SPF_PASS=-0.001, URIBL_SBL=0.644,\r
+ URIBL_SBL_A=0.1] autolearn=disabled\r
+Received: from arlo.cworth.org ([127.0.0.1])\r
+ by localhost (arlo.cworth.org [127.0.0.1]) (amavisd-new, port 10024)\r
+ with ESMTP id cQJCuIO_PvI6 for <notmuch@notmuchmail.org>;\r
+ Sun, 25 Oct 2015 10:28:56 -0700 (PDT)\r
+Received: from mail-wi0-f181.google.com (mail-wi0-f181.google.com\r
+ [209.85.212.181])\r
+ by arlo.cworth.org (Postfix) with ESMTPS id D37CD6DE103A\r
+ for <notmuch@notmuchmail.org>; Sun, 25 Oct 2015 10:28:55 -0700 (PDT)\r
+Received: by wicfx6 with SMTP id fx6so86070720wic.1\r
+ for <notmuch@notmuchmail.org>; Sun, 25 Oct 2015 10:28:54 -0700 (PDT)\r
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20120113;\r
+ h=from:to:subject:in-reply-to:references:user-agent:date:message-id\r
+ :mime-version:content-type;\r
+ bh=RW3haxCKEIU+sZJGWEqrLjHRBKyelkL35i6t2K4o+nw=;\r
+ b=KuXhunkumEmms/6/Bs1NE15Jwj0r3Q5ase++ux03zRu89zc41ESFqUDqahQnrVenTW\r
+ zsWMvOZ80kVOhNDi0r0HCfcqUGMIdUONjBlZLzXIK7VJT4G9N61IZH+ZBif26WQobS1g\r
+ XQ0ncGfQFMiuHpc6nDbak63c8xf9lqNpEqS6t7a+D0/r4YInaniygll6N9g7htuRswoT\r
+ xFm1X8OUIp65aEq+cd2BcsRhAGhOcu0Oqrzpz+tomn9p346VX9yI2YEwg6UJhLz/B1cy\r
+ wn4BuZ6fqXGrnLTEC92hG8pS/tFZw7pRRBspreFdb4WBIs9TmwofuLsboVzgpeqNk1MG\r
+ eEfg==\r
+X-Received: by 10.194.7.97 with SMTP id i1mr39451402wja.87.1445794134012;\r
+ Sun, 25 Oct 2015 10:28:54 -0700 (PDT)\r
+Received: from localhost (5751dfa2.skybroadband.com. [87.81.223.162])\r
+ by smtp.gmail.com with ESMTPSA id kr10sm34477258wjc.25.2015.10.25.10.28.52\r
+ (version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);\r
+ Sun, 25 Oct 2015 10:28:53 -0700 (PDT)\r
+From: Mark Walters <markwalters1009@gmail.com>\r
+To: David Bremner <david@tethera.net>, notmuch@notmuchmail.org\r
+Subject: Re: [Patch v7 2/3] Emacs: Add address completion mechanism\r
+ implemented in elisp\r
+In-Reply-To: <1445786638-13763-3-git-send-email-david@tethera.net>\r
+References: <1445786638-13763-1-git-send-email-david@tethera.net>\r
+ <1445786638-13763-3-git-send-email-david@tethera.net>\r
+User-Agent: Notmuch/0.21~rc1+29~g057f24d (http://notmuchmail.org) Emacs/23.4.1\r
+ (x86_64-pc-linux-gnu)\r
+Date: Sun, 25 Oct 2015 17:28:51 +0000\r
+Message-ID: <878u6qdfho.fsf@qmul.ac.uk>\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset=us-ascii\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.20\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+ <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <https://notmuchmail.org/mailman/options/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch/>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <https://notmuchmail.org/mailman/listinfo/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Sun, 25 Oct 2015 17:28:58 -0000\r
+\r
+On Sun, 25 Oct 2015, David Bremner <david@tethera.net> wrote:\r
+> From: Michal Sojka <sojkam1@fel.cvut.cz>\r
+>\r
+> Currently, notmuch has an address completion mechanism that requires\r
+> external command to provide completion candidates. This patch adds a\r
+> completion mechanism inspired by https://github.com/tjim/nevermore,\r
+> which is implemented in Emacs lisp only.\r
+>\r
+> The preexisting address completion mechanism, activated by pressing TAB\r
+> on To/Cc lines, is extended to use the new mechanism when no external\r
+> command is configured, i.e. when notmuch-address-command to nil, which\r
+> is the new default.\r
+>\r
+> The core of the new mechanism is the function notmuch-address-harvest,\r
+> which collects the completion candidates from the notmuch database and\r
+> stores them in notmuch-address-completions variable. The address\r
+> harvesting can run either synchronously (same as with the previous\r
+> mechanism) or asynchronously. When the user presses TAB for the first\r
+> time, synchronous harvesting limited to user entered text is performed.\r
+> If the entered text is reasonably long, this operation is relatively\r
+> fast. Then, asynchronous harvesting over the full database is triggered.\r
+> This operation may take long time (minutes on rotating disk). After it\r
+> finishes, no harvesting is normally performed again and subsequent\r
+> completion requests use the harvested data cached in memory. Completion\r
+> cache is updated after 24 hours.\r
+>\r
+> Note that this commit restores (different) completion functionality for\r
+> users when the user used external command named "notmuch-addresses",\r
+> i.e. the old default.  The result will be that the user will use\r
+> the new mechanism instead of this command. I believe that many users may\r
+> not even recognize this because the new mechanism works the same as\r
+> http://commonmeasure.org/~jkr/git/notmuch_addresses.git and perhaps also\r
+> as other commands suggested at\r
+> http://notmuchmail.org/emacstips/#address_completion.\r
+> ---\r
+>  emacs/notmuch-address.el | 192 ++++++++++++++++++++++++++++++++++++++---------\r
+>  emacs/notmuch-lib.el     |   3 +\r
+>  2 files changed, 159 insertions(+), 36 deletions(-)\r
+>\r
+> diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el\r
+> index e2af879..aa6228d 100644\r
+> --- a/emacs/notmuch-address.el\r
+> +++ b/emacs/notmuch-address.el\r
+> @@ -20,14 +20,17 @@\r
+>  ;; Authors: David Edmondson <dme@dme.org>\r
+>  \r
+>  (require 'message)\r
+> -\r
+> +(require 'notmuch-parser)\r
+> +(require 'notmuch-lib)\r
+>  ;;\r
+>  \r
+> -(defcustom notmuch-address-command nil\r
+> +(defcustom notmuch-address-command 'internal\r
+>    "The command which generates possible addresses. It must take a\r
+>  single argument and output a list of possible matches, one per\r
+> -line. The default value of nil disables address completion."\r
+> +line. The default value of `internal' uses built-in address\r
+> +completion."\r
+>    :type '(radio\r
+> +      (const :tag "Use internal address completion" internal)\r
+>        (const :tag "Disable address completion" nil)\r
+>        (string :tag "Use external completion command" "notmuch-addresses"))\r
+>    :group 'notmuch-send\r
+> @@ -44,15 +47,25 @@ to know how address selection is made by default."\r
+>    :group 'notmuch-send\r
+>    :group 'notmuch-external)\r
+>  \r
+> +(defvar notmuch-address-last-harvest 0\r
+> +  "Time of last address harvest")\r
+> +\r
+> +(defvar notmuch-address-completions (make-hash-table :test 'equal)\r
+> +  "Hash of email addresses for completion during email composition.\r
+> +  This variable is set by calling `notmuch-address-harvest'.")\r
+> +\r
+> +(defvar notmuch-address-full-harvest-finished nil\r
+> +  "t indicates that full completion address harvesting has been\r
+> +finished")\r
+> +\r
+>  (defun notmuch-address-selection-function (prompt collection initial-input)\r
+>    "Call (`completing-read'\r
+>        PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)"\r
+>    (completing-read\r
+>     prompt collection nil nil initial-input 'notmuch-address-history))\r
+>  \r
+> -(defvar notmuch-address-message-alist-member\r
+> -  '("^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):"\r
+> -          . notmuch-address-expand-name))\r
+> +(defvar notmuch-address-completion-headers-regexp\r
+> +  "^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):")\r
+>  \r
+>  (defvar notmuch-address-history nil)\r
+>  \r
+> @@ -60,39 +73,67 @@ to know how address selection is made by default."\r
+>    (message "calling notmuch-address-message-insinuate is no longer needed"))\r
+>  \r
+>  (defun notmuch-address-setup ()\r
+> -  (unless (memq notmuch-address-message-alist-member message-completion-alist)\r
+> -    (setq message-completion-alist\r
+> -      (push notmuch-address-message-alist-member message-completion-alist))))\r
+> +  (let ((pair (cons notmuch-address-completion-headers-regexp\r
+> +                #'notmuch-address-expand-name)))\r
+> +      (unless (memq pair message-completion-alist)\r
+> +    (setq message-completion-alist\r
+> +          (push pair message-completion-alist)))))\r
+> +\r
+> +(defun notmuch-address-matching (substring)\r
+> +  "Returns a list of completion candidates matching SUBSTRING.\r
+> +The candidates are taked form `notmuch-address-completions'."\r
+\r
+Just one minor typo: "taked form" should be "taken from".\r
+\r
+Best wishes\r
+\r
+Mark\r
+\r
+> +  (let ((candidates)\r
+> +    (re (regexp-quote substring)))\r
+> +    (maphash (lambda (key val)\r
+> +           (when (string-match re key)\r
+> +             (push key candidates)))\r
+> +         notmuch-address-completions)\r
+> +    candidates))\r
+> +\r
+>  (defun notmuch-address-options (original)\r
+> -  (process-lines notmuch-address-command original))\r
+> +  "Returns a list of completion candidates. Uses either\r
+> +elisp-based implementation or older implementation requiring\r
+> +external commands."\r
+> +  (cond\r
+> +   ((eq notmuch-address-command 'internal)\r
+> +    (when (not notmuch-address-full-harvest-finished)\r
+> +      ;; First, run quick synchronous harvest based on what the user\r
+> +      ;; entered so far\r
+> +      (notmuch-address-harvest (format "to:%s*" original) t))\r
+> +    (prog1 (notmuch-address-matching original)\r
+> +      ;; Then (re)start potentially long-running full asynchronous harvesting\r
+> +      (notmuch-address-harvest-trigger)))\r
+> +   (t\r
+> +    (process-lines notmuch-address-command original))))\r
+>  \r
+>  (defun notmuch-address-expand-name ()\r
+> -  (let* ((end (point))\r
+> -     (beg (save-excursion\r
+> -            (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")\r
+> -            (goto-char (match-end 0))\r
+> -            (point)))\r
+> -     (orig (buffer-substring-no-properties beg end))\r
+> -     (completion-ignore-case t)\r
+> -     (options (with-temp-message "Looking for completion candidates..."\r
+> -                (notmuch-address-options orig)))\r
+> -     (num-options (length options))\r
+> -     (chosen (cond\r
+> -              ((eq num-options 0)\r
+> -               nil)\r
+> -              ((eq num-options 1)\r
+> -               (car options))\r
+> -              (t\r
+> -               (funcall notmuch-address-selection-function\r
+> -                        (format "Address (%s matches): " num-options)\r
+> -                        (cdr options) (car options))))))\r
+> -    (if chosen\r
+> -    (progn\r
+> -      (push chosen notmuch-address-history)\r
+> -      (delete-region beg end)\r
+> -      (insert chosen))\r
+> -      (message "No matches.")\r
+> -      (ding))))\r
+> +  (when notmuch-address-command\r
+> +    (let* ((end (point))\r
+> +       (beg (save-excursion\r
+> +              (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")\r
+> +              (goto-char (match-end 0))\r
+> +              (point)))\r
+> +       (orig (buffer-substring-no-properties beg end))\r
+> +       (completion-ignore-case t)\r
+> +       (options (with-temp-message "Looking for completion candidates..."\r
+> +                  (notmuch-address-options orig)))\r
+> +       (num-options (length options))\r
+> +       (chosen (cond\r
+> +                ((eq num-options 0)\r
+> +                 nil)\r
+> +                ((eq num-options 1)\r
+> +                 (car options))\r
+> +                (t\r
+> +                 (funcall notmuch-address-selection-function\r
+> +                          (format "Address (%s matches): " num-options)\r
+> +                          (cdr options) (car options))))))\r
+> +      (if chosen\r
+> +      (progn\r
+> +        (push chosen notmuch-address-history)\r
+> +        (delete-region beg end)\r
+> +        (insert chosen))\r
+> +    (message "No matches.")\r
+> +    (ding)))))\r
+>  \r
+>  ;; Copied from `w3m-which-command'.\r
+>  (defun notmuch-address-locate-command (command)\r
+> @@ -113,4 +154,83 @@ to know how address selection is made by default."\r
+>                         (not (file-directory-p bin))))\r
+>            (throw 'found-command bin))))))))\r
+>  \r
+> +(defun notmuch-address-harvest-addr (result)\r
+> +  (let ((name-addr (plist-get result :name-addr)))\r
+> +    (puthash name-addr t notmuch-address-completions)))\r
+> +\r
+> +(defun notmuch-address-harvest-handle-result (obj)\r
+> +  (notmuch-address-harvest-addr obj))\r
+> +\r
+> +(defun notmuch-address-harvest-filter (proc string)\r
+> +  (when (buffer-live-p (process-buffer proc))\r
+> +    (with-current-buffer (process-buffer proc)\r
+> +      (save-excursion\r
+> +    (goto-char (point-max))\r
+> +    (insert string))\r
+> +      (notmuch-sexp-parse-partial-list\r
+> +       'notmuch-address-harvest-handle-result (process-buffer proc)))))\r
+> +\r
+> +(defvar notmuch-address-harvest-procs '(nil . nil)\r
+> +  "The currently running harvests.\r
+> +\r
+> +The car is a partial harvest, and the cdr is a full harvest")\r
+> +\r
+> +(defun notmuch-address-harvest (&optional filter-query synchronous callback)\r
+> +  "Collect addresses completion candidates. It queries the\r
+> +notmuch database for all messages sent by the user optionally\r
+> +matching FILTER-QUERY (if not nil). It collects the destination\r
+> +addresses from those messages and stores them in\r
+> +`notmuch-address-completions'. Address harvesting may take some\r
+> +time so the address collection runs asynchronously unless\r
+> +SYNCHRONOUS is t. In case of asynchronous execution, CALLBACK is\r
+> +called when harvesting finishes."\r
+> +  (let* ((from-me-query (mapconcat (lambda (x) (concat "from:" x)) (notmuch-user-emails) " or "))\r
+> +     (query (if filter-query\r
+> +                (format "(%s) and (%s)" from-me-query filter-query)\r
+> +              from-me-query))\r
+> +     (args `("address" "--format=sexp" "--format-version=2"\r
+> +             "--output=recipients"\r
+> +             "--deduplicate=address"\r
+> +             ,query)))\r
+> +    (if synchronous\r
+> +    (mapc #'notmuch-address-harvest-addr\r
+> +                               (apply 'notmuch-call-notmuch-sexp args))\r
+> +      ;; Asynchronous\r
+> +      (let* ((current-proc (if filter-query\r
+> +                           (car notmuch-address-harvest-procs)\r
+> +                         (cdr notmuch-address-harvest-procs)))\r
+> +         (proc-name (format "notmuch-address-%s-harvest"\r
+> +                            (if filter-query "partial" "full")))\r
+> +         (proc-buf (concat " *" proc-name "*")))\r
+> +    ;; Kill any existing process\r
+> +    (when current-proc\r
+> +      (kill-buffer (process-buffer current-proc))) ; this also kills the process\r
+> +\r
+> +    (setq current-proc\r
+> +          (apply 'notmuch-start-notmuch proc-name proc-buf\r
+> +                 callback                           ; process sentinel\r
+> +                 args))\r
+> +    (set-process-filter current-proc 'notmuch-address-harvest-filter)\r
+> +    (set-process-query-on-exit-flag current-proc nil)\r
+> +    (if filter-query\r
+> +        (setcar notmuch-address-harvest-procs current-proc)\r
+> +      (setcdr notmuch-address-harvest-procs current-proc)))))\r
+> +  ;; return value\r
+> +  nil)\r
+> +\r
+> +(defun notmuch-address-harvest-trigger ()\r
+> +  (let ((now (float-time)))\r
+> +    (when (> (- now notmuch-address-last-harvest) 86400)\r
+> +      (setq notmuch-address-last-harvest now)\r
+> +      (notmuch-address-harvest nil nil\r
+> +                           (lambda (proc event)\r
+> +                             ;; If harvest fails, we want to try\r
+> +                             ;; again when the trigger is next\r
+> +                             ;; called\r
+> +                             (if (string= event "finished\n")\r
+> +                                 (setq notmuch-address-full-harvest-finished t)\r
+> +                               (setq notmuch-address-last-harvest 0)))))))\r
+> +\r
+> +;;\r
+> +\r
+>  (provide 'notmuch-address)\r
+> diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el\r
+> index 201d7ec..1c3a9fe 100644\r
+> --- a/emacs/notmuch-lib.el\r
+> +++ b/emacs/notmuch-lib.el\r
+> @@ -232,6 +232,9 @@ on the command line, and then retry your notmuch command")))\r
+>    "Return the user.other_email value (as a list) from the notmuch configuration."\r
+>    (split-string (notmuch-config-get "user.other_email") "\n" t))\r
+>  \r
+> +(defun notmuch-user-emails ()\r
+> +  (cons (notmuch-user-primary-email) (notmuch-user-other-email)))\r
+> +\r
+>  (defun notmuch-poll ()\r
+>    "Run \"notmuch new\" or an external script to import mail.\r
+>  \r
+> -- \r
+> 2.6.1\r
+>\r
+> _______________________________________________\r
+> notmuch mailing list\r
+> notmuch@notmuchmail.org\r
+> https://notmuchmail.org/mailman/listinfo/notmuch\r