]> git.notmuchmail.org Git - notmuch/blobdiff - emacs/notmuch.el
emacs: wrap current search in parens when filtering
[notmuch] / emacs / notmuch.el
index 8ad0b68054ce228080120f1c96549a8281bb2876..463b9262212ecad69b7cff10420359b3c071888d 100644 (file)
@@ -36,7 +36,7 @@
 ;;
 ;; Then, to actually run it, add:
 ;;
-;;     (require 'notmuch)
+;;     (autoload 'notmuch "notmuch" "Notmuch mail" t)
 ;;
 ;; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
 ;; or run:
 (require 'notmuch-lib)
 (require 'notmuch-tag)
 (require 'notmuch-show)
+(require 'notmuch-tree)
 (require 'notmuch-mua)
 (require 'notmuch-hello)
 (require 'notmuch-maildir-fcc)
 (require 'notmuch-message)
+(require 'notmuch-parser)
 
 (defcustom notmuch-search-result-format
-  `(("date" . "%s ")
+  `(("date" . "%12s ")
     ("count" . "%-7s ")
     ("authors" . "%-20s ")
     ("subject" . "%s ")
        date, count, authors, subject, tags
 For example:
        (setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\)
-                                            \(\"subject\" . \"%s\"\)\)\)"
+                                            \(\"subject\" . \"%s\"\)\)\)
+Line breaks are permitted in format strings (though this is
+currently experimental).  Note that a line break at the end of an
+\"authors\" field will get elided if the authors list is long;
+place it instead at the beginning of the following field.  To
+enter a line break when setting this variable with setq, use \\n.
+To enter a line break in customize, press \\[quoted-insert] C-j."
   :type '(alist :key-type (string) :value-type (string))
   :group 'notmuch-search)
 
+;; The name of this variable `notmuch-init-file' is consistent with the
+;; convention used in e.g. emacs and gnus. The value, `notmuch-config[.el[c]]'
+;; is consistent with notmuch cli configuration file `~/.notmuch-config'.
+(defcustom notmuch-init-file (locate-user-emacs-file "notmuch-config")
+  "Your Notmuch Emacs-Lisp configuration file name.
+If a file with one of the suffixes defined by `get-load-suffixes' exists,
+it will be read instead.
+This file is read once when notmuch is loaded; the notmuch hooks added
+there will be called at other points of notmuch execution."
+  :type 'file
+  :group 'notmuch)
+
 (defvar notmuch-query-history nil
   "Variable to store minibuffer history for notmuch queries")
 
@@ -112,83 +132,6 @@ For example:
             (mm-save-part p))))
    mm-handle))
 
-(defun notmuch-documentation-first-line (symbol)
-  "Return the first line of the documentation string for SYMBOL."
-  (let ((doc (documentation symbol)))
-    (if doc
-       (with-temp-buffer
-         (insert (documentation symbol t))
-         (goto-char (point-min))
-         (let ((beg (point)))
-           (end-of-line)
-           (buffer-substring beg (point))))
-      "")))
-
-(defun notmuch-prefix-key-description (key)
-  "Given a prefix key code, return a human-readable string representation.
-
-This is basically just `format-kbd-macro' but we also convert ESC to M-."
-  (let ((desc (format-kbd-macro (vector key))))
-    (if (string= desc "ESC")
-       "M-"
-      (concat desc " "))))
-
-;; I would think that emacs would have code handy for walking a keymap
-;; and generating strings for each key, and I would prefer to just call
-;; that. But I couldn't find any (could be all implemented in C I
-;; suppose), so I wrote my own here.
-(defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
-  "For a key binding, return a string showing a human-readable
-representation of the prefixed key as well as the first line of
-documentation from the bound function.
-
-For a mouse binding, return nil."
-  (let ((key (car binding))
-       (action (cdr binding)))
-    (if (mouse-event-p key)
-       nil
-      (if (keymapp action)
-         (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))
-               (as-list))
-           (map-keymap (lambda (a b)
-                         (push (cons a b) as-list))
-                       action)
-           (mapconcat substitute as-list "\n"))
-       (concat prefix (format-kbd-macro (vector key))
-               "\t"
-               (notmuch-documentation-first-line action))))))
-
-(defun notmuch-substitute-command-keys-one (key)
-  ;; A `keymap' key indicates inheritance from a parent keymap - the
-  ;; inherited mappings follow, so there is nothing to print for
-  ;; `keymap' itself.
-  (when (not (eq key 'keymap))
-    (notmuch-substitute-one-command-key-with-prefix nil key)))
-
-(defun notmuch-substitute-command-keys (doc)
-  "Like `substitute-command-keys' but with documentation, not function names."
-  (let ((beg 0))
-    (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
-      (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1)))
-            (keymap (symbol-value (intern keymap-name))))
-       (setq doc (replace-match
-                  (mapconcat #'notmuch-substitute-command-keys-one
-                             (cdr keymap) "\n")
-                  1 1 doc)))
-      (setq beg (match-end 0)))
-    doc))
-
-(defun notmuch-help ()
-  "Display help for the current notmuch mode."
-  (interactive)
-  (let* ((mode major-mode)
-        (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
-    (with-current-buffer (generate-new-buffer "*notmuch-help*")
-      (insert doc)
-      (goto-char (point-min))
-      (set-buffer-modified-p nil)
-      (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
-
 (require 'hl-line)
 
 (defun notmuch-hl-line-mode ()
@@ -205,9 +148,8 @@ For a mouse binding, return nil."
 
 (defvar notmuch-search-mode-map
   (let ((map (make-sparse-keymap)))
-    (define-key map "?" 'notmuch-help)
-    (define-key map "q" 'notmuch-search-quit)
-    (define-key map "x" 'notmuch-search-quit)
+    (set-keymap-parent map notmuch-common-keymap)
+    (define-key map "x" 'notmuch-bury-or-kill-this-buffer)
     (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
     (define-key map "b" 'notmuch-search-scroll-down)
     (define-key map " " 'notmuch-search-scroll-up)
@@ -217,12 +159,8 @@ For a mouse binding, return nil."
     (define-key map "n" 'notmuch-search-next-thread)
     (define-key map "r" 'notmuch-search-reply-to-thread-sender)
     (define-key map "R" 'notmuch-search-reply-to-thread)
-    (define-key map "m" 'notmuch-mua-new-mail)
-    (define-key map "s" 'notmuch-search)
     (define-key map "o" 'notmuch-search-toggle-order)
     (define-key map "c" 'notmuch-search-stash-map)
-    (define-key map "=" 'notmuch-search-refresh-view)
-    (define-key map "G" 'notmuch-search-poll-and-refresh-view)
     (define-key map "t" 'notmuch-search-filter-by-tag)
     (define-key map "f" 'notmuch-search-filter)
     (define-key map [mouse-1] 'notmuch-search-show-thread)
@@ -231,6 +169,7 @@ For a mouse binding, return nil."
     (define-key map "-" 'notmuch-search-remove-tag)
     (define-key map "+" 'notmuch-search-add-tag)
     (define-key map (kbd "RET") 'notmuch-search-show-thread)
+    (define-key map "Z" 'notmuch-tree-from-search-current-query)
     map)
   "Keymap for \"notmuch search\" buffers.")
 (fset 'notmuch-search-mode-map notmuch-search-mode-map)
@@ -238,6 +177,8 @@ For a mouse binding, return nil."
 (defvar notmuch-search-stash-map
   (let ((map (make-sparse-keymap)))
     (define-key map "i" 'notmuch-search-stash-thread-id)
+    (define-key map "q" 'notmuch-stash-query)
+    (define-key map "?" 'notmuch-subkeymap-help)
     map)
   "Submap for stash commands")
 (fset 'notmuch-search-stash-map notmuch-search-stash-map)
@@ -247,21 +188,17 @@ For a mouse binding, return nil."
   (interactive)
   (notmuch-common-do-stash (notmuch-search-find-thread-id)))
 
+(defun notmuch-stash-query ()
+  "Copy current query to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash (notmuch-search-get-query)))
+
 (defvar notmuch-search-query-string)
 (defvar notmuch-search-target-thread)
 (defvar notmuch-search-target-line)
-(defvar notmuch-search-continuation)
 
 (defvar notmuch-search-disjunctive-regexp      "\\<[oO][rR]\\>")
 
-(defun notmuch-search-quit ()
-  "Exit the search buffer, calling any defined continuation function."
-  (interactive)
-  (let ((continuation notmuch-search-continuation))
-    (notmuch-kill-this-buffer)
-    (when continuation
-      (funcall continuation))))
-
 (defun notmuch-search-scroll-up ()
   "Move forward through search results by one window's worth."
   (interactive)
@@ -287,18 +224,25 @@ For a mouse binding, return nil."
 (defun notmuch-search-next-thread ()
   "Select the next thread in the search results."
   (interactive)
-  (forward-line 1))
+  (when (notmuch-search-get-result)
+    (goto-char (notmuch-search-result-end))))
 
 (defun notmuch-search-previous-thread ()
   "Select the previous thread in the search results."
   (interactive)
-  (forward-line -1))
+  (if (notmuch-search-get-result)
+      (unless (bobp)
+       (goto-char (notmuch-search-result-beginning (- (point) 1))))
+    ;; We must be past the end; jump to the last result
+    (notmuch-search-last-thread)))
 
 (defun notmuch-search-last-thread ()
   "Select the last thread in the search results."
   (interactive)
   (goto-char (point-max))
-  (forward-line -2))
+  (forward-line -2)
+  (let ((beg (notmuch-search-result-beginning)))
+    (when beg (goto-char beg))))
 
 (defun notmuch-search-first-thread ()
   "Select the first thread in the search results."
@@ -372,16 +316,22 @@ number of matched messages and total messages in the thread,
 participants in the thread, a representative subject line, and
 any tags).
 
-Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]'
-keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key
-is a convenience for archiving a thread (removing the \"inbox\"
-tag). The '\\[notmuch-search-tag-all]' key can be used to add or remove a tag from all
-threads in the current buffer.
-
-Other useful commands are '\\[notmuch-search-filter]' for filtering the current search
-based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include
-only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
-search.
+Pressing \\[notmuch-search-show-thread] on any line displays that
+thread. The '\\[notmuch-search-add-tag]' and
+'\\[notmuch-search-remove-tag]' keys can be used to add or remove
+tags from a thread. The '\\[notmuch-search-archive-thread]' key
+is a convenience for archiving a thread (applying changes in
+`notmuch-archive-tags'). The '\\[notmuch-search-tag-all]' key can
+be used to add and/or remove tags from all messages (as opposed
+to threads) that match the current query.  Use with caution, as
+this will also tag matching messages that arrived *after*
+constructing the buffer.
+
+Other useful commands are '\\[notmuch-search-filter]' for
+filtering the current search based on an additional query string,
+'\\[notmuch-search-filter-by-tag]' for filtering to include only
+messages with a given tag, and '\\[notmuch-search]' to execute a
+new, global search.
 
 Complete list of currently available key bindings:
 
@@ -392,7 +342,7 @@ Complete list of currently available key bindings:
   (make-local-variable 'notmuch-search-oldest-first)
   (make-local-variable 'notmuch-search-target-thread)
   (make-local-variable 'notmuch-search-target-line)
-  (set (make-local-variable 'notmuch-search-continuation) nil)
+  (setq notmuch-buffer-refresh-function #'notmuch-search-refresh-view)
   (set (make-local-variable 'scroll-preserve-screen-position) t)
   (add-to-invisibility-spec (cons 'ellipsis t))
   (use-local-map notmuch-search-mode-map)
@@ -401,59 +351,140 @@ Complete list of currently available key bindings:
        mode-name "notmuch-search")
   (setq buffer-read-only t))
 
+(defun notmuch-search-get-result (&optional pos)
+  "Return the result object for the thread at POS (or point).
+
+If there is no thread at POS (or point), returns nil."
+  (get-text-property (or pos (point)) 'notmuch-search-result))
+
+(defun notmuch-search-result-beginning (&optional pos)
+  "Return the point at the beginning of the thread at POS (or point).
+
+If there is no thread at POS (or point), returns nil."
+  (when (notmuch-search-get-result pos)
+    ;; We pass 1+point because previous-single-property-change starts
+    ;; searching one before the position we give it.
+    (previous-single-property-change (1+ (or pos (point)))
+                                    'notmuch-search-result nil (point-min))))
+
+(defun notmuch-search-result-end (&optional pos)
+  "Return the point at the end of the thread at POS (or point).
+
+The returned point will be just after the newline character that
+ends the result line.  If there is no thread at POS (or point),
+returns nil"
+  (when (notmuch-search-get-result pos)
+    (next-single-property-change (or pos (point)) 'notmuch-search-result
+                                nil (point-max))))
+
+(defun notmuch-search-foreach-result (beg end function)
+  "Invoke FUNCTION for each result between BEG and END.
+
+FUNCTION should take one argument.  It will be applied to the
+character position of the beginning of each result that overlaps
+the region between points BEG and END.  As a special case, if (=
+BEG END), FUNCTION will be applied to the result containing point
+BEG."
+
+  (lexical-let ((pos (notmuch-search-result-beginning beg))
+               ;; End must be a marker in case function changes the
+               ;; text.
+               (end (copy-marker end))
+               ;; Make sure we examine at least one result, even if
+               ;; (= beg end).
+               (first t))
+    ;; We have to be careful if the region extends beyond the results.
+    ;; In this case, pos could be null or there could be no result at
+    ;; pos.
+    (while (and pos (or (< pos end) first))
+      (when (notmuch-search-get-result pos)
+       (funcall function pos))
+      (setq pos (notmuch-search-result-end pos)
+           first nil))))
+;; Unindent the function argument of notmuch-search-foreach-result so
+;; the indentation of callers doesn't get out of hand.
+(put 'notmuch-search-foreach-result 'lisp-indent-function 2)
+
 (defun notmuch-search-properties-in-region (property beg end)
-  (save-excursion
-    (let ((output nil)
-         (last-line (line-number-at-pos end))
-         (max-line (- (line-number-at-pos (point-max)) 2)))
-      (goto-char beg)
-      (beginning-of-line)
-      (while (<= (line-number-at-pos) (min last-line max-line))
-       (setq output (cons (get-text-property (point) property) output))
-       (forward-line 1))
-      output)))
-
-(defun notmuch-search-find-thread-id ()
-  "Return the thread for the current thread"
-  (get-text-property (point) 'notmuch-search-thread-id))
-
-(defun notmuch-search-find-thread-id-region (beg end)
-  "Return a list of threads for the current region"
-  (notmuch-search-properties-in-region 'notmuch-search-thread-id beg end))
-
-(defun notmuch-search-find-thread-id-region-search (beg end)
-  "Return a search string for threads for the current region"
-  (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or "))
+  (let (output)
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (push (plist-get (notmuch-search-get-result pos) property) output)))
+    output))
+
+(defun notmuch-search-find-thread-id (&optional bare)
+  "Return the thread for the current thread
+
+If BARE is set then do not prefix with \"thread:\""
+  (let ((thread (plist-get (notmuch-search-get-result) :thread)))
+    (when thread (concat (unless bare "thread:") thread))))
+
+(defun notmuch-search-find-stable-query ()
+  "Return the stable queries for the current thread.
+
+This returns a list (MATCHED-QUERY UNMATCHED-QUERY) for the
+matched and unmatched messages in the current thread."
+  (plist-get (notmuch-search-get-result) :query))
+
+(defun notmuch-search-find-stable-query-region (beg end &optional only-matched)
+  "Return the stable query for the current region.
+
+If ONLY-MATCHED is non-nil, include only matched messages.  If it
+is nil, include both matched and unmatched messages. If there are
+no messages in the region then return nil."
+  (let ((query-list nil) (all (not only-matched)))
+    (dolist (queries (notmuch-search-properties-in-region :query beg end))
+      (when (first queries)
+       (push (first queries) query-list))
+      (when (and all (second queries))
+       (push (second queries) query-list)))
+    (when query-list
+      (concat "(" (mapconcat 'identity query-list ") or (") ")"))))
 
 (defun notmuch-search-find-authors ()
   "Return the authors for the current thread"
-  (get-text-property (point) 'notmuch-search-authors))
+  (plist-get (notmuch-search-get-result) :authors))
 
 (defun notmuch-search-find-authors-region (beg end)
   "Return a list of authors for the current region"
-  (notmuch-search-properties-in-region 'notmuch-search-authors beg end))
+  (notmuch-search-properties-in-region :authors beg end))
 
 (defun notmuch-search-find-subject ()
   "Return the subject for the current thread"
-  (get-text-property (point) 'notmuch-search-subject))
+  (plist-get (notmuch-search-get-result) :subject))
 
 (defun notmuch-search-find-subject-region (beg end)
   "Return a list of authors for the current region"
-  (notmuch-search-properties-in-region 'notmuch-search-subject beg end))
+  (notmuch-search-properties-in-region :subject beg end))
 
-(defun notmuch-search-show-thread ()
+(defun notmuch-search-show-thread (&optional elide-toggle)
   "Display the currently selected thread."
-  (interactive)
+  (interactive "P")
   (let ((thread-id (notmuch-search-find-thread-id))
        (subject (notmuch-search-find-subject)))
     (if (> (length thread-id) 0)
        (notmuch-show thread-id
+                     elide-toggle
                      (current-buffer)
                      notmuch-search-query-string
                      ;; Name the buffer based on the subject.
                      (concat "*" (truncate-string-to-width subject 30 nil nil t) "*"))
       (message "End of search results."))))
 
+(defun notmuch-tree-from-search-current-query ()
+  "Call notmuch tree with the current query"
+  (interactive)
+  (notmuch-tree notmuch-search-query-string))
+
+(defun notmuch-tree-from-search-thread ()
+  "Show the selected thread with notmuch-tree"
+  (interactive)
+  (notmuch-tree (notmuch-search-find-thread-id)
+                notmuch-search-query-string
+               nil
+                (notmuch-prettify-subject (notmuch-search-find-subject))
+               t))
+
 (defun notmuch-search-reply-to-thread (&optional prompt-for-sender)
   "Begin composing a reply-all to the entire current thread in a new buffer."
   (interactive "P")
@@ -466,100 +497,121 @@ Complete list of currently available key bindings:
   (let ((message-id (notmuch-search-find-thread-id)))
     (notmuch-mua-new-reply message-id prompt-for-sender nil)))
 
-(defun notmuch-call-notmuch-process (&rest args)
-  "Synchronously invoke \"notmuch\" with the given list of arguments.
-
-Output from the process will be presented to the user as an error
-and will also appear in a buffer named \"*Notmuch errors*\"."
-  (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
-    (with-current-buffer error-buffer
-       (erase-buffer))
-    (if (eq (apply 'call-process notmuch-command nil error-buffer nil args) 0)
-       (point)
-      (progn
-       (with-current-buffer error-buffer
-         (let ((beg (point-min))
-               (end (- (point-max) 1)))
-           (error (buffer-substring beg end))
-           ))))))
-
-(defun notmuch-search-set-tags (tags)
-  (save-excursion
-    (end-of-line)
-    (re-search-backward "(")
-    (forward-char)
-    (let ((beg (point))
-         (inhibit-read-only t))
-      (re-search-forward ")")
-      (backward-char)
-      (let ((end (point)))
-       (delete-region beg end)
-       (insert (propertize (mapconcat  'identity tags " ")
-                           'face 'notmuch-tag-face))))))
-
-(defun notmuch-search-get-tags ()
-  (save-excursion
-    (end-of-line)
-    (re-search-backward "(")
-    (let ((beg (+ (point) 1)))
-      (re-search-forward ")")
-      (let ((end (- (point) 1)))
-       (split-string (buffer-substring-no-properties beg end))))))
+(defun notmuch-search-set-tags (tags &optional pos)
+  (let ((new-result (plist-put (notmuch-search-get-result pos) :tags tags)))
+    (notmuch-search-update-result new-result pos)))
+
+(defun notmuch-search-get-tags (&optional pos)
+  (plist-get (notmuch-search-get-result pos) :tags))
 
 (defun notmuch-search-get-tags-region (beg end)
-  (save-excursion
-    (let ((output nil)
-         (last-line (line-number-at-pos end))
-         (max-line (- (line-number-at-pos (point-max)) 2)))
-      (goto-char beg)
-      (while (<= (line-number-at-pos) (min last-line max-line))
-       (setq output (append output (notmuch-search-get-tags)))
-       (forward-line 1))
-      output)))
-
-(defun notmuch-search-tag-region (beg end &optional tag-changes)
-  "Change tags for threads in the given region."
-  (let ((search-string (notmuch-search-find-thread-id-region-search beg end)))
-    (setq tag-changes (funcall 'notmuch-tag search-string tag-changes))
-    (save-excursion
-      (let ((last-line (line-number-at-pos end))
-           (max-line (- (line-number-at-pos (point-max)) 2)))
-       (goto-char beg)
-       (while (<= (line-number-at-pos) (min last-line max-line))
-         (notmuch-search-set-tags
-          (notmuch-update-tags (notmuch-search-get-tags) tag-changes))
-         (forward-line))))))
-
-(defun notmuch-search-tag (&optional tag-changes)
+  (let (output)
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (setq output (append output (notmuch-search-get-tags pos)))))
+    output))
+
+(defun notmuch-search-interactive-region ()
+  "Return the bounds of the current interactive region.
+
+This returns (BEG END), where BEG and END are the bounds of the
+region if the region is active, or both `point' otherwise."
+  (if (region-active-p)
+      (list (region-beginning) (region-end))
+    (list (point) (point))))
+
+(defun notmuch-search-interactive-tag-changes (&optional initial-input)
+  "Prompt for tag changes for the current thread or region.
+
+Returns (TAG-CHANGES REGION-BEGIN REGION-END)."
+  (let* ((region (notmuch-search-interactive-region))
+        (beg (first region)) (end (second region))
+        (prompt (if (= beg end) "Tag thread" "Tag region")))
+    (cons (notmuch-read-tag-changes
+          (notmuch-search-get-tags-region beg end) prompt initial-input)
+         region)))
+
+(defun notmuch-search-tag (tag-changes &optional beg end only-matched)
   "Change tags for the currently selected thread or region.
 
-See `notmuch-tag' for information on the format of TAG-CHANGES."
-  (interactive)
-  (let* ((beg (if (region-active-p) (region-beginning) (point)))
-        (end (if (region-active-p) (region-end) (point))))
-    (funcall 'notmuch-search-tag-region beg end tag-changes)))
-
-(defun notmuch-search-add-tag ()
-  "Same as `notmuch-search-tag' but sets initial input to '+'."
-  (interactive)
-  (notmuch-search-tag "+"))
-
-(defun notmuch-search-remove-tag ()
-  "Same as `notmuch-search-tag' but sets initial input to '-'."
-  (interactive)
-  (notmuch-search-tag "-"))
-
-(defun notmuch-search-archive-thread ()
-  "Archive the currently selected thread (remove its \"inbox\" tag).
+See `notmuch-tag' for information on the format of TAG-CHANGES.
+When called interactively, this uses the region if the region is
+active.  When called directly, BEG and END provide the region.
+If these are nil or not provided, this applies to the thread at
+point.
+
+If ONLY-MATCHED is non-nil, only tag matched messages."
+  (interactive (notmuch-search-interactive-tag-changes))
+  (unless (and beg end) (setq beg (point) end (point)))
+  (let ((search-string (notmuch-search-find-stable-query-region
+                       beg end only-matched)))
+    (notmuch-tag search-string tag-changes)
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (notmuch-search-set-tags
+        (notmuch-update-tags (notmuch-search-get-tags pos) tag-changes)
+        pos)))))
+
+(defun notmuch-search-add-tag (tag-changes &optional beg end)
+  "Change tags for the current thread or region (defaulting to add).
+
+Same as `notmuch-search-tag' but sets initial input to '+'."
+  (interactive (notmuch-search-interactive-tag-changes "+"))
+  (notmuch-search-tag tag-changes beg end))
+
+(defun notmuch-search-remove-tag (tag-changes &optional beg end)
+  "Change tags for the current thread or region (defaulting to remove).
+
+Same as `notmuch-search-tag' but sets initial input to '-'."
+  (interactive (notmuch-search-interactive-tag-changes "-"))
+  (notmuch-search-tag tag-changes beg end))
+
+(put 'notmuch-search-archive-thread 'notmuch-prefix-doc
+     "Un-archive the currently selected thread.")
+(defun notmuch-search-archive-thread (&optional unarchive beg end)
+  "Archive the currently selected thread or region.
+
+Archive each message in the currently selected thread by applying
+the tag changes in `notmuch-archive-tags' to each (remove the
+\"inbox\" tag by default). If a prefix argument is given, the
+messages will be \"unarchived\" (i.e. the tag changes in
+`notmuch-archive-tags' will be reversed).
 
 This function advances the next thread when finished."
-  (interactive)
-  (notmuch-search-tag '("-inbox"))
-  (notmuch-search-next-thread))
-
-(defvar notmuch-search-process-filter-data nil
-  "Data that has not yet been processed.")
-(make-variable-buffer-local 'notmuch-search-process-filter-data)
+  (interactive (cons current-prefix-arg (notmuch-search-interactive-region)))
+  (when notmuch-archive-tags
+    (notmuch-search-tag
+     (notmuch-tag-change-list notmuch-archive-tags unarchive) beg end))
+  (when (eq beg end)
+    (notmuch-search-next-thread)))
+
+(defun notmuch-search-update-result (result &optional pos)
+  "Replace the result object of the thread at POS (or point) by
+RESULT and redraw it.
+
+This will keep point in a reasonable location.  However, if there
+are enclosing save-excursions and the saved point is in the
+result being updated, the point will be restored to the beginning
+of the result."
+  (let ((start (notmuch-search-result-beginning pos))
+       (end (notmuch-search-result-end pos))
+       (init-point (point))
+       (inhibit-read-only t))
+    ;; Delete the current thread
+    (delete-region start end)
+    ;; Insert the updated thread
+    (notmuch-search-show-result result start)
+    ;; If point was inside the old result, make an educated guess
+    ;; about where to place it now.  Unfortunately, this won't work
+    ;; with save-excursion (or any other markers that would be nice to
+    ;; preserve, such as the window start), but there's nothing we can
+    ;; do about that without a way to retrieve markers in a region.
+    (when (and (>= init-point start) (<= init-point end))
+      (let* ((new-end (notmuch-search-result-end start))
+            (new-point (if (= init-point end)
+                           new-end
+                         (min init-point (- new-end 1)))))
+       (goto-char new-point)))))
 
 (defun notmuch-search-process-sentinel (proc msg)
   "Add a message to let user know when \"notmuch search\" exits"
@@ -567,7 +619,9 @@ This function advances the next thread when finished."
        (status (process-status proc))
        (exit-status (process-exit-status proc))
        (never-found-target-thread nil))
-    (if (memq status '(exit signal))
+    (when (memq status '(exit signal))
+      (catch 'return
+       (kill-buffer (process-get proc 'parse-buf))
        (if (buffer-live-p buffer)
            (with-current-buffer buffer
              (save-excursion
@@ -577,19 +631,19 @@ This function advances the next thread when finished."
                  (if (eq status 'signal)
                      (insert "Incomplete search results (search process was killed).\n"))
                  (when (eq status 'exit)
-                   (if notmuch-search-process-filter-data
-                       (insert (concat "Error: Unexpected output from notmuch search:\n" notmuch-search-process-filter-data)))
-                   (insert "End of search results.")
-                   (unless (= exit-status 0)
-                     (insert (format " (process returned %d)" exit-status)))
-                   (insert "\n")
+                   (insert "End of search results.\n")
+                   ;; For version mismatch, there's no point in
+                   ;; showing the search buffer
+                   (when (or (= exit-status 20) (= exit-status 21))
+                     (kill-buffer)
+                     (throw 'return nil))
                    (if (and atbob
                             (not (string= notmuch-search-target-thread "found")))
                        (set 'never-found-target-thread t)))))
              (when (and never-found-target-thread
                       notmuch-search-target-line)
                  (goto-char (point-min))
-                 (forward-line (1- notmuch-search-target-line))))))))
+                 (forward-line (1- notmuch-search-target-line)))))))))
 
 (defcustom notmuch-search-line-faces '(("unread" :weight bold)
                                       ("flagged" :foreground "blue"))
@@ -598,12 +652,12 @@ This function advances the next thread when finished."
 Here is an example of how to color search results based on tags.
  (the following text would be placed in your ~/.emacs file):
 
- (setq notmuch-search-line-faces '((\"deleted\" . (:foreground \"red\"
-                                                 :background \"blue\"))
-                                   (\"unread\" . (:foreground \"green\"))))
+ (setq notmuch-search-line-faces '((\"unread\" . (:foreground \"green\"))
+                                   (\"deleted\" . (:foreground \"red\"
+                                                 :background \"blue\"))))
 
-The attributes defined for matching tags are merged, with later
-attributes overriding earlier. A message having both \"deleted\"
+The attributes defined for matching tags are merged, with earlier
+attributes overriding later. A message having both \"deleted\"
 and \"unread\" tags with the above settings would have a green
 foreground and blue background."
   :type '(alist :key-type (string) :value-type (custom-face-edit))
@@ -612,20 +666,13 @@ foreground and blue background."
 
 (defun notmuch-search-color-line (start end line-tag-list)
   "Colorize lines in `notmuch-show' based on tags."
-  ;; Create the overlay only if the message has tags which match one
-  ;; of those specified in `notmuch-search-line-faces'.
-  (let (overlay)
-    (mapc (lambda (elem)
-           (let ((tag (car elem))
-                 (attributes (cdr elem)))
-             (when (member tag line-tag-list)
-               (when (not overlay)
-                 (setq overlay (make-overlay start end)))
-               ;; Merge the specified properties with any already
-               ;; applied from an earlier match.
-               (overlay-put overlay 'face
-                            (append (overlay-get overlay 'face) attributes)))))
-         notmuch-search-line-faces)))
+  (mapc (lambda (elem)
+         (let ((tag (car elem))
+               (attributes (cdr elem)))
+           (when (member tag line-tag-list)
+             (notmuch-apply-face nil attributes nil start end))))
+       ;; Reverse the list so earlier entries take precedence
+       (reverse notmuch-search-line-faces)))
 
 (defun notmuch-search-author-propertize (authors)
   "Split `authors' into matching and non-matching authors and
@@ -707,86 +754,77 @@ non-authors is found, assume that all of the authors match."
          (overlay-put overlay 'isearch-open-invisible #'delete-overlay)))
       (insert padding))))
 
-(defun notmuch-search-insert-field (field format-string date count authors subject tags)
+(defun notmuch-search-insert-field (field format-string result)
   (cond
    ((string-equal field "date")
-    (insert (propertize (format format-string date)
+    (insert (propertize (format format-string (plist-get result :date_relative))
                        'face 'notmuch-search-date)))
    ((string-equal field "count")
-    (insert (propertize (format format-string count)
+    (insert (propertize (format format-string
+                               (format "[%s/%s]" (plist-get result :matched)
+                                       (plist-get result :total)))
                        'face 'notmuch-search-count)))
    ((string-equal field "subject")
-    (insert (propertize (format format-string subject)
+    (insert (propertize (format format-string
+                               (notmuch-sanitize (plist-get result :subject)))
                        'face 'notmuch-search-subject)))
 
    ((string-equal field "authors")
-    (notmuch-search-insert-authors format-string authors))
+    (notmuch-search-insert-authors
+     format-string (notmuch-sanitize (plist-get result :authors))))
 
    ((string-equal field "tags")
-    ;; Ignore format-string here because notmuch-search-set-tags
-    ;; depends on the format of this
-    (insert (concat "(" (propertize tags 'font-lock-face 'notmuch-tag-face) ")")))))
-
-(defun notmuch-search-show-result (date count authors subject tags)
-  (dolist (spec notmuch-search-result-format)
-    (notmuch-search-insert-field (car spec) (cdr spec)
-                                date count authors subject tags))
-  (insert "\n"))
+    (let ((tags (plist-get result :tags))
+         (orig-tags (plist-get result :orig-tags)))
+      (insert (format format-string (notmuch-tag-format-tags tags orig-tags)))))))
+
+(defun notmuch-search-show-result (result pos)
+  "Insert RESULT at POS."
+  ;; Ignore excluded matches
+  (unless (= (plist-get result :matched) 0)
+    (save-excursion
+      (goto-char pos)
+      (dolist (spec notmuch-search-result-format)
+       (notmuch-search-insert-field (car spec) (cdr spec) result))
+      (insert "\n")
+      (notmuch-search-color-line pos (point) (plist-get result :tags))
+      (put-text-property pos (point) 'notmuch-search-result result))))
+
+(defun notmuch-search-append-result (result)
+  "Insert RESULT at the end of the buffer.
+
+This is only called when a result is first inserted so it also
+sets the :orig-tag property."
+  (let ((new-result (plist-put result :orig-tags (plist-get result :tags)))
+       (pos (point-max)))
+    (notmuch-search-show-result new-result pos)
+    (when (string= (plist-get result :thread) notmuch-search-target-thread)
+      (setq notmuch-search-target-thread "found")
+      (goto-char pos))))
 
 (defun notmuch-search-process-filter (proc string)
   "Process and filter the output of \"notmuch search\""
-  (let ((buffer (process-buffer proc))
-       (found-target nil))
-    (if (buffer-live-p buffer)
-       (with-current-buffer buffer
-         (save-excursion
-           (let ((line 0)
-                 (more t)
-                 (inhibit-read-only t)
-                 (string (concat notmuch-search-process-filter-data string)))
-             (setq notmuch-search-process-filter-data nil)
-             (while more
-               (while (and (< line (length string)) (= (elt string line) ?\n))
-                 (setq line (1+ line)))
-               (if (string-match "^\\(thread:[0-9A-Fa-f]*\\) \\([^][]*\\) \\(\\[[0-9/]*\\]\\) \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" string line)
-                   (let* ((thread-id (match-string 1 string))
-                          (date (match-string 2 string))
-                          (count (match-string 3 string))
-                          (authors (match-string 4 string))
-                          (subject (match-string 5 string))
-                          (tags (match-string 6 string))
-                          (tag-list (if tags (save-match-data (split-string tags)))))
-                     (goto-char (point-max))
-                     (if (/= (match-beginning 1) line)
-                         (insert (concat "Error: Unexpected output from notmuch search:\n" (substring string line (match-beginning 1)) "\n")))
-                     ;; We currently just throw away excluded matches.
-                     (unless (eq (aref count 1) ?0)
-                       (let ((beg (point)))
-                         (notmuch-search-show-result date count authors subject tags)
-                         (notmuch-search-color-line beg (point) tag-list)
-                         (put-text-property beg (point) 'notmuch-search-thread-id thread-id)
-                         (put-text-property beg (point) 'notmuch-search-authors authors)
-                         (put-text-property beg (point) 'notmuch-search-subject subject)
-                         (when (string= thread-id notmuch-search-target-thread)
-                           (set 'found-target beg)
-                           (set 'notmuch-search-target-thread "found"))))
-                     (set 'line (match-end 0)))
-                 (set 'more nil)
-                 (while (and (< line (length string)) (= (elt string line) ?\n))
-                   (setq line (1+ line)))
-                 (if (< line (length string))
-                     (setq notmuch-search-process-filter-data (substring string line)))
-                 ))))
-         (if found-target
-             (goto-char found-target)))
-      (delete-process proc))))
-
-(defun notmuch-search-tag-all (&optional tag-changes)
+  (let ((results-buf (process-buffer proc))
+       (parse-buf (process-get proc 'parse-buf))
+       (inhibit-read-only t)
+       done)
+    (when (buffer-live-p results-buf)
+      (with-current-buffer parse-buf
+       ;; Insert new data
+       (save-excursion
+         (goto-char (point-max))
+         (insert string))
+       (notmuch-sexp-parse-partial-list 'notmuch-search-append-result
+                                        results-buf)))))
+
+(defun notmuch-search-tag-all (tag-changes)
   "Add/remove tags from all messages in current search buffer.
 
 See `notmuch-tag' for information on the format of TAG-CHANGES."
-  (interactive)
-  (apply 'notmuch-tag notmuch-search-query-string tag-changes))
+  (interactive
+   (list (notmuch-read-tag-changes
+         (notmuch-search-get-tags-region (point-min) (point-max)) "Tag all")))
+  (notmuch-search-tag tag-changes (point-min) (point-max) t))
 
 (defun notmuch-search-buffer-title (query)
   "Returns the title for a buffer with notmuch search results."
@@ -794,14 +832,14 @@ See `notmuch-tag' for information on the format of TAG-CHANGES."
          (let (longest
                (longest-length 0))
            (loop for tuple in notmuch-saved-searches
-                 if (let ((quoted-query (regexp-quote (cdr tuple))))
+                 if (let ((quoted-query (regexp-quote (notmuch-saved-search-get tuple :query))))
                       (and (string-match (concat "^" quoted-query) query)
                            (> (length (match-string 0 query))
                               longest-length)))
                  do (setq longest tuple))
            longest))
-        (saved-search-name (car saved-search))
-        (saved-search-query (cdr saved-search)))
+        (saved-search-name (notmuch-saved-search-get saved-search :name))
+        (saved-search-query (notmuch-saved-search-get saved-search :query)))
     (cond ((and saved-search (equal saved-search-query query))
           ;; Query is the same as saved search (ignoring case)
           (concat "*notmuch-saved-search-" saved-search-name "*"))
@@ -819,14 +857,20 @@ See `notmuch-tag' for information on the format of TAG-CHANGES."
   "Read a notmuch-query from the minibuffer with completion.
 
 PROMPT is the string to prompt with."
-  (lexical-let
-      ((completions
-       (append (list "folder:" "thread:" "id:" "date:" "from:" "to:"
-                     "subject:" "attachment:")
-               (mapcar (lambda (tag)
-                         (concat "tag:" tag))
-                       (process-lines notmuch-command "search" "--output=tags" "*")))))
+  (lexical-let*
+      ((all-tags
+        (mapcar (lambda (tag) (notmuch-escape-boolean-term tag))
+                (process-lines notmuch-command "search" "--output=tags" "*")))
+       (completions
+        (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:"
+                      "subject:" "attachment:" "mimetype:")
+                (mapcar (lambda (tag) (concat "tag:" tag)) all-tags)
+                (mapcar (lambda (tag) (concat "is:" tag)) all-tags))))
     (let ((keymap (copy-keymap minibuffer-local-map))
+         (current-query (case major-mode
+                          (notmuch-search-mode (notmuch-search-get-query))
+                          (notmuch-show-mode (notmuch-show-get-query))
+                          (notmuch-tree-mode (notmuch-tree-get-query))))
          (minibuffer-completion-table
           (completion-table-dynamic
            (lambda (string)
@@ -844,24 +888,38 @@ PROMPT is the string to prompt with."
       (define-key keymap (kbd "TAB") 'minibuffer-complete)
       (let ((history-delete-duplicates t))
        (read-from-minibuffer prompt nil keymap nil
-                             'notmuch-search-history nil nil)))))
+                             'notmuch-search-history current-query nil)))))
+
+(defun notmuch-search-get-query ()
+  "Return the current query in this search buffer"
+  notmuch-search-query-string)
 
 ;;;###autoload
-(defun notmuch-search (&optional query oldest-first target-thread target-line continuation)
-  "Run \"notmuch search\" with the given `query' and display results.
+(put 'notmuch-search 'notmuch-doc "Search for messages.")
+(defun notmuch-search (&optional query oldest-first target-thread target-line)
+  "Display threads matching QUERY in a notmuch-search buffer.
 
-If `query' is nil, it is read interactively from the minibuffer.
+If QUERY is nil, it is read interactively from the minibuffer.
 Other optional parameters are used as follows:
 
-  oldest-first: A Boolean controlling the sort order of returned threads
-  target-thread: A thread ID (with the thread: prefix) that will be made
+  OLDEST-FIRST: A Boolean controlling the sort order of returned threads
+  TARGET-THREAD: A thread ID (without the thread: prefix) that will be made
                  current if it appears in the search results.
-  target-line: The line number to move to if the target thread does not
-               appear in the search results."
-  (interactive)
-  (if (null query)
-      (setq query (notmuch-read-query "Notmuch search: ")))
-  (let ((buffer (get-buffer-create (notmuch-search-buffer-title query))))
+  TARGET-LINE: The line number to move to if the target thread does not
+               appear in the search results.
+
+When called interactively, this will prompt for a query and use
+the configured default sort order."
+  (interactive
+   (list
+    ;; Prompt for a query
+    nil
+    ;; Use the default search order (if we're doing a search from a
+    ;; search buffer, ignore any buffer-local overrides)
+    (default-value 'notmuch-search-oldest-first)))
+
+  (let* ((query (or query (notmuch-read-query "Notmuch search: ")))
+        (buffer (get-buffer-create (notmuch-search-buffer-title query))))
     (switch-to-buffer buffer)
     (notmuch-search-mode)
     ;; Don't track undo information for this buffer
@@ -870,7 +928,7 @@ Other optional parameters are used as follows:
     (set 'notmuch-search-oldest-first oldest-first)
     (set 'notmuch-search-target-thread target-thread)
     (set 'notmuch-search-target-line target-line)
-    (set 'notmuch-search-continuation continuation)
+    (notmuch-tag-clear-cache)
     (let ((proc (get-buffer-process (current-buffer)))
          (inhibit-read-only t))
       (if proc
@@ -879,14 +937,18 @@ Other optional parameters are used as follows:
       (erase-buffer)
       (goto-char (point-min))
       (save-excursion
-       (let ((proc (start-process
-                    "notmuch-search" buffer
-                    notmuch-command "search"
+       (let ((proc (notmuch-start-notmuch
+                    "notmuch-search" buffer #'notmuch-search-process-sentinel
+                    "search" "--format=sexp" "--format-version=2"
                     (if oldest-first
                         "--sort=oldest-first"
                       "--sort=newest-first")
-                    query)))
-         (set-process-sentinel proc 'notmuch-search-process-sentinel)
+                    query))
+             ;; Use a scratch buffer to accumulate partial output.
+             ;; This buffer will be killed by the sentinel, which
+             ;; should be called no matter how the process dies.
+             (parse-buf (generate-new-buffer " *notmuch search parse*")))
+         (process-put proc 'parse-buf parse-buf)
          (set-process-filter proc 'notmuch-search-process-filter)
          (set-process-query-on-exit-flag proc nil))))
     (run-hooks 'notmuch-search-hook)))
@@ -899,90 +961,45 @@ query string as the current search. If the current thread is in
 the new search results, then point will be placed on the same
 thread. Otherwise, point will be moved to attempt to be in the
 same relative position within the new buffer."
-  (interactive)
   (let ((target-line (line-number-at-pos))
        (oldest-first notmuch-search-oldest-first)
-       (target-thread (notmuch-search-find-thread-id))
-       (query notmuch-search-query-string)
-       (continuation notmuch-search-continuation))
-    (notmuch-kill-this-buffer)
-    (notmuch-search query oldest-first target-thread target-line continuation)
+       (target-thread (notmuch-search-find-thread-id 'bare))
+       (query notmuch-search-query-string))
+    (notmuch-bury-or-kill-this-buffer)
+    (notmuch-search query oldest-first target-thread target-line)
     (goto-char (point-min))))
 
-(defcustom notmuch-poll-script nil
-  "An external script to incorporate new mail into the notmuch database.
-
-This variable controls the action invoked by
-`notmuch-search-poll-and-refresh-view' and
-`notmuch-hello-poll-and-update' (each have a default keybinding
-of 'G') to incorporate new mail into the notmuch database.
-
-If set to nil (the default), new mail is processed by invoking
-\"notmuch new\". Otherwise, this should be set to a string that
-gives the name of an external script that processes new mail. If
-set to the empty string, no command will be run.
-
-The external script could do any of the following depending on
-the user's needs:
-
-1. Invoke a program to transfer mail to the local mail store
-2. Invoke \"notmuch new\" to incorporate the new mail
-3. Invoke one or more \"notmuch tag\" commands to classify the mail
-
-Note that the recommended way of achieving the same is using
-\"notmuch new\" hooks."
-  :type '(choice (const :tag "notmuch new" nil)
-                (const :tag "Disabled" "")
-                (string :tag "Custom script"))
-  :group 'notmuch-external)
-
-(defun notmuch-poll ()
-  "Run \"notmuch new\" or an external script to import mail.
-
-Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
-depending on the value of `notmuch-poll-script'."
-  (interactive)
-  (if (stringp notmuch-poll-script)
-      (unless (string= notmuch-poll-script "")
-       (call-process notmuch-poll-script nil nil))
-    (call-process notmuch-command nil nil nil "new")))
-
-(defun notmuch-search-poll-and-refresh-view ()
-  "Invoke `notmuch-poll' to import mail, then refresh the current view."
-  (interactive)
-  (notmuch-poll)
-  (notmuch-search-refresh-view))
-
 (defun notmuch-search-toggle-order ()
   "Toggle the current search order.
 
-By default, the \"inbox\" view created by `notmuch' is displayed
-in chronological order (oldest thread at the beginning of the
-buffer), while any global searches created by `notmuch-search'
-are displayed in reverse-chronological order (newest thread at
-the beginning of the buffer).
-
-This command toggles the sort order for the current search.
-
-Note that any filtered searches created by
-`notmuch-search-filter' retain the search order of the parent
-search."
+This command toggles the sort order for the current search. The
+default sort order is defined by `notmuch-search-oldest-first'."
   (interactive)
   (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
   (notmuch-search-refresh-view))
 
+(defun notmuch-group-disjunctive-query-string (query-string)
+  "Group query if it contains a complex expression.
+
+Enclose QUERY-STRING in parentheses if it matches
+`notmuch-search-disjunctive-regexp'."
+  (if (string-match-p notmuch-search-disjunctive-regexp query-string)
+      (concat "( " query-string " )")
+    query-string))
+
 (defun notmuch-search-filter (query)
   "Filter the current search results based on an additional query string.
 
 Runs a new search matching only messages that match both the
 current search results AND the additional query string provided."
   (interactive (list (notmuch-read-query "Filter search: ")))
-  (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query)
-                          (concat "( " query " )")
-                        query)))
-    (notmuch-search (if (string= notmuch-search-query-string "*")
+  (let ((grouped-query (notmuch-group-disjunctive-query-string query))
+       (grouped-original-query (notmuch-group-disjunctive-query-string
+                                notmuch-search-query-string)))
+    (notmuch-search (if (string= grouped-original-query "*")
                        grouped-query
-                     (concat notmuch-search-query-string " and " grouped-query)) notmuch-search-oldest-first)))
+                     (concat grouped-original-query " and " grouped-query))
+                   notmuch-search-oldest-first)))
 
 (defun notmuch-search-filter-by-tag (tag)
   "Filter the current search results based on a single tag.
@@ -1037,3 +1054,9 @@ notmuch buffers exist, run `notmuch'."
 (setq mail-user-agent 'notmuch-user-agent)
 
 (provide 'notmuch)
+
+;; After provide to avoid loops if notmuch was require'd via notmuch-init-file.
+(if init-file-user ; don't load init file if the -q option was used.
+    (let ((init-file (locate-file notmuch-init-file '("/")
+                                 (get-load-suffixes))))
+      (if init-file (load init-file nil t t))))