Use different labels for wash buttons when text is visible or hidden.
[notmuch] / emacs / notmuch-wash.el
1 ;; notmuch-wash.el --- cleaning up message bodies
2 ;;
3 ;; Copyright © Carl Worth
4 ;; Copyright © David Edmondson
5 ;;
6 ;; This file is part of Notmuch.
7 ;;
8 ;; Notmuch is free software: you can redistribute it and/or modify it
9 ;; under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
12 ;;
13 ;; Notmuch is distributed in the hope that it will be useful, but
14 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 ;; General Public License for more details.
17 ;;
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
20 ;;
21 ;; Authors: Carl Worth <cworth@cworth.org>
22 ;;          David Edmondson <dme@dme.org>
23
24 (require 'coolj)
25
26 (declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth))
27
28 ;;
29
30 (defvar notmuch-wash-signature-regexp
31   "^\\(-- ?\\|_+\\)$"
32   "Pattern to match a line that separates content from signature.")
33
34 (defvar notmuch-wash-citation-regexp
35   "\\(^[[:space:]]*>.*\n\\)+"
36   "Pattern to match citation lines.")
37
38 (defvar notmuch-wash-button-signature-hidden-format
39   "[ %d-line signature. Click/Enter to show. ]"
40   "String used to construct button text for hidden signatures.
41 Can use up to one integer format parameter, i.e. %d")
42
43 (defvar notmuch-wash-button-signature-visible-format
44   "[ %d-line signature. Click/Enter to hide. ]"
45   "String used to construct button text for visible signatures.
46 Can use up to one integer format parameter, i.e. %d")
47
48 (defvar notmuch-wash-button-citation-hidden-format
49   "[ %d more citation lines. Click/Enter to show. ]"
50   "String used to construct button text for hidden citations.
51 Can use up to one integer format parameter, i.e. %d")
52
53 (defvar notmuch-wash-button-citation-visible-format
54   "[ %d more citation lines. Click/Enter to hide. ]"
55   "String used to construct button text for visible citations.
56 Can use up to one integer format parameter, i.e. %d")
57
58 (defvar notmuch-wash-signature-lines-max 12
59   "Maximum length of signature that will be hidden by default.")
60
61 (defvar notmuch-wash-citation-lines-prefix 3
62   "Always show at least this many lines from the start of a citation.
63
64 If there is one more line than the sum of
65 `notmuch-wash-citation-lines-prefix' and
66 `notmuch-wash-citation-lines-suffix', show that, otherwise
67 collapse the remaining lines into a button.")
68
69 (defvar notmuch-wash-citation-lines-suffix 3
70   "Always show at least this many lines from the end of a citation.
71
72 If there is one more line than the sum of
73 `notmuch-wash-citation-lines-prefix' and
74 `notmuch-wash-citation-lines-suffix', show that, otherwise
75 collapse the remaining lines into a button.")
76
77 (defun notmuch-wash-toggle-invisible-action (cite-button)
78   (let ((invis-spec (button-get cite-button 'invisibility-spec)))
79     (if (invisible-p invis-spec)
80         (remove-from-invisibility-spec invis-spec)
81       (add-to-invisibility-spec invis-spec)))
82   (let* ((new-start (button-start cite-button))
83          (overlay (button-get cite-button 'overlay))
84          (button-label (notmuch-wash-button-label overlay))
85          (inhibit-read-only t))
86     (save-excursion
87       (goto-char new-start)
88       (insert button-label)
89       (let ((old-end (button-end cite-button)))
90         (move-overlay cite-button new-start (point))
91         (delete-region (point) old-end))))
92   (force-window-update)
93   (redisplay t))
94
95 (define-button-type 'notmuch-wash-button-invisibility-toggle-type
96   'action 'notmuch-wash-toggle-invisible-action
97   'follow-link t
98   'face 'font-lock-comment-face)
99
100 (define-button-type 'notmuch-wash-button-citation-toggle-type
101   'help-echo "mouse-1, RET: Show citation"
102   :supertype 'notmuch-wash-button-invisibility-toggle-type)
103
104 (define-button-type 'notmuch-wash-button-signature-toggle-type
105   'help-echo "mouse-1, RET: Show signature"
106   :supertype 'notmuch-wash-button-invisibility-toggle-type)
107
108 (defun notmuch-wash-region-isearch-show (overlay)
109   (remove-from-invisibility-spec (overlay-get overlay 'invisible)))
110
111 (defun notmuch-wash-button-label (overlay)
112   (let* ((type (overlay-get overlay 'type))
113          (invis-spec (overlay-get overlay 'invisible))
114          (state (if (invisible-p invis-spec) "hidden" "visible"))
115          (label-format (symbol-value (intern-soft (concat "notmuch-wash-button-"
116                                                           type "-" state "-format"))))
117          (lines-count (count-lines (overlay-start overlay) (overlay-end overlay))))
118     (format label-format lines-count)))
119
120 (defun notmuch-wash-region-to-button (beg end type prefix)
121   "Auxilary function to do the actual making of overlays and buttons
122
123 BEG and END are buffer locations. TYPE should a string, either
124 \"citation\" or \"signature\". PREFIX is some arbitrary text to
125 insert before the button, probably for indentation."
126
127   ;; This uses some slightly tricky conversions between strings and
128   ;; symbols because of the way the button code works. Note that
129   ;; replacing intern-soft with make-symbol will cause this to fail,
130   ;; since the newly created symbol has no plist.
131
132   (let ((overlay (make-overlay beg end))
133         (invis-spec (make-symbol (concat "notmuch-" type "-region")))
134         (button-type (intern-soft (concat "notmuch-wash-button-"
135                                           type "-toggle-type"))))
136     (add-to-invisibility-spec invis-spec)
137     (overlay-put overlay 'invisible invis-spec)
138     (overlay-put overlay 'isearch-open-invisible #'notmuch-wash-region-isearch-show)
139     (overlay-put overlay 'type type)
140     (goto-char (1+ end))
141     (save-excursion
142       (goto-char (1- beg))
143       (insert prefix)
144       (insert-button (notmuch-wash-button-label overlay)
145                      'invisibility-spec invis-spec
146                      'overlay overlay
147                      :type button-type))))
148
149 (defun notmuch-wash-excerpt-citations (depth)
150   "Excerpt citations and up to one signature."
151   (goto-char (point-min))
152   (beginning-of-line)
153   (while (and (< (point) (point-max))
154               (re-search-forward notmuch-wash-citation-regexp nil t))
155     (let* ((cite-start (match-beginning 0))
156            (cite-end (match-end 0))
157            (cite-lines (count-lines cite-start cite-end)))
158       (overlay-put (make-overlay cite-start cite-end) 'face 'message-cited-text-face)
159       (when (> cite-lines (+ notmuch-wash-citation-lines-prefix
160                              notmuch-wash-citation-lines-suffix
161                              1))
162         (goto-char cite-start)
163         (forward-line notmuch-wash-citation-lines-prefix)
164         (let ((hidden-start (point-marker)))
165           (goto-char cite-end)
166           (forward-line (- notmuch-wash-citation-lines-suffix))
167           (notmuch-wash-region-to-button
168            hidden-start (point-marker)
169            "citation" "\n")))))
170   (if (and (not (eobp))
171            (re-search-forward notmuch-wash-signature-regexp nil t))
172       (let* ((sig-start (match-beginning 0))
173              (sig-end (match-end 0))
174              (sig-lines (count-lines sig-start (point-max))))
175         (if (<= sig-lines notmuch-wash-signature-lines-max)
176             (let ((sig-start-marker (make-marker))
177                   (sig-end-marker (make-marker)))
178               (set-marker sig-start-marker sig-start)
179               (set-marker sig-end-marker (point-max))
180               (overlay-put (make-overlay sig-start-marker sig-end-marker) 'face 'message-cited-text-face)
181               (notmuch-wash-region-to-button
182                sig-start-marker sig-end-marker
183                "signature" "\n"))))))
184
185 ;;
186
187 (defun notmuch-wash-elide-blank-lines (depth)
188   "Elide leading, trailing and successive blank lines."
189
190   ;; Algorithm derived from `article-strip-multiple-blank-lines' in
191   ;; `gnus-art.el'.
192
193   ;; Make all blank lines empty.
194   (goto-char (point-min))
195   (while (re-search-forward "^[[:space:]\t]+$" nil t)
196     (replace-match "" nil t))
197
198   ;; Replace multiple empty lines with a single empty line.
199   (goto-char (point-min))
200   (while (re-search-forward "^\n\\(\n+\\)" nil t)
201     (delete-region (match-beginning 1) (match-end 1)))
202
203   ;; Remove a leading blank line.
204   (goto-char (point-min))
205   (if (looking-at "\n")
206       (delete-region (match-beginning 0) (match-end 0)))
207
208   ;; Remove a trailing blank line.
209   (goto-char (point-max))
210   (if (looking-at "\n")
211       (delete-region (match-beginning 0) (match-end 0))))
212
213 ;;
214
215 (defun notmuch-wash-tidy-citations (depth)
216   "Improve the display of cited regions of a message.
217
218 Perform several transformations on the message body:
219
220 - Remove lines of repeated citation leaders with no other
221   content,
222 - Remove citation leaders standing alone before a block of cited
223   text,
224 - Remove citation trailers standing alone after a block of cited
225   text."
226
227   ;; Remove lines of repeated citation leaders with no other content.
228   (goto-char (point-min))
229   (while (re-search-forward "\\(^>[> ]*\n\\)\\{2,\\}" nil t)
230     (replace-match "\\1"))
231
232   ;; Remove citation leaders standing alone before a block of cited
233   ;; text.
234   (goto-char (point-min))
235   (while (re-search-forward "\\(\n\\|^[^>].*\\)\n\\(^>[> ]*\n\\)" nil t)
236     (replace-match "\\1\n"))
237
238   ;; Remove citation trailers standing alone after a block of cited
239   ;; text.
240   (goto-char (point-min))
241   (while (re-search-forward "\\(^>[> ]*\n\\)\\(^$\\|^[^>].*\\)" nil t)
242     (replace-match "\\2")))
243
244 ;;
245
246 (defun notmuch-wash-wrap-long-lines (depth)
247   "Wrap any long lines in the message to the width of the window.
248
249 When doing so, maintaining citation leaders in the wrapped text."
250
251   (let ((coolj-wrap-follows-window-size nil)
252         (fill-column (- (window-width)
253                         depth
254                         ;; 2 to avoid poor interaction with
255                         ;; `word-wrap'.
256                         2)))
257     (coolj-wrap-region (point-min) (point-max))))
258
259 ;;
260
261 (require 'diff-mode)
262
263 (defvar diff-file-header-re) ; From `diff-mode.el'.
264
265 (defun notmuch-wash-convert-inline-patch-to-part (depth)
266   "Convert an inline patch into a fake 'text/x-diff' attachment.
267
268 Given that this function guesses whether a buffer includes a
269 patch and then guesses the extent of the patch, there is scope
270 for error."
271
272   (goto-char (point-min))
273   (if (re-search-forward diff-file-header-re nil t)
274       (progn
275         (beginning-of-line -1)
276         (let ((patch-start (point))
277               (patch-end (point-max))
278               part)
279           (goto-char patch-start)
280           (if (or
281                ;; Patch ends with signature.
282                (re-search-forward notmuch-wash-signature-regexp nil t)
283                ;; Patch ends with bugtraq comment.
284                (re-search-forward "^\\*\\*\\* " nil t))
285               (setq patch-end (match-beginning 0)))
286           (save-restriction
287             (narrow-to-region patch-start patch-end)
288             (setq part (plist-put part :content-type "text/x-diff"))
289             (setq part (plist-put part :content (buffer-string)))
290             (setq part (plist-put part :id -1))
291             (setq part (plist-put part :filename "inline patch"))
292             (delete-region (point-min) (point-max))
293             (notmuch-show-insert-bodypart nil part depth))))))
294
295 ;;
296
297 (provide 'notmuch-wash)