]> git.notmuchmail.org Git - notmuch/blobdiff - emacs/notmuch.el
emacs: Move ?, q, s, m, =, and G to the common keymap
[notmuch] / emacs / notmuch.el
index fd1836f1a83555edaa14fb5e214df96c8c00d6e2..4de6229d0c6471660b094e815da831d2d7e33566 100644 (file)
@@ -58,6 +58,7 @@
 (require 'notmuch-hello)
 (require 'notmuch-maildir-fcc)
 (require 'notmuch-message)
+(require 'notmuch-parser)
 
 (defcustom notmuch-search-result-format
   `(("date" . "%12s ")
@@ -139,48 +140,34 @@ This is basically just `format-kbd-macro' but we also convert ESC to M-."
        "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-describe-keymap (keymap &optional prefix tail)
+  "Return a list of strings, each describing one key in KEYMAP.
+
+Each string gives a human-readable description of the key and the
+first line of documentation for the bound function."
+  (map-keymap
+   (lambda (key binding)
+     (cond ((mouse-event-p key) nil)
+          ((keymapp binding)
+           (setq tail
+                 (notmuch-describe-keymap
+                  binding (notmuch-prefix-key-description key) tail)))
+          (t
+           (push (concat prefix (format-kbd-macro (vector key)) "\t"
+                         (notmuch-documentation-first-line binding))
+                 tail))))
+   keymap)
+  tail)
 
 (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)))
+            (keymap (symbol-value (intern keymap-name)))
+            (desc-list (notmuch-describe-keymap keymap))
+            (desc (mapconcat #'identity desc-list "\n")))
+       (setq doc (replace-match desc 1 1 doc)))
       (setq beg (match-end 0)))
     doc))
 
@@ -211,9 +198,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-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)
@@ -223,12 +209,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)
@@ -256,18 +238,9 @@ For a mouse binding, return nil."
 (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)
@@ -293,7 +266,7 @@ For a mouse binding, return nil."
 (defun notmuch-search-next-thread ()
   "Select the next thread in the search results."
   (interactive)
-  (when (notmuch-search-get-result (notmuch-search-result-end))
+  (when (notmuch-search-get-result)
     (goto-char (notmuch-search-result-end))))
 
 (defun notmuch-search-previous-thread ()
@@ -385,16 +358,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:
 
@@ -405,7 +384,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)
@@ -475,10 +454,12 @@ BEG."
        (push (plist-get (notmuch-search-get-result pos) property) output)))
     output))
 
-(defun notmuch-search-find-thread-id ()
-  "Return the thread for the current thread"
+(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 "thread:" thread))))
+    (when thread (concat (unless bare "thread:") thread))))
 
 (defun notmuch-search-find-thread-id-region (beg end)
   "Return a list of threads for the current region"
@@ -533,19 +514,13 @@ BEG."
 (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))
