Mercurial > emacs
comparison lisp/mail/mh-seq.el @ 48595:8aaba207e44b
Upgraded to MH-E version 7.0.
author | Bill Wohler <wohler@newt.com> |
---|---|
date | Fri, 29 Nov 2002 18:15:21 +0000 |
parents | 2568d5a27317 |
children | 30c4902b654d |
comparison
equal
deleted
inserted
replaced
48594:1ccd9291b0b2 | 48595:8aaba207e44b |
---|---|
1 ;;; mh-seq.el --- mh-e sequences support | 1 ;;; mh-seq.el --- MH-E sequences support |
2 | 2 |
3 ;; Copyright (C) 1993, 1995, 2001, 2002 Free Software Foundation, Inc. | 3 ;; Copyright (C) 1993, 1995, 2001, 2002 Free Software Foundation, Inc. |
4 | 4 |
5 ;; Author: Bill Wohler <wohler@newt.com> | 5 ;; Author: Bill Wohler <wohler@newt.com> |
6 ;; Maintainer: Bill Wohler <wohler@newt.com> | 6 ;; Maintainer: Bill Wohler <wohler@newt.com> |
23 ;; along with GNU Emacs; see the file COPYING. If not, write to the | 23 ;; along with GNU Emacs; see the file COPYING. If not, write to the |
24 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, | 24 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, |
25 ;; Boston, MA 02111-1307, USA. | 25 ;; Boston, MA 02111-1307, USA. |
26 | 26 |
27 ;;; Commentary: | 27 ;;; Commentary: |
28 | 28 ;; |
29 ;; Internal support for mh-e package. | 29 ;; This tries to implement the algorithm described at: |
30 ;; http://www.jwz.org/doc/threading.html | |
31 ;; It is also a start to implementing the IMAP Threading extension RFC. The | |
32 ;; implementation lacks the reference and subject canonicalization of the | |
33 ;; RFC. | |
34 ;; | |
35 ;; In the presentation buffer, children messages are shown indented with | |
36 ;; either [ ] or < > around them. Square brackets ([ ]) denote that the | |
37 ;; algorithm can point out some headers which when taken together implies | |
38 ;; that the unindented message is an ancestor of the indented message. If | |
39 ;; no such proof exists then angles (< >) are used. | |
40 ;; | |
41 ;; Some issues and problems are as follows: | |
42 ;; | |
43 ;; (1) Scan truncates the fields at length 512. So longer references: | |
44 ;; headers get mutilated. The same kind of MH format string works when | |
45 ;; composing messages. Is there a way to avoid this? My scan command | |
46 ;; is as follows: | |
47 ;; scan +folder -width 10000 \ | |
48 ;; -format "%(msg)\n%{message-id}\n%{references}\n%{subject}\n" | |
49 ;; I would really appreciate it if someone would help me with this. | |
50 ;; | |
51 ;; (2) Implement heuristics to recognize message-id's in In-Reply-To: | |
52 ;; header. Right now it just assumes that the last text between angles | |
53 ;; (< and >) is the message-id. There is the chance that this will | |
54 ;; incorrectly use an email address like a message-id. | |
55 ;; | |
56 ;; (3) Error checking of found message-id's should be done. | |
57 ;; | |
58 ;; (4) Since this breaks the assumption that message indices increase as | |
59 ;; one goes down the buffer, the binary search based mh-goto-msg | |
60 ;; doesn't work. I have a simpler replacement which may be less | |
61 ;; efficient. | |
62 ;; | |
63 ;; (5) Better canonicalizing for message-id and subject strings. | |
64 ;; | |
65 | |
66 ;; Internal support for MH-E package. | |
30 | 67 |
31 ;;; Change Log: | 68 ;;; Change Log: |
32 | 69 |
33 ;; $Id: mh-seq.el,v 1.14 2002/04/07 19:20:56 wohler Exp $ | 70 ;; $Id: mh-seq.el,v 1.71 2002/11/14 20:41:12 wohler Exp $ |
34 | 71 |
35 ;;; Code: | 72 ;;; Code: |
36 | 73 |
37 (provide 'mh-seq) | 74 (require 'cl) |
38 (require 'mh-e) | 75 (require 'mh-e) |
39 | 76 |
77 ;; Shush the byte-compiler | |
78 (defvar tool-bar-mode) | |
79 | |
80 ;;; Data structures (used in message threading)... | |
81 (defstruct (mh-thread-message (:conc-name mh-message-) | |
82 (:constructor mh-thread-make-message)) | |
83 (id nil) | |
84 (references ()) | |
85 (subject "") | |
86 (subject-re-p nil)) | |
87 | |
88 (defstruct (mh-thread-container (:conc-name mh-container-) | |
89 (:constructor mh-thread-make-container)) | |
90 message parent children | |
91 (real-child-p t)) | |
92 | |
93 | |
40 ;;; Internal variables: | 94 ;;; Internal variables: |
41 | 95 (defvar mh-last-seq-used nil |
42 (defvar mh-last-seq-used nil) ;Name of seq to which a msg was last added. | 96 "Name of seq to which a msg was last added.") |
43 | 97 |
44 (defvar mh-non-seq-mode-line-annotation nil) ;Saved value of mh-mode-line-annotation when narrowed to a seq. | 98 (defvar mh-non-seq-mode-line-annotation nil |
45 | 99 "Saved value of `mh-mode-line-annotation' when narrowed to a seq.") |
100 | |
101 ;;; Maps and hashes... | |
102 (defvar mh-thread-id-hash nil | |
103 "Hashtable used to canonicalize message-id strings.") | |
104 (defvar mh-thread-subject-hash nil | |
105 "Hashtable used to canonicalize subject strings.") | |
106 (defvar mh-thread-id-table nil | |
107 "Thread ID table maps from message-id's to message containers.") | |
108 (defvar mh-thread-id-index-map nil | |
109 "Table to lookup message index number from message-id.") | |
110 (defvar mh-thread-index-id-map nil | |
111 "Table to lookup message-id from message index.") | |
112 (defvar mh-thread-scan-line-map nil | |
113 "Map of message index to various parts of the scan line.") | |
114 (defvar mh-thread-old-scan-line-map nil | |
115 "Old map of message index to various parts of the scan line. | |
116 This is the original map that is stored when the folder is narrowed.") | |
117 (defvar mh-thread-subject-container-hash nil | |
118 "Hashtable used to group messages by subject.") | |
119 (defvar mh-thread-duplicates nil | |
120 "Hashtable used to remember multiple messages with the same message-id.") | |
121 (defvar mh-thread-history () | |
122 "Variable to remember the transformations to the thread tree. | |
123 When new messages are added, these transformations are rewound, then the | |
124 links are added from the newly seen messages. Finally the transformations are | |
125 redone to get the new thread tree. This makes incremental threading easier.") | |
126 (defvar mh-thread-body-width nil | |
127 "Width of scan substring that contains subject and body of message.") | |
128 | |
129 (make-variable-buffer-local 'mh-thread-id-hash) | |
130 (make-variable-buffer-local 'mh-thread-subject-hash) | |
131 (make-variable-buffer-local 'mh-thread-id-table) | |
132 (make-variable-buffer-local 'mh-thread-id-index-map) | |
133 (make-variable-buffer-local 'mh-thread-index-id-map) | |
134 (make-variable-buffer-local 'mh-thread-scan-line-map) | |
135 (make-variable-buffer-local 'mh-thread-old-scan-line-map) | |
136 (make-variable-buffer-local 'mh-thread-subject-container-hash) | |
137 (make-variable-buffer-local 'mh-thread-duplicates) | |
138 (make-variable-buffer-local 'mh-thread-history) | |
46 | 139 |
47 (defun mh-delete-seq (sequence) | 140 (defun mh-delete-seq (sequence) |
48 "Delete the SEQUENCE." | 141 "Delete the SEQUENCE." |
49 (interactive (list (mh-read-seq-default "Delete" t))) | 142 (interactive (list (mh-read-seq-default "Delete" t))) |
50 (mh-map-to-seq-msgs 'mh-notate-if-in-one-seq sequence ? (1+ mh-cmd-note) | 143 (mh-map-to-seq-msgs 'mh-notate-if-in-one-seq sequence ? (1+ mh-cmd-note) |
51 sequence) | 144 sequence) |
52 (mh-undefine-sequence sequence '("all")) | 145 (mh-undefine-sequence sequence '("all")) |
53 (mh-delete-seq-locally sequence)) | 146 (mh-delete-seq-locally sequence)) |
54 | 147 |
148 ;; Avoid compiler warnings | |
149 (defvar view-exit-action) | |
55 | 150 |
56 (defun mh-list-sequences (folder) | 151 (defun mh-list-sequences (folder) |
57 "List the sequences defined in FOLDER." | 152 "List the sequences defined in FOLDER." |
58 (interactive (list (mh-prompt-for-folder "List sequences in" | 153 (interactive (list (mh-prompt-for-folder "List sequences in" |
59 mh-current-folder t))) | 154 mh-current-folder t))) |
84 (goto-char (point-min)) | 179 (goto-char (point-min)) |
85 (view-mode 1) | 180 (view-mode 1) |
86 (setq view-exit-action 'kill-buffer) | 181 (setq view-exit-action 'kill-buffer) |
87 (message "Listing sequences...done"))))) | 182 (message "Listing sequences...done"))))) |
88 | 183 |
89 | |
90 (defun mh-msg-is-in-seq (message) | 184 (defun mh-msg-is-in-seq (message) |
91 "Display the sequences that contain MESSAGE (default: current message)." | 185 "Display the sequences that contain MESSAGE (default: current message)." |
92 (interactive (list (mh-get-msg-num t))) | 186 (interactive (list (mh-get-msg-num t))) |
93 (message "Message %d is in sequences: %s" | 187 (let* ((dest-folder (loop for seq in mh-refile-list |
94 message | 188 when (member message (cdr seq)) |
95 (mapconcat 'concat | 189 return (car seq))) |
96 (mh-list-to-string (mh-seq-containing-msg message t)) | 190 (deleted-flag (unless dest-folder (member message mh-delete-list)))) |
97 " "))) | 191 (message "Message %d%s is in sequences: %s" |
98 | 192 message |
193 (cond (dest-folder (format " (to be refiled to %s)" dest-folder)) | |
194 (deleted-flag (format " (to be deleted)")) | |
195 (t "")) | |
196 (mapconcat 'concat | |
197 (mh-list-to-string (mh-seq-containing-msg message t)) | |
198 " ")))) | |
99 | 199 |
100 (defun mh-narrow-to-seq (sequence) | 200 (defun mh-narrow-to-seq (sequence) |
101 "Restrict display of this folder to just messages in SEQUENCE. | 201 "Restrict display of this folder to just messages in SEQUENCE. |
102 Use \\<mh-folder-mode-map>\\[mh-widen] to undo this command." | 202 Use \\<mh-folder-mode-map>\\[mh-widen] to undo this command." |
103 (interactive (list (mh-read-seq "Narrow to" t))) | 203 (interactive (list (mh-read-seq "Narrow to" t))) |
104 (with-mh-folder-updating (t) | 204 (with-mh-folder-updating (t) |
105 (cond ((mh-seq-to-msgs sequence) | 205 (cond ((mh-seq-to-msgs sequence) |
106 (mh-widen) | 206 (mh-widen) |
107 (let ((eob (point-max))) | 207 (mh-remove-all-notation) |
108 (mh-copy-seq-to-point sequence eob) | 208 (let ((eob (point-max)) |
109 (narrow-to-region eob (point-max)) | 209 (msg-at-cursor (mh-get-msg-num nil))) |
210 (setq mh-thread-old-scan-line-map mh-thread-scan-line-map) | |
211 (setq mh-thread-scan-line-map (make-hash-table :test #'eql)) | |
212 (mh-copy-seq-to-eob sequence) | |
213 (narrow-to-region eob (point-max)) | |
214 (mh-notate-user-sequences) | |
215 (mh-notate-deleted-and-refiled) | |
216 (mh-notate-seq 'cur mh-note-cur mh-cmd-note) | |
217 (when msg-at-cursor (mh-goto-msg msg-at-cursor t t)) | |
110 (make-variable-buffer-local 'mh-non-seq-mode-line-annotation) | 218 (make-variable-buffer-local 'mh-non-seq-mode-line-annotation) |
111 (setq mh-non-seq-mode-line-annotation mh-mode-line-annotation) | 219 (setq mh-non-seq-mode-line-annotation mh-mode-line-annotation) |
112 (setq mh-mode-line-annotation (symbol-name sequence)) | 220 (setq mh-mode-line-annotation (symbol-name sequence)) |
113 (mh-make-folder-mode-line) | 221 (mh-make-folder-mode-line) |
114 (mh-recenter nil) | 222 (mh-recenter nil) |
115 (if (and (boundp 'tool-bar-mode) tool-bar-mode) | 223 (if (and (boundp 'tool-bar-mode) tool-bar-mode) |
116 (set (make-local-variable 'tool-bar-map) | 224 (set (make-local-variable 'tool-bar-map) |
117 mh-folder-seq-tool-bar-map)) | 225 mh-folder-seq-tool-bar-map)) |
118 (setq mh-narrowed-to-seq sequence))) | 226 (setq mh-narrowed-to-seq sequence) |
227 (push 'widen mh-view-ops))) | |
119 (t | 228 (t |
120 (error "No messages in sequence `%s'" (symbol-name sequence)))))) | 229 (error "No messages in sequence `%s'" (symbol-name sequence)))))) |
121 | 230 |
122 | |
123 (defun mh-put-msg-in-seq (msg-or-seq sequence) | 231 (defun mh-put-msg-in-seq (msg-or-seq sequence) |
124 "Add MSG-OR-SEQ (default: displayed message) to SEQUENCE. | 232 "Add MSG-OR-SEQ (default: displayed message) to SEQUENCE. |
125 If optional prefix argument provided, then prompt for the message sequence." | 233 If optional prefix argument provided, then prompt for the message sequence. |
126 (interactive (list (if current-prefix-arg | 234 If variable `transient-mark-mode' is non-nil and the mark is active, then |
127 (mh-read-seq-default "Add messages from" t) | 235 the selected region is added to the sequence." |
128 (mh-get-msg-num t)) | 236 (interactive (list (cond |
237 ((mh-mark-active-p t) | |
238 (mh-region-to-sequence (region-beginning) (region-end)) | |
239 'region) | |
240 (current-prefix-arg | |
241 (mh-read-seq-default "Add messages from" t)) | |
242 (t | |
243 (mh-get-msg-num t))) | |
129 (mh-read-seq-default "Add to" nil))) | 244 (mh-read-seq-default "Add to" nil))) |
130 (if (not (mh-internal-seq sequence)) | 245 (if (not (mh-internal-seq sequence)) |
131 (setq mh-last-seq-used sequence)) | 246 (setq mh-last-seq-used sequence)) |
132 (mh-add-msgs-to-seq (if (numberp msg-or-seq) | 247 (mh-add-msgs-to-seq (if (numberp msg-or-seq) |
133 msg-or-seq | 248 msg-or-seq |
134 (mh-seq-to-msgs msg-or-seq)) | 249 (mh-seq-to-msgs msg-or-seq)) |
135 sequence)) | 250 sequence)) |
136 | 251 |
252 (defun mh-valid-view-change-operation-p (op) | |
253 "Check if the view change operation can be performed. | |
254 OP is one of 'widen and 'unthread." | |
255 (cond ((eq (car mh-view-ops) op) | |
256 (pop mh-view-ops)) | |
257 (t nil))) | |
137 | 258 |
138 (defun mh-widen () | 259 (defun mh-widen () |
139 "Remove restrictions from current folder, thereby showing all messages." | 260 "Remove restrictions from current folder, thereby showing all messages." |
140 (interactive) | 261 (interactive) |
141 (let ((msg (mh-get-msg-num nil))) | 262 (let ((msg (mh-get-msg-num nil))) |
142 (when mh-narrowed-to-seq | 263 (when mh-narrowed-to-seq |
264 (cond ((mh-valid-view-change-operation-p 'widen) nil) | |
265 ((memq 'widen mh-view-ops) | |
266 (while (not (eq (car mh-view-ops) 'widen)) | |
267 (setq mh-view-ops (cdr mh-view-ops))) | |
268 (pop mh-view-ops)) | |
269 (t (error "Widening is not applicable"))) | |
270 (when (memq 'unthread mh-view-ops) | |
271 (setq mh-thread-scan-line-map mh-thread-old-scan-line-map)) | |
143 (with-mh-folder-updating (t) | 272 (with-mh-folder-updating (t) |
144 (delete-region (point-min) (point-max)) | 273 (delete-region (point-min) (point-max)) |
145 (widen) | 274 (widen) |
146 (setq mh-mode-line-annotation mh-non-seq-mode-line-annotation) | 275 (setq mh-mode-line-annotation mh-non-seq-mode-line-annotation) |
147 (mh-make-folder-mode-line)) | 276 (mh-make-folder-mode-line)) |
148 (if msg | 277 (if msg |
149 (mh-goto-msg msg t nil)))) | 278 (mh-goto-msg msg t t)) |
150 (mh-notate-deleted-and-refiled) | 279 (mh-notate-deleted-and-refiled) |
280 (mh-notate-user-sequences) | |
281 (mh-notate-seq 'cur mh-note-cur mh-cmd-note) | |
282 (mh-recenter nil))) | |
151 (if (and (boundp 'tool-bar-mode) tool-bar-mode) | 283 (if (and (boundp 'tool-bar-mode) tool-bar-mode) |
152 (set (make-local-variable 'tool-bar-map) mh-folder-tool-bar-map)) | 284 (set (make-local-variable 'tool-bar-map) mh-folder-tool-bar-map)) |
153 (setq mh-narrowed-to-seq nil)) | 285 (setq mh-narrowed-to-seq nil)) |
154 | 286 |
155 | |
156 ;; FIXME? We may want to clear all notations and add one for current-message | 287 ;; FIXME? We may want to clear all notations and add one for current-message |
157 ;; and process user sequences. | 288 ;; and process user sequences. |
158 (defun mh-notate-deleted-and-refiled () | 289 (defun mh-notate-deleted-and-refiled () |
159 ;; notate the sequence 'deleted as well as all the sequences in | 290 "Notate messages marked for deletion or refiling. |
160 ;; mh-refile-list. | 291 Messages to be deleted are given by `mh-delete-list' while messages to be |
161 ;; | 292 refiled are present in `mh-refile-list'." |
162 ;; First, the 'deleted sequence is straightforward | 293 (mh-mapc #'(lambda (msg) (mh-notate msg mh-note-deleted mh-cmd-note)) |
163 (mh-notate-seq 'deleted mh-note-deleted mh-cmd-note) | 294 mh-delete-list) |
164 ;; Second, refiles are stored in multiple sequences, one for each folder | 295 (mh-mapc #'(lambda (dest-msg-list) |
165 ;; name to refile to. This list of buffer names is stored in | 296 ;; foreach folder name, get the keyed sequence from mh-seq-list |
166 ;; mh-refile-list | 297 (let ((msg-list (cdr dest-msg-list))) |
167 (mh-mapc | 298 (mh-mapc #'(lambda (msg) |
168 (function | 299 (mh-notate msg mh-note-refiled mh-cmd-note)) |
169 (lambda (dest) | 300 msg-list))) |
170 ;; foreach folder name, get the keyed sequence from mh-seq-list | 301 mh-refile-list)) |
171 (let ((msg-list (cdr (assoc dest mh-seq-list)))) | 302 |
172 (mapcar (lambda (msg) | |
173 ;; foreach msg in a sequence, do the mh-notate | |
174 (mh-notate msg mh-note-refiled mh-cmd-note)) | |
175 msg-list)))) | |
176 mh-refile-list)) | |
177 | 303 |
178 | 304 |
179 ;;; Commands to manipulate sequences. Sequences are stored in an alist | 305 ;;; Commands to manipulate sequences. Sequences are stored in an alist |
180 ;;; of the form: | 306 ;;; of the form: |
181 ;;; ((seq-name msgs ...) (seq-name msgs ...) ...) | 307 ;;; ((seq-name msgs ...) (seq-name msgs ...) ...) |
182 | 308 |
183 | |
184 (defun mh-read-seq-default (prompt not-empty) | 309 (defun mh-read-seq-default (prompt not-empty) |
185 ;; Read and return sequence name with default narrowed or previous sequence. | 310 "Read and return sequence name with default narrowed or previous sequence. |
311 PROMPT is the prompt to use when reading. If NOT-EMPTY is non-nil then a | |
312 non-empty sequence is read." | |
186 (mh-read-seq prompt not-empty | 313 (mh-read-seq prompt not-empty |
187 (or mh-narrowed-to-seq | 314 (or mh-narrowed-to-seq |
188 mh-last-seq-used | 315 mh-last-seq-used |
189 (car (mh-seq-containing-msg (mh-get-msg-num nil) nil))))) | 316 (car (mh-seq-containing-msg (mh-get-msg-num nil) nil))))) |
190 | 317 |
191 | |
192 (defun mh-read-seq (prompt not-empty &optional default) | 318 (defun mh-read-seq (prompt not-empty &optional default) |
193 ;; Read and return a sequence name. Prompt with PROMPT, raise an error | 319 "Read and return a sequence name. |
194 ;; if the sequence is empty and the NOT-EMPTY flag is non-nil, and supply | 320 Prompt with PROMPT, raise an error if the sequence is empty and the NOT-EMPTY |
195 ;; an optional DEFAULT sequence. | 321 flag is non-nil, and supply an optional DEFAULT sequence. A reply of '%' |
196 ;; A reply of '%' defaults to the first sequence containing the current | 322 defaults to the first sequence containing the current message." |
197 ;; message. | |
198 (let* ((input (completing-read (format "%s %s %s" prompt "sequence:" | 323 (let* ((input (completing-read (format "%s %s %s" prompt "sequence:" |
199 (if default | 324 (if default |
200 (format "[%s] " default) | 325 (format "[%s] " default) |
201 "")) | 326 "")) |
202 (mh-seq-names mh-seq-list))) | 327 (mh-seq-names mh-seq-list))) |
207 (msgs (mh-seq-to-msgs seq))) | 332 (msgs (mh-seq-to-msgs seq))) |
208 (if (and (null msgs) not-empty) | 333 (if (and (null msgs) not-empty) |
209 (error "No messages in sequence `%s'" seq)) | 334 (error "No messages in sequence `%s'" seq)) |
210 seq)) | 335 seq)) |
211 | 336 |
212 | |
213 (defun mh-seq-names (seq-list) | 337 (defun mh-seq-names (seq-list) |
214 ;; Return an alist containing the names of the SEQUENCES. | 338 "Return an alist containing the names of the SEQ-LIST." |
215 (mapcar (function (lambda (entry) (list (symbol-name (mh-seq-name entry))))) | 339 (mapcar (lambda (entry) (list (symbol-name (mh-seq-name entry)))) |
216 seq-list)) | 340 seq-list)) |
217 | |
218 | 341 |
219 (defun mh-rename-seq (sequence new-name) | 342 (defun mh-rename-seq (sequence new-name) |
220 "Rename SEQUENCE to have NEW-NAME." | 343 "Rename SEQUENCE to have NEW-NAME." |
221 (interactive (list (mh-read-seq "Old" t) | 344 (interactive (list (mh-read-seq "Old" t) |
222 (intern (read-string "New sequence name: ")))) | 345 (intern (read-string "New sequence name: ")))) |
226 ;; create new sequence first, since it might raise an error. | 349 ;; create new sequence first, since it might raise an error. |
227 (mh-define-sequence new-name (mh-seq-msgs old-seq)) | 350 (mh-define-sequence new-name (mh-seq-msgs old-seq)) |
228 (mh-undefine-sequence sequence (mh-seq-msgs old-seq)) | 351 (mh-undefine-sequence sequence (mh-seq-msgs old-seq)) |
229 (rplaca old-seq new-name))) | 352 (rplaca old-seq new-name))) |
230 | 353 |
231 | |
232 (defun mh-map-to-seq-msgs (func seq &rest args) | 354 (defun mh-map-to-seq-msgs (func seq &rest args) |
233 ;; Invoke the FUNCTION at each message in the SEQUENCE, passing the | 355 "Invoke the FUNC at each message in the SEQ. |
234 ;; remaining ARGS as arguments. | 356 The remaining ARGS are passed as arguments to FUNC." |
235 (save-excursion | 357 (save-excursion |
236 (let ((msgs (mh-seq-to-msgs seq))) | 358 (let ((msgs (mh-seq-to-msgs seq))) |
237 (while msgs | 359 (while msgs |
238 (if (mh-goto-msg (car msgs) t t) | 360 (if (mh-goto-msg (car msgs) t t) |
239 (apply func (car msgs) args)) | 361 (apply func (car msgs) args)) |
240 (setq msgs (cdr msgs)))))) | 362 (setq msgs (cdr msgs)))))) |
241 | 363 |
242 | |
243 (defun mh-notate-seq (seq notation offset) | 364 (defun mh-notate-seq (seq notation offset) |
244 ;; Mark the scan listing of all messages in the SEQUENCE with the CHARACTER | 365 "Mark the scan listing. |
245 ;; at the given OFFSET from the beginning of the listing line. | 366 All messages in SEQ are marked with NOTATION at OFFSET from the beginning of |
367 the line." | |
246 (mh-map-to-seq-msgs 'mh-notate seq notation offset)) | 368 (mh-map-to-seq-msgs 'mh-notate seq notation offset)) |
247 | 369 |
248 | |
249 (defun mh-add-to-sequence (seq msgs) | 370 (defun mh-add-to-sequence (seq msgs) |
371 "The sequence SEQ is augmented with the messages in MSGS." | |
250 ;; Add to a SEQUENCE each message the list of MSGS. | 372 ;; Add to a SEQUENCE each message the list of MSGS. |
251 (if (not (mh-folder-name-p seq)) | 373 (if (not (mh-folder-name-p seq)) |
252 (if msgs | 374 (if msgs |
253 (apply 'mh-exec-cmd "mark" mh-current-folder "-add" | 375 (apply 'mh-exec-cmd "mark" mh-current-folder "-add" |
254 "-sequence" (symbol-name seq) | 376 "-sequence" (symbol-name seq) |
255 (mh-coalesce-msg-list msgs))))) | 377 (mh-coalesce-msg-list msgs))))) |
256 | 378 |
257 | 379 ;; This has a tricky bug. mh-map-to-seq-msgs uses mh-goto-msg, which assumes |
258 (defun mh-copy-seq-to-point (seq location) | 380 ;; that the folder buffer is sorted. However in this case that assumption |
259 ;; Copy the scan listing of the messages in SEQUENCE to after the point | 381 ;; doesn't hold. So we will do this the dumb way. |
260 ;; LOCATION in the current buffer. | 382 ;(defun mh-copy-seq-to-point (seq location) |
261 (mh-map-to-seq-msgs 'mh-copy-line-to-point seq location)) | 383 ; ;; Copy the scan listing of the messages in SEQUENCE to after the point |
262 | 384 ; ;; LOCATION in the current buffer. |
385 ; (mh-map-to-seq-msgs 'mh-copy-line-to-point seq location)) | |
386 | |
387 (defun mh-copy-seq-to-eob (seq) | |
388 "Copy SEQ to the end of the buffer." | |
389 ;; It is quite involved to write something which will work at any place in | |
390 ;; the buffer, so we will write something which works only at the end of | |
391 ;; the buffer. If we ever need to insert sequences in the middle of the | |
392 ;; buffer, this will need to be fixed. | |
393 (save-excursion | |
394 (let* ((msgs (mh-seq-to-msgs seq)) | |
395 (coalesced-msgs (mh-coalesce-msg-list msgs))) | |
396 (goto-char (point-max)) | |
397 (save-restriction | |
398 (narrow-to-region (point) (point)) | |
399 (mh-regenerate-headers coalesced-msgs t) | |
400 (when (memq 'unthread mh-view-ops) | |
401 ;; Populate restricted scan-line map | |
402 (goto-char (point-min)) | |
403 (while (not (eobp)) | |
404 (setf (gethash (mh-get-msg-num nil) mh-thread-scan-line-map) | |
405 (mh-thread-parse-scan-line)) | |
406 (forward-line)) | |
407 ;; Remove scan lines and read results from pre-computed thread tree | |
408 (delete-region (point-min) (point-max)) | |
409 (let ((thread-tree (mh-thread-generate mh-current-folder ())) | |
410 (mh-thread-body-width | |
411 (- (window-width) mh-cmd-note | |
412 (1- mh-scan-field-subject-start-offset)))) | |
413 (mh-thread-generate-scan-lines thread-tree -2))))))) | |
263 | 414 |
264 (defun mh-copy-line-to-point (msg location) | 415 (defun mh-copy-line-to-point (msg location) |
416 "Copy current message line to a specific location. | |
417 The argument MSG is not used. The message in the current line is copied to | |
418 LOCATION." | |
419 ;; msg is not used? | |
265 ;; Copy the current line to the LOCATION in the current buffer. | 420 ;; Copy the current line to the LOCATION in the current buffer. |
266 (beginning-of-line) | 421 (beginning-of-line) |
267 (save-excursion | 422 (save-excursion |
268 (let ((beginning-of-line (point)) | 423 (let ((beginning-of-line (point)) |
269 end) | 424 end) |
276 "Define sequence 'region as the messages between point and mark. | 431 "Define sequence 'region as the messages between point and mark. |
277 When called programmatically, use arguments BEGIN and END to define region." | 432 When called programmatically, use arguments BEGIN and END to define region." |
278 (interactive "r") | 433 (interactive "r") |
279 (mh-delete-seq-locally 'region) | 434 (mh-delete-seq-locally 'region) |
280 (save-excursion | 435 (save-excursion |
436 ;; If end is end of buffer back up one position | |
437 (setq end (if (equal end (point-max)) (1- end) end)) | |
281 (goto-char begin) | 438 (goto-char begin) |
282 (while (<= (point) end) | 439 (while (<= (point) end) |
283 (mh-add-msgs-to-seq (mh-get-msg-num t) 'region t) | 440 (mh-add-msgs-to-seq (mh-get-msg-num t) 'region t) |
284 (forward-line 1)))) | 441 (forward-line 1)))) |
285 | 442 |
286 | 443 |
444 | |
287 ;;; Commands to handle new 'subject sequence. | 445 ;;; Commands to handle new 'subject sequence. |
288 ;;; Or "Poor man's threading" by psg. | 446 ;;; Or "Poor man's threading" by psg. |
289 (defun mh-subject-thread-to-sequence (all) | 447 |
448 (defun mh-subject-to-sequence (all) | |
290 "Put all following messages with same subject in sequence 'subject. | 449 "Put all following messages with same subject in sequence 'subject. |
291 If arg ALL is t, move to beginning of folder buffer to collect all messages. | 450 If arg ALL is t, move to beginning of folder buffer to collect all messages. |
292 If arg ALL is nil, collect only messages fron current one on forward. | 451 If arg ALL is nil, collect only messages fron current one on forward. |
452 | |
293 Return number of messages put in the sequence: | 453 Return number of messages put in the sequence: |
454 | |
294 nil -> there was no subject line. | 455 nil -> there was no subject line. |
295 0 -> there were no later messages with the same subject (sequence not made) | 456 0 -> there were no later messages with the same subject (sequence not made) |
296 >1 -> the total number of messages including current one." | 457 >1 -> the total number of messages including current one." |
297 (if (not (eq major-mode 'mh-folder-mode)) | 458 (if (not (eq major-mode 'mh-folder-mode)) |
298 (error "Not in a folder buffer")) | 459 (error "Not in a folder buffer")) |
299 (save-excursion | 460 (save-excursion |
300 (beginning-of-line) | 461 (beginning-of-line) |
301 (if (or (not (looking-at mh-scan-subject-regexp)) | 462 (if (or (not (looking-at mh-scan-subject-regexp)) |
302 (not (match-string 2)) | 463 (not (match-string 3)) |
303 (string-equal "" (match-string 2))) | 464 (string-equal "" (match-string 3))) |
304 (progn (message "No subject line.") | 465 (progn (message "No subject line.") |
305 nil) | 466 nil) |
306 (let ((subject (match-string-no-properties 2)) | 467 (let ((subject (match-string-no-properties 3)) |
307 (end (point-max)) | |
308 (list)) | 468 (list)) |
309 (if (> (length subject) 41) | 469 (if (> (length subject) 41) |
310 (setq subject (substring subject 0 41))) | 470 (setq subject (substring subject 0 41))) |
311 (save-excursion | 471 (save-excursion |
312 (if all | 472 (if all |
313 (goto-char (point-min))) | 473 (goto-char (point-min))) |
314 (while (re-search-forward mh-scan-subject-regexp nil t) | 474 (while (re-search-forward mh-scan-subject-regexp nil t) |
315 (let ((this-subject (match-string-no-properties 2))) | 475 (let ((this-subject (match-string-no-properties 3))) |
316 (if (> (length this-subject) 41) | 476 (if (> (length this-subject) 41) |
317 (setq this-subject (substring this-subject 0 41))) | 477 (setq this-subject (substring this-subject 0 41))) |
318 (if (string-equal this-subject subject) | 478 (if (string-equal this-subject subject) |
319 (setq list (cons (mh-get-msg-num t) list)))))) | 479 (setq list (cons (mh-get-msg-num t) list)))))) |
320 (cond | 480 (cond |
321 (list | 481 (list |
322 ;; If we created a new sequence, add the initial message to it too. | 482 ;; If we created a new sequence, add the initial message to it too. |
323 (if (not (member (mh-get-msg-num t) list)) | 483 (if (not (member (mh-get-msg-num t) list)) |
324 (setq list (cons (mh-get-msg-num t) list))) | 484 (setq list (cons (mh-get-msg-num t) list))) |
325 (mh-delete-seq-locally 'subject) | 485 (if (member '("subject") (mh-seq-names mh-seq-list)) |
486 (mh-delete-seq 'subject)) | |
326 ;; sort the result into a sequence | 487 ;; sort the result into a sequence |
327 (let ((sorted-list (sort (copy-sequence list) 'mh-lessp)) | 488 (let ((sorted-list (sort (copy-sequence list) 'mh-lessp))) |
328 (msg)) | |
329 (while sorted-list | 489 (while sorted-list |
330 (mh-add-msgs-to-seq (car sorted-list) 'subject t) | 490 (mh-add-msgs-to-seq (car sorted-list) 'subject nil) |
331 (setq sorted-list (cdr sorted-list))) | 491 (setq sorted-list (cdr sorted-list))) |
332 (safe-length list))) | 492 (safe-length list))) |
333 (t | 493 (t |
334 0)))))) | 494 0)))))) |
335 | 495 |
336 (defun mh-narrow-to-subject-thread () | 496 (defun mh-narrow-to-subject () |
337 "Narrow to a sequence containing all following messages with same subject." | 497 "Narrow to a sequence containing all following messages with same subject." |
338 (interactive) | 498 (interactive) |
339 (let ((num (mh-get-msg-num nil)) | 499 (let ((num (mh-get-msg-num nil)) |
340 (count (mh-subject-thread-to-sequence t))) | 500 (count (mh-subject-to-sequence t))) |
341 (cond | 501 (cond |
342 ((not count) ; No subject line, delete msg anyway | 502 ((not count) ; No subject line, delete msg anyway |
343 nil) | 503 nil) |
344 ((= 0 count) ; No other msgs, delete msg anyway. | 504 ((= 0 count) ; No other msgs, delete msg anyway. |
345 (message "No other messages with same Subject following this one.") | 505 (message "No other messages with same Subject following this one.") |
348 (message "Found %d messages for subject sequence." count) | 508 (message "Found %d messages for subject sequence." count) |
349 (mh-narrow-to-seq 'subject) | 509 (mh-narrow-to-seq 'subject) |
350 (if (numberp num) | 510 (if (numberp num) |
351 (mh-goto-msg num t t)))))) | 511 (mh-goto-msg num t t)))))) |
352 | 512 |
353 (defun mh-toggle-subject-thread () | 513 (defun mh-delete-subject () |
354 "Narrow to or widen from a sequence containing current subject sequence." | 514 "Mark all following messages with same subject to be deleted. |
515 This puts the messages in a sequence named subject. You can undo the last | |
516 deletion marks using `mh-undo' with a prefix argument and then specifying the | |
517 subject sequence." | |
355 (interactive) | 518 (interactive) |
356 (if (and (stringp mh-mode-line-annotation) | 519 (let ((count (mh-subject-to-sequence nil))) |
357 (string-equal mh-mode-line-annotation "subject")) | |
358 (progn | |
359 (goto-char (point-min)) | |
360 (mh-widen)) | |
361 (mh-narrow-to-subject-thread))) | |
362 | |
363 (defun mh-delete-subject-thread () | |
364 "Mark all following messages with same subject to be deleted." | |
365 (interactive) | |
366 (let ((count (mh-subject-thread-to-sequence nil))) | |
367 (cond | 520 (cond |
368 ((not count) ; No subject line, delete msg anyway | 521 ((not count) ; No subject line, delete msg anyway |
369 (mh-delete-msg (mh-get-msg-num t))) | 522 (mh-delete-msg (mh-get-msg-num t))) |
370 ((= 0 count) ; No other msgs, delete msg anyway. | 523 ((= 0 count) ; No other msgs, delete msg anyway. |
371 (message "No other messages with same Subject following this one.") | 524 (message "No other messages with same Subject following this one.") |
372 (mh-delete-msg (mh-get-msg-num t))) | 525 (mh-delete-msg (mh-get-msg-num t))) |
373 (t ; We have a subject sequence. | 526 (t ; We have a subject sequence. |
374 (message "Marked %d messages for deletion" count) | 527 (message "Marked %d messages for deletion" count) |
375 (mh-delete-msg 'subject))))) | 528 (mh-delete-msg 'subject))))) |
376 | 529 |
377 (defun mh-next-unseen-subject-thread () | 530 ;;; Message threading: |
378 "Get the next unseen subject thread." | 531 |
532 (defun mh-thread-initialize () | |
533 "Make hash tables, otherwise clear them." | |
534 (cond | |
535 (mh-thread-id-hash | |
536 (clrhash mh-thread-id-hash) | |
537 (clrhash mh-thread-subject-hash) | |
538 (clrhash mh-thread-id-table) | |
539 (clrhash mh-thread-id-index-map) | |
540 (clrhash mh-thread-index-id-map) | |
541 (clrhash mh-thread-scan-line-map) | |
542 (clrhash mh-thread-subject-container-hash) | |
543 (clrhash mh-thread-duplicates) | |
544 (setq mh-thread-history ())) | |
545 (t (setq mh-thread-id-hash (make-hash-table :test #'equal)) | |
546 (setq mh-thread-subject-hash (make-hash-table :test #'equal)) | |
547 (setq mh-thread-id-table (make-hash-table :test #'eq)) | |
548 (setq mh-thread-id-index-map (make-hash-table :test #'eq)) | |
549 (setq mh-thread-index-id-map (make-hash-table :test #'eql)) | |
550 (setq mh-thread-scan-line-map (make-hash-table :test #'eql)) | |
551 (setq mh-thread-subject-container-hash (make-hash-table :test #'eq)) | |
552 (setq mh-thread-duplicates (make-hash-table :test #'eq)) | |
553 (setq mh-thread-history ())))) | |
554 | |
555 (defsubst mh-thread-id-container (id) | |
556 "Given ID, return the corresponding container in `mh-thread-id-table'. | |
557 If no container exists then a suitable container is created and the id-table | |
558 is updated." | |
559 (when (not id) | |
560 (error "1")) | |
561 (or (gethash id mh-thread-id-table) | |
562 (setf (gethash id mh-thread-id-table) | |
563 (let ((message (mh-thread-make-message :id id))) | |
564 (mh-thread-make-container :message message))))) | |
565 | |
566 (defsubst mh-thread-remove-parent-link (child) | |
567 "Remove parent link of CHILD if it exists." | |
568 (let* ((child-container (if (mh-thread-container-p child) | |
569 child (mh-thread-id-container child))) | |
570 (parent-container (mh-container-parent child-container))) | |
571 (when parent-container | |
572 (setf (mh-container-children parent-container) | |
573 (remove* child-container (mh-container-children parent-container) | |
574 :test #'eq)) | |
575 (setf (mh-container-parent child-container) nil)))) | |
576 | |
577 (defsubst mh-thread-add-link (parent child &optional at-end-p) | |
578 "Add links so that PARENT becomes a parent of CHILD. | |
579 Doesn't make any changes if CHILD is already an ancestor of PARENT. If | |
580 optional argument AT-END-P is non-nil, the CHILD is added to the end of the | |
581 children list of PARENT." | |
582 (let ((parent-container (cond ((null parent) nil) | |
583 ((mh-thread-container-p parent) parent) | |
584 (t (mh-thread-id-container parent)))) | |
585 (child-container (if (mh-thread-container-p child) | |
586 child (mh-thread-id-container child)))) | |
587 (when (and parent-container | |
588 (not (mh-thread-ancestor-p child-container parent-container)) | |
589 (not (mh-thread-ancestor-p parent-container child-container))) | |
590 (mh-thread-remove-parent-link child-container) | |
591 (cond ((not at-end-p) | |
592 (push child-container (mh-container-children parent-container))) | |
593 ((null (mh-container-children parent-container)) | |
594 (push child-container (mh-container-children parent-container))) | |
595 (t (let ((last-child (mh-container-children parent-container))) | |
596 (while (cdr last-child) | |
597 (setq last-child (cdr last-child))) | |
598 (setcdr last-child (cons child-container nil))))) | |
599 (setf (mh-container-parent child-container) parent-container)) | |
600 (unless parent-container | |
601 (mh-thread-remove-parent-link child-container)))) | |
602 | |
603 (defun mh-thread-ancestor-p (ancestor successor) | |
604 "Return t if ANCESTOR is really an ancestor of SUCCESSOR and nil otherwise. | |
605 In the limit, the function returns t if ANCESTOR and SUCCESSOR are the same | |
606 containers." | |
607 (block nil | |
608 (while successor | |
609 (when (eq ancestor successor) (return t)) | |
610 (setq successor (mh-container-parent successor))) | |
611 nil)) | |
612 | |
613 (defsubst mh-thread-get-message-container (message) | |
614 "Return container which has MESSAGE in it. | |
615 If there is no container present then a new container is allocated." | |
616 (let* ((id (mh-message-id message)) | |
617 (container (gethash id mh-thread-id-table))) | |
618 (cond (container (setf (mh-container-message container) message) | |
619 container) | |
620 (t (setf (gethash id mh-thread-id-table) | |
621 (mh-thread-make-container :message message)))))) | |
622 | |
623 (defsubst mh-thread-get-message (id subject-re-p subject refs) | |
624 "Return appropriate message. | |
625 Otherwise update message already present to have the proper ID, SUBJECT-RE-P, | |
626 SUBJECT and REFS fields." | |
627 (let* ((container (gethash id mh-thread-id-table)) | |
628 (message (if container (mh-container-message container) nil))) | |
629 (cond (message | |
630 (setf (mh-message-subject-re-p message) subject-re-p) | |
631 (setf (mh-message-subject message) subject) | |
632 (setf (mh-message-id message) id) | |
633 (setf (mh-message-references message) refs) | |
634 message) | |
635 (container | |
636 (setf (mh-container-message container) | |
637 (mh-thread-make-message :subject subject | |
638 :subject-re-p subject-re-p | |
639 :id id :references refs))) | |
640 (t (let ((message (mh-thread-make-message | |
641 :subject subject | |
642 :subject-re-p subject-re-p | |
643 :id id :references refs))) | |
644 (prog1 message | |
645 (mh-thread-get-message-container message))))))) | |
646 | |
647 (defsubst mh-thread-canonicalize-id (id) | |
648 "Produce canonical string representation for ID. | |
649 This allows cheap string comparison with EQ." | |
650 (or (and (equal id "") (copy-sequence "")) | |
651 (gethash id mh-thread-id-hash) | |
652 (setf (gethash id mh-thread-id-hash) id))) | |
653 | |
654 (defsubst mh-thread-prune-subject (subject) | |
655 "Prune leading Re:'s, Fwd:'s etc. and trailing (fwd)'s from SUBJECT. | |
656 If the result after pruning is not the empty string then it is canonicalized | |
657 so that subjects can be tested for equality with eq. This is done so that all | |
658 the messages without a subject are not put into a single thread." | |
659 (let ((case-fold-search t) | |
660 (subject-pruned-flag nil)) | |
661 ;; Prune subject leader | |
662 (while (or (string-match "^[ \t]*\\(re\\|fwd?\\)\\(\\[[0-9]*\\]\\)?:[ \t]*" | |
663 subject) | |
664 (string-match "^[ \t]*\\[[^\\]][ \t]*" subject)) | |
665 (setq subject-pruned-flag t) | |
666 (setq subject (substring subject (match-end 0)))) | |
667 ;; Prune subject trailer | |
668 (while (or (string-match "(fwd)$" subject) | |
669 (string-match "[ \t]+$" subject)) | |
670 (setq subject-pruned-flag t) | |
671 (setq subject (substring subject 0 (match-beginning 0)))) | |
672 ;; Canonicalize subject only if it is non-empty | |
673 (cond ((equal subject "") (values subject subject-pruned-flag)) | |
674 (t (values | |
675 (or (gethash subject mh-thread-subject-hash) | |
676 (setf (gethash subject mh-thread-subject-hash) subject)) | |
677 subject-pruned-flag))))) | |
678 | |
679 (defun mh-thread-container-subject (container) | |
680 "Return the subject of CONTAINER. | |
681 If CONTAINER is empty return the subject info of one of its children." | |
682 (cond ((and (mh-container-message container) | |
683 (mh-message-id (mh-container-message container))) | |
684 (mh-message-subject (mh-container-message container))) | |
685 (t (block nil | |
686 (dolist (kid (mh-container-children container)) | |
687 (when (and (mh-container-message kid) | |
688 (mh-message-id (mh-container-message kid))) | |
689 (let ((kid-message (mh-container-message kid))) | |
690 (return (mh-message-subject kid-message))))) | |
691 (error "This can't happen!"))))) | |
692 | |
693 (defun mh-thread-rewind-pruning () | |
694 "Restore the thread tree to its state before pruning." | |
695 (while mh-thread-history | |
696 (let ((action (pop mh-thread-history))) | |
697 (cond ((eq (car action) 'DROP) | |
698 (mh-thread-remove-parent-link (cadr action)) | |
699 (mh-thread-add-link (caddr action) (cadr action))) | |
700 ((eq (car action) 'PROMOTE) | |
701 (let ((node (cadr action)) | |
702 (parent (caddr action)) | |
703 (children (cdddr action))) | |
704 (dolist (child children) | |
705 (mh-thread-remove-parent-link child) | |
706 (mh-thread-add-link node child)) | |
707 (mh-thread-add-link parent node))) | |
708 ((eq (car action) 'SUBJECT) | |
709 (let ((node (cadr action))) | |
710 (mh-thread-remove-parent-link node) | |
711 (setf (mh-container-real-child-p node) t))))))) | |
712 | |
713 (defun mh-thread-prune-containers (roots) | |
714 "Prune empty containers in the containers ROOTS." | |
715 (let ((dfs-ordered-nodes ()) | |
716 (work-list roots)) | |
717 (while work-list | |
718 (let ((node (pop work-list))) | |
719 (dolist (child (mh-container-children node)) | |
720 (push child work-list)) | |
721 (push node dfs-ordered-nodes))) | |
722 (while dfs-ordered-nodes | |
723 (let ((node (pop dfs-ordered-nodes))) | |
724 (cond ((gethash (mh-message-id (mh-container-message node)) | |
725 mh-thread-id-index-map) | |
726 ;; Keep it | |
727 (setf (mh-container-children node) | |
728 (mh-thread-sort-containers (mh-container-children node)))) | |
729 ((and (mh-container-children node) | |
730 (or (null (cdr (mh-container-children node))) | |
731 (mh-container-parent node))) | |
732 ;; Promote kids | |
733 (let ((children ())) | |
734 (dolist (kid (mh-container-children node)) | |
735 (mh-thread-remove-parent-link kid) | |
736 (mh-thread-add-link (mh-container-parent node) kid) | |
737 (push kid children)) | |
738 (push `(PROMOTE ,node ,(mh-container-parent node) ,@children) | |
739 mh-thread-history) | |
740 (mh-thread-remove-parent-link node))) | |
741 ((mh-container-children node) | |
742 ;; Promote the first orphan to parent and add the other kids as | |
743 ;; his children | |
744 (setf (mh-container-children node) | |
745 (mh-thread-sort-containers (mh-container-children node))) | |
746 (let ((new-parent (car (mh-container-children node))) | |
747 (other-kids (cdr (mh-container-children node)))) | |
748 (mh-thread-remove-parent-link new-parent) | |
749 (dolist (kid other-kids) | |
750 (mh-thread-remove-parent-link kid) | |
751 (setf (mh-container-real-child-p kid) nil) | |
752 (mh-thread-add-link new-parent kid t)) | |
753 (push `(PROMOTE ,node ,(mh-container-parent node) | |
754 ,new-parent ,@other-kids) | |
755 mh-thread-history) | |
756 (mh-thread-remove-parent-link node))) | |
757 (t | |
758 ;; Drop it | |
759 (push `(DROP ,node ,(mh-container-parent node)) | |
760 mh-thread-history) | |
761 (mh-thread-remove-parent-link node))))) | |
762 (let ((results ())) | |
763 (maphash #'(lambda (k v) | |
764 (declare (ignore k)) | |
765 (when (and (null (mh-container-parent v)) | |
766 (gethash (mh-message-id (mh-container-message v)) | |
767 mh-thread-id-index-map)) | |
768 (push v results))) | |
769 mh-thread-id-table) | |
770 (mh-thread-sort-containers results)))) | |
771 | |
772 (defun mh-thread-sort-containers (containers) | |
773 "Sort a list of message CONTAINERS to be in ascending order wrt index." | |
774 (sort containers | |
775 #'(lambda (x y) | |
776 (when (and (mh-container-message x) (mh-container-message y)) | |
777 (let* ((id-x (mh-message-id (mh-container-message x))) | |
778 (id-y (mh-message-id (mh-container-message y))) | |
779 (index-x (gethash id-x mh-thread-id-index-map)) | |
780 (index-y (gethash id-y mh-thread-id-index-map))) | |
781 (and (integerp index-x) (integerp index-y) | |
782 (< index-x index-y))))))) | |
783 | |
784 (defsubst mh-thread-group-by-subject (roots) | |
785 "Group the set of message containers, ROOTS based on subject. | |
786 Bug: Check for and make sure that something without Re: is made the parent in | |
787 preference to something that has it." | |
788 (clrhash mh-thread-subject-container-hash) | |
789 (let ((results ())) | |
790 (dolist (root roots) | |
791 (let* ((subject (mh-thread-container-subject root)) | |
792 (parent (gethash subject mh-thread-subject-container-hash))) | |
793 (cond (parent (mh-thread-remove-parent-link root) | |
794 (mh-thread-add-link parent root t) | |
795 (setf (mh-container-real-child-p root) nil) | |
796 (push `(SUBJECT ,root) mh-thread-history)) | |
797 (t | |
798 (setf (gethash subject mh-thread-subject-container-hash) root) | |
799 (push root results))))) | |
800 (nreverse results))) | |
801 | |
802 (defsubst mh-thread-process-in-reply-to (reply-to-header) | |
803 "Extract message id's from REPLY-TO-HEADER. | |
804 Ideally this should have some regexp which will try to guess if a string | |
805 between < and > is a message id and not an email address. For now it will | |
806 take the last string inside angles." | |
807 (let ((end (search ">" reply-to-header :from-end t))) | |
808 (when (numberp end) | |
809 (let ((begin (search "<" reply-to-header :from-end t :end2 end))) | |
810 (when (numberp begin) | |
811 (list (substring reply-to-header begin (1+ end)))))))) | |
812 | |
813 (defun mh-thread-set-tables (folder) | |
814 "Use the tables of FOLDER in current buffer." | |
815 (flet ((mh-get-table (symbol) | |
816 (save-excursion (set-buffer folder) (symbol-value symbol)))) | |
817 (setq mh-thread-id-hash (mh-get-table 'mh-thread-id-hash)) | |
818 (setq mh-thread-subject-hash (mh-get-table 'mh-thread-subject-hash)) | |
819 (setq mh-thread-id-table (mh-get-table 'mh-thread-id-table)) | |
820 (setq mh-thread-id-index-map (mh-get-table 'mh-thread-id-index-map)) | |
821 (setq mh-thread-index-id-map (mh-get-table 'mh-thread-index-id-map)) | |
822 (setq mh-thread-scan-line-map (mh-get-table 'mh-thread-scan-line-map)) | |
823 (setq mh-thread-subject-container-hash | |
824 (mh-get-table 'mh-thread-subject-container-hash)) | |
825 (setq mh-thread-duplicates (mh-get-table 'mh-thread-duplicates)) | |
826 (setq mh-thread-history (mh-get-table 'mh-thread-history)))) | |
827 | |
828 (defsubst mh-thread-update-id-index-maps (id index) | |
829 "Message with id, ID is the message in INDEX. | |
830 The function also checks for duplicate messages (that is multiple messages | |
831 with the same ID). These messages are put in the `mh-thread-duplicates' hash | |
832 table." | |
833 (let ((old-index (gethash id mh-thread-id-index-map))) | |
834 (when old-index (push old-index (gethash id mh-thread-duplicates))) | |
835 (setf (gethash id mh-thread-id-index-map) index) | |
836 (setf (gethash index mh-thread-index-id-map) id))) | |
837 | |
838 | |
839 | |
840 ;;; Generate Threads... | |
841 | |
842 (defun mh-thread-generate (folder msg-list) | |
843 "Scan FOLDER to get info for threading. | |
844 Only information about messages in MSG-LIST are added to the tree." | |
845 (save-excursion | |
846 (set-buffer (get-buffer-create "*mh-thread*")) | |
847 (mh-thread-set-tables folder) | |
848 (erase-buffer) | |
849 (when msg-list | |
850 (apply | |
851 #'call-process (expand-file-name mh-scan-prog mh-progs) nil '(t nil) nil | |
852 "-width" "10000" "-format" | |
853 "%(msg)\n%{message-id}\n%{references}\n%{in-reply-to}\n%{subject}\n" | |
854 (mapcar #'(lambda (x) (format "%s" x)) msg-list))) | |
855 (goto-char (point-min)) | |
856 (let ((roots ()) | |
857 (case-fold-search t)) | |
858 (block nil | |
859 (while (not (eobp)) | |
860 (block process-message | |
861 (let* ((index-line | |
862 (prog1 (buffer-substring (point) (line-end-position)) | |
863 (forward-line))) | |
864 (index (car (read-from-string index-line))) | |
865 (id (prog1 (buffer-substring (point) (line-end-position)) | |
866 (forward-line))) | |
867 (refs (prog1 (buffer-substring (point) (line-end-position)) | |
868 (forward-line))) | |
869 (in-reply-to (prog1 (buffer-substring (point) | |
870 (line-end-position)) | |
871 (forward-line))) | |
872 (subject (prog1 | |
873 (buffer-substring (point) (line-end-position)) | |
874 (forward-line))) | |
875 (subject-re-p nil)) | |
876 (unless (gethash index mh-thread-scan-line-map) | |
877 (return-from process-message)) | |
878 (unless (integerp index) (return)) ;Error message here | |
879 (multiple-value-setq (subject subject-re-p) | |
880 (mh-thread-prune-subject subject)) | |
881 (setq in-reply-to (mh-thread-process-in-reply-to in-reply-to)) | |
882 (setq refs (append (split-string refs) in-reply-to)) | |
883 (setq id (mh-thread-canonicalize-id id)) | |
884 (mh-thread-update-id-index-maps id index) | |
885 (setq refs (mapcar #'mh-thread-canonicalize-id refs)) | |
886 (mh-thread-get-message id subject-re-p subject refs) | |
887 (do ((ancestors refs (cdr ancestors))) | |
888 ((null (cdr ancestors)) | |
889 (when (car ancestors) | |
890 (mh-thread-remove-parent-link id) | |
891 (mh-thread-add-link (car ancestors) id))) | |
892 (mh-thread-add-link (car ancestors) (cadr ancestors))))))) | |
893 (maphash #'(lambda (k v) | |
894 (declare (ignore k)) | |
895 (when (null (mh-container-parent v)) | |
896 (push v roots))) | |
897 mh-thread-id-table) | |
898 (setq roots (mh-thread-prune-containers roots)) | |
899 (prog1 (setq roots (mh-thread-group-by-subject roots)) | |
900 (let ((history mh-thread-history)) | |
901 (set-buffer folder) | |
902 (setq mh-thread-history history)))))) | |
903 | |
904 (defun mh-thread-inc (folder start-point) | |
905 "Update thread tree for FOLDER. | |
906 All messages after START-POINT are added to the thread tree." | |
907 (mh-thread-rewind-pruning) | |
908 (goto-char start-point) | |
909 (let ((msg-list ())) | |
910 (while (not (eobp)) | |
911 (let ((index (mh-get-msg-num nil))) | |
912 (push index msg-list) | |
913 (setf (gethash index mh-thread-scan-line-map) | |
914 (mh-thread-parse-scan-line)) | |
915 (forward-line))) | |
916 (let ((thread-tree (mh-thread-generate folder msg-list)) | |
917 (buffer-read-only nil) | |
918 (old-buffer-modified-flag (buffer-modified-p))) | |
919 (delete-region (point-min) (point-max)) | |
920 (let ((mh-thread-body-width (- (window-width) mh-cmd-note | |
921 (1- mh-scan-field-subject-start-offset)))) | |
922 (mh-thread-generate-scan-lines thread-tree -2)) | |
923 (mh-notate-user-sequences) | |
924 (mh-notate-deleted-and-refiled) | |
925 (mh-notate-seq 'cur mh-note-cur mh-cmd-note) | |
926 (set-buffer-modified-p old-buffer-modified-flag)))) | |
927 | |
928 (defun mh-thread-generate-scan-lines (tree level) | |
929 "Generate scan lines. | |
930 TREE is the hierarchical tree of messages, SCAN-LINE-MAP maps message indices | |
931 to the corresponding scan lines and LEVEL used to determine indentation of | |
932 the message." | |
933 (cond ((null tree) nil) | |
934 ((mh-thread-container-p tree) | |
935 (let* ((message (mh-container-message tree)) | |
936 (id (mh-message-id message)) | |
937 (index (gethash id mh-thread-id-index-map)) | |
938 (duplicates (gethash id mh-thread-duplicates)) | |
939 (new-level (+ level 2)) | |
940 (dupl-flag t) | |
941 (increment-level-flag nil)) | |
942 (dolist (scan-line (mapcar (lambda (x) | |
943 (gethash x mh-thread-scan-line-map)) | |
944 (reverse (cons index duplicates)))) | |
945 (when scan-line | |
946 (insert (car scan-line) | |
947 (format (format "%%%ss" | |
948 (if dupl-flag level new-level)) "") | |
949 (if (and (mh-container-real-child-p tree) dupl-flag) | |
950 "[" "<") | |
951 (cadr scan-line) | |
952 (if (and (mh-container-real-child-p tree) dupl-flag) | |
953 "]" ">") | |
954 (truncate-string-to-width | |
955 (caddr scan-line) (- mh-thread-body-width | |
956 (if dupl-flag level new-level))) | |
957 "\n") | |
958 (setq increment-level-flag t) | |
959 (setq dupl-flag nil))) | |
960 (unless increment-level-flag (setq new-level level)) | |
961 (dolist (child (mh-container-children tree)) | |
962 (mh-thread-generate-scan-lines child new-level)))) | |
963 (t (let ((nlevel (+ level 2))) | |
964 (dolist (ch tree) | |
965 (mh-thread-generate-scan-lines ch nlevel)))))) | |
966 | |
967 ;; Another and may be better approach would be to generate all the info from | |
968 ;; the scan which generates the threading info. For now this will have to do. | |
969 (defun mh-thread-parse-scan-line (&optional string) | |
970 "Parse a scan line. | |
971 If optional argument STRING is given then that is assumed to be the scan line. | |
972 Otherwise uses the line at point as the scan line to parse." | |
973 (let* ((string (or string | |
974 (buffer-substring-no-properties (line-beginning-position) | |
975 (line-end-position)))) | |
976 (first-string (substring string 0 (+ mh-cmd-note 8)))) | |
977 (setf (elt first-string mh-cmd-note) ? ) | |
978 (when (equal (elt first-string (1+ mh-cmd-note)) (elt mh-note-seq 0)) | |
979 (setf (elt first-string (1+ mh-cmd-note)) ? )) | |
980 (list first-string | |
981 (substring string | |
982 (+ mh-cmd-note mh-scan-field-from-start-offset) | |
983 (+ mh-cmd-note mh-scan-field-from-end-offset -2)) | |
984 (substring string (+ mh-cmd-note mh-scan-field-from-end-offset)) | |
985 string))) | |
986 | |
987 (defun mh-thread-add-spaces (count) | |
988 "Add COUNT spaces to each scan line in `mh-thread-scan-line-map'." | |
989 (let ((spaces (format (format "%%%ss" count) ""))) | |
990 (while (not (eobp)) | |
991 (let* ((msg-num (mh-get-msg-num nil)) | |
992 (old-line (nth 3 (gethash msg-num mh-thread-scan-line-map)))) | |
993 (setf (gethash msg-num mh-thread-scan-line-map) | |
994 (mh-thread-parse-scan-line (format "%s%s" spaces old-line)))) | |
995 (forward-line 1)))) | |
996 | |
997 (defun mh-thread-folder () | |
998 "Generate thread view of folder." | |
999 (message "Threading %s..." (buffer-name)) | |
1000 (mh-thread-initialize) | |
1001 (goto-char (point-min)) | |
1002 (while (not (eobp)) | |
1003 (setf (gethash (mh-get-msg-num nil) mh-thread-scan-line-map) | |
1004 (mh-thread-parse-scan-line)) | |
1005 (forward-line)) | |
1006 (let* ((range (format "%s-%s" mh-first-msg-num mh-last-msg-num)) | |
1007 (thread-tree (mh-thread-generate (buffer-name) (list range))) | |
1008 (buffer-read-only nil) | |
1009 (old-buffer-modified-p (buffer-modified-p))) | |
1010 (delete-region (point-min) (point-max)) | |
1011 (let ((mh-thread-body-width (- (window-width) mh-cmd-note | |
1012 (1- mh-scan-field-subject-start-offset)))) | |
1013 (mh-thread-generate-scan-lines thread-tree -2)) | |
1014 (mh-notate-user-sequences) | |
1015 (mh-notate-deleted-and-refiled) | |
1016 (mh-notate-seq 'cur mh-note-cur mh-cmd-note) | |
1017 (set-buffer-modified-p old-buffer-modified-p) | |
1018 (message "Threading %s...done" (buffer-name)))) | |
1019 | |
1020 (defun mh-toggle-threads () | |
1021 "Toggle threaded view of folder. | |
1022 The conversion of normal view to threaded view is exact, that is the same | |
1023 messages are displayed in the folder buffer before and after threading. However | |
1024 the conversion from threaded view to normal view is inexact. So more messages | |
1025 than were originally present may be shown as a result." | |
379 (interactive) | 1026 (interactive) |
380 (if (and mh-mode-line-annotation | 1027 (let ((msg-at-point (mh-get-msg-num nil))) |
381 (string-equal mh-mode-line-annotation "subject")) | 1028 (cond ((and (memq 'unthread mh-view-ops) mh-narrowed-to-seq) |
382 (goto-char (point-min))) | 1029 (unless (mh-valid-view-change-operation-p 'unthread) |
383 (if (or (not mh-mode-line-annotation) | 1030 (error "Can't unthread folder")) |
384 (not (string-equal mh-mode-line-annotation "unseen"))) | 1031 (mh-scan-folder mh-current-folder |
385 (mh-narrow-to-seq 'unseen)) | 1032 (format "%s" mh-narrowed-to-seq) |
386 (mh-next-undeleted-msg) | 1033 t)) |
387 (mh-narrow-to-subject-thread)) | 1034 ((memq 'unthread mh-view-ops) |
1035 (unless (mh-valid-view-change-operation-p 'unthread) | |
1036 (error "Can't unthread folder")) | |
1037 (mh-scan-folder mh-current-folder | |
1038 (format "%s-%s" mh-first-msg-num mh-last-msg-num) | |
1039 t)) | |
1040 (t (mh-thread-folder) | |
1041 (push 'unthread mh-view-ops))) | |
1042 (when msg-at-point (mh-goto-msg msg-at-point t t)) | |
1043 (mh-recenter nil))) | |
1044 | |
1045 (defun mh-thread-forget-message (index) | |
1046 "Forget the message INDEX from the threading tables." | |
1047 (let* ((id (gethash index mh-thread-index-id-map)) | |
1048 (id-index (gethash id mh-thread-id-index-map)) | |
1049 (duplicates (gethash id mh-thread-duplicates))) | |
1050 (remhash index mh-thread-index-id-map) | |
1051 (cond ((and (eql index id-index) (null duplicates)) | |
1052 (remhash id mh-thread-id-index-map)) | |
1053 ((eql index id-index) | |
1054 (setf (gethash id mh-thread-id-index-map) (car duplicates)) | |
1055 (setf (gethash (car duplicates) mh-thread-index-id-map) id) | |
1056 (setf (gethash id mh-thread-duplicates) (cdr duplicates))) | |
1057 (t | |
1058 (setf (gethash id mh-thread-duplicates) | |
1059 (remove index duplicates)))))) | |
1060 | |
1061 (provide 'mh-seq) | |
1062 | |
1063 ;;; Local Variables: | |
1064 ;;; sentence-end-double-space: nil | |
1065 ;;; End: | |
388 | 1066 |
389 ;;; mh-seq.el ends here | 1067 ;;; mh-seq.el ends here |