This patch adds some functionality to display the outline for threads
displayed by notmuch-show.  The entries in the outline buffer are
links to the corresponding message in the notmuch-show buffer.
---
 emacs/notmuch-lib.el  |   12 +++
 emacs/notmuch-show.el |  195 ++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 206 insertions(+), 1 deletions(-)

diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el
index f93c957..e346571 100644
--- a/emacs/notmuch-lib.el
+++ b/emacs/notmuch-lib.el
@@ -43,6 +43,10 @@
 (defvar notmuch-folders nil
   "Deprecated name for what is now known as `notmuch-saved-searches'.")

+(defvar notmuch-show-outline-buffer nil
+  "Outline buffer associated with a notmuch-show buffer.")
+(make-variable-buffer-local 'notmuch-show-outline-buffer)
+
 (defun notmuch-saved-searches ()
   "Common function for querying the notmuch-saved-searches variable.

@@ -91,9 +95,17 @@ the user hasn't set this variable with the old or new value."
   "Return the user.other_email value (as a list) from the notmuch 
configuration."
   (split-string (notmuch-config-get "user.other_email") "\n"))

+(declare-function notmuch-show-outline-buffer-name  "notmuch-show" (&optional 
buf))
+
 (defun notmuch-kill-this-buffer ()
   "Kill the current buffer."
   (interactive)
+  ;; if we are in a notmuch-show buffer, kill the associated outline buffer, 
if any
+  (when (eq major-mode 'notmuch-show-mode)
+    (let ((outline-buf notmuch-show-outline-buffer))
+      (when outline-buf
+       (mapc #'delete-window (get-buffer-window-list outline-buf))
+       (kill-buffer outline-buf))))
   (kill-buffer (current-buffer)))

 ;;
diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el
index 262addb..cd3eefb 100644
--- a/emacs/notmuch-show.el
+++ b/emacs/notmuch-show.el
@@ -96,6 +96,57 @@ any given message."
   :group 'notmuch
   :type 'boolean)

+(defcustom notmuch-always-show-outline nil
+  "Always open an outline buffer when viewing a thread?"
+  :group 'notmuch
+  :type 'boolean)
+
+(defcustom notmuch-outline-format
+  (list "%a - %r")
+  "Format used for thread-outline lines.
+
+This is a list supporting the following types of elements:
+For a symbol, its value is used if non-nil.
+A string is inserted verbatim with the exception
+ of the following %-constructs:
+ %a - Author
+ %d - Date
+ %s - Subject
+ %r - Relative date
+For a list of the form `(:eval FORM)', form is evaluated
+ and its result displayed.
+
+The variables author, subject, date and reldate will be bound to
+their respective values when this is interpreted, and can be
+used in (:eval ..)-elements or directly as symbols."
+  :group 'notmuch
+  :type
+  '(repeat (choice (const :tag "Author" author)
+                  (const :tag "Date" date)
+                  (const :tag "Relative date" reldate)
+                  (string :tag "Format string")
+                  (list :tag "Custom expression (will be evaluated when 
rendering)"
+                        (const :tag "" :eval)
+                        sexp))))
+
+(defface notmuch-outline '((t :inherit default))
+  "Face used to display (unhighlighted) lines in thread outlines"
+  :group 'notmuch)
+
+(defface notmuch-outline-highlighted
+  '((((class color) (background light)) (:background "#f0f0f0"))
+    (((class color) (background dark)) (:background "#303030")))
+  "Face used to display highlight the current message in the outline buffer"
+  :group 'notmuch)
+
+(defvar notmuch-outline-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "n" 'next-line)
+    (define-key map "p" 'previous-line)
+    (define-key map "q" 'kill-buffer-and-window)
+    (define-key map "x" 'kill-buffer-and-window)
+    map))
+
 (defmacro with-current-notmuch-show-message (&rest body)
   "Evaluate body with current buffer set to the text of current message"
   `(save-excursion
@@ -741,12 +792,27 @@ current buffer, if possible."
     ;; message.
     (put-text-property message-start message-end :notmuch-message-extent (cons 
message-start message-end))

+    ;; Save the indentation depth, used by `notmuch-show-outline'
+    (put-text-property message-start message-end :notmuch-depth depth)
+
     (let ((headers-overlay (make-overlay headers-start headers-end))
           (invis-specs (list headers-invis-spec message-invis-spec)))
       (overlay-put headers-overlay 'invisible invis-specs)
       (overlay-put headers-overlay 'priority 10))
     (overlay-put (make-overlay body-start body-end) 'invisible 
message-invis-spec)

+    ;; Add callbacks that update the outline buffer when moving between 
messages.
+    ;; Due to the mindbogglingly absurd semantics of point-entered and 
point-left
+    ;; this function will will be run up to _four_ times when moving between 
messages:
+    (let ((goto-msg-func
+          `(lambda (before after)
+             (if (and (>= after (marker-position ,message-start))
+                      (< after (marker-position ,message-end)))
+                 (notmuch-outline-highlight-message ,message-start)))))
+      (add-text-properties message-start message-end
+                          (list 'point-entered goto-msg-func
+                                'point-left goto-msg-func)))
+
     ;; Save the properties for this message. Currently this saves the
     ;; entire message (augmented it with other stuff), which seems
     ;; like overkill. We might save a reduced subset (for example, not
@@ -778,6 +844,130 @@ current buffer, if possible."
 (defvar notmuch-show-parent-buffer nil)
 (make-variable-buffer-local 'notmuch-show-parent-buffer)

+(defun notmuch-show-message-is-visible ()
+  "Return t if current message is visible."
+  (plist-get (notmuch-show-get-message-properties) :message-visible))
+
+(defun notmuch-outline-render-format (format)
+  "Render FORMAT, as described in `notmuch-outline-format'"
+  (let ((author (notmuch-show-get-from))
+       (date (notmuch-show-get-date))
+       (subject (notmuch-show-get-subject))
+       (reldate (plist-get (notmuch-show-get-message-properties)
+                           :date_relative)))
+    (mapconcat (lambda (elem)
+                (cond
+                 ((symbolp elem) (or (symbol-value elem) ""))
+                 ((stringp elem)
+                  (let ((str elem))
+                    (mapc (lambda (subst)
+                            (setq str
+                                  (replace-regexp-in-string (car subst)
+                                                            (cdr subst)
+                                                            str)))
+                          `(("%a" . ,author)
+                            ("%s" . ,subject)
+                            ("%d" . ,date)
+                            ("%r" . ,reldate)))
+                    str))
+                 ((and (listp elem) (eq (car elem) :eval))
+                  (eval (second elem)))
+                 (t (error "Unknown element in `notmuch-outline-format': %S" 
elem))))
+              format
+              "")))
+
+(defun notmuch-outline-highlight-message (msg-start)
+  "Highlight message starting at MSG-START.
+
+The highlighting will take place in the outline buffer, while
+MSG-START refers to a position in the corresponding notmuch-show buffer."
+  (when (buffer-live-p notmuch-show-outline-buffer)
+    (with-current-buffer notmuch-show-outline-buffer
+      (remove-overlays nil nil 'current-message t)
+      (save-excursion
+       (goto-char (point-min))
+       (while (and (not (equal (get-text-property (point) :message-start)
+                           msg-start))
+                 (not (eobp)))
+         (forward-line))
+       (unless (eobp)
+         (let ((ovl
+                (make-overlay (line-beginning-position)
+                              (line-end-position))))
+         (overlay-put ovl 'face 'notmuch-outline-highlighted)
+         (overlay-put ovl 'current-message t)))))))
+
+(defun notmuch-show-create-outline-buffer (&optional buf)
+  "Create an outline buffer for show-buffer BUF.
+
+Returns the created buffer."
+
+  (generate-new-buffer (concat (buffer-name buf) " - outline")))
+
+(defun notmuch-outline-message ()
+  "Outline the message under the point.
+
+Expects the point to be on the beginning of the first line of the message."
+  (lexical-let*
+      ((msg-start (car (notmuch-show-message-extent)))
+       (outline-buf notmuch-show-outline-buffer)
+       (goto-message
+       (lambda (btn)
+         (let ((win (get-buffer-window outline-buf)))
+           (when win
+             (select-window (get-buffer-window outline-buf))
+             (when (marker-buffer msg-start)
+               (switch-to-buffer-other-window (marker-buffer msg-start))
+               (notmuch-outline-highlight-message msg-start)
+               (goto-char (marker-position msg-start))
+               (when (not (notmuch-show-message-is-visible))
+                 (notmuch-show-toggle-message))))))))
+    (let ((indentation (or (get-text-property (point) :notmuch-depth) 0))
+         (button-label (notmuch-outline-render-format
+                        notmuch-outline-format)))
+      (with-current-buffer outline-buf
+       (indent-to indentation)
+       (insert button-label)
+       (make-text-button (line-beginning-position) (line-end-position)
+                         'action goto-message
+                         'follow-link t
+                         'help-echo "mouse-1, RET: show this message"
+                         'face 'notmuch-outline)
+       (put-text-property (line-beginning-position) (line-end-position)
+                          :message-start msg-start)
+       (insert "\n")))))
+
+(defun notmuch-show-outline ()
+  "Generate an outline for the current buffer.
+
+This function must only be called in a notmuch-show buffer."
+  (interactive)
+  (if (buffer-live-p notmuch-show-outline-buffer)
+      (switch-to-buffer-other-window notmuch-show-outline-buffer)
+    (let ((outline-buf (notmuch-show-create-outline-buffer))
+         (inhibit-point-motion-hooks t))
+      (setq notmuch-show-outline-buffer outline-buf)
+      (save-excursion
+       (with-current-buffer outline-buf
+         (notmuch-outline-mode))
+       (goto-char (point-min))
+       (while (not (eobp))
+         (notmuch-outline-message)
+         (goto-char (marker-position (cdr (notmuch-show-message-extent)))))
+       (with-current-buffer outline-buf
+         (setq buffer-read-only t)))
+      (notmuch-outline-highlight-message (car (notmuch-show-message-extent)))
+      (let ((win (selected-window)))
+       (switch-to-buffer-other-window outline-buf)
+       (select-window win)))))
+
+(defun notmuch-outline-mode ()
+  (interactive)
+  (kill-all-local-variables)
+  (use-local-map notmuch-outline-mode-map)
+  (setq major-mode 'notmuch-show-outline-mode
+       mode-name "notmuch-show-outline"))
+
 ;;;###autoload
 (defun notmuch-show (thread-id &optional parent-buffer query-context 
buffer-name crypto-switch)
   "Run \"notmuch show\" with the given thread ID and display results.
@@ -837,7 +1027,9 @@ function is used. "
     ;; Set the header line to the subject of the first open message.
     (setq header-line-format (notmuch-show-strip-re 
(notmuch-show-get-subject)))

-    (notmuch-show-mark-read)))
+    (notmuch-show-mark-read)
+    (when notmuch-always-show-outline
+      (notmuch-show-outline))))

 (defvar notmuch-show-stash-map
   (let ((map (make-sparse-keymap)))
@@ -879,6 +1071,7 @@ function is used. "
        (define-key map "P" 'notmuch-show-previous-message)
        (define-key map "n" 'notmuch-show-next-open-message)
        (define-key map "p" 'notmuch-show-previous-open-message)
+       (define-key map "o" 'notmuch-show-outline)
        (define-key map (kbd "DEL") 'notmuch-show-rewind)
        (define-key map " " 'notmuch-show-advance-and-archive)
        (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
-- 
1.7.5.4

Reply via email to