]> git.notmuchmail.org Git - notmuch/blobdiff - emacs/notmuch-lib.el
emacs: Introduce `notmuch-call-notmuch-sexp'
[notmuch] / emacs / notmuch-lib.el
index 6907a5f910b4e33b8efc9f6d65bee3708a1a2c82..c82c6c2a63246ede7878fce974d4aa7206c11399 100644 (file)
 
 ;; This is an part of an emacs-based interface to the notmuch mail system.
 
-(eval-when-compile (require 'cl))
+(require 'mm-view)
+(require 'mm-decode)
+(require 'json)
+(require 'cl)
 
 (defvar notmuch-command "notmuch"
   "Command to run the notmuch binary.")
   :group 'notmuch)
 
 (defcustom notmuch-search-oldest-first t
-  "Show the oldest mail first when searching."
+  "Show the oldest mail first when searching.
+
+This variable defines the default sort order for displaying
+search results. Note that any filtered searches created by
+`notmuch-search-filter' retain the search order of the parent
+search."
   :type 'boolean
   :group 'notmuch-search)
 
 (defvar notmuch-search-history nil
   "Variable to store notmuch searches history.")
 
-(defcustom notmuch-saved-searches nil
+(defcustom notmuch-saved-searches '(("inbox" . "tag:inbox")
+                                   ("unread" . "tag:unread"))
   "A list of saved searches to display."
   :type '(alist :key-type string :value-type string)
   :group 'notmuch-hello)
 
-(defvar notmuch-folders nil
-  "Deprecated name for what is now known as `notmuch-saved-searches'.")
-
-(defun notmuch-saved-searches ()
-  "Common function for querying the notmuch-saved-searches variable.
-
-We do this as a function to support the old name of the
-variable (`notmuch-folders') as well as for the default value if
-the user hasn't set this variable with the old or new value."
-  (if notmuch-saved-searches
-      notmuch-saved-searches
-    (if notmuch-folders
-       notmuch-folders
-      '(("inbox" . "tag:inbox")
-       ("unread" . "tag:unread")))))
+(defcustom notmuch-archive-tags '("-inbox")
+  "List of tag changes to apply to a message or a thread when it is archived.
+
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message or thread being archived.
+
+For example, if you wanted to remove an \"inbox\" tag and add an
+\"archived\" tag, you would set:
+    (\"-inbox\" \"+archived\")"
+  :type '(repeat string)
+  :group 'notmuch-search
+  :group 'notmuch-show)
+
+;; By default clicking on a button does not select the window
+;; containing the button (as opposed to clicking on a widget which
+;; does). This means that the button action is then executed in the
+;; current selected window which can cause problems if the button
+;; changes the buffer (e.g., id: links) or moves point.
+;;
+;; This provides a button type which overrides mouse-action so that
+;; the button's window is selected before the action is run. Other
+;; notmuch buttons can get the same behaviour by inheriting from this
+;; button type.
+(define-button-type 'notmuch-button-type
+  'mouse-action (lambda (button)
+                 (select-window (posn-window (event-start last-input-event)))
+                 (button-activate button)))
+
+(defun notmuch-command-to-string (&rest args)
+  "Synchronously invoke \"notmuch\" with the given list of arguments.
+
+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.
+
+Otherwise the output will be returned"
+  (with-temp-buffer
+    (let* ((status (apply #'call-process notmuch-command nil t nil args))
+          (output (buffer-string)))
+      (notmuch-check-exit-status status (cons notmuch-command args) output)
+      output)))
 
 (defun notmuch-version ()
   "Return a string with the notmuch version number."
   (let ((long-string
         ;; Trim off the trailing newline.
-        (substring (shell-command-to-string
-                    (concat notmuch-command " --version"))
-                   0 -1)))
+        (substring (notmuch-command-to-string "--version") 0 -1)))
     (if (string-match "^notmuch\\( version\\)? \\(.*\\)$"
                      long-string)
        (match-string 2 long-string)
@@ -110,9 +144,7 @@ the user hasn't set this variable with the old or new value."
 (defun notmuch-config-get (item)
   "Return a value from the notmuch configuration."
   ;; Trim off the trailing newline
-  (substring (shell-command-to-string
-             (concat notmuch-command " config get " item))
-             0 -1))
+  (substring (notmuch-command-to-string "config" "get" item) 0 -1))
 
 (defun notmuch-database-path ()
   "Return the database.path value from the notmuch configuration."
@@ -144,16 +176,36 @@ the user hasn't set this variable with the old or new value."
        "[No Subject]"
       subject)))
 
+(defun notmuch-escape-boolean-term (term)
+  "Escape a boolean term for use in a query.
+
+The caller is responsible for prepending the term prefix and a
+colon.  This performs minimal escaping in order to produce
+user-friendly queries."
+
+  (save-match-data
+    (if (or (equal term "")
+           (string-match "[ ()]\\|^\"" term))
+       ;; Requires escaping
+       (concat "\"" (replace-regexp-in-string "\"" "\"\"" term t t) "\"")
+      term)))
+
 (defun notmuch-id-to-query (id)
   "Return a query that matches the message with id ID."
-  (concat "id:\"" (replace-regexp-in-string "\"" "\"\"" id t t) "\""))
+  (concat "id:" (notmuch-escape-boolean-term id)))
 
 ;;
 
 (defun notmuch-common-do-stash (text)
   "Common function to stash text in kill ring, and display in minibuffer."
-  (kill-new text)
-  (message "Stashed: %s" text))
+  (if text
+      (progn
+       (kill-new text)
+       (message "Stashed: %s" text))
+    ;; There is nothing to stash so stash an empty string so the user
+    ;; doesn't accidentally paste something else somewhere.
+    (kill-new "")
+    (message "Nothing to stash!")))
 
 ;;
 
@@ -166,19 +218,6 @@ the user hasn't set this variable with the old or new value."
       (setq list (cdr list)))
     (nreverse out)))
 
-;; This lets us avoid compiling these replacement functions when emacs
-;; is sufficiently new enough to supply them alone. We do the macro
-;; treatment rather than just wrapping our defun calls in a when form
-;; specifically so that the compiler never sees the code on new emacs,
-;; (since the code is triggering warnings that we don't know how to get
-;; rid of.
-;;
-;; A more clever macro here would accept a condition and a list of forms.
-(defmacro compile-on-emacs-prior-to-23 (form)
-  "Conditionally evaluate form only on emacs < emacs-23."
-  (list 'when (< emacs-major-version 23)
-       form))
-
 (defun notmuch-split-content-type (content-type)
   "Split content/type into 'content' and 'type'"
   (split-string content-type "/"))
@@ -237,6 +276,41 @@ the given type."
   (or (plist-get part :content)
       (notmuch-get-bodypart-internal (notmuch-id-to-query (plist-get msg :id)) nth process-crypto)))
 
+;; Workaround: The call to `mm-display-part' below triggers a bug in
+;; Emacs 24 if it attempts to use the shr renderer to display an HTML
+;; part with images in it (demonstrated in 24.1 and 24.2 on Debian and
+;; Fedora 17, though unreproducable in other configurations).
+;; `mm-shr' references the variable `gnus-inhibit-images' without
+;; first loading gnus-art, which defines it, resulting in a
+;; void-variable error.  Hence, we advise `mm-shr' to ensure gnus-art
+;; is loaded.
+(if (>= emacs-major-version 24)
+    (defadvice mm-shr (before load-gnus-arts activate)
+      (require 'gnus-art nil t)
+      (ad-disable-advice 'mm-shr 'before 'load-gnus-arts)))
+
+(defun notmuch-mm-display-part-inline (msg part nth content-type process-crypto)
+  "Use the mm-decode/mm-view functions to display a part in the
+current buffer, if possible."
+  (let ((display-buffer (current-buffer)))
+    (with-temp-buffer
+      ;; In case there is :content, the content string is already converted
+      ;; into emacs internal format. `gnus-decoded' is a fake charset,
+      ;; which means no further decoding (to be done by mm- functions).
+      (let* ((charset (if (plist-member part :content)
+                         'gnus-decoded
+                       (plist-get part :content-charset)))
+            (handle (mm-make-handle (current-buffer) `(,content-type (charset . ,charset)))))
+       ;; If the user wants the part inlined, insert the content and
+       ;; test whether we are able to inline it (which includes both
+       ;; capability and suitability tests).
+       (when (mm-inlined-p handle)
+         (insert (notmuch-get-bodypart-content msg part nth process-crypto))
+         (when (mm-inlinable-p handle)
+           (set-buffer display-buffer)
+           (mm-display-part handle)
+           t))))))
+
 ;; Converts a plist of headers to an alist of headers. The input plist should
 ;; have symbols of the form :Header as keys, and the resulting alist will have
 ;; symbols of the form 'Header as keys.
@@ -244,33 +318,537 @@ the given type."
   (loop for (key value . rest) on plist by #'cddr
        collect (cons (intern (substring (symbol-name key) 1)) value)))
 
-;; Compatibility functions for versions of emacs before emacs 23.
-;;
-;; Both functions here were copied from emacs 23 with the following copyright:
-;;
-;; Copyright (C) 1985, 1986, 1992, 1994, 1995, 1999, 2000, 2001, 2002, 2003,
-;;   2004, 2005, 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc.
-;;
-;; and under the GPL version 3 (or later) exactly as notmuch itself.
-(compile-on-emacs-prior-to-23
- (defun apply-partially (fun &rest args)
-   "Return a function that is a partial application of FUN to ARGS.
-ARGS is a list of the first N arguments to pass to FUN.
-The result is a new function which does the same as FUN, except that
-the first N arguments are fixed at the values with which this function
-was called."
-   (lexical-let ((fun fun) (args1 args))
-     (lambda (&rest args2) (apply fun (append args1 args2))))))
-
-(compile-on-emacs-prior-to-23
- (defun mouse-event-p (object)
-   "Return non-nil if OBJECT is a mouse click event."
-   (memq (event-basic-type object) '(mouse-1 mouse-2 mouse-3 mouse-movement))))
+(defun notmuch-face-ensure-list-form (face)
+  "Return FACE in face list form.
+
+If FACE is already a face list, it will be returned as-is.  If
+FACE is a face name or face plist, it will be returned as a
+single element face list."
+  (if (and (listp face) (not (keywordp (car face))))
+      face
+    (list face)))
+
+(defun notmuch-combine-face-text-property (start end face &optional below object)
+  "Combine FACE into the 'face text property between START and END.
+
+This function combines FACE with any existing faces between START
+and END in OBJECT (which defaults to the current buffer).
+Attributes specified by FACE take precedence over existing
+attributes unless BELOW is non-nil.  FACE must be a face name (a
+symbol or string), a property list of face attributes, or a list
+of these.  For convenience when applied to strings, this returns
+OBJECT."
+
+  ;; A face property can have three forms: a face name (a string or
+  ;; symbol), a property list, or a list of these two forms.  In the
+  ;; list case, the faces will be combined, with the earlier faces
+  ;; taking precedent.  Here we canonicalize everything to list form
+  ;; to make it easy to combine.
+  (let ((pos start)
+       (face-list (notmuch-face-ensure-list-form face)))
+    (while (< pos end)
+      (let* ((cur (get-text-property pos 'face object))
+            (cur-list (notmuch-face-ensure-list-form cur))
+            (new (cond ((null cur-list) face)
+                       (below (append cur-list face-list))
+                       (t (append face-list cur-list))))
+            (next (next-single-property-change pos 'face object end)))
+       (put-text-property pos next 'face new object)
+       (setq pos next))))
+  object)
+
+(defun notmuch-combine-face-text-property-string (string face &optional below)
+  (notmuch-combine-face-text-property
+   0
+   (length string)
+   face
+   below
+   string))
+
+(defun notmuch-map-text-property (start end prop func &optional object)
+  "Transform text property PROP using FUNC.
+
+Applies FUNC to each distinct value of the text property PROP
+between START and END of OBJECT, setting PROP to the value
+returned by FUNC."
+  (while (< start end)
+    (let ((value (get-text-property start prop object))
+         (next (next-single-property-change start prop object end)))
+      (put-text-property start next prop (funcall func value) object)
+      (setq start next))))
+
+(defun notmuch-logged-error (msg &optional extra)
+  "Log MSG and EXTRA to *Notmuch errors* and signal MSG.
+
+This logs MSG and EXTRA to the *Notmuch errors* buffer and
+signals MSG as an error.  If EXTRA is non-nil, text referring the
+user to the *Notmuch errors* buffer will be appended to the
+signaled error.  This function does not return."
+
+  (with-current-buffer (get-buffer-create "*Notmuch errors*")
+    (goto-char (point-max))
+    (unless (bobp)
+      (newline))
+    (save-excursion
+      (insert "[" (current-time-string) "]\n" msg)
+      (unless (bolp)
+       (newline))
+      (when extra
+       (insert extra)
+       (unless (bolp)
+         (newline)))))
+  (error "%s" (concat msg (when extra
+                           " (see *Notmuch errors* for more details)"))))
+
+(defun notmuch-check-async-exit-status (proc msg &optional command err-file)
+  "If PROC exited abnormally, pop up an error buffer and signal an error.
+
+This is a wrapper around `notmuch-check-exit-status' for
+asynchronous process sentinels.  PROC and MSG must be the
+arguments passed to the sentinel.  COMMAND and ERR-FILE, if
+provided, are passed to `notmuch-check-exit-status'.  If COMMAND
+is not provided, it is taken from `process-command'."
+  (let ((exit-status
+        (case (process-status proc)
+          ((exit) (process-exit-status proc))
+          ((signal) msg))))
+    (when exit-status
+      (notmuch-check-exit-status exit-status (or command (process-command proc))
+                                nil err-file))))
+
+(defun notmuch-check-exit-status (exit-status command &optional output err-file)
+  "If EXIT-STATUS is non-zero, pop up an error buffer and signal an error.
+
+If EXIT-STATUS is non-zero, pop up a notmuch error buffer
+describing the error and signal an Elisp error.  EXIT-STATUS must
+be a number indicating the exit status code of a process or a
+string describing the signal that terminated the process (such as
+returned by `call-process').  COMMAND must be a list giving the
+command and its arguments.  OUTPUT, if provided, is a string
+giving the output of command.  ERR-FILE, if provided, is the name
+of a file containing the error output of command.  OUTPUT and the
+contents of ERR-FILE will be included in the error message."
+
+  (cond
+   ((eq exit-status 0) t)
+   ((eq exit-status 20)
+    (notmuch-logged-error "notmuch CLI version mismatch
+Emacs requested an older output format than supported by the notmuch CLI.
+You may need to restart Emacs or upgrade your notmuch Emacs package."))
+   ((eq exit-status 21)
+    (notmuch-logged-error "notmuch CLI version mismatch
+Emacs requested a newer output format than supported by the notmuch CLI.
+You may need to restart Emacs or upgrade your notmuch package."))
+   (t
+    (let* ((err (when err-file
+                 (with-temp-buffer
+                   (insert-file-contents err-file)
+                   (unless (eobp)
+                     (buffer-string)))))
+          (extra
+           (concat
+            "command: " (mapconcat #'shell-quote-argument command " ") "\n"
+            (if (integerp exit-status)
+                (format "exit status: %s\n" exit-status)
+              (format "exit signal: %s\n" exit-status))
+            (when err
+              (concat "stderr:\n" err))
+            (when output
+              (concat "stdout:\n" output)))))
+       (if err
+           ;; We have an error message straight from the CLI.
+           (notmuch-logged-error
+            (replace-regexp-in-string "[ \n\r\t\f]*\\'" "" err) extra)
+         ;; We only have combined output from the CLI; don't inundate
+         ;; the user with it.  Mimic `process-lines'.
+         (notmuch-logged-error (format "%s exited with status %s"
+                                       (car command) exit-status)
+                               extra))
+       ;; `notmuch-logged-error' does not return.
+       ))))
+
+(defun notmuch-call-notmuch-json (&rest args)
+  "Invoke `notmuch-command' with ARGS and return the parsed JSON output.
+
+The returned output will represent objects using property lists
+and arrays as lists.  If notmuch exits with a non-zero status,
+this will pop up a buffer containing notmuch's output and signal
+an error."
+
+  (with-temp-buffer
+    (let ((err-file (make-temp-file "nmerr")))
+      (unwind-protect
+         (let ((status (apply #'call-process
+                              notmuch-command nil (list t err-file) nil args)))
+           (notmuch-check-exit-status status (cons notmuch-command args)
+                                      (buffer-string) err-file)
+           (goto-char (point-min))
+           (let ((json-object-type 'plist)
+                 (json-array-type 'list)
+                 (json-false 'nil))
+             (json-read)))
+       (delete-file err-file)))))
+
+(defun notmuch-call-notmuch-sexp (&rest args)
+  "Invoke `notmuch-command' with ARGS and return the parsed S-exp output.
+
+If notmuch exits with a non-zero status, this will pop up a
+buffer containing notmuch's output and signal an error."
+
+  (with-temp-buffer
+    (let ((err-file (make-temp-file "nmerr")))
+      (unwind-protect
+         (let ((status (apply #'call-process
+                              notmuch-command nil (list t err-file) nil args)))
+           (notmuch-check-exit-status status (cons notmuch-command args)
+                                      (buffer-string) err-file)
+           (goto-char (point-min))
+           (read (current-buffer)))
+       (delete-file err-file)))))
+
+(defun notmuch-start-notmuch (name buffer sentinel &rest args)
+  "Start and return an asynchronous notmuch command.
+
+This starts and returns an asynchronous process running
+`notmuch-command' with ARGS.  The exit status is checked via
+`notmuch-check-async-exit-status'.  Output written to stderr is
+redirected and displayed when the process exits (even if the
+process exits successfully).  NAME and BUFFER are the same as in
+`start-process'.  SENTINEL is a process sentinel function to call
+when the process exits, or nil for none.  The caller must *not*
+invoke `set-process-sentinel' directly on the returned process,
+as that will interfere with the handling of stderr and the exit
+status."
+
+  ;; There is no way (as of Emacs 24.3) to capture stdout and stderr
+  ;; separately for asynchronous processes, or even to redirect stderr
+  ;; to a file, so we use a trivial shell wrapper to send stderr to a
+  ;; temporary file and clean things up in the sentinel.
+  (let* ((err-file (make-temp-file "nmerr"))
+        ;; Use a pipe
+        (process-connection-type nil)
+        ;; Find notmuch using Emacs' `exec-path'
+        (command (or (executable-find notmuch-command)
+                     (error "command not found: %s" notmuch-command)))
+        (proc (apply #'start-process name buffer
+                     "/bin/sh" "-c"
+                     "exec 2>\"$1\"; shift; exec \"$0\" \"$@\""
+                     command err-file args)))
+    (process-put proc 'err-file err-file)
+    (process-put proc 'sub-sentinel sentinel)
+    (process-put proc 'real-command (cons notmuch-command args))
+    (set-process-sentinel proc #'notmuch-start-notmuch-sentinel)
+    proc))
+
+(defun notmuch-start-notmuch-sentinel (proc event)
+  (let ((err-file (process-get proc 'err-file))
+       (sub-sentinel (process-get proc 'sub-sentinel))
+       (real-command (process-get proc 'real-command)))
+    (condition-case err
+       (progn
+         ;; Invoke the sub-sentinel, if any
+         (when sub-sentinel
+           (funcall sub-sentinel proc event))
+         ;; Check the exit status.  This will signal an error if the
+         ;; exit status is non-zero.  Don't do this if the process
+         ;; buffer is dead since that means Emacs killed the process
+         ;; and there's no point in telling the user that (but we
+         ;; still check for and report stderr output below).
+         (when (buffer-live-p (process-buffer proc))
+           (notmuch-check-async-exit-status proc event real-command err-file))
+         ;; If that didn't signal an error, then any error output was
+         ;; really warning output.  Show warnings, if any.
+         (let ((warnings
+                (with-temp-buffer
+                  (unless (= (second (insert-file-contents err-file)) 0)
+                    (end-of-line)
+                    ;; Show first line; stuff remaining lines in the
+                    ;; errors buffer.
+                    (let ((l1 (buffer-substring (point-min) (point))))
+                      (skip-chars-forward "\n")
+                      (cons l1 (unless (eobp)
+                                 (buffer-substring (point) (point-max)))))))))
+           (when warnings
+             (notmuch-logged-error (car warnings) (cdr warnings)))))
+      (error
+       ;; Emacs behaves strangely if an error escapes from a sentinel,
+       ;; so turn errors into messages.
+       (message "%s" (error-message-string err))))
+    (ignore-errors (delete-file err-file))))
 
 ;; This variable is used only buffer local, but it needs to be
 ;; declared globally first to avoid compiler warnings.
 (defvar notmuch-show-process-crypto nil)
 (make-variable-buffer-local 'notmuch-show-process-crypto)
 
+;; Incremental JSON parsing
+
+;; These two variables are internal variables to the parsing
+;; routines. They are always used buffer local but need to be declared
+;; globally to avoid compiler warnings.
+
+(defvar notmuch-json-parser nil
+  "Internal incremental JSON parser object: local to the buffer being parsed.")
+
+(defvar notmuch-json-state nil
+  "State of the internal JSON parser: local to the buffer being parsed.")
+
+(defun notmuch-json-create-parser (buffer)
+  "Return a streaming JSON parser that consumes input from BUFFER.
+
+This parser is designed to read streaming JSON whose structure is
+known to the caller.  Like a typical JSON parsing interface, it
+provides a function to read a complete JSON value from the input.
+However, it extends this with an additional function that
+requires the next value in the input to be a compound value and
+descends into it, allowing its elements to be read one at a time
+or further descended into.  Both functions can return 'retry to
+indicate that not enough input is available.
+
+The parser always consumes input from BUFFER's point.  Hence, the
+caller is allowed to delete and data before point and may
+resynchronize after an error by moving point."
+
+  (list buffer
+       ;; Terminator stack: a stack of characters that indicate the
+       ;; end of the compound values enclosing point
+       '()
+       ;; Next: One of
+       ;; * 'expect-value if the next token must be a value, but a
+       ;;   value has not yet been reached
+       ;; * 'value if point is at the beginning of a value
+       ;; * 'expect-comma if the next token must be a comma
+       'expect-value
+       ;; Allow terminator: non-nil if the next token may be a
+       ;; terminator
+       nil
+       ;; Partial parse position: If state is 'value, a marker for
+       ;; the position of the partial parser or nil if no partial
+       ;; parsing has happened yet
+       nil
+       ;; Partial parse state: If state is 'value, the current
+       ;; `parse-partial-sexp' state
+       nil))
+
+(defmacro notmuch-json-buffer (jp) `(first ,jp))
+(defmacro notmuch-json-term-stack (jp) `(second ,jp))
+(defmacro notmuch-json-next (jp) `(third ,jp))
+(defmacro notmuch-json-allow-term (jp) `(fourth ,jp))
+(defmacro notmuch-json-partial-pos (jp) `(fifth ,jp))
+(defmacro notmuch-json-partial-state (jp) `(sixth ,jp))
+
+(defvar notmuch-json-syntax-table
+  (let ((table (make-syntax-table)))
+    ;; The standard syntax table is what we need except that "." needs
+    ;; to have word syntax instead of punctuation syntax.
+    (modify-syntax-entry ?. "w" table)
+    table)
+  "Syntax table used for incremental JSON parsing.")
+
+(defun notmuch-json-scan-to-value (jp)
+  ;; Helper function that consumes separators, terminators, and
+  ;; whitespace from point.  Returns nil if it successfully reached
+  ;; the beginning of a value, 'end if it consumed a terminator, or
+  ;; 'retry if not enough input was available to reach a value.  Upon
+  ;; nil return, (notmuch-json-next jp) is always 'value.
+
+  (if (eq (notmuch-json-next jp) 'value)
+      ;; We're already at a value
+      nil
+    ;; Drive the state toward 'expect-value
+    (skip-chars-forward " \t\r\n")
+    (or (when (eobp) 'retry)
+       ;; Test for the terminator for the current compound
+       (when (and (notmuch-json-allow-term jp)
+                  (eq (char-after) (car (notmuch-json-term-stack jp))))
+         ;; Consume it and expect a comma or terminator next
+         (forward-char)
+         (setf (notmuch-json-term-stack jp) (cdr (notmuch-json-term-stack jp))
+               (notmuch-json-next jp) 'expect-comma
+               (notmuch-json-allow-term jp) t)
+         'end)
+       ;; Test for a separator
+       (when (eq (notmuch-json-next jp) 'expect-comma)
+         (when (/= (char-after) ?,)
+           (signal 'json-readtable-error (list "expected ','")))
+         ;; Consume it, switch to 'expect-value, and disallow a
+         ;; terminator
+         (forward-char)
+         (skip-chars-forward " \t\r\n")
+         (setf (notmuch-json-next jp) 'expect-value
+               (notmuch-json-allow-term jp) nil)
+         ;; We moved point, so test for eobp again and fall through
+         ;; to the next test if there's more input
+         (when (eobp) 'retry))
+       ;; Next must be 'expect-value and we know this isn't
+       ;; whitespace, EOB, or a terminator, so point must be on a
+       ;; value
+       (progn
+         (assert (eq (notmuch-json-next jp) 'expect-value))
+         (setf (notmuch-json-next jp) 'value)
+         nil))))
+
+(defun notmuch-json-begin-compound (jp)
+  "Parse the beginning of a compound value and traverse inside it.
+
+Returns 'retry if there is insufficient input to parse the
+beginning of the compound.  If this is able to parse the
+beginning of a compound, it moves point past the token that opens
+the compound and returns t.  Later calls to `notmuch-json-read'
+will return the compound's elements.
+
+Entering JSON objects is currently unimplemented."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    ;; Disallow terminators
+    (setf (notmuch-json-allow-term jp) nil)
+    ;; Save "next" so we can restore it if there's a syntax error
+    (let ((saved-next (notmuch-json-next jp)))
+      (or (notmuch-json-scan-to-value jp)
+         (if (/= (char-after) ?\[)
+             (progn
+               (setf (notmuch-json-next jp) saved-next)
+               (signal 'json-readtable-error (list "expected '['")))
+           (forward-char)
+           (push ?\] (notmuch-json-term-stack jp))
+           ;; Expect a value or terminator next
+           (setf (notmuch-json-next jp) 'expect-value
+                 (notmuch-json-allow-term jp) t)
+           t)))))
+
+(defun notmuch-json-read (jp)
+  "Parse the value at point in JP's buffer.
+
+Returns 'retry if there is insufficient input to parse a complete
+JSON value (though it may still move point over separators or
+whitespace).  If the parser is currently inside a compound value
+and the next token ends the list or object, this moves point just
+past the terminator and returns 'end.  Otherwise, this moves
+point to just past the end of the value and returns the value."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    (or
+     ;; Get to a value state
+     (notmuch-json-scan-to-value jp)
+
+     ;; Can we parse a complete value?
+     (let ((complete
+           (if (looking-at "[-+0-9tfn]")
+               ;; This is a number or a keyword, so the partial
+               ;; parser isn't going to help us because a truncated
+               ;; number or keyword looks like a complete symbol to
+               ;; it.  Look for something that clearly ends it.
+               (save-excursion
+                 (skip-chars-forward "^]},: \t\r\n")
+                 (not (eobp)))
+
+             ;; We're looking at a string, object, or array, which we
+             ;; can partial parse.  If we just reached the value, set
+             ;; up the partial parser.
+             (when (null (notmuch-json-partial-state jp))
+               (setf (notmuch-json-partial-pos jp) (point-marker)))
+
+             ;; Extend the partial parse until we either reach EOB or
+             ;; get the whole value
+             (save-excursion
+               (let ((pstate
+                      (with-syntax-table notmuch-json-syntax-table
+                        (parse-partial-sexp
+                         (notmuch-json-partial-pos jp) (point-max) 0 nil
+                         (notmuch-json-partial-state jp)))))
+                 ;; A complete value is available if we've reached
+                 ;; depth 0 or less and encountered a complete
+                 ;; subexpression.
+                 (if (and (<= (first pstate) 0) (third pstate))
+                     t
+                   ;; Not complete.  Update the partial parser state
+                   (setf (notmuch-json-partial-pos jp) (point-marker)
+                         (notmuch-json-partial-state jp) pstate)
+                   nil))))))
+
+       (if (not complete)
+          'retry
+        ;; We have a value.  Reset the partial parse state and expect
+        ;; a comma or terminator after the value.
+        (setf (notmuch-json-next jp) 'expect-comma
+              (notmuch-json-allow-term jp) t
+              (notmuch-json-partial-pos jp) nil
+              (notmuch-json-partial-state jp) nil)
+        ;; Parse the value
+        (let ((json-object-type 'plist)
+              (json-array-type 'list)
+              (json-false nil))
+          (json-read)))))))
+
+(defun notmuch-json-eof (jp)
+  "Signal a json-error if there is more data in JP's buffer.
+
+Moves point to the beginning of any trailing data or to the end
+of the buffer if there is only trailing whitespace."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    (skip-chars-forward " \t\r\n")
+    (unless (eobp)
+      (signal 'json-error (list "Trailing garbage following JSON data")))))
+
+(defun notmuch-json-parse-partial-list (result-function error-function results-buf)
+  "Parse a partial JSON list from current buffer.
+
+This function consumes a JSON list from the current buffer,
+applying RESULT-FUNCTION in buffer RESULT-BUFFER to each complete
+value in the list.  It operates incrementally and should be
+called whenever the buffer has been extended with additional
+data.
+
+If there is a syntax error, this will attempt to resynchronize
+with the input and will apply ERROR-FUNCTION in buffer
+RESULT-BUFFER to any input that was skipped.
+
+It sets up all the needed internal variables: the caller just
+needs to call it with point in the same place that the parser
+left it."
+  (let (done)
+    (unless (local-variable-p 'notmuch-json-parser)
+      (set (make-local-variable 'notmuch-json-parser)
+          (notmuch-json-create-parser (current-buffer)))
+      (set (make-local-variable 'notmuch-json-state) 'begin))
+    (while (not done)
+      (condition-case nil
+         (case notmuch-json-state
+               ((begin)
+                ;; Enter the results list
+                (if (eq (notmuch-json-begin-compound
+                         notmuch-json-parser) 'retry)
+                    (setq done t)
+                  (setq notmuch-json-state 'result)))
+               ((result)
+                ;; Parse a result
+                (let ((result (notmuch-json-read notmuch-json-parser)))
+                  (case result
+                        ((retry) (setq done t))
+                        ((end) (setq notmuch-json-state 'end))
+                        (otherwise (with-current-buffer results-buf
+                                     (funcall result-function result))))))
+               ((end)
+                ;; Any trailing data is unexpected
+                (notmuch-json-eof notmuch-json-parser)
+                (setq done t)))
+       (json-error
+        ;; Do our best to resynchronize and ensure forward
+        ;; progress
+        (let ((bad (buffer-substring (line-beginning-position)
+                                     (line-end-position))))
+          (forward-line)
+          (with-current-buffer results-buf
+            (funcall error-function "%s" bad))))))
+    ;; Clear out what we've parsed
+    (delete-region (point-min) (point))))
+
+
+
+
 (provide 'notmuch-lib)
 
+;; Local Variables:
+;; byte-compile-warnings: (not cl-functions)
+;; End: