(menu-bar-options-menu): Delete "Syntax
[bpt/emacs.git] / lisp / mh-e / mh-seq.el
CommitLineData
bdcfe844 1;;; mh-seq.el --- MH-E sequences support
c26cf6c8 2
e495eaec 3;; Copyright (C) 1993, 1995,
deceef67 4;; 2001, 2002, 2003, 2004, 2005 Free Software Foundation, Inc.
a1b4049d
BW
5
6;; Author: Bill Wohler <wohler@newt.com>
7;; Maintainer: Bill Wohler <wohler@newt.com>
8;; Keywords: mail
9;; See: mh-e.el
c26cf6c8 10
60370d40 11;; This file is part of GNU Emacs.
c26cf6c8 12
9b7bc076 13;; GNU Emacs is free software; you can redistribute it and/or modify
c26cf6c8
RS
14;; it under the terms of the GNU General Public License as published by
15;; the Free Software Foundation; either version 2, or (at your option)
16;; any later version.
17
9b7bc076 18;; GNU Emacs is distributed in the hope that it will be useful,
c26cf6c8
RS
19;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21;; GNU General Public License for more details.
22
23;; You should have received a copy of the GNU General Public License
b578f267 24;; along with GNU Emacs; see the file COPYING. If not, write to the
3a35cf56
LK
25;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
26;; Boston, MA 02110-1301, USA.
c26cf6c8
RS
27
28;;; Commentary:
bdcfe844
BW
29;;
30;; This tries to implement the algorithm described at:
31;; http://www.jwz.org/doc/threading.html
32;; It is also a start to implementing the IMAP Threading extension RFC. The
33;; implementation lacks the reference and subject canonicalization of the
34;; RFC.
35;;
36;; In the presentation buffer, children messages are shown indented with
37;; either [ ] or < > around them. Square brackets ([ ]) denote that the
38;; algorithm can point out some headers which when taken together implies
39;; that the unindented message is an ancestor of the indented message. If
40;; no such proof exists then angles (< >) are used.
41;;
42;; Some issues and problems are as follows:
43;;
44;; (1) Scan truncates the fields at length 512. So longer references:
45;; headers get mutilated. The same kind of MH format string works when
46;; composing messages. Is there a way to avoid this? My scan command
47;; is as follows:
48;; scan +folder -width 10000 \
49;; -format "%(msg)\n%{message-id}\n%{references}\n%{subject}\n"
50;; I would really appreciate it if someone would help me with this.
51;;
3d7ca223
BW
52;; (2) Implement heuristics to recognize message identifiers in
53;; In-Reply-To: header. Right now it just assumes that the last text
54;; between angles (< and >) is the message identifier. There is the
55;; chance that this will incorrectly use an email address like a
56;; message identifier.
bdcfe844 57;;
3d7ca223 58;; (3) Error checking of found message identifiers should be done.
bdcfe844
BW
59;;
60;; (4) Since this breaks the assumption that message indices increase as
61;; one goes down the buffer, the binary search based mh-goto-msg
62;; doesn't work. I have a simpler replacement which may be less
63;; efficient.
64;;
3d7ca223 65;; (5) Better canonicalizing for message identifier and subject strings.
bdcfe844 66;;
c26cf6c8 67
bdcfe844 68;; Internal support for MH-E package.
c26cf6c8 69
847b8219
KH
70;;; Change Log:
71
c26cf6c8
RS
72;;; Code:
73
f0d73c14 74(eval-when-compile (require 'mh-acros))
a66894d8 75(mh-require-cl)
c26cf6c8
RS
76(require 'mh-e)
77
bdcfe844
BW
78;; Shush the byte-compiler
79(defvar tool-bar-mode)
80
81;;; Data structures (used in message threading)...
f0d73c14
BW
82(mh-defstruct (mh-thread-message (:conc-name mh-message-)
83 (:constructor mh-thread-make-message))
bdcfe844
BW
84 (id nil)
85 (references ())
86 (subject "")
87 (subject-re-p nil))
88
f0d73c14
BW
89(mh-defstruct (mh-thread-container (:conc-name mh-container-)
90 (:constructor mh-thread-make-container))
bdcfe844
BW
91 message parent children
92 (real-child-p t))
93
94
847b8219 95;;; Internal variables:
bdcfe844
BW
96(defvar mh-last-seq-used nil
97 "Name of seq to which a msg was last added.")
847b8219 98
bdcfe844
BW
99(defvar mh-non-seq-mode-line-annotation nil
100 "Saved value of `mh-mode-line-annotation' when narrowed to a seq.")
847b8219 101
bdcfe844
BW
102;;; Maps and hashes...
103(defvar mh-thread-id-hash nil
3d7ca223 104 "Hashtable used to canonicalize message identifiers.")
bdcfe844
BW
105(defvar mh-thread-subject-hash nil
106 "Hashtable used to canonicalize subject strings.")
107(defvar mh-thread-id-table nil
3d7ca223 108 "Thread ID table maps from message identifiers to message containers.")
bdcfe844 109(defvar mh-thread-id-index-map nil
3d7ca223 110 "Table to look up message index number from message identifier.")
bdcfe844 111(defvar mh-thread-index-id-map nil
3d7ca223 112 "Table to look up message identifier from message index.")
bdcfe844
BW
113(defvar mh-thread-scan-line-map nil
114 "Map of message index to various parts of the scan line.")
a66894d8 115(defvar mh-thread-scan-line-map-stack nil
bdcfe844
BW
116 "Old map of message index to various parts of the scan line.
117This is the original map that is stored when the folder is narrowed.")
118(defvar mh-thread-subject-container-hash nil
119 "Hashtable used to group messages by subject.")
120(defvar mh-thread-duplicates nil
3d7ca223 121 "Hashtable used to associate messages with the same message identifier.")
bdcfe844
BW
122(defvar mh-thread-history ()
123 "Variable to remember the transformations to the thread tree.
124When new messages are added, these transformations are rewound, then the
125links are added from the newly seen messages. Finally the transformations are
126redone to get the new thread tree. This makes incremental threading easier.")
127(defvar mh-thread-body-width nil
128 "Width of scan substring that contains subject and body of message.")
c26cf6c8 129
bdcfe844
BW
130(make-variable-buffer-local 'mh-thread-id-hash)
131(make-variable-buffer-local 'mh-thread-subject-hash)
132(make-variable-buffer-local 'mh-thread-id-table)
133(make-variable-buffer-local 'mh-thread-id-index-map)
134(make-variable-buffer-local 'mh-thread-index-id-map)
135(make-variable-buffer-local 'mh-thread-scan-line-map)
a66894d8 136(make-variable-buffer-local 'mh-thread-scan-line-map-stack)
bdcfe844
BW
137(make-variable-buffer-local 'mh-thread-subject-container-hash)
138(make-variable-buffer-local 'mh-thread-duplicates)
139(make-variable-buffer-local 'mh-thread-history)
c26cf6c8 140
c3d9274a 141;;;###mh-autoload
847b8219 142(defun mh-delete-seq (sequence)
be33fce4
BW
143 "Delete SEQUENCE.
144
145You are prompted for the sequence to delete. Note that this deletes only the
146sequence, not the messages in the sequence. If you want to delete the
147messages, use \"\\[universal-argument] \\[mh-delete-msg]\"."
c26cf6c8 148 (interactive (list (mh-read-seq-default "Delete" t)))
a66894d8
BW
149 (let ((msg-list (mh-seq-to-msgs sequence))
150 (internal-flag (mh-internal-seq sequence))
151 (folders-changed (list mh-current-folder)))
152 (mh-iterate-on-range msg sequence
153 (mh-remove-sequence-notation msg internal-flag))
3d7ca223
BW
154 (mh-undefine-sequence sequence '("all"))
155 (mh-delete-seq-locally sequence)
a66894d8
BW
156 (when mh-index-data
157 (setq folders-changed
158 (append folders-changed
159 (mh-index-delete-from-sequence sequence msg-list))))
160 (when (and (eq sequence mh-unseen-seq) (mh-speed-flists-active-p))
161 (apply #'mh-speed-flists t folders-changed))))
c26cf6c8 162
bdcfe844
BW
163;; Avoid compiler warnings
164(defvar view-exit-action)
c26cf6c8 165
c3d9274a
BW
166;;;###mh-autoload
167(defun mh-list-sequences ()
be33fce4
BW
168 "List all sequences in folder.
169The list appears in a buffer named \"*MH-E Sequences*\"."
c3d9274a
BW
170 (interactive)
171 (let ((folder mh-current-folder)
3d7ca223 172 (temp-buffer mh-sequences-buffer)
c3d9274a
BW
173 (seq-list mh-seq-list)
174 (max-len 0))
c26cf6c8
RS
175 (with-output-to-temp-buffer temp-buffer
176 (save-excursion
c3d9274a
BW
177 (set-buffer temp-buffer)
178 (erase-buffer)
179 (message "Listing sequences ...")
180 (insert "Sequences in folder " folder ":\n")
181 (let ((seq-list seq-list))
182 (while seq-list
183 (setq max-len
184 (max (length (symbol-name (mh-seq-name (pop seq-list))))
185 max-len)))
186 (setq max-len (+ 2 max-len)))
187 (while seq-list
188 (let ((name (mh-seq-name (car seq-list)))
189 (sorted-seq-msgs
190 (mh-coalesce-msg-list
191 (sort (copy-sequence (mh-seq-msgs (car seq-list))) '<)))
192 name-spec)
193 (insert (setq name-spec (format (format "%%%ss:" max-len) name)))
194 (while sorted-seq-msgs
195 (let ((next-element (format " %s" (pop sorted-seq-msgs))))
196 (when (>= (+ (current-column) (length next-element))
197 (window-width))
198 (insert "\n")
199 (insert (format (format "%%%ss" (length name-spec)) "")))
200 (insert next-element)))
201 (insert "\n"))
202 (setq seq-list (cdr seq-list)))
203 (goto-char (point-min))
e495eaec 204 (view-mode-enter)
c3d9274a
BW
205 (setq view-exit-action 'kill-buffer)
206 (message "Listing sequences...done")))))
207
208;;;###mh-autoload
847b8219 209(defun mh-msg-is-in-seq (message)
f0d73c14
BW
210 "Display the sequences in which the current message appears.
211Use a prefix argument to display the sequences in which another MESSAGE
212appears."
213 (interactive "P")
214 (if (not message)
215 (setq message (mh-get-msg-num t)))
bdcfe844 216 (let* ((dest-folder (loop for seq in mh-refile-list
f0d73c14
BW
217 when (member message (cdr seq)) return (car seq)
218 finally return nil))
bdcfe844
BW
219 (deleted-flag (unless dest-folder (member message mh-delete-list))))
220 (message "Message %d%s is in sequences: %s"
221 message
222 (cond (dest-folder (format " (to be refiled to %s)" dest-folder))
223 (deleted-flag (format " (to be deleted)"))
224 (t ""))
225 (mapconcat 'concat
226 (mh-list-to-string (mh-seq-containing-msg message t))
227 " "))))
c26cf6c8 228
924df208
BW
229;; Avoid compiler warning
230(defvar tool-bar-map)
231
a10f4ace
BW
232(make-variable-buffer-local 'mh-non-seq-mode-line-annotation)
233
c3d9274a 234;;;###mh-autoload
847b8219 235(defun mh-narrow-to-seq (sequence)
be33fce4
BW
236 "Restrict display to messages in SEQUENCE.
237
238You are prompted for the name of the sequence. What this command does is show
239only those messages that are in the selected sequence in the MH-Folder buffer.
240In addition, it limits further MH-E searches to just those messages.
241
242When you want to widen the view to all your messages again, use \\[mh-widen]."
c26cf6c8 243 (interactive (list (mh-read-seq "Narrow to" t)))
847b8219
KH
244 (with-mh-folder-updating (t)
245 (cond ((mh-seq-to-msgs sequence)
bdcfe844 246 (mh-remove-all-notation)
c3d9274a 247 (let ((eob (point-max))
bdcfe844 248 (msg-at-cursor (mh-get-msg-num nil)))
a66894d8 249 (push mh-thread-scan-line-map mh-thread-scan-line-map-stack)
bdcfe844 250 (setq mh-thread-scan-line-map (make-hash-table :test #'eql))
c3d9274a 251 (mh-copy-seq-to-eob sequence)
a66894d8
BW
252 (push (buffer-substring-no-properties (point-min) eob)
253 mh-folder-view-stack)
254 (delete-region (point-min) eob)
bdcfe844 255 (mh-notate-deleted-and-refiled)
3d7ca223 256 (mh-notate-cur)
bdcfe844 257 (when msg-at-cursor (mh-goto-msg msg-at-cursor t t))
c3d9274a
BW
258 (setq mh-non-seq-mode-line-annotation mh-mode-line-annotation)
259 (setq mh-mode-line-annotation (symbol-name sequence))
260 (mh-make-folder-mode-line)
261 (mh-recenter nil)
924df208
BW
262 (when (and (boundp 'tool-bar-mode) tool-bar-mode)
263 (set (make-local-variable 'tool-bar-map)
264 mh-folder-seq-tool-bar-map)
265 (when (buffer-live-p (get-buffer mh-show-buffer))
266 (save-excursion
267 (set-buffer (get-buffer mh-show-buffer))
268 (set (make-local-variable 'tool-bar-map)
269 mh-show-seq-tool-bar-map))))
bdcfe844 270 (push 'widen mh-view-ops)))
c3d9274a
BW
271 (t
272 (error "No messages in sequence `%s'" (symbol-name sequence))))))
c26cf6c8 273
c3d9274a 274;;;###mh-autoload
a66894d8
BW
275(defun mh-put-msg-in-seq (range sequence)
276 "Add RANGE to SEQUENCE.
277
278Check the documentation of `mh-interactive-range' to see how RANGE is read in
279interactive use."
280 (interactive (list (mh-interactive-range "Add messages from")
c3d9274a 281 (mh-read-seq-default "Add to" nil)))
a66894d8
BW
282 (unless (mh-valid-seq-p sequence)
283 (error "Can't put message in invalid sequence `%s'" sequence))
924df208 284 (let* ((internal-seq-flag (mh-internal-seq sequence))
a66894d8
BW
285 (original-msgs (mh-seq-msgs (mh-find-seq sequence)))
286 (folders (list mh-current-folder))
f0d73c14
BW
287 (msg-list (mh-range-to-msg-list range)))
288 (mh-add-msgs-to-seq msg-list sequence nil t)
a66894d8 289 (mh-iterate-on-range m range
a66894d8
BW
290 (unless (memq m original-msgs)
291 (mh-add-sequence-notation m internal-seq-flag)))
3d7ca223 292 (if (not internal-seq-flag)
924df208 293 (setq mh-last-seq-used sequence))
a66894d8
BW
294 (when mh-index-data
295 (setq folders
296 (append folders (mh-index-add-to-sequence sequence msg-list))))
924df208 297 (when (and (eq sequence mh-unseen-seq) (mh-speed-flists-active-p))
a66894d8 298 (apply #'mh-speed-flists t folders))))
c26cf6c8 299
bdcfe844
BW
300(defun mh-valid-view-change-operation-p (op)
301 "Check if the view change operation can be performed.
302OP is one of 'widen and 'unthread."
303 (cond ((eq (car mh-view-ops) op)
304 (pop mh-view-ops))
305 (t nil)))
c26cf6c8 306
c3d9274a 307;;;###mh-autoload
a66894d8 308(defun mh-widen (&optional all-flag)
be33fce4 309 "Remove last restriction.
f0d73c14 310If optional prefix argument ALL-FLAG is non-nil, remove all limits."
a66894d8 311 (interactive "P")
a1b4049d 312 (let ((msg (mh-get-msg-num nil)))
a66894d8
BW
313 (when mh-folder-view-stack
314 (cond (all-flag
315 (while (cdr mh-view-ops)
316 (setq mh-view-ops (cdr mh-view-ops)))
317 (when (eq (car mh-view-ops) 'widen)
318 (setq mh-view-ops (cdr mh-view-ops))))
319 ((mh-valid-view-change-operation-p 'widen) nil)
bdcfe844
BW
320 ((memq 'widen mh-view-ops)
321 (while (not (eq (car mh-view-ops) 'widen))
322 (setq mh-view-ops (cdr mh-view-ops)))
a66894d8 323 (setq mh-view-ops (cdr mh-view-ops)))
bdcfe844 324 (t (error "Widening is not applicable")))
a66894d8
BW
325 ;; If ALL-FLAG is non-nil then rewind stacks
326 (when all-flag
327 (while (cdr mh-thread-scan-line-map-stack)
328 (setq mh-thread-scan-line-map-stack
329 (cdr mh-thread-scan-line-map-stack)))
330 (while (cdr mh-folder-view-stack)
331 (setq mh-folder-view-stack (cdr mh-folder-view-stack))))
332 (setq mh-thread-scan-line-map (pop mh-thread-scan-line-map-stack))
c26cf6c8 333 (with-mh-folder-updating (t)
a1b4049d 334 (delete-region (point-min) (point-max))
a66894d8
BW
335 (insert (pop mh-folder-view-stack))
336 (mh-remove-all-notation)
a1b4049d
BW
337 (setq mh-mode-line-annotation mh-non-seq-mode-line-annotation)
338 (mh-make-folder-mode-line))
339 (if msg
bdcfe844
BW
340 (mh-goto-msg msg t t))
341 (mh-notate-deleted-and-refiled)
342 (mh-notate-user-sequences)
3d7ca223 343 (mh-notate-cur)
bdcfe844 344 (mh-recenter nil)))
a66894d8 345 (when (and (null mh-folder-view-stack) (boundp 'tool-bar-mode) tool-bar-mode)
924df208
BW
346 (set (make-local-variable 'tool-bar-map) mh-folder-tool-bar-map)
347 (when (buffer-live-p (get-buffer mh-show-buffer))
348 (save-excursion
349 (set-buffer (get-buffer mh-show-buffer))
350 (set (make-local-variable 'tool-bar-map) mh-show-tool-bar-map)))))
c26cf6c8 351
a1b4049d
BW
352;; FIXME? We may want to clear all notations and add one for current-message
353;; and process user sequences.
a66894d8 354;;;###mh-autoload
a1b4049d 355(defun mh-notate-deleted-and-refiled ()
bdcfe844
BW
356 "Notate messages marked for deletion or refiling.
357Messages to be deleted are given by `mh-delete-list' while messages to be
358refiled are present in `mh-refile-list'."
3d7ca223
BW
359 (let ((refiled-hash (make-hash-table))
360 (deleted-hash (make-hash-table)))
361 (dolist (msg mh-delete-list)
362 (setf (gethash msg deleted-hash) t))
363 (dolist (dest-msg-list mh-refile-list)
364 (dolist (msg (cdr dest-msg-list))
365 (setf (gethash msg refiled-hash) t)))
366 (mh-iterate-on-messages-in-region msg (point-min) (point-max)
367 (cond ((gethash msg refiled-hash)
368 (mh-notate nil mh-note-refiled mh-cmd-note))
369 ((gethash msg deleted-hash)
370 (mh-notate nil mh-note-deleted mh-cmd-note))))))
bdcfe844 371
c26cf6c8
RS
372\f
373
374;;; Commands to manipulate sequences. Sequences are stored in an alist
375;;; of the form:
c3d9274a 376;;; ((seq-name msgs ...) (seq-name msgs ...) ...)
c26cf6c8 377
a66894d8
BW
378(defvar mh-sequence-history ())
379
380;;;###mh-autoload
c26cf6c8 381(defun mh-read-seq-default (prompt not-empty)
bdcfe844
BW
382 "Read and return sequence name with default narrowed or previous sequence.
383PROMPT is the prompt to use when reading. If NOT-EMPTY is non-nil then a
384non-empty sequence is read."
847b8219 385 (mh-read-seq prompt not-empty
a66894d8 386 (or mh-last-seq-used
c3d9274a 387 (car (mh-seq-containing-msg (mh-get-msg-num nil) nil)))))
c26cf6c8 388
c26cf6c8 389(defun mh-read-seq (prompt not-empty &optional default)
bdcfe844
BW
390 "Read and return a sequence name.
391Prompt with PROMPT, raise an error if the sequence is empty and the NOT-EMPTY
392flag is non-nil, and supply an optional DEFAULT sequence. A reply of '%'
393defaults to the first sequence containing the current message."
c26cf6c8 394 (let* ((input (completing-read (format "%s %s %s" prompt "sequence:"
c3d9274a
BW
395 (if default
396 (format "[%s] " default)
397 ""))
a66894d8
BW
398 (mh-seq-names mh-seq-list)
399 nil nil nil 'mh-sequence-history))
c3d9274a
BW
400 (seq (cond ((equal input "%")
401 (car (mh-seq-containing-msg (mh-get-msg-num t) nil)))
402 ((equal input "") default)
403 (t (intern input))))
404 (msgs (mh-seq-to-msgs seq)))
c26cf6c8 405 (if (and (null msgs) not-empty)
c3d9274a 406 (error "No messages in sequence `%s'" seq))
c26cf6c8
RS
407 seq))
408
a66894d8
BW
409;;; Functions to read ranges with completion...
410(defvar mh-range-seq-names)
411(defvar mh-range-history ())
412(defvar mh-range-completion-map (copy-keymap minibuffer-local-completion-map))
413(define-key mh-range-completion-map " " 'self-insert-command)
414
415(defun mh-range-completion-function (string predicate flag)
416 "Programmable completion of message ranges.
417STRING is the user input that is to be completed. PREDICATE if non-nil is a
418function used to filter the possible choices and FLAG determines whether the
419completion is over."
420 (let* ((candidates mh-range-seq-names)
421 (last-char (and (not (equal string ""))
422 (aref string (1- (length string)))))
423 (last-word (cond ((null last-char) "")
424 ((memq last-char '(? ?- ?:)) "")
425 (t (car (last (split-string string "[ -:]+"))))))
426 (prefix (substring string 0 (- (length string) (length last-word)))))
427 (cond ((eq flag nil)
428 (let ((res (try-completion last-word candidates predicate)))
429 (cond ((null res) nil)
430 ((eq res t) t)
431 (t (concat prefix res)))))
432 ((eq flag t)
433 (all-completions last-word candidates predicate))
434 ((eq flag 'lambda)
435 (loop for x in candidates
436 when (equal x last-word) return t
437 finally return nil)))))
438
439;;;###mh-autoload
440(defun mh-read-range (prompt &optional folder default
441 expand-flag ask-flag number-as-range-flag)
442 "Read a message range with PROMPT.
443
444If FOLDER is non-nil then a range is read from that folder, otherwise use
445`mh-current-folder'.
446
447If DEFAULT is a string then use that as default range to return. If DEFAULT is
448nil then ask user with default answer a range based on the sequences that seem
449relevant. Finally if DEFAULT is t, try to avoid prompting the user. Unseen
450messages, if present, are returned. If the folder has fewer than
451`mh-large-folder' messages then \"all\" messages are returned. Finally as a
452last resort prompt the user.
453
454If EXPAND-FLAG is non-nil then a list of message numbers corresponding to the
455input is returned. If this list is empty then an error is raised. If
456EXPAND-FLAG is nil just return the input string. In this case we don't check
457if the range is empty.
458
459If ASK-FLAG is non-nil, then the user is always queried for a range of
460messages. If ASK-FLAG is nil, then the function checks if the unseen sequence
461is non-empty. If that is the case, `mh-unseen-seq', or the list of messages in
462it depending on the value of EXPAND, is returned. Otherwise if the folder has
463fewer than `mh-large-folder' messages then the list of messages corresponding
464to \"all\" is returned. If neither of the above holds then as a last resort
465the user is queried for a range of messages.
466
467If NUMBER-AS-RANGE-FLAG is non-nil, then if a number, N is read as input, it
468is interpreted as the range \"last:N\".
469
470This function replaces the existing function `mh-read-msg-range'. Calls to:
471 (mh-read-msg-range folder flag)
472should be replaced with:
473 (mh-read-range \"Suitable prompt\" folder t nil flag
474 mh-interpret-number-as-range-flag)"
475 (setq default (or default mh-last-seq-used
476 (car (mh-seq-containing-msg (mh-get-msg-num nil) t)))
477 prompt (format "%s range" prompt))
478 (let* ((folder (or folder mh-current-folder))
479 (default (cond ((or (eq default t) (stringp default)) default)
480 ((symbolp default) (symbol-name default))))
481 (guess (eq default t))
482 (counts (and guess (mh-folder-size folder)))
483 (unseen (and counts (> (cadr counts) 0)))
484 (large (and counts mh-large-folder (> (car counts) mh-large-folder)))
485 (str (cond ((and guess large
486 (setq default (format "last:%s" mh-large-folder)
487 prompt (format "%s (folder has %s messages)"
488 prompt (car counts)))
489 nil))
490 ((and guess (not large) (setq default "all") nil))
491 ((eq default nil) "")
492 (t (format "[%s] " default))))
493 (minibuffer-local-completion-map mh-range-completion-map)
494 (seq-list (if (eq folder mh-current-folder)
495 mh-seq-list
496 (mh-read-folder-sequences folder nil)))
497 (mh-range-seq-names
498 (append '(("first") ("last") ("all") ("prev") ("next"))
499 (mh-seq-names seq-list)))
500 (input (cond ((and (not ask-flag) unseen) (symbol-name mh-unseen-seq))
501 ((and (not ask-flag) (not large)) "all")
502 (t (completing-read (format "%s: %s" prompt str)
503 'mh-range-completion-function nil nil
504 nil 'mh-range-history default))))
505 msg-list)
506 (when (and number-as-range-flag
507 (string-match "^[ \t]*\\([0-9]+\\)[ \t]*$" input))
508 (setq input (concat "last:" (match-string 1 input))))
509 (cond ((not expand-flag) input)
510 ((assoc (intern input) seq-list)
511 (cdr (assoc (intern input) seq-list)))
512 ((setq msg-list (mh-translate-range folder input)) msg-list)
513 (t (error "No messages in range `%s'" input)))))
514
515;;;###mh-autoload
516(defun mh-translate-range (folder expr)
517 "In FOLDER, translate the string EXPR to a list of messages numbers."
518 (save-excursion
519 (let ((strings (delete "" (split-string expr "[ \t\n]")))
520 (result ()))
521 (ignore-errors
522 (apply #'mh-exec-cmd-quiet nil "mhpath" folder strings)
523 (set-buffer mh-temp-buffer)
524 (goto-char (point-min))
525 (while (re-search-forward "/\\([0-9]*\\)$" nil t)
526 (push (car (read-from-string (match-string 1))) result))
527 (nreverse result)))))
528
c26cf6c8 529(defun mh-seq-names (seq-list)
bdcfe844
BW
530 "Return an alist containing the names of the SEQ-LIST."
531 (mapcar (lambda (entry) (list (symbol-name (mh-seq-name entry))))
c3d9274a 532 seq-list))
c26cf6c8 533
c3d9274a 534;;;###mh-autoload
847b8219
KH
535(defun mh-rename-seq (sequence new-name)
536 "Rename SEQUENCE to have NEW-NAME."
c26cf6c8 537 (interactive (list (mh-read-seq "Old" t)
c3d9274a 538 (intern (read-string "New sequence name: "))))
847b8219 539 (let ((old-seq (mh-find-seq sequence)))
c26cf6c8 540 (or old-seq
c3d9274a 541 (error "Sequence %s does not exist" sequence))
847b8219 542 ;; create new sequence first, since it might raise an error.
c26cf6c8 543 (mh-define-sequence new-name (mh-seq-msgs old-seq))
847b8219 544 (mh-undefine-sequence sequence (mh-seq-msgs old-seq))
c26cf6c8
RS
545 (rplaca old-seq new-name)))
546
3d7ca223
BW
547;;;###mh-autoload
548(defun mh-notate-cur ()
549 "Mark the MH sequence cur.
550In addition to notating the current message with `mh-note-cur' the function
551uses `overlay-arrow-position' to put a marker in the fringe."
552 (let ((cur (car (mh-seq-to-msgs 'cur))))
553 (when (and cur (mh-goto-msg cur t t))
3d7ca223 554 (beginning-of-line)
924df208
BW
555 (when (looking-at mh-scan-good-msg-regexp)
556 (mh-notate nil mh-note-cur mh-cmd-note))
3d7ca223
BW
557 (setq mh-arrow-marker (set-marker mh-arrow-marker (point)))
558 (setq overlay-arrow-position mh-arrow-marker))))
c26cf6c8 559
c3d9274a 560;;;###mh-autoload
c26cf6c8 561(defun mh-add-to-sequence (seq msgs)
bdcfe844 562 "The sequence SEQ is augmented with the messages in MSGS."
c26cf6c8 563 ;; Add to a SEQUENCE each message the list of MSGS.
a66894d8 564 (if (and (mh-valid-seq-p seq) (not (mh-folder-name-p seq)))
c26cf6c8 565 (if msgs
c3d9274a
BW
566 (apply 'mh-exec-cmd "mark" mh-current-folder "-add"
567 "-sequence" (symbol-name seq)
568 (mh-coalesce-msg-list msgs)))))
c26cf6c8 569
924df208
BW
570(defvar mh-thread-last-ancestor)
571
bdcfe844
BW
572(defun mh-copy-seq-to-eob (seq)
573 "Copy SEQ to the end of the buffer."
574 ;; It is quite involved to write something which will work at any place in
575 ;; the buffer, so we will write something which works only at the end of
576 ;; the buffer. If we ever need to insert sequences in the middle of the
577 ;; buffer, this will need to be fixed.
578 (save-excursion
579 (let* ((msgs (mh-seq-to-msgs seq))
580 (coalesced-msgs (mh-coalesce-msg-list msgs)))
581 (goto-char (point-max))
582 (save-restriction
583 (narrow-to-region (point) (point))
584 (mh-regenerate-headers coalesced-msgs t)
c3d9274a
BW
585 (cond ((memq 'unthread mh-view-ops)
586 ;; Populate restricted scan-line map
a66894d8
BW
587 (mh-remove-all-notation)
588 (mh-iterate-on-range msg (cons (point-min) (point-max))
589 (setf (gethash msg mh-thread-scan-line-map)
590 (mh-thread-parse-scan-line)))
c3d9274a
BW
591 ;; Remove scan lines and read results from pre-computed tree
592 (delete-region (point-min) (point-max))
924df208 593 (mh-thread-print-scan-lines
a66894d8
BW
594 (mh-thread-generate mh-current-folder ()))
595 (mh-notate-user-sequences))
c3d9274a
BW
596 (mh-index-data
597 (mh-index-insert-folder-headers)))))))
c26cf6c8 598
3d7ca223
BW
599;;;###mh-autoload
600(defmacro mh-iterate-on-messages-in-region (var begin end &rest body)
601 "Iterate over region.
602VAR is bound to the message on the current line as we loop starting from BEGIN
603till END. In each step BODY is executed.
604
605If VAR is nil then the loop is executed without any binding."
606 (unless (symbolp var)
607 (error "Can not bind the non-symbol %s" var))
608 (let ((binding-needed-flag var))
609 `(save-excursion
610 (goto-char ,begin)
924df208 611 (beginning-of-line)
3d7ca223
BW
612 (while (and (<= (point) ,end) (not (eobp)))
613 (when (looking-at mh-scan-valid-regexp)
614 (let ,(if binding-needed-flag `((,var (mh-get-msg-num t))) ())
615 ,@body))
616 (forward-line 1)))))
617
924df208
BW
618(put 'mh-iterate-on-messages-in-region 'lisp-indent-hook 'defun)
619
620;;;###mh-autoload
a66894d8 621(defmacro mh-iterate-on-range (var range &rest body)
924df208
BW
622 "Iterate an operation over a region or sequence.
623
a66894d8
BW
624VAR is bound to each message in turn in a loop over RANGE, which can be a
625message number, a list of message numbers, a sequence, a region in a cons
626cell, or a MH range (something like last:20) in a string. In each iteration,
627BODY is executed.
924df208 628
a66894d8 629The parameter RANGE is usually created with `mh-interactive-range'
924df208
BW
630in order to provide a uniform interface to MH-E functions."
631 (unless (symbolp var)
632 (error "Can not bind the non-symbol %s" var))
633 (let ((binding-needed-flag var)
634 (msgs (make-symbol "msgs"))
635 (seq-hash-table (make-symbol "seq-hash-table")))
a66894d8
BW
636 `(cond ((numberp ,range)
637 (when (mh-goto-msg ,range t t)
638 (let ,(if binding-needed-flag `((,var ,range)) ())
924df208 639 ,@body)))
a66894d8
BW
640 ((and (consp ,range)
641 (numberp (car ,range)) (numberp (cdr ,range)))
924df208 642 (mh-iterate-on-messages-in-region ,var
a66894d8 643 (car ,range) (cdr ,range)
924df208 644 ,@body))
a66894d8
BW
645 (t (let ((,msgs (cond ((and ,range (symbolp ,range))
646 (mh-seq-to-msgs ,range))
647 ((stringp ,range)
648 (mh-translate-range mh-current-folder
649 ,range))
650 (t ,range)))
924df208
BW
651 (,seq-hash-table (make-hash-table)))
652 (dolist (msg ,msgs)
653 (setf (gethash msg ,seq-hash-table) t))
654 (mh-iterate-on-messages-in-region v (point-min) (point-max)
655 (when (gethash v ,seq-hash-table)
656 (let ,(if binding-needed-flag `((,var v)) ())
657 ,@body))))))))
658
a66894d8 659(put 'mh-iterate-on-range 'lisp-indent-hook 'defun)
924df208
BW
660
661;;;###mh-autoload
a66894d8
BW
662(defun mh-range-to-msg-list (range)
663 "Return a list of messages for RANGE.
664RANGE can be a message number, a list of message numbers, a sequence, or
924df208
BW
665a region in a cons cell."
666 (let (msg-list)
a66894d8 667 (mh-iterate-on-range msg range
924df208
BW
668 (push msg msg-list))
669 (nreverse msg-list)))
670
671;;;###mh-autoload
f0d73c14 672(defun mh-interactive-range (range-prompt &optional default)
a66894d8
BW
673 "Return interactive specification for message, sequence, range or region.
674By convention, the name of this argument is RANGE.
924df208
BW
675
676If variable `transient-mark-mode' is non-nil and the mark is active, then this
677function returns a cons-cell of the region.
a66894d8
BW
678
679If optional prefix argument is provided, then prompt for message range with
680RANGE-PROMPT. A list of messages in that range is returned.
681
682If a MH range is given, say something like last:20, then a list containing
683the messages in that range is returned.
684
f0d73c14
BW
685If DEFAULT non-nil then it is returned.
686
924df208
BW
687Otherwise, the message number at point is returned.
688
a66894d8
BW
689This function is usually used with `mh-iterate-on-range' in order to provide
690a uniform interface to MH-E functions."
691 (cond ((mh-mark-active-p t) (cons (region-beginning) (region-end)))
692 (current-prefix-arg (mh-read-range range-prompt nil nil t t))
f0d73c14 693 (default default)
a66894d8 694 (t (mh-get-msg-num t))))
924df208 695
a1b4049d 696\f
bdcfe844 697
a1b4049d
BW
698;;; Commands to handle new 'subject sequence.
699;;; Or "Poor man's threading" by psg.
bdcfe844 700
a66894d8
BW
701;;; XXX: The function mh-subject-to-sequence-unthreaded uses the magic number
702;;; 41 for the max size of the subject part. Avoiding this would be desirable.
bdcfe844 703(defun mh-subject-to-sequence (all)
a1b4049d
BW
704 "Put all following messages with same subject in sequence 'subject.
705If arg ALL is t, move to beginning of folder buffer to collect all messages.
706If arg ALL is nil, collect only messages fron current one on forward.
bdcfe844 707
a66894d8
BW
708Return number of messages put in the sequence:
709
710 nil -> there was no subject line.
711 0 -> there were no later messages with the same subject (sequence not made)
712 >1 -> the total number of messages including current one."
713 (if (memq 'unthread mh-view-ops)
714 (mh-subject-to-sequence-threaded all)
715 (mh-subject-to-sequence-unthreaded all)))
716
717(defun mh-subject-to-sequence-unthreaded (all)
718 "Put all following messages with same subject in sequence 'subject.
719This function only works with an unthreaded folder. If arg ALL is t, move to
720beginning of folder buffer to collect all messages. If arg ALL is nil, collect
721only messages fron current one on forward.
722
a1b4049d 723Return number of messages put in the sequence:
bdcfe844 724
a1b4049d
BW
725 nil -> there was no subject line.
726 0 -> there were no later messages with the same subject (sequence not made)
727 >1 -> the total number of messages including current one."
728 (if (not (eq major-mode 'mh-folder-mode))
729 (error "Not in a folder buffer"))
730 (save-excursion
731 (beginning-of-line)
732 (if (or (not (looking-at mh-scan-subject-regexp))
bdcfe844
BW
733 (not (match-string 3))
734 (string-equal "" (match-string 3)))
f0d73c14 735 (progn (message "No subject line")
a1b4049d 736 nil)
bdcfe844 737 (let ((subject (match-string-no-properties 3))
a1b4049d
BW
738 (list))
739 (if (> (length subject) 41)
740 (setq subject (substring subject 0 41)))
741 (save-excursion
742 (if all
743 (goto-char (point-min)))
744 (while (re-search-forward mh-scan-subject-regexp nil t)
bdcfe844 745 (let ((this-subject (match-string-no-properties 3)))
a1b4049d
BW
746 (if (> (length this-subject) 41)
747 (setq this-subject (substring this-subject 0 41)))
748 (if (string-equal this-subject subject)
749 (setq list (cons (mh-get-msg-num t) list))))))
750 (cond
751 (list
752 ;; If we created a new sequence, add the initial message to it too.
753 (if (not (member (mh-get-msg-num t) list))
754 (setq list (cons (mh-get-msg-num t) list)))
a66894d8 755 (if (assoc 'subject mh-seq-list) (mh-delete-seq 'subject))
a1b4049d 756 ;; sort the result into a sequence
bdcfe844 757 (let ((sorted-list (sort (copy-sequence list) 'mh-lessp)))
a1b4049d 758 (while sorted-list
bdcfe844 759 (mh-add-msgs-to-seq (car sorted-list) 'subject nil)
a1b4049d
BW
760 (setq sorted-list (cdr sorted-list)))
761 (safe-length list)))
762 (t
763 0))))))
764
a66894d8
BW
765(defun mh-subject-to-sequence-threaded (all)
766 "Put all messages with the same subject in the 'subject sequence.
767This function works when the folder is threaded. In this situation the subject
768could get truncated and so the normal matching doesn't work.
769
770The parameter ALL is non-nil then all the messages in the buffer are
771considered, otherwise only the messages after the current one are taken into
772account."
773 (let* ((cur (mh-get-msg-num nil))
774 (subject (mh-thread-find-msg-subject cur))
775 region msgs)
776 (if (null subject)
777 (and (message "No subject line") nil)
778 (setq region (cons (if all (point-min) (point)) (point-max)))
779 (mh-iterate-on-range msg region
780 (when (eq (mh-thread-find-msg-subject msg) subject)
781 (push msg msgs)))
782 (setq msgs (sort msgs #'mh-lessp))
783 (if (null msgs)
784 0
785 (when (assoc 'subject mh-seq-list)
786 (mh-delete-seq 'subject))
787 (mh-add-msgs-to-seq msgs 'subject)
788 (length msgs)))))
789
790(defun mh-thread-find-msg-subject (msg)
791 "Find canonicalized subject of MSG.
792This function can only be used the folder is threaded."
793 (ignore-errors
794 (mh-message-subject
795 (mh-container-message (gethash (gethash msg mh-thread-index-id-map)
796 mh-thread-id-table)))))
797
f0d73c14
BW
798(defun mh-edit-pick-expr (default)
799 "With prefix arg edit a pick expression.
a66894d8
BW
800If no prefix arg is given, then return DEFAULT."
801 (let ((default-string (loop for x in default concat (format " %s" x))))
802 (if (or current-prefix-arg (equal default-string ""))
e495eaec
BW
803 (mh-pick-args-list (read-string "Pick expression: "
804 default-string))
a66894d8
BW
805 default)))
806
e495eaec
BW
807(defun mh-pick-args-list (s)
808 "Form list by grouping elements in string S suitable for pick arguments.
809For example, the string \"-subject a b c -from Joe User <user@domain.com>\"
810is converted to (\"-subject\" \"a b c\" \"-from\"
811\"Joe User <user@domain.com>\""
812 (let ((full-list (split-string s))
813 current-arg collection arg-list)
814 (while full-list
815 (setq current-arg (car full-list))
816 (if (null (string-match "^-" current-arg))
817 (setq collection
818 (if (null collection)
819 current-arg
820 (format "%s %s" collection current-arg)))
821 (when collection
822 (setq arg-list (append arg-list (list collection)))
823 (setq collection nil))
824 (setq arg-list (append arg-list (list current-arg))))
825 (setq full-list (cdr full-list)))
826 (when collection
827 (setq arg-list (append arg-list (list collection))))
828 arg-list))
829
a66894d8 830;;;###mh-autoload
f0d73c14
BW
831(defun mh-narrow-to-subject (&optional pick-expr)
832 "Limit to messages with same subject.
833With a prefix argument, edit PICK-EXPR.
834
835Use \\<mh-folder-mode-map>\\[mh-widen] to undo this command."
a66894d8 836 (interactive
f0d73c14
BW
837 (list (mh-edit-pick-expr (mh-current-message-header-field 'subject))))
838 (mh-narrow-to-header-field 'subject pick-expr))
a66894d8
BW
839
840;;;###mh-autoload
f0d73c14
BW
841(defun mh-narrow-to-from (&optional pick-expr)
842 "Limit to messages with the same `From:' field.
843With a prefix argument, edit PICK-EXPR.
844
845Use \\<mh-folder-mode-map>\\[mh-widen] to undo this command."
a66894d8 846 (interactive
f0d73c14
BW
847 (list (mh-edit-pick-expr (mh-current-message-header-field 'from))))
848 (mh-narrow-to-header-field 'from pick-expr))
a66894d8
BW
849
850;;;###mh-autoload
f0d73c14
BW
851(defun mh-narrow-to-cc (&optional pick-expr)
852 "Limit to messages with the same `Cc:' field.
853With a prefix argument, edit PICK-EXPR.
854
855Use \\<mh-folder-mode-map>\\[mh-widen] to undo this command."
a66894d8 856 (interactive
f0d73c14
BW
857 (list (mh-edit-pick-expr (mh-current-message-header-field 'cc))))
858 (mh-narrow-to-header-field 'cc pick-expr))
a66894d8 859
f0d73c14
BW
860;;;###mh-autoload
861(defun mh-narrow-to-to (&optional pick-expr)
862 "Limit to messages with the same `To:' field.
863With a prefix argument, edit PICK-EXPR.
864
865Use \\<mh-folder-mode-map>\\[mh-widen] to undo this command."
866 (interactive
867 (list (mh-edit-pick-expr (mh-current-message-header-field 'to))))
868 (mh-narrow-to-header-field 'to pick-expr))
869
870(defun mh-narrow-to-header-field (header-field pick-expr)
871 "Limit to messages whose HEADER-FIELD match PICK-EXPR.
a66894d8
BW
872The MH command pick is used to do the match."
873 (let ((folder mh-current-folder)
874 (original (mh-coalesce-msg-list
875 (mh-range-to-msg-list (cons (point-min) (point-max)))))
876 (msg-list ()))
877 (with-temp-buffer
878 (apply #'mh-exec-cmd-output "pick" nil folder
f0d73c14 879 (append original (list "-list") pick-expr))
a66894d8
BW
880 (goto-char (point-min))
881 (while (not (eobp))
882 (let ((num (read-from-string
883 (buffer-substring (point) (line-end-position)))))
884 (when (numberp (car num)) (push (car num) msg-list))
885 (forward-line))))
886 (if (null msg-list)
887 (message "No matches")
888 (when (assoc 'header mh-seq-list) (mh-delete-seq 'header))
889 (mh-add-msgs-to-seq msg-list 'header)
890 (mh-narrow-to-seq 'header))))
891
892(defun mh-current-message-header-field (header-field)
893 "Return a pick regexp to match HEADER-FIELD of the message at point."
894 (let ((num (mh-get-msg-num nil)))
895 (when num
896 (let ((folder mh-current-folder))
897 (with-temp-buffer
898 (insert-file-contents-literally (mh-msg-filename num folder))
899 (goto-char (point-min))
900 (when (search-forward "\n\n" nil t)
901 (narrow-to-region (point-min) (point)))
902 (let* ((field (or (message-fetch-field (format "%s" header-field))
903 ""))
904 (field-option (format "-%s" header-field))
905 (patterns (loop for x in (split-string field "[ ]*,[ ]*")
906 unless (equal x "")
907 collect (if (string-match "<\\(.*@.*\\)>" x)
908 (match-string 1 x)
909 x))))
910 (when patterns
911 (loop with accum = `(,field-option ,(car patterns))
912 for e in (cdr patterns)
913 do (setq accum `(,field-option ,e "-or" ,@accum))
914 finally return accum))))))))
915
916;;;###mh-autoload
917(defun mh-narrow-to-range (range)
918 "Limit to messages in RANGE.
919
920Check the documentation of `mh-interactive-range' to see how RANGE is read in
f0d73c14
BW
921interactive use.
922
923Use \\<mh-folder-mode-map>\\[mh-widen] to undo this command."
a66894d8
BW
924 (interactive (list (mh-interactive-range "Narrow to")))
925 (when (assoc 'range mh-seq-list) (mh-delete-seq 'range))
926 (mh-add-msgs-to-seq (mh-range-to-msg-list range) 'range)
927 (mh-narrow-to-seq 'range))
928
929
c3d9274a 930;;;###mh-autoload
bdcfe844
BW
931(defun mh-delete-subject ()
932 "Mark all following messages with same subject to be deleted.
933This puts the messages in a sequence named subject. You can undo the last
934deletion marks using `mh-undo' with a prefix argument and then specifying the
935subject sequence."
a1b4049d 936 (interactive)
bdcfe844 937 (let ((count (mh-subject-to-sequence nil)))
a1b4049d
BW
938 (cond
939 ((not count) ; No subject line, delete msg anyway
940 (mh-delete-msg (mh-get-msg-num t)))
941 ((= 0 count) ; No other msgs, delete msg anyway.
f0d73c14 942 (message "No other messages with same Subject following this one")
a1b4049d
BW
943 (mh-delete-msg (mh-get-msg-num t)))
944 (t ; We have a subject sequence.
945 (message "Marked %d messages for deletion" count)
946 (mh-delete-msg 'subject)))))
947
c3d9274a
BW
948;;;###mh-autoload
949(defun mh-delete-subject-or-thread ()
950 "Mark messages for deletion intelligently.
951If the folder is threaded then `mh-thread-delete' is used to mark the current
952message and all its descendants for deletion. Otherwise `mh-delete-subject' is
953used to mark the current message and all messages following it with the same
954subject for deletion."
955 (interactive)
956 (if (memq 'unthread mh-view-ops)
957 (mh-thread-delete)
958 (mh-delete-subject)))
959
bdcfe844
BW
960;;; Message threading:
961
a66894d8
BW
962(defmacro mh-thread-initialize-hash (var test)
963 "Initialize the hash table in VAR.
964TEST is the test to use when creating a new hash table."
965 (unless (symbolp var) (error "Expected a symbol: %s" var))
966 `(if ,var (clrhash ,var) (setq ,var (make-hash-table :test ,test))))
967
bdcfe844 968(defun mh-thread-initialize ()
a66894d8
BW
969 "Make new hash tables, or clear them if already present."
970 (mh-thread-initialize-hash mh-thread-id-hash #'equal)
971 (mh-thread-initialize-hash mh-thread-subject-hash #'equal)
972 (mh-thread-initialize-hash mh-thread-id-table #'eq)
973 (mh-thread-initialize-hash mh-thread-id-index-map #'eq)
974 (mh-thread-initialize-hash mh-thread-index-id-map #'eql)
975 (mh-thread-initialize-hash mh-thread-scan-line-map #'eql)
976 (mh-thread-initialize-hash mh-thread-subject-container-hash #'eq)
977 (mh-thread-initialize-hash mh-thread-duplicates #'eq)
978 (setq mh-thread-history ()))
bdcfe844
BW
979
980(defsubst mh-thread-id-container (id)
981 "Given ID, return the corresponding container in `mh-thread-id-table'.
982If no container exists then a suitable container is created and the id-table
983is updated."
984 (when (not id)
985 (error "1"))
986 (or (gethash id mh-thread-id-table)
987 (setf (gethash id mh-thread-id-table)
988 (let ((message (mh-thread-make-message :id id)))
989 (mh-thread-make-container :message message)))))
990
991(defsubst mh-thread-remove-parent-link (child)
992 "Remove parent link of CHILD if it exists."
993 (let* ((child-container (if (mh-thread-container-p child)
994 child (mh-thread-id-container child)))
995 (parent-container (mh-container-parent child-container)))
996 (when parent-container
997 (setf (mh-container-children parent-container)
c3d9274a
BW
998 (loop for elem in (mh-container-children parent-container)
999 unless (eq child-container elem) collect elem))
bdcfe844
BW
1000 (setf (mh-container-parent child-container) nil))))
1001
1002(defsubst mh-thread-add-link (parent child &optional at-end-p)
1003 "Add links so that PARENT becomes a parent of CHILD.
1004Doesn't make any changes if CHILD is already an ancestor of PARENT. If
1005optional argument AT-END-P is non-nil, the CHILD is added to the end of the
1006children list of PARENT."
1007 (let ((parent-container (cond ((null parent) nil)
1008 ((mh-thread-container-p parent) parent)
1009 (t (mh-thread-id-container parent))))
1010 (child-container (if (mh-thread-container-p child)
1011 child (mh-thread-id-container child))))
1012 (when (and parent-container
1013 (not (mh-thread-ancestor-p child-container parent-container))
1014 (not (mh-thread-ancestor-p parent-container child-container)))
1015 (mh-thread-remove-parent-link child-container)
1016 (cond ((not at-end-p)
1017 (push child-container (mh-container-children parent-container)))
1018 ((null (mh-container-children parent-container))
1019 (push child-container (mh-container-children parent-container)))
1020 (t (let ((last-child (mh-container-children parent-container)))
1021 (while (cdr last-child)
1022 (setq last-child (cdr last-child)))
1023 (setcdr last-child (cons child-container nil)))))
1024 (setf (mh-container-parent child-container) parent-container))
1025 (unless parent-container
1026 (mh-thread-remove-parent-link child-container))))
1027
1028(defun mh-thread-ancestor-p (ancestor successor)
1029 "Return t if ANCESTOR is really an ancestor of SUCCESSOR and nil otherwise.
1030In the limit, the function returns t if ANCESTOR and SUCCESSOR are the same
1031containers."
1032 (block nil
1033 (while successor
1034 (when (eq ancestor successor) (return t))
1035 (setq successor (mh-container-parent successor)))
1036 nil))
1037
1038(defsubst mh-thread-get-message-container (message)
1039 "Return container which has MESSAGE in it.
1040If there is no container present then a new container is allocated."
1041 (let* ((id (mh-message-id message))
1042 (container (gethash id mh-thread-id-table)))
1043 (cond (container (setf (mh-container-message container) message)
1044 container)
1045 (t (setf (gethash id mh-thread-id-table)
1046 (mh-thread-make-container :message message))))))
1047
1048(defsubst mh-thread-get-message (id subject-re-p subject refs)
1049 "Return appropriate message.
1050Otherwise update message already present to have the proper ID, SUBJECT-RE-P,
1051SUBJECT and REFS fields."
1052 (let* ((container (gethash id mh-thread-id-table))
1053 (message (if container (mh-container-message container) nil)))
1054 (cond (message
1055 (setf (mh-message-subject-re-p message) subject-re-p)
1056 (setf (mh-message-subject message) subject)
1057 (setf (mh-message-id message) id)
1058 (setf (mh-message-references message) refs)
1059 message)
1060 (container
1061 (setf (mh-container-message container)
f0d73c14
BW
1062 (mh-thread-make-message :id id :references refs
1063 :subject subject
1064 :subject-re-p subject-re-p)))
1065 (t (let ((message (mh-thread-make-message :id id :references refs
1066 :subject-re-p subject-re-p
1067 :subject subject)))
bdcfe844
BW
1068 (prog1 message
1069 (mh-thread-get-message-container message)))))))
1070
1071(defsubst mh-thread-canonicalize-id (id)
1072 "Produce canonical string representation for ID.
1073This allows cheap string comparison with EQ."
1074 (or (and (equal id "") (copy-sequence ""))
1075 (gethash id mh-thread-id-hash)
1076 (setf (gethash id mh-thread-id-hash) id)))
1077
1078(defsubst mh-thread-prune-subject (subject)
1079 "Prune leading Re:'s, Fwd:'s etc. and trailing (fwd)'s from SUBJECT.
1080If the result after pruning is not the empty string then it is canonicalized
1081so that subjects can be tested for equality with eq. This is done so that all
1082the messages without a subject are not put into a single thread."
1083 (let ((case-fold-search t)
1084 (subject-pruned-flag nil))
1085 ;; Prune subject leader
1086 (while (or (string-match "^[ \t]*\\(re\\|fwd?\\)\\(\\[[0-9]*\\]\\)?:[ \t]*"
1087 subject)
1088 (string-match "^[ \t]*\\[[^\\]][ \t]*" subject))
1089 (setq subject-pruned-flag t)
1090 (setq subject (substring subject (match-end 0))))
1091 ;; Prune subject trailer
1092 (while (or (string-match "(fwd)$" subject)
1093 (string-match "[ \t]+$" subject))
1094 (setq subject-pruned-flag t)
1095 (setq subject (substring subject 0 (match-beginning 0))))
1096 ;; Canonicalize subject only if it is non-empty
1097 (cond ((equal subject "") (values subject subject-pruned-flag))
1098 (t (values
1099 (or (gethash subject mh-thread-subject-hash)
1100 (setf (gethash subject mh-thread-subject-hash) subject))
1101 subject-pruned-flag)))))
1102
1103(defun mh-thread-container-subject (container)
1104 "Return the subject of CONTAINER.
1105If CONTAINER is empty return the subject info of one of its children."
1106 (cond ((and (mh-container-message container)
1107 (mh-message-id (mh-container-message container)))
1108 (mh-message-subject (mh-container-message container)))
1109 (t (block nil
1110 (dolist (kid (mh-container-children container))
1111 (when (and (mh-container-message kid)
1112 (mh-message-id (mh-container-message kid)))
1113 (let ((kid-message (mh-container-message kid)))
1114 (return (mh-message-subject kid-message)))))
1115 (error "This can't happen!")))))
1116
1117(defun mh-thread-rewind-pruning ()
1118 "Restore the thread tree to its state before pruning."
1119 (while mh-thread-history
1120 (let ((action (pop mh-thread-history)))
1121 (cond ((eq (car action) 'DROP)
1122 (mh-thread-remove-parent-link (cadr action))
1123 (mh-thread-add-link (caddr action) (cadr action)))
1124 ((eq (car action) 'PROMOTE)
1125 (let ((node (cadr action))
1126 (parent (caddr action))
1127 (children (cdddr action)))
1128 (dolist (child children)
1129 (mh-thread-remove-parent-link child)
1130 (mh-thread-add-link node child))
1131 (mh-thread-add-link parent node)))
1132 ((eq (car action) 'SUBJECT)
1133 (let ((node (cadr action)))
1134 (mh-thread-remove-parent-link node)
1135 (setf (mh-container-real-child-p node) t)))))))
1136
1137(defun mh-thread-prune-containers (roots)
c3d9274a 1138 "Prune empty containers in the containers ROOTS."
bdcfe844
BW
1139 (let ((dfs-ordered-nodes ())
1140 (work-list roots))
1141 (while work-list
1142 (let ((node (pop work-list)))
1143 (dolist (child (mh-container-children node))
1144 (push child work-list))
1145 (push node dfs-ordered-nodes)))
1146 (while dfs-ordered-nodes
1147 (let ((node (pop dfs-ordered-nodes)))
1148 (cond ((gethash (mh-message-id (mh-container-message node))
1149 mh-thread-id-index-map)
1150 ;; Keep it
1151 (setf (mh-container-children node)
1152 (mh-thread-sort-containers (mh-container-children node))))
1153 ((and (mh-container-children node)
1154 (or (null (cdr (mh-container-children node)))
1155 (mh-container-parent node)))
1156 ;; Promote kids
1157 (let ((children ()))
1158 (dolist (kid (mh-container-children node))
1159 (mh-thread-remove-parent-link kid)
1160 (mh-thread-add-link (mh-container-parent node) kid)
1161 (push kid children))
1162 (push `(PROMOTE ,node ,(mh-container-parent node) ,@children)
1163 mh-thread-history)
1164 (mh-thread-remove-parent-link node)))
1165 ((mh-container-children node)
1166 ;; Promote the first orphan to parent and add the other kids as
1167 ;; his children
1168 (setf (mh-container-children node)
1169 (mh-thread-sort-containers (mh-container-children node)))
1170 (let ((new-parent (car (mh-container-children node)))
1171 (other-kids (cdr (mh-container-children node))))
1172 (mh-thread-remove-parent-link new-parent)
1173 (dolist (kid other-kids)
1174 (mh-thread-remove-parent-link kid)
1175 (setf (mh-container-real-child-p kid) nil)
1176 (mh-thread-add-link new-parent kid t))
1177 (push `(PROMOTE ,node ,(mh-container-parent node)
1178 ,new-parent ,@other-kids)
1179 mh-thread-history)
1180 (mh-thread-remove-parent-link node)))
1181 (t
1182 ;; Drop it
1183 (push `(DROP ,node ,(mh-container-parent node))
1184 mh-thread-history)
1185 (mh-thread-remove-parent-link node)))))
1186 (let ((results ()))
1187 (maphash #'(lambda (k v)
1188 (declare (ignore k))
1189 (when (and (null (mh-container-parent v))
1190 (gethash (mh-message-id (mh-container-message v))
1191 mh-thread-id-index-map))
1192 (push v results)))
1193 mh-thread-id-table)
1194 (mh-thread-sort-containers results))))
1195
1196(defun mh-thread-sort-containers (containers)
1197 "Sort a list of message CONTAINERS to be in ascending order wrt index."
1198 (sort containers
1199 #'(lambda (x y)
1200 (when (and (mh-container-message x) (mh-container-message y))
1201 (let* ((id-x (mh-message-id (mh-container-message x)))
1202 (id-y (mh-message-id (mh-container-message y)))
1203 (index-x (gethash id-x mh-thread-id-index-map))
1204 (index-y (gethash id-y mh-thread-id-index-map)))
1205 (and (integerp index-x) (integerp index-y)
1206 (< index-x index-y)))))))
1207
1208(defsubst mh-thread-group-by-subject (roots)
1209 "Group the set of message containers, ROOTS based on subject.
1210Bug: Check for and make sure that something without Re: is made the parent in
1211preference to something that has it."
1212 (clrhash mh-thread-subject-container-hash)
1213 (let ((results ()))
1214 (dolist (root roots)
1215 (let* ((subject (mh-thread-container-subject root))
1216 (parent (gethash subject mh-thread-subject-container-hash)))
1217 (cond (parent (mh-thread-remove-parent-link root)
1218 (mh-thread-add-link parent root t)
1219 (setf (mh-container-real-child-p root) nil)
1220 (push `(SUBJECT ,root) mh-thread-history))
1221 (t
1222 (setf (gethash subject mh-thread-subject-container-hash) root)
1223 (push root results)))))
1224 (nreverse results)))
1225
a66894d8 1226(defun mh-thread-process-in-reply-to (reply-to-header)
bdcfe844
BW
1227 "Extract message id's from REPLY-TO-HEADER.
1228Ideally this should have some regexp which will try to guess if a string
1229between < and > is a message id and not an email address. For now it will
1230take the last string inside angles."
c3d9274a 1231 (let ((end (mh-search-from-end ?> reply-to-header)))
bdcfe844 1232 (when (numberp end)
c3d9274a 1233 (let ((begin (mh-search-from-end ?< (substring reply-to-header 0 end))))
bdcfe844
BW
1234 (when (numberp begin)
1235 (list (substring reply-to-header begin (1+ end))))))))
1236
1237(defun mh-thread-set-tables (folder)
1238 "Use the tables of FOLDER in current buffer."
1239 (flet ((mh-get-table (symbol)
c3d9274a
BW
1240 (save-excursion
1241 (set-buffer folder)
1242 (symbol-value symbol))))
bdcfe844
BW
1243 (setq mh-thread-id-hash (mh-get-table 'mh-thread-id-hash))
1244 (setq mh-thread-subject-hash (mh-get-table 'mh-thread-subject-hash))
1245 (setq mh-thread-id-table (mh-get-table 'mh-thread-id-table))
1246 (setq mh-thread-id-index-map (mh-get-table 'mh-thread-id-index-map))
1247 (setq mh-thread-index-id-map (mh-get-table 'mh-thread-index-id-map))
1248 (setq mh-thread-scan-line-map (mh-get-table 'mh-thread-scan-line-map))
1249 (setq mh-thread-subject-container-hash
1250 (mh-get-table 'mh-thread-subject-container-hash))
1251 (setq mh-thread-duplicates (mh-get-table 'mh-thread-duplicates))
1252 (setq mh-thread-history (mh-get-table 'mh-thread-history))))
1253
1254(defsubst mh-thread-update-id-index-maps (id index)
1255 "Message with id, ID is the message in INDEX.
1256The function also checks for duplicate messages (that is multiple messages
1257with the same ID). These messages are put in the `mh-thread-duplicates' hash
1258table."
1259 (let ((old-index (gethash id mh-thread-id-index-map)))
1260 (when old-index (push old-index (gethash id mh-thread-duplicates)))
1261 (setf (gethash id mh-thread-id-index-map) index)
1262 (setf (gethash index mh-thread-index-id-map) id)))
1263
1264\f
1265
1266;;; Generate Threads...
1267
3d7ca223
BW
1268(defvar mh-message-id-regexp "^<.*@.*>$"
1269 "Regexp to recognize whether a string is a message identifier.")
1270
bdcfe844
BW
1271(defun mh-thread-generate (folder msg-list)
1272 "Scan FOLDER to get info for threading.
1273Only information about messages in MSG-LIST are added to the tree."
3d7ca223 1274 (with-temp-buffer
bdcfe844 1275 (mh-thread-set-tables folder)
bdcfe844
BW
1276 (when msg-list
1277 (apply
1278 #'call-process (expand-file-name mh-scan-prog mh-progs) nil '(t nil) nil
1279 "-width" "10000" "-format"
1280 "%(msg)\n%{message-id}\n%{references}\n%{in-reply-to}\n%{subject}\n"
c3d9274a 1281 folder (mapcar #'(lambda (x) (format "%s" x)) msg-list)))
bdcfe844
BW
1282 (goto-char (point-min))
1283 (let ((roots ())
1284 (case-fold-search t))
1285 (block nil
1286 (while (not (eobp))
1287 (block process-message
1288 (let* ((index-line
c3d9274a
BW
1289 (prog1 (buffer-substring (point) (line-end-position))
1290 (forward-line)))
bdcfe844
BW
1291 (index (car (read-from-string index-line)))
1292 (id (prog1 (buffer-substring (point) (line-end-position))
1293 (forward-line)))
1294 (refs (prog1 (buffer-substring (point) (line-end-position))
1295 (forward-line)))
1296 (in-reply-to (prog1 (buffer-substring (point)
1297 (line-end-position))
1298 (forward-line)))
1299 (subject (prog1
1300 (buffer-substring (point) (line-end-position))
1301 (forward-line)))
1302 (subject-re-p nil))
1303 (unless (gethash index mh-thread-scan-line-map)
1304 (return-from process-message))
1305 (unless (integerp index) (return)) ;Error message here
1306 (multiple-value-setq (subject subject-re-p)
1307 (mh-thread-prune-subject subject))
1308 (setq in-reply-to (mh-thread-process-in-reply-to in-reply-to))
3d7ca223
BW
1309 (setq refs (loop for x in (append (split-string refs) in-reply-to)
1310 when (string-match mh-message-id-regexp x)
1311 collect x))
bdcfe844
BW
1312 (setq id (mh-thread-canonicalize-id id))
1313 (mh-thread-update-id-index-maps id index)
1314 (setq refs (mapcar #'mh-thread-canonicalize-id refs))
1315 (mh-thread-get-message id subject-re-p subject refs)
1316 (do ((ancestors refs (cdr ancestors)))
1317 ((null (cdr ancestors))
1318 (when (car ancestors)
1319 (mh-thread-remove-parent-link id)
1320 (mh-thread-add-link (car ancestors) id)))
1321 (mh-thread-add-link (car ancestors) (cadr ancestors)))))))
1322 (maphash #'(lambda (k v)
1323 (declare (ignore k))
1324 (when (null (mh-container-parent v))
1325 (push v roots)))
1326 mh-thread-id-table)
1327 (setq roots (mh-thread-prune-containers roots))
1328 (prog1 (setq roots (mh-thread-group-by-subject roots))
1329 (let ((history mh-thread-history))
1330 (set-buffer folder)
1331 (setq mh-thread-history history))))))
1332
c3d9274a 1333;;;###mh-autoload
bdcfe844
BW
1334(defun mh-thread-inc (folder start-point)
1335 "Update thread tree for FOLDER.
1336All messages after START-POINT are added to the thread tree."
1337 (mh-thread-rewind-pruning)
a66894d8 1338 (mh-remove-all-notation)
bdcfe844
BW
1339 (goto-char start-point)
1340 (let ((msg-list ()))
1341 (while (not (eobp))
1342 (let ((index (mh-get-msg-num nil)))
c3d9274a
BW
1343 (when (numberp index)
1344 (push index msg-list)
1345 (setf (gethash index mh-thread-scan-line-map)
1346 (mh-thread-parse-scan-line)))
bdcfe844
BW
1347 (forward-line)))
1348 (let ((thread-tree (mh-thread-generate folder msg-list))
1349 (buffer-read-only nil)
1350 (old-buffer-modified-flag (buffer-modified-p)))
1351 (delete-region (point-min) (point-max))
924df208 1352 (mh-thread-print-scan-lines thread-tree)
e495eaec 1353 (mh-notate-user-sequences)
bdcfe844 1354 (mh-notate-deleted-and-refiled)
3d7ca223 1355 (mh-notate-cur)
bdcfe844
BW
1356 (set-buffer-modified-p old-buffer-modified-flag))))
1357
1358(defun mh-thread-generate-scan-lines (tree level)
1359 "Generate scan lines.
1360TREE is the hierarchical tree of messages, SCAN-LINE-MAP maps message indices
1361to the corresponding scan lines and LEVEL used to determine indentation of
1362the message."
1363 (cond ((null tree) nil)
1364 ((mh-thread-container-p tree)
1365 (let* ((message (mh-container-message tree))
1366 (id (mh-message-id message))
1367 (index (gethash id mh-thread-id-index-map))
1368 (duplicates (gethash id mh-thread-duplicates))
1369 (new-level (+ level 2))
1370 (dupl-flag t)
c3d9274a 1371 (force-angle-flag nil)
bdcfe844
BW
1372 (increment-level-flag nil))
1373 (dolist (scan-line (mapcar (lambda (x)
1374 (gethash x mh-thread-scan-line-map))
1375 (reverse (cons index duplicates))))
1376 (when scan-line
c3d9274a
BW
1377 (when (and dupl-flag (equal level 0)
1378 (mh-thread-ancestor-p mh-thread-last-ancestor tree))
1379 (setq level (+ level 2)
1380 new-level (+ new-level 2)
1381 force-angle-flag t))
1382 (when (equal level 0)
1383 (setq mh-thread-last-ancestor tree)
1384 (while (mh-container-parent mh-thread-last-ancestor)
1385 (setq mh-thread-last-ancestor
1386 (mh-container-parent mh-thread-last-ancestor))))
3d7ca223
BW
1387 (let* ((lev (if dupl-flag level new-level))
1388 (square-flag (or (and (mh-container-real-child-p tree)
1389 (not force-angle-flag)
1390 dupl-flag)
1391 (equal lev 0))))
1392 (insert (car scan-line)
1393 (format (format "%%%ss" lev) "")
1394 (if square-flag "[" "<")
1395 (cadr scan-line)
1396 (if square-flag "]" ">")
1397 (truncate-string-to-width
1398 (caddr scan-line) (- mh-thread-body-width lev))
1399 "\n"))
bdcfe844
BW
1400 (setq increment-level-flag t)
1401 (setq dupl-flag nil)))
1402 (unless increment-level-flag (setq new-level level))
1403 (dolist (child (mh-container-children tree))
1404 (mh-thread-generate-scan-lines child new-level))))
1405 (t (let ((nlevel (+ level 2)))
1406 (dolist (ch tree)
1407 (mh-thread-generate-scan-lines ch nlevel))))))
1408
1409;; Another and may be better approach would be to generate all the info from
1410;; the scan which generates the threading info. For now this will have to do.
1411(defun mh-thread-parse-scan-line (&optional string)
1412 "Parse a scan line.
1413If optional argument STRING is given then that is assumed to be the scan line.
1414Otherwise uses the line at point as the scan line to parse."
1415 (let* ((string (or string
1416 (buffer-substring-no-properties (line-beginning-position)
1417 (line-end-position))))
a66894d8
BW
1418 (address-start (+ mh-cmd-note mh-scan-field-from-start-offset))
1419 (body-start (+ mh-cmd-note mh-scan-field-from-end-offset))
1420 (first-string (substring string 0 address-start)))
bdcfe844 1421 (list first-string
a66894d8
BW
1422 (substring string address-start (- body-start 2))
1423 (substring string body-start)
bdcfe844
BW
1424 string)))
1425
a66894d8
BW
1426;;;###mh-autoload
1427(defun mh-thread-update-scan-line-map (msg notation offset)
1428 "In threaded view update `mh-thread-scan-line-map'.
1429MSG is the message being notated with NOTATION at OFFSET."
1430 (let* ((msg (or msg (mh-get-msg-num nil)))
1431 (cur-scan-line (and mh-thread-scan-line-map
1432 (gethash msg mh-thread-scan-line-map)))
1433 (old-scan-lines (loop for map in mh-thread-scan-line-map-stack
f0d73c14 1434 collect (and map (gethash msg map)))))
a66894d8
BW
1435 (when cur-scan-line
1436 (setf (aref (car cur-scan-line) offset) notation))
1437 (dolist (line old-scan-lines)
1438 (when line (setf (aref (car line) offset) notation)))))
1439
c3d9274a 1440;;;###mh-autoload
bdcfe844
BW
1441(defun mh-thread-add-spaces (count)
1442 "Add COUNT spaces to each scan line in `mh-thread-scan-line-map'."
1443 (let ((spaces (format (format "%%%ss" count) "")))
1444 (while (not (eobp))
1445 (let* ((msg-num (mh-get-msg-num nil))
1446 (old-line (nth 3 (gethash msg-num mh-thread-scan-line-map))))
c3d9274a
BW
1447 (when (numberp msg-num)
1448 (setf (gethash msg-num mh-thread-scan-line-map)
1449 (mh-thread-parse-scan-line (format "%s%s" spaces old-line)))))
bdcfe844
BW
1450 (forward-line 1))))
1451
924df208
BW
1452(defun mh-thread-print-scan-lines (thread-tree)
1453 "Print scan lines in THREAD-TREE in threaded mode."
1454 (let ((mh-thread-body-width (- (window-width) mh-cmd-note
1455 (1- mh-scan-field-subject-start-offset)))
1456 (mh-thread-last-ancestor nil))
1457 (if (null mh-index-data)
1458 (mh-thread-generate-scan-lines thread-tree -2)
1459 (loop for x in (mh-index-group-by-folder)
1460 do (let* ((old-map mh-thread-scan-line-map)
1461 (mh-thread-scan-line-map (make-hash-table)))
1462 (setq mh-thread-last-ancestor nil)
1463 (loop for msg in (cdr x)
1464 do (let ((v (gethash msg old-map)))
1465 (when v
1466 (setf (gethash msg mh-thread-scan-line-map) v))))
1467 (when (> (hash-table-count mh-thread-scan-line-map) 0)
1468 (insert (if (bobp) "" "\n") (car x) "\n")
f0d73c14
BW
1469 (mh-thread-generate-scan-lines thread-tree -2))))
1470 (mh-index-create-imenu-index))))
924df208 1471
bdcfe844
BW
1472(defun mh-thread-folder ()
1473 "Generate thread view of folder."
1474 (message "Threading %s..." (buffer-name))
1475 (mh-thread-initialize)
1476 (goto-char (point-min))
a66894d8 1477 (mh-remove-all-notation)
3d7ca223 1478 (let ((msg-list ()))
a66894d8
BW
1479 (mh-iterate-on-range msg (cons (point-min) (point-max))
1480 (setf (gethash msg mh-thread-scan-line-map) (mh-thread-parse-scan-line))
1481 (push msg msg-list))
3d7ca223
BW
1482 (let* ((range (mh-coalesce-msg-list msg-list))
1483 (thread-tree (mh-thread-generate (buffer-name) range)))
1484 (delete-region (point-min) (point-max))
924df208 1485 (mh-thread-print-scan-lines thread-tree)
3d7ca223
BW
1486 (mh-notate-user-sequences)
1487 (mh-notate-deleted-and-refiled)
1488 (mh-notate-cur)
1489 (message "Threading %s...done" (buffer-name)))))
bdcfe844 1490
c3d9274a 1491;;;###mh-autoload
bdcfe844 1492(defun mh-toggle-threads ()
3d7ca223 1493 "Toggle threaded view of folder."
a1b4049d 1494 (interactive)
c3d9274a
BW
1495 (let ((msg-at-point (mh-get-msg-num nil))
1496 (old-buffer-modified-flag (buffer-modified-p))
1497 (buffer-read-only nil))
3d7ca223 1498 (cond ((memq 'unthread mh-view-ops)
bdcfe844
BW
1499 (unless (mh-valid-view-change-operation-p 'unthread)
1500 (error "Can't unthread folder"))
3d7ca223
BW
1501 (let ((msg-list ()))
1502 (goto-char (point-min))
1503 (while (not (eobp))
924df208 1504 (let ((index (mh-get-msg-num nil)))
3d7ca223
BW
1505 (when index
1506 (push index msg-list)))
1507 (forward-line))
1508 (mh-scan-folder mh-current-folder
1509 (mapcar #'(lambda (x) (format "%s" x))
1510 (mh-coalesce-msg-list msg-list))
1511 t))
c3d9274a 1512 (when mh-index-data
3d7ca223
BW
1513 (mh-index-insert-folder-headers)
1514 (mh-notate-cur)))
bdcfe844
BW
1515 (t (mh-thread-folder)
1516 (push 'unthread mh-view-ops)))
1517 (when msg-at-point (mh-goto-msg msg-at-point t t))
c3d9274a 1518 (set-buffer-modified-p old-buffer-modified-flag)
bdcfe844
BW
1519 (mh-recenter nil)))
1520
c3d9274a 1521;;;###mh-autoload
bdcfe844
BW
1522(defun mh-thread-forget-message (index)
1523 "Forget the message INDEX from the threading tables."
1524 (let* ((id (gethash index mh-thread-index-id-map))
1525 (id-index (gethash id mh-thread-id-index-map))
1526 (duplicates (gethash id mh-thread-duplicates)))
1527 (remhash index mh-thread-index-id-map)
924df208 1528 (remhash index mh-thread-scan-line-map)
bdcfe844
BW
1529 (cond ((and (eql index id-index) (null duplicates))
1530 (remhash id mh-thread-id-index-map))
1531 ((eql index id-index)
1532 (setf (gethash id mh-thread-id-index-map) (car duplicates))
1533 (setf (gethash (car duplicates) mh-thread-index-id-map) id)
1534 (setf (gethash id mh-thread-duplicates) (cdr duplicates)))
1535 (t
1536 (setf (gethash id mh-thread-duplicates)
1537 (remove index duplicates))))))
1538
c3d9274a
BW
1539\f
1540
1541;;; Operations on threads
1542
1543(defun mh-thread-current-indentation-level ()
1544 "Find the number of spaces by which current message is indented."
1545 (save-excursion
1546 (let ((address-start-offset (+ mh-cmd-note mh-scan-date-flag-width
1547 mh-scan-date-width 1))
1548 (level 0))
1549 (beginning-of-line)
1550 (forward-char address-start-offset)
1551 (while (char-equal (char-after) ? )
1552 (incf level)
1553 (forward-char))
1554 level)))
1555
1556;;;###mh-autoload
1557(defun mh-thread-next-sibling (&optional previous-flag)
1558 "Jump to next sibling.
1559With non-nil optional argument PREVIOUS-FLAG jump to the previous sibling."
1560 (interactive)
1561 (cond ((not (memq 'unthread mh-view-ops))
1562 (error "Folder isn't threaded"))
1563 ((eobp)
1564 (error "No message at point")))
1565 (beginning-of-line)
1566 (let ((point (point))
1567 (done nil)
1568 (my-level (mh-thread-current-indentation-level)))
1569 (while (and (not done)
1570 (equal (forward-line (if previous-flag -1 1)) 0)
1571 (not (eobp)))
1572 (let ((level (mh-thread-current-indentation-level)))
1573 (cond ((equal level my-level)
1574 (setq done 'success))
1575 ((< level my-level)
1576 (message "No %s sibling" (if previous-flag "previous" "next"))
1577 (setq done 'failure)))))
1578 (cond ((eq done 'success) (mh-maybe-show))
1579 ((eq done 'failure) (goto-char point))
1580 (t (message "No %s sibling" (if previous-flag "previous" "next"))
1581 (goto-char point)))))
1582
1583;;;###mh-autoload
1584(defun mh-thread-previous-sibling ()
1585 "Jump to previous sibling."
1586 (interactive)
1587 (mh-thread-next-sibling t))
1588
1589(defun mh-thread-immediate-ancestor ()
1590 "Jump to immediate ancestor in thread tree."
1591 (beginning-of-line)
1592 (let ((point (point))
1593 (ancestor-level (- (mh-thread-current-indentation-level) 2))
1594 (done nil))
1595 (if (< ancestor-level 0)
1596 nil
1597 (while (and (not done) (equal (forward-line -1) 0))
1598 (when (equal ancestor-level (mh-thread-current-indentation-level))
1599 (setq done t)))
1600 (unless done
1601 (goto-char point))
1602 done)))
1603
1604;;;###mh-autoload
1605(defun mh-thread-ancestor (&optional thread-root-flag)
1606 "Jump to the ancestor of current message.
1607If optional argument THREAD-ROOT-FLAG is non-nil then jump to the root of the
1608thread tree the message belongs to."
1609 (interactive "P")
1610 (beginning-of-line)
1611 (cond ((not (memq 'unthread mh-view-ops))
1612 (error "Folder isn't threaded"))
1613 ((eobp)
1614 (error "No message at point")))
1615 (let ((current-level (mh-thread-current-indentation-level)))
1616 (cond (thread-root-flag
1617 (while (mh-thread-immediate-ancestor))
1618 (mh-maybe-show))
1619 ((equal current-level 1)
1620 (message "Message has no ancestor"))
1621 (t (mh-thread-immediate-ancestor)
1622 (mh-maybe-show)))))
1623
1624(defun mh-thread-find-children ()
1625 "Return a region containing the current message and its children.
1626The result is returned as a list of two elements. The first is the point at the
1627start of the region and the second is the point at the end."
1628 (beginning-of-line)
1629 (if (eobp)
1630 nil
1631 (let ((address-start-offset (+ mh-cmd-note mh-scan-date-flag-width
1632 mh-scan-date-width 1))
1633 (level (mh-thread-current-indentation-level))
1634 spaces begin)
1635 (setq begin (point))
1636 (setq spaces (format (format "%%%ss" (1+ level)) ""))
1637 (forward-line)
1638 (block nil
1639 (while (not (eobp))
1640 (forward-char address-start-offset)
1641 (unless (equal (string-match spaces (buffer-substring-no-properties
1642 (point) (line-end-position)))
1643 0)
1644 (beginning-of-line)
1645 (backward-char)
1646 (return))
1647 (forward-line)))
1648 (list begin (point)))))
1649
1650;;;###mh-autoload
1651(defun mh-thread-delete ()
1652 "Mark current message and all its children for subsequent deletion."
1653 (interactive)
1654 (cond ((not (memq 'unthread mh-view-ops))
1655 (error "Folder isn't threaded"))
1656 ((eobp)
1657 (error "No message at point"))
3d7ca223
BW
1658 (t (let ((region (mh-thread-find-children)))
1659 (mh-iterate-on-messages-in-region () (car region) (cadr region)
1660 (mh-delete-a-msg nil))
1661 (mh-next-msg)))))
c3d9274a 1662
c3d9274a
BW
1663;;;###mh-autoload
1664(defun mh-thread-refile (folder)
1665 "Mark current message and all its children for refiling to FOLDER."
3d7ca223 1666 (interactive (list (intern (mh-prompt-for-refile-folder))))
c3d9274a
BW
1667 (cond ((not (memq 'unthread mh-view-ops))
1668 (error "Folder isn't threaded"))
1669 ((eobp)
1670 (error "No message at point"))
3d7ca223
BW
1671 (t (let ((region (mh-thread-find-children)))
1672 (mh-iterate-on-messages-in-region () (car region) (cadr region)
1673 (mh-refile-a-msg nil folder))
1674 (mh-next-msg)))))
c3d9274a 1675
924df208
BW
1676\f
1677
1678;; Tick mark handling
1679
924df208 1680;;;###mh-autoload
a66894d8 1681(defun mh-toggle-tick (range)
be33fce4
BW
1682 "Toggle tick mark of all messages in RANGE.
1683
1684This command adds messages to the \"tick\" sequence (which you can customize
1685via the option `mh-tick-seq'). This sequence can be viewed later with the
1686\\[mh-index-ticked-messages] command.
1687
1688Check the documentation of `mh-interactive-range' to see how RANGE is read in
1689interactive use."
a66894d8 1690 (interactive (list (mh-interactive-range "Tick")))
924df208
BW
1691 (unless mh-tick-seq
1692 (error "Enable ticking by customizing `mh-tick-seq'"))
1693 (let* ((tick-seq (mh-find-seq mh-tick-seq))
a66894d8
BW
1694 (tick-seq-msgs (mh-seq-msgs tick-seq))
1695 (ticked ())
1696 (unticked ()))
1697 (mh-iterate-on-range msg range
924df208 1698 (cond ((member msg tick-seq-msgs)
a66894d8 1699 (push msg unticked)
924df208
BW
1700 (setcdr tick-seq (delq msg (cdr tick-seq)))
1701 (when (null (cdr tick-seq)) (setq mh-last-seq-used nil))
f0d73c14 1702 (mh-remove-sequence-notation msg (mh-colors-in-use-p)))
924df208 1703 (t
a66894d8 1704 (push msg ticked)
924df208 1705 (setq mh-last-seq-used mh-tick-seq)
f0d73c14
BW
1706 (let ((mh-seq-list (cons `(,mh-tick-seq ,msg) mh-seq-list)))
1707 (mh-add-sequence-notation msg (mh-colors-in-use-p))))))
a66894d8
BW
1708 (mh-add-msgs-to-seq ticked mh-tick-seq nil t)
1709 (mh-undefine-sequence mh-tick-seq unticked)
1710 (when mh-index-data
1711 (mh-index-add-to-sequence mh-tick-seq ticked)
1712 (mh-index-delete-from-sequence mh-tick-seq unticked))))
924df208
BW
1713
1714;;;###mh-autoload
1715(defun mh-narrow-to-tick ()
be33fce4 1716 "Limit to ticked messages.
f0d73c14 1717
be33fce4
BW
1718What this command does is show only those messages that are in the \"tick\"
1719sequence (which you can customize via the `mh-tick-seq' option) in the
1720MH-Folder buffer. In addition, it limits further MH-E searches to just those
1721messages. When you want to widen the view to all your messages again, use
1722\\[mh-widen]."
924df208
BW
1723 (interactive)
1724 (cond ((not mh-tick-seq)
1725 (error "Enable ticking by customizing `mh-tick-seq'"))
1726 ((null (mh-seq-msgs (mh-find-seq mh-tick-seq)))
f0d73c14 1727 (message "No messages in %s sequence" mh-tick-seq))
924df208
BW
1728 (t (mh-narrow-to-seq mh-tick-seq))))
1729
bdcfe844
BW
1730(provide 'mh-seq)
1731
1732;;; Local Variables:
c3d9274a 1733;;; indent-tabs-mode: nil
bdcfe844
BW
1734;;; sentence-end-double-space: nil
1735;;; End:
a1b4049d 1736
ab5796a9 1737;;; arch-tag: 8e952711-01a2-485b-bf21-c9e3ad4de942
60370d40 1738;;; mh-seq.el ends here