mirror of
https://git.notmuchmail.org/git/notmuch
synced 2024-11-24 20:08:10 +01:00
emacs: Switch from text to JSON format for search results
The JSON format eliminates the complex escaping issues that have plagued the text search format. This uses the incremental JSON parser so that, like the text parser, it can output search results incrementally. This slows down the parser by about ~4X, but puts us in a good position to optimize either by improving the JSON parser (evidence suggests this can reduce the overhead to ~40% over the text format) or by switching to S-expressions (evidence suggests this will more than double performance over the text parser). [1] This also fixes the incremental search parsing test. This has one minor side-effect on search result formatting. Previously, the date field was always padded to a fixed width of 12 characters because of how the text parser's regexp was written. The JSON format doesn't do this. We could pad it out in Emacs before formatting it, but, since all of the other fields are variable width, we instead fix notmuch-search-result-format to take the variable-width field and pad it out. For users who have customized this variable, we'll mention in the NEWS how to fix this slight format change. [1] id:"20110720205007.GB21316@mit.edu"
This commit is contained in:
parent
889dda3731
commit
9c5ea07cc6
2 changed files with 64 additions and 47 deletions
110
emacs/notmuch.el
110
emacs/notmuch.el
|
@ -60,7 +60,7 @@
|
||||||
(require 'notmuch-message)
|
(require 'notmuch-message)
|
||||||
|
|
||||||
(defcustom notmuch-search-result-format
|
(defcustom notmuch-search-result-format
|
||||||
`(("date" . "%s ")
|
`(("date" . "%12s ")
|
||||||
("count" . "%-7s ")
|
("count" . "%-7s ")
|
||||||
("authors" . "%-20s ")
|
("authors" . "%-20s ")
|
||||||
("subject" . "%s ")
|
("subject" . "%s ")
|
||||||
|
@ -557,17 +557,14 @@ This function advances the next thread when finished."
|
||||||
(notmuch-search-tag '("-inbox"))
|
(notmuch-search-tag '("-inbox"))
|
||||||
(notmuch-search-next-thread))
|
(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)
|
|
||||||
|
|
||||||
(defun notmuch-search-process-sentinel (proc msg)
|
(defun notmuch-search-process-sentinel (proc msg)
|
||||||
"Add a message to let user know when \"notmuch search\" exits"
|
"Add a message to let user know when \"notmuch search\" exits"
|
||||||
(let ((buffer (process-buffer proc))
|
(let ((buffer (process-buffer proc))
|
||||||
(status (process-status proc))
|
(status (process-status proc))
|
||||||
(exit-status (process-exit-status proc))
|
(exit-status (process-exit-status proc))
|
||||||
(never-found-target-thread nil))
|
(never-found-target-thread nil))
|
||||||
(if (memq status '(exit signal))
|
(when (memq status '(exit signal))
|
||||||
|
(kill-buffer (process-get proc 'parse-buf))
|
||||||
(if (buffer-live-p buffer)
|
(if (buffer-live-p buffer)
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(save-excursion
|
(save-excursion
|
||||||
|
@ -577,8 +574,6 @@ This function advances the next thread when finished."
|
||||||
(if (eq status 'signal)
|
(if (eq status 'signal)
|
||||||
(insert "Incomplete search results (search process was killed).\n"))
|
(insert "Incomplete search results (search process was killed).\n"))
|
||||||
(when (eq status 'exit)
|
(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.")
|
(insert "End of search results.")
|
||||||
(unless (= exit-status 0)
|
(unless (= exit-status 0)
|
||||||
(insert (format " (process returned %d)" exit-status)))
|
(insert (format " (process returned %d)" exit-status)))
|
||||||
|
@ -758,45 +753,59 @@ non-authors is found, assume that all of the authors match."
|
||||||
(insert (apply #'format string objects))
|
(insert (apply #'format string objects))
|
||||||
(insert "\n")))
|
(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)
|
(defun notmuch-search-process-filter (proc string)
|
||||||
"Process and filter the output of \"notmuch search\""
|
"Process and filter the output of \"notmuch search\""
|
||||||
(let ((buffer (process-buffer proc)))
|
(let ((results-buf (process-buffer proc))
|
||||||
(if (buffer-live-p buffer)
|
(parse-buf (process-get proc 'parse-buf))
|
||||||
(with-current-buffer buffer
|
(inhibit-read-only t)
|
||||||
(let ((line 0)
|
done)
|
||||||
(more t)
|
(if (not (buffer-live-p results-buf))
|
||||||
(inhibit-read-only t)
|
(delete-process proc)
|
||||||
(string (concat notmuch-search-process-filter-data string)))
|
(with-current-buffer parse-buf
|
||||||
(setq notmuch-search-process-filter-data nil)
|
;; Insert new data
|
||||||
(while more
|
(save-excursion
|
||||||
(while (and (< line (length string)) (= (elt string line) ?\n))
|
(goto-char (point-max))
|
||||||
(setq line (1+ line)))
|
(insert string)))
|
||||||
(if (string-match "^thread:\\([0-9A-Fa-f]*\\) \\([^][]*\\) \\[\\([0-9]*\\)/\\([0-9]*\\)\\] \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" string line)
|
(with-current-buffer results-buf
|
||||||
(let* ((thread-id (match-string 1 string))
|
(while (not done)
|
||||||
(tags-str (match-string 7 string))
|
(condition-case nil
|
||||||
(result (list :thread thread-id
|
(case notmuch-search-process-state
|
||||||
:date_relative (match-string 2 string)
|
((begin)
|
||||||
:matched (string-to-number
|
;; Enter the results list
|
||||||
(match-string 3 string))
|
(if (eq (notmuch-json-begin-compound
|
||||||
:total (string-to-number
|
notmuch-search-json-parser) 'retry)
|
||||||
(match-string 4 string))
|
(setq done t)
|
||||||
:authors (match-string 5 string)
|
(setq notmuch-search-process-state 'result)))
|
||||||
:subject (match-string 6 string)
|
((result)
|
||||||
:tags (if tags-str
|
;; Parse a result
|
||||||
(save-match-data
|
(let ((result (notmuch-json-read notmuch-search-json-parser)))
|
||||||
(split-string tags-str))))))
|
(case result
|
||||||
(if (/= (match-beginning 0) line)
|
((retry) (setq done t))
|
||||||
(notmuch-search-show-error
|
((end) (setq notmuch-search-process-state 'end))
|
||||||
(substring string line (match-beginning 0))))
|
(otherwise (notmuch-search-show-result result)))))
|
||||||
(notmuch-search-show-result result)
|
((end)
|
||||||
(set 'line (match-end 0)))
|
;; Any trailing data is unexpected
|
||||||
(set 'more nil)
|
(notmuch-json-eof notmuch-search-json-parser)
|
||||||
(while (and (< line (length string)) (= (elt string line) ?\n))
|
(setq done t)))
|
||||||
(setq line (1+ line)))
|
(json-error
|
||||||
(if (< line (length string))
|
;; Do our best to resynchronize and ensure forward
|
||||||
(setq notmuch-search-process-filter-data (substring string line)))
|
;; progress
|
||||||
))))
|
(notmuch-search-show-error
|
||||||
(delete-process proc))))
|
"%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)))))))
|
||||||
|
|
||||||
(defun notmuch-search-tag-all (&optional tag-changes)
|
(defun notmuch-search-tag-all (&optional tag-changes)
|
||||||
"Add/remove tags from all messages in current search buffer.
|
"Add/remove tags from all messages in current search buffer.
|
||||||
|
@ -899,10 +908,19 @@ Other optional parameters are used as follows:
|
||||||
(let ((proc (start-process
|
(let ((proc (start-process
|
||||||
"notmuch-search" buffer
|
"notmuch-search" buffer
|
||||||
notmuch-command "search"
|
notmuch-command "search"
|
||||||
|
"--format=json"
|
||||||
(if oldest-first
|
(if oldest-first
|
||||||
"--sort=oldest-first"
|
"--sort=oldest-first"
|
||||||
"--sort=newest-first")
|
"--sort=newest-first")
|
||||||
query)))
|
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*")))
|
||||||
|
(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-sentinel proc 'notmuch-search-process-sentinel)
|
||||||
(set-process-filter proc 'notmuch-search-process-filter)
|
(set-process-filter proc 'notmuch-search-process-filter)
|
||||||
(set-process-query-on-exit-flag proc nil))))
|
(set-process-query-on-exit-flag proc nil))))
|
||||||
|
|
|
@ -36,7 +36,6 @@ test_emacs '(notmuch-search "tag:inbox")
|
||||||
test_expect_equal_file OUTPUT $EXPECTED/notmuch-search-tag-inbox
|
test_expect_equal_file OUTPUT $EXPECTED/notmuch-search-tag-inbox
|
||||||
|
|
||||||
test_begin_subtest "Incremental parsing of search results"
|
test_begin_subtest "Incremental parsing of search results"
|
||||||
test_subtest_known_broken
|
|
||||||
test_emacs "(ad-enable-advice 'notmuch-search-process-filter 'around 'pessimal)
|
test_emacs "(ad-enable-advice 'notmuch-search-process-filter 'around 'pessimal)
|
||||||
(ad-activate 'notmuch-search-process-filter)
|
(ad-activate 'notmuch-search-process-filter)
|
||||||
(notmuch-search \"tag:inbox\")
|
(notmuch-search \"tag:inbox\")
|
||||||
|
|
Loading…
Reference in a new issue