(require 'coolj)
-(declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth))
+(declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth &optional hide))
;;
-(defvar notmuch-wash-signature-regexp
- "^\\(-- ?\\|_+\\)$"
- "Pattern to match a line that separates content from signature.")
+(defgroup notmuch-wash nil
+ "Cleaning up messages for display."
+ :group 'notmuch)
-(defvar notmuch-wash-citation-regexp
- "\\(^[[:space:]]*>.*\n\\)+"
- "Pattern to match citation lines.")
+(defcustom notmuch-wash-signature-regexp "^\\(-- ?\\|_+\\)$"
+ "Pattern to match a line that separates content from signature."
+ :type 'regexp
+ :group 'notmuch-wash)
-(defvar notmuch-wash-signature-button-format
- "[ %d-line signature. Click/Enter to toggle visibility. ]"
- "String used to construct button text for hidden signatures.
-Can use up to one integer format parameter, i.e. %d")
-
-(defvar notmuch-wash-citation-button-format
- "[ %d more citation lines. Click/Enter to toggle visibility. ]"
- "String used to construct button text for hidden citations.
-Can use up to one integer format parameter, i.e. %d")
+(defcustom notmuch-wash-citation-regexp "\\(^[[:space:]]*>.*\n\\)+"
+ "Pattern to match citation lines."
+ :type 'regexp
+ :group 'notmuch-wash)
-(defvar notmuch-wash-signature-lines-max 12
- "Maximum length of signature that will be hidden by default.")
+(defcustom notmuch-wash-original-regexp "^\\(--+\s?[oO]riginal [mM]essage\s?--+\\)$"
+ "Pattern to match a line that separates original message from
+reply in top-posted message."
+ :type 'regexp
+ :group 'notmuch-wash)
-(defvar notmuch-wash-citation-lines-prefix 3
+(defcustom notmuch-wash-button-signature-hidden-format
+ "[ %d-line signature. Click/Enter to show. ]"
+ "String used to construct button text for hidden signatures.
+Can use up to one integer format parameter, i.e. %d."
+ :type 'string
+ :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-signature-visible-format
+ "[ %d-line signature. Click/Enter to hide. ]"
+ "String used to construct button text for visible signatures.
+Can use up to one integer format parameter, i.e. %d."
+ :type 'string
+ :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-citation-hidden-format
+ "[ %d more citation lines. Click/Enter to show. ]"
+ "String used to construct button text for hidden citations.
+Can use up to one integer format parameter, i.e. %d."
+ :type 'string
+ :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-citation-visible-format
+ "[ %d more citation lines. Click/Enter to hide. ]"
+ "String used to construct button text for visible citations.
+Can use up to one integer format parameter, i.e. %d."
+ :type 'string
+ :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-original-hidden-format
+ "[ %d-line hidden original message. Click/Enter to show. ]"
+ "String used to construct button text for hidden citations.
+Can use up to one integer format parameter, i.e. %d."
+ :type 'string
+ :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-original-visible-format
+ "[ %d-line original message. Click/Enter to hide. ]"
+ "String used to construct button text for visible citations.
+Can use up to one integer format parameter, i.e. %d."
+ :type 'string
+ :group 'notmuch-wash)
+
+(defcustom notmuch-wash-signature-lines-max 12
+ "Maximum length of signature that will be hidden by default."
+ :type 'integer
+ :group 'notmuch-wash)
+
+(defcustom notmuch-wash-citation-lines-prefix 3
"Always show at least this many lines from the start of a citation.
If there is one more line than the sum of
`notmuch-wash-citation-lines-prefix' and
`notmuch-wash-citation-lines-suffix', show that, otherwise
-collapse the remaining lines into a button.")
+collapse the remaining lines into a button."
+ :type 'integer
+ :group 'notmuch-wash)
-(defvar notmuch-wash-citation-lines-suffix 3
+(defcustom notmuch-wash-citation-lines-suffix 3
"Always show at least this many lines from the end of a citation.
If there is one more line than the sum of
`notmuch-wash-citation-lines-prefix' and
`notmuch-wash-citation-lines-suffix', show that, otherwise
-collapse the remaining lines into a button.")
+collapse the remaining lines into a button."
+ :type 'integer
+ :group 'notmuch-wash)
+
+(defcustom notmuch-wash-wrap-lines-length nil
+ "Wrap line after at most this many characters.
+
+If this is nil, lines in messages will be wrapped to fit in the
+current window. If this is a number, lines will be wrapped after
+this many characters or at the window width (whichever one is
+lower)."
+ :type '(choice (const :tag "window width" nil)
+ (integer :tag "number of characters"))
+ :group 'notmuch-wash)
+
+(defface notmuch-wash-toggle-button
+ '((t (:inherit font-lock-comment-face)))
+ "Face used for buttons toggling the visibility of washed away
+message parts."
+ :group 'notmuch-wash
+ :group 'notmuch-faces)
+
+(defface notmuch-wash-cited-text
+ '((t (:inherit message-cited-text)))
+ "Face used for cited text."
+ :group 'notmuch-wash
+ :group 'notmuch-faces)
(defun notmuch-wash-toggle-invisible-action (cite-button)
- (let ((invis-spec (button-get cite-button 'invisibility-spec)))
- (if (invisible-p invis-spec)
- (remove-from-invisibility-spec invis-spec)
- (add-to-invisibility-spec invis-spec)))
- (force-window-update)
- (redisplay t))
+ ;; Toggle overlay visibility
+ (let ((overlay (button-get cite-button 'overlay)))
+ (overlay-put overlay 'invisible (not (overlay-get overlay 'invisible))))
+ ;; Update button text
+ (let* ((new-start (button-start cite-button))
+ (overlay (button-get cite-button 'overlay))
+ (button-label (notmuch-wash-button-label overlay))
+ (old-point (point))
+ (properties (text-properties-at (point)))
+ (inhibit-read-only t))
+ (goto-char new-start)
+ (insert button-label)
+ (set-text-properties new-start (point) properties)
+ (let ((old-end (button-end cite-button)))
+ (move-overlay cite-button new-start (point))
+ (delete-region (point) old-end))
+ (goto-char (min old-point (1- (button-end cite-button))))))
(define-button-type 'notmuch-wash-button-invisibility-toggle-type
'action 'notmuch-wash-toggle-invisible-action
'follow-link t
- 'face 'font-lock-comment-face)
+ 'face 'notmuch-wash-toggle-button
+ :supertype 'notmuch-button-type)
(define-button-type 'notmuch-wash-button-citation-toggle-type
'help-echo "mouse-1, RET: Show citation"
'help-echo "mouse-1, RET: Show signature"
:supertype 'notmuch-wash-button-invisibility-toggle-type)
+(define-button-type 'notmuch-wash-button-original-toggle-type
+ 'help-echo "mouse-1, RET: Show original message"
+ :supertype 'notmuch-wash-button-invisibility-toggle-type)
+
(defun notmuch-wash-region-isearch-show (overlay)
- (remove-from-invisibility-spec (overlay-get overlay 'invisible)))
+ (notmuch-wash-toggle-invisible-action
+ (overlay-get overlay 'notmuch-wash-button)))
+
+(defun notmuch-wash-button-label (overlay)
+ (let* ((type (overlay-get overlay 'type))
+ (invis-spec (overlay-get overlay 'invisible))
+ (state (if (invisible-p invis-spec) "hidden" "visible"))
+ (label-format (symbol-value (intern-soft (concat "notmuch-wash-button-"
+ type "-" state "-format"))))
+ (lines-count (count-lines (overlay-start overlay) (overlay-end overlay))))
+ (format label-format lines-count)))
-(defun notmuch-wash-region-to-button (beg end type prefix button-text)
- "Auxilary function to do the actual making of overlays and buttons
+(defun notmuch-wash-region-to-button (msg beg end type &optional prefix)
+ "Auxiliary function to do the actual making of overlays and buttons
BEG and END are buffer locations. TYPE should a string, either
-\"citation\" or \"signature\". PREFIX is some arbitrary text to
-insert before the button, probably for indentation. BUTTON-TEXT
-is what to put on the button."
+\"citation\" or \"signature\". Optional PREFIX is some arbitrary
+text to insert before the button, probably for indentation. Note
+that PREFIX should not include a newline."
;; This uses some slightly tricky conversions between strings and
;; symbols because of the way the button code works. Note that
;; since the newly created symbol has no plist.
(let ((overlay (make-overlay beg end))
- (invis-spec (make-symbol (concat "notmuch-" type "-region")))
(button-type (intern-soft (concat "notmuch-wash-button-"
type "-toggle-type"))))
- (add-to-invisibility-spec invis-spec)
- (overlay-put overlay 'invisible invis-spec)
+ (overlay-put overlay 'invisible t)
(overlay-put overlay 'isearch-open-invisible #'notmuch-wash-region-isearch-show)
+ (overlay-put overlay 'type type)
(goto-char (1+ end))
(save-excursion
- (goto-char (1- beg))
- (insert prefix)
- (insert-button button-text
- 'invisibility-spec invis-spec
- :type button-type))))
-
-(defun notmuch-wash-excerpt-citations (depth)
+ (goto-char beg)
+ (if prefix
+ (insert-before-markers prefix))
+ (let ((button-beg (point)))
+ (insert-before-markers (notmuch-wash-button-label overlay) "\n")
+ (let ((button (make-button button-beg (1- (point))
+ 'overlay overlay
+ :type button-type)))
+ (overlay-put overlay 'notmuch-wash-button button))))))
+
+(defun notmuch-wash-excerpt-citations (msg depth)
"Excerpt citations and up to one signature."
(goto-char (point-min))
(beginning-of-line)
+ (if (and (< (point) (point-max))
+ (re-search-forward notmuch-wash-original-regexp nil t))
+ (let* ((msg-start (match-beginning 0))
+ (msg-end (point-max))
+ (msg-lines (count-lines msg-start msg-end)))
+ (notmuch-wash-region-to-button
+ msg msg-start msg-end "original")))
(while (and (< (point) (point-max))
(re-search-forward notmuch-wash-citation-regexp nil t))
(let* ((cite-start (match-beginning 0))
(cite-end (match-end 0))
(cite-lines (count-lines cite-start cite-end)))
- (overlay-put (make-overlay cite-start cite-end) 'face 'message-cited-text-face)
+ (overlay-put (make-overlay cite-start cite-end) 'face 'notmuch-wash-cited-text)
(when (> cite-lines (+ notmuch-wash-citation-lines-prefix
notmuch-wash-citation-lines-suffix
1))
(goto-char cite-end)
(forward-line (- notmuch-wash-citation-lines-suffix))
(notmuch-wash-region-to-button
- hidden-start (point-marker)
- "citation" "\n"
- (format notmuch-wash-citation-button-format
- (- cite-lines
- notmuch-wash-citation-lines-prefix
- notmuch-wash-citation-lines-suffix)))))))
+ msg hidden-start (point-marker)
+ "citation")))))
(if (and (not (eobp))
(re-search-forward notmuch-wash-signature-regexp nil t))
(let* ((sig-start (match-beginning 0))
(sig-end-marker (make-marker)))
(set-marker sig-start-marker sig-start)
(set-marker sig-end-marker (point-max))
- (overlay-put (make-overlay sig-start-marker sig-end-marker) 'face 'message-cited-text-face)
+ (overlay-put (make-overlay sig-start-marker sig-end-marker) 'face 'message-cited-text)
(notmuch-wash-region-to-button
- sig-start-marker sig-end-marker
- "signature" "\n"
- (format notmuch-wash-signature-button-format sig-lines)))))))
+ msg sig-start-marker sig-end-marker
+ "signature"))))))
;;
-(defun notmuch-wash-elide-blank-lines (depth)
+(defun notmuch-wash-elide-blank-lines (msg depth)
"Elide leading, trailing and successive blank lines."
;; Algorithm derived from `article-strip-multiple-blank-lines' in
;;
-(defun notmuch-wash-tidy-citations (depth)
+(defun notmuch-wash-tidy-citations (msg depth)
"Improve the display of cited regions of a message.
Perform several transformations on the message body:
;;
-(defun notmuch-wash-wrap-long-lines (depth)
- "Wrap any long lines in the message to the width of the window.
-
-When doing so, maintaining citation leaders in the wrapped text."
-
- (let ((coolj-wrap-follows-window-size nil)
- (fill-column (- (window-width)
- depth
- ;; 2 to avoid poor interaction with
- ;; `word-wrap'.
- 2)))
+(defun notmuch-wash-wrap-long-lines (msg depth)
+ "Wrap long lines in the message.
+
+If `notmuch-wash-wrap-lines-length' is a number, this will wrap
+the message lines to the minimum of the width of the window or
+its value. Otherwise, this function will wrap long lines in the
+message at the window width. When doing so, citation leaders in
+the wrapped text are maintained."
+
+ (let* ((coolj-wrap-follows-window-size nil)
+ (limit (if (numberp notmuch-wash-wrap-lines-length)
+ (min notmuch-wash-wrap-lines-length
+ (window-width))
+ (window-width)))
+ (fill-column (- limit
+ depth
+ ;; 2 to avoid poor interaction with
+ ;; `word-wrap'.
+ 2)))
(coolj-wrap-region (point-min) (point-max))))
;;
(defvar diff-file-header-re) ; From `diff-mode.el'.
-(defun notmuch-wash-convert-inline-patch-to-part (depth)
+(defun notmuch-wash-subject-to-filename (subject &optional maxlen)
+ "Convert a mail SUBJECT into a filename.
+
+The resulting filename is similar to the names generated by \"git
+format-patch\", without the leading patch sequence number
+\"0001-\" and \".patch\" extension. Any leading \"[PREFIX]\"
+style strings are removed prior to conversion.
+
+Optional argument MAXLEN is the maximum length of the resulting
+filename, before trimming any trailing . and - characters."
+ (let* ((s (replace-regexp-in-string "^ *\\(\\[[^]]*\\] *\\)*" "" subject))
+ (s (replace-regexp-in-string "[^A-Za-z0-9._]+" "-" s))
+ (s (replace-regexp-in-string "\\.+" "." s))
+ (s (if maxlen (substring s 0 (min (length s) maxlen)) s))
+ (s (replace-regexp-in-string "[.-]*$" "" s)))
+ s))
+
+(defun notmuch-wash-subject-to-patch-sequence-number (subject)
+ "Convert a patch mail SUBJECT into a patch sequence number.
+
+Return the patch sequence number N from the last \"[PATCH N/M]\"
+style prefix in SUBJECT, or nil if such a prefix can't be found."
+ (when (string-match
+ "^ *\\(\\[[^]]*\\] *\\)*\\[[^]]*?\\([0-9]+\\)/[0-9]+[^]]*\\].*"
+ subject)
+ (string-to-number (substring subject (match-beginning 2) (match-end 2)))))
+
+(defun notmuch-wash-subject-to-patch-filename (subject)
+ "Convert a patch mail SUBJECT into a filename.
+
+The resulting filename is similar to the names generated by \"git
+format-patch\". If the patch mail was generated and sent using
+\"git format-patch/send-email\", this should re-create the
+original filename the sender had."
+ (format "%04d-%s.patch"
+ (or (notmuch-wash-subject-to-patch-sequence-number subject) 1)
+ (notmuch-wash-subject-to-filename subject 52)))
+
+(defun notmuch-wash-convert-inline-patch-to-part (msg depth)
"Convert an inline patch into a fake 'text/x-diff' attachment.
Given that this function guesses whether a buffer includes a
for error."
(goto-char (point-min))
- (if (re-search-forward diff-file-header-re nil t)
- (progn
- (beginning-of-line -1)
- (let ((patch-start (point))
- (patch-end (point-max))
- part)
- (goto-char patch-start)
- (if (or
- ;; Patch ends with signature.
- (re-search-forward notmuch-wash-signature-regexp nil t)
- ;; Patch ends with bugtraq comment.
- (re-search-forward "^\\*\\*\\* " nil t))
- (setq patch-end (match-beginning 0)))
- (save-restriction
- (narrow-to-region patch-start patch-end)
- (setq part (plist-put part :content-type "text/x-diff"))
- (setq part (plist-put part :content (buffer-string)))
- (setq part (plist-put part :id -1))
- (setq part (plist-put part :filename "inline patch"))
- (delete-region (point-min) (point-max))
- (notmuch-show-insert-bodypart nil part depth))))))
+ (when (re-search-forward diff-file-header-re nil t)
+ (beginning-of-line -1)
+ (let ((patch-start (point))
+ (patch-end (point-max))
+ part)
+ (goto-char patch-start)
+ (if (or
+ ;; Patch ends with signature.
+ (re-search-forward notmuch-wash-signature-regexp nil t)
+ ;; Patch ends with bugtraq comment.
+ (re-search-forward "^\\*\\*\\* " nil t))
+ (setq patch-end (match-beginning 0)))
+ (save-restriction
+ (narrow-to-region patch-start patch-end)
+ (setq part (plist-put part :content-type "inline patch"))
+ (setq part (plist-put part :content (buffer-string)))
+ (setq part (plist-put part :id -1))
+ (setq part (plist-put part :filename
+ (notmuch-wash-subject-to-patch-filename
+ (plist-get
+ (plist-get msg :headers) :Subject))))
+ (delete-region (point-min) (point-max))
+ (notmuch-show-insert-bodypart nil part depth)))))
;;