-           ))))))
+If notmuch exits with a non-zero status, output from the process
+will appear in a buffer named \"*Notmuch errors*\" and an error
+will be signaled."
+  (with-temp-buffer
+    (let ((status (apply #'call-process notmuch-command nil t nil args)))
+      (notmuch-check-exit-status status (cons notmuch-command args)
+                                (buffer-string)))))
 
 (defun notmuch-search-set-tags (tags &optional pos)
   (let ((new-result (plist-put (notmuch-search-get-result pos) :tags tags)))
@@ -564,7 +539,7 @@ and will also appear in a buffer named \"*Notmuch errors*\"."
 (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))
+    (setq tag-changes (notmuch-tag search-string tag-changes))
     (notmuch-search-foreach-result beg end
       (lambda (pos)
        (notmuch-search-set-tags
@@ -578,7 +553,7 @@ 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)))
+    (notmuch-search-tag-region beg end tag-changes)))
 
 (defun notmuch-search-add-tag ()
   "Same as `notmuch-search-tag' but sets initial input to '+'."
@@ -590,12 +565,20 @@ See `notmuch-tag' for information on the format of TAG-CHANGES."
   (interactive)
   (notmuch-search-tag "-"))
 
-(defun notmuch-search-archive-thread ()
-  "Archive the currently selected thread (remove its \"inbox\" tag).
+(defun notmuch-search-archive-thread (&optional unarchive)
+  "Archive the currently selected thread.
+
+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"))
+  (interactive "P")
+  (when notmuch-archive-tags
+    (notmuch-search-tag
+     (notmuch-tag-change-list notmuch-archive-tags unarchive)))
   (notmuch-search-next-thread))
 
 (defun notmuch-search-update-result (result &optional pos)
@@ -633,6 +616,7 @@ of the result."
        (exit-status (process-exit-status proc))
        (never-found-target-thread nil))
     (when (memq status '(exit signal))
+      (catch 'return
        (kill-buffer (process-get proc 'parse-buf))
        (if (buffer-live-p buffer)
            (with-current-buffer buffer
@@ -643,17 +627,19 @@ of the result."
                  (if (eq status 'signal)
                      (insert "Incomplete search results (search process was killed).\n"))
                  (when (eq status 'exit)
-                   (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"))
@@ -782,9 +768,8 @@ non-authors is found, assume that all of the authors match."
     (notmuch-search-insert-authors format-string (plist-get result :authors)))
 
    ((string-equal field "tags")
-    (let ((tags-str (mapconcat 'identity (plist-get result :tags) " ")))
-      (insert (propertize (format format-string tags-str)
-                         'face 'notmuch-tag-face))))))
+    (let ((tags (plist-get result :tags)))
+      (insert (format format-string (notmuch-tag-format-tags tags)))))))
 
 (defun notmuch-search-show-result (result &optional pos)
   "Insert RESULT at POS or the end of the buffer if POS is null."
@@ -802,66 +787,20 @@ non-authors is found, assume that all of the authors match."
        (setq notmuch-search-target-thread "found")
        (goto-char beg)))))
 
-(defun notmuch-search-show-error (string &rest objects)
-  (save-excursion
-    (goto-char (point-max))
-    (insert "Error: Unexpected output from notmuch search:\n")
-    (insert (apply #'format string objects))
-    (insert "\n")))
-
-(defvar notmuch-search-process-state nil
-  "Parsing state of the search process filter.")
-
-(defvar notmuch-search-json-parser nil
-  "Incremental JSON parser for the search process filter.")
-
 (defun notmuch-search-process-filter (proc string)
   "Process and filter the output of \"notmuch search\""
   (let ((results-buf (process-buffer proc))
        (parse-buf (process-get proc 'parse-buf))
        (inhibit-read-only t)
        done)
-    (if (not (buffer-live-p results-buf))
-       (delete-process proc)
+    (when (buffer-live-p results-buf)
       (with-current-buffer parse-buf
        ;; Insert new data
        (save-excursion
          (goto-char (point-max))
-         (insert string)))
-      (with-current-buffer results-buf
-       (while (not done)
-         (condition-case nil
-             (case notmuch-search-process-state
-               ((begin)
-                ;; Enter the results list
-                (if (eq (notmuch-json-begin-compound
-                         notmuch-search-json-parser) 'retry)
-                    (setq done t)
-                  (setq notmuch-search-process-state 'result)))
-               ((result)
-                ;; Parse a result
-                (let ((result (notmuch-json-read notmuch-search-json-parser)))
-                  (case result
-                    ((retry) (setq done t))
-                    ((end) (setq notmuch-search-process-state 'end))
-                    (otherwise (notmuch-search-show-result result)))))
-               ((end)
-                ;; Any trailing data is unexpected
-                (notmuch-json-eof notmuch-search-json-parser)
-                (setq done t)))
-           (json-error
-            ;; Do our best to resynchronize and ensure forward
-            ;; progress
-            (notmuch-search-show-error
-             "%s"
-             (with-current-buffer parse-buf
-               (let ((bad (buffer-substring (line-beginning-position)
-                                            (line-end-position))))
-                 (forward-line)
-                 bad))))))
-       ;; Clear out what we've parsed
-       (with-current-buffer parse-buf
-         (delete-region (point-min) (point)))))))
+         (insert string))
+       (notmuch-sexp-parse-partial-list 'notmuch-search-show-result
+                                        results-buf)))))
 
 (defun notmuch-search-tag-all (&optional tag-changes)
   "Add/remove tags from all messages in current search buffer.
@@ -906,7 +845,7 @@ PROMPT is the string to prompt with."
        (append (list "folder:" "thread:" "id:" "date:" "from:" "to:"
                      "subject:" "attachment:")
                (mapcar (lambda (tag)
-                         (concat "tag:" tag))
+                         (concat "tag:" (notmuch-escape-boolean-term tag)))
                        (process-lines notmuch-command "search" "--output=tags" "*")))))
     (let ((keymap (copy-keymap minibuffer-local-map))
          (minibuffer-completion-table
@@ -929,21 +868,30 @@ PROMPT is the string to prompt with."
                              'notmuch-search-history nil nil)))))
 
 ;;;###autoload
-(defun notmuch-search (&optional query oldest-first target-thread target-line continuation)
+(defun notmuch-search (&optional query oldest-first target-thread target-line)
   "Run \"notmuch search\" with the given `query' and display results.
 
 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
+  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))))
+               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
@@ -952,7 +900,6 @@ 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)
     (let ((proc (get-buffer-process (current-buffer)))
          (inhibit-read-only t))
       (if proc
@@ -961,10 +908,9 @@ 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"
-                    "--format=json"
+       (let ((proc (notmuch-start-notmuch
+                    "notmuch-search" buffer #'notmuch-search-process-sentinel
+                    "search" "--format=sexp" "--format-version=1"
                     (if oldest-first
                         "--sort=oldest-first"
                       "--sort=newest-first")
@@ -973,11 +919,7 @@ Other optional parameters are used as follows:
              ;; 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*")))
-         (set (make-local-variable 'notmuch-search-process-state) 'begin)
-         (set (make-local-variable 'notmuch-search-json-parser)
-              (notmuch-json-create-parser parse-buf))
          (process-put proc 'parse-buf parse-buf)
-         (set-process-sentinel proc 'notmuch-search-process-sentinel)
          (set-process-filter proc 'notmuch-search-process-filter)
          (set-process-query-on-exit-flag proc nil))))
     (run-hooks 'notmuch-search-hook)))
@@ -990,74 +932,19 @@ 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))
+       (target-thread (notmuch-search-find-thread-id 'bare))
+       (query notmuch-search-query-string))
     (notmuch-kill-this-buffer)
-    (notmuch-search query oldest-first target-thread target-line continuation)
+    (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))