Convert consecutive FSF copyright years to ranges.
[bpt/emacs.git] / lisp / mh-e / mh-utils.el
CommitLineData
dda00b2c 1;;; mh-utils.el --- MH-E general utilities
c26cf6c8 2
73b0cd50 3;; Copyright (C) 1993, 1995, 1997, 2000-2011
dcf71371 4;; 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
5e809f55 13;; GNU Emacs is free software: you can redistribute it and/or modify
c26cf6c8 14;; it under the terms of the GNU General Public License as published by
5e809f55
GM
15;; the Free Software Foundation, either version 3 of the License, or
16;; (at your option) any later version.
c26cf6c8 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
5e809f55 24;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
c26cf6c8
RS
25
26;;; Commentary:
27
a1b4049d
BW
28;;; Change Log:
29
c26cf6c8
RS
30;;; Code:
31
dda00b2c 32(require 'mh-e)
a66894d8 33(mh-require-cl)
7094eefe 34
c3d9274a 35(require 'font-lock)
cee9f5c6 36
c3d9274a 37;;; CL Replacements
cee9f5c6 38
dda00b2c 39;;;###mh-autoload
c3d9274a
BW
40(defun mh-search-from-end (char string)
41 "Return the position of last occurrence of CHAR in STRING.
2dcf34f9
BW
42If CHAR is not present in STRING then return nil. The function is
43used in lieu of `search' in the CL package."
c3d9274a
BW
44 (loop for index from (1- (length string)) downto 0
45 when (equal (aref string index) char) return index
46 finally return nil))
47
cee9f5c6
BW
48\f
49
dda00b2c 50;;; General Utilities
c26cf6c8 51
dda00b2c
BW
52;;;###mh-autoload
53(defun mh-beginning-of-word (&optional n)
54 "Return position of the N th word backwards."
55 (unless n (setq n 1))
56 (let ((syntax-table (syntax-table)))
57 (unwind-protect
58 (save-excursion
06e7028b 59 (mh-mail-abbrev-make-syntax-table)
dda00b2c
BW
60 (set-syntax-table mail-abbrev-syntax-table)
61 (backward-word n)
62 (point))
63 (set-syntax-table syntax-table))))
64
65;;;###mh-autoload
66(defun mh-colors-available-p ()
67 "Check if colors are available in the Emacs being used."
a3269bc4 68 (or (featurep 'xemacs)
d5dc8c56 69 (let ((color-cells (mh-display-color-cells)))
dda00b2c
BW
70 (and (numberp color-cells) (>= color-cells 8)))))
71
72;;;###mh-autoload
73(defun mh-colors-in-use-p ()
74 "Check if colors are being used in the folder buffer."
75 (and mh-colors-available-flag font-lock-mode))
76
77;;;###mh-autoload
78(defun mh-delete-line (lines)
79 "Delete the next LINES lines."
80 (delete-region (point) (progn (forward-line lines) (point))))
a1b4049d 81
dda00b2c
BW
82;;;###mh-autoload
83(defun mh-make-local-vars (&rest pairs)
84 "Initialize local variables according to the variable-value PAIRS."
85 (while pairs
86 (set (make-local-variable (car pairs)) (car (cdr pairs)))
87 (setq pairs (cdr (cdr pairs)))))
88
89;;;###mh-autoload
90(defun mh-mapc (function list)
91 "Apply FUNCTION to each element of LIST for side effects only."
92 (while list
93 (funcall function (car list))
94 (setq list (cdr list))))
95
052df334
BW
96(defvar mh-pick-regexp-chars ".*$["
97 "List of special characters in pick regular expressions.")
98
99;;;###mh-autoload
100(defun mh-quote-pick-expr (pick-expr)
101 "Quote `mh-pick-regexp-chars' in PICK-EXPR.
102PICK-EXPR is a list of strings. Return nil if PICK-EXPR is nil."
103 (let ((quoted-pick-expr))
104 (dolist (string pick-expr)
105 (when (and string
106 (not (string-equal string "")))
107 (loop for i from 0 to (1- (length mh-pick-regexp-chars)) do
108 (let ((s (string ?\\ (aref mh-pick-regexp-chars i))))
109 (setq string (mh-replace-regexp-in-string s s string t t))))
110 (setq quoted-pick-expr (append quoted-pick-expr (list string)))))
111 quoted-pick-expr))
112
dda00b2c
BW
113;;;###mh-autoload
114(defun mh-replace-string (old new)
115 "Replace all occurrences of OLD with NEW in the current buffer.
116Ignores case when searching for OLD."
117 (goto-char (point-min))
118 (let ((case-fold-search t))
119 (while (search-forward old nil t)
120 (replace-match new t t))))
c26cf6c8 121
cee9f5c6
BW
122\f
123
dda00b2c 124;;; Logo Display
bdcfe844 125
3d7ca223
BW
126(defvar mh-logo-cache nil)
127
f875b154
BW
128;; Shush compiler.
129(defvar image-load-path)
130
dda00b2c 131;;;###mh-autoload
3d7ca223
BW
132(defun mh-logo-display ()
133 "Modify mode line to display MH-E logo."
924df208 134 (mh-do-in-gnu-emacs
44e3f440 135 (let* ((load-path (mh-image-load-path-for-library "mh-e" "mh-logo.xpm"))
f875b154
BW
136 (image-load-path (cons (car load-path)
137 (when (boundp 'image-load-path)
138 image-load-path))))
efc27af6
BW
139 (add-text-properties
140 0 2
141 `(display ,(or mh-logo-cache
142 (setq mh-logo-cache
143 (mh-funcall-if-exists
144 find-image '((:type xpm :ascent center
145 :file "mh-logo.xpm"))))))
146 (car mode-line-buffer-identification))))
924df208 147 (mh-do-in-xemacs
efc27af6
BW
148 (setq modeline-buffer-identification
149 (list
150 (if mh-modeline-glyph
151 (cons modeline-buffer-id-left-extent mh-modeline-glyph)
152 (cons modeline-buffer-id-left-extent "XEmacs%N:"))
153 (cons modeline-buffer-id-right-extent " %17b")))))
924df208 154
dda00b2c 155\f
d1699462 156
dda00b2c
BW
157;;; Read MH Profile
158
159(defvar mh-find-path-run nil
160 "Non-nil if `mh-find-path' has been run already.
161Do not access this variable; `mh-find-path' already uses it to
162avoid running more than once.")
163
164;;;###mh-autoload
165(defun mh-find-path ()
166 "Set variables from user's MH profile.
167
168This function sets `mh-user-path' from your \"Path:\" MH profile
169component (but defaults to \"Mail\" if one isn't present),
170`mh-draft-folder' from \"Draft-Folder:\", `mh-unseen-seq' from
171\"Unseen-Sequence:\", `mh-previous-seq' from
172\"Previous-Sequence:\", and `mh-inbox' from \"Inbox:\" (defaults
173to \"+inbox\").
174
175The hook `mh-find-path-hook' is run after these variables have
176been set. This hook can be used the change the value of these
177variables if you need to run with different values between MH and
178MH-E."
179 (unless mh-find-path-run
180 ;; Sanity checks.
181 (if (and (getenv "MH")
182 (not (file-readable-p (getenv "MH"))))
183 (error "MH environment variable contains unreadable file %s"
184 (getenv "MH")))
185 (if (null (mh-variants))
186 (error "Install MH and run install-mh before running MH-E"))
b78a11dc
BW
187 (if (not (or (getenv "MH") (file-readable-p "~/.mh_profile")))
188 (error "Run install-mh before running MH-E"))
dda00b2c
BW
189 ;; Read MH profile.
190 (setq mh-user-path (mh-profile-component "Path"))
191 (if (not mh-user-path)
192 (setq mh-user-path "Mail"))
193 (setq mh-user-path
194 (file-name-as-directory
195 (expand-file-name mh-user-path (expand-file-name "~"))))
196 (mh-set-x-image-cache-directory (expand-file-name ".mhe-x-image-cache"
197 mh-user-path))
198 (setq mh-draft-folder (mh-profile-component "Draft-Folder"))
199 (if mh-draft-folder
200 (progn
201 (if (not (mh-folder-name-p mh-draft-folder))
202 (setq mh-draft-folder (format "+%s" mh-draft-folder)))
203 (if (not (file-exists-p (mh-expand-file-name mh-draft-folder)))
204 (error
205 "Draft folder \"%s\" not found; create it and try again"
206 (mh-expand-file-name mh-draft-folder)))))
207 (setq mh-inbox (mh-profile-component "Inbox"))
208 (cond ((not mh-inbox)
209 (setq mh-inbox "+inbox"))
210 ((not (mh-folder-name-p mh-inbox))
211 (setq mh-inbox (format "+%s" mh-inbox))))
212 (setq mh-unseen-seq (mh-profile-component "Unseen-Sequence"))
213 (if mh-unseen-seq
214 (setq mh-unseen-seq (intern mh-unseen-seq))
215 (setq mh-unseen-seq 'unseen)) ;old MH default?
216 (setq mh-previous-seq (mh-profile-component "Previous-Sequence"))
217 (if mh-previous-seq
218 (setq mh-previous-seq (intern mh-previous-seq)))
219 (run-hooks 'mh-find-path-hook)
220 (mh-collect-folder-names)
221 (setq mh-find-path-run t)))
b6d4ab05 222
dda00b2c 223\f
b6d4ab05 224
dda00b2c 225;;; Help Functions
b6d4ab05 226
dda00b2c
BW
227;;;###mh-autoload
228(defun mh-ephem-message (string)
229 "Display STRING in the minibuffer momentarily."
230 (message "%s" string)
231 (sit-for 5)
232 (message ""))
c26cf6c8 233
dda00b2c
BW
234(defvar mh-help-default nil
235 "Mode to use if messages are not present for the current mode.")
cee9f5c6 236
dda00b2c
BW
237(defvar mh-help-messages nil
238 "Help messages for all modes.
239This is an alist of alists. The primary key is a symbol
240representing the mode; the value is described in `mh-set-help'.")
241
242;;;###mh-autoload
243(defun mh-set-help (messages &optional default)
244 "Set help messages.
245
246The MESSAGES are assumed to be an associative array. It is used
247to show help for the most common commands in the current mode.
248The key is a prefix char. The value is one or more strings which
249are concatenated together and displayed in a help buffer if ? is
250pressed after the prefix character. The special key nil is used
251to display the non-prefixed commands.
252
253The substitutions described in `substitute-command-keys' are performed as
254well.
255
256If optional argument DEFAULT is non-nil, then these messages will
257be used if help is asked for an unknown mode."
258 (add-to-list 'mh-help-messages (cons major-mode messages))
259 (if default
260 (setq mh-help-default major-mode)))
261
262;;;###mh-autoload
263(defun mh-help (&optional help-messages)
264 "Display cheat sheet for the MH-E commands.
265See `mh-set-help' for setting the help messages.
266HELP-MESSAGES are used instead if given.
267This is a list of one or more strings which are concatenated together
268and displayed in a help buffer."
269 (interactive)
270 (let* ((help (or help-messages
271 (cdr (assoc nil (assoc major-mode mh-help-messages)))))
272 (text (substitute-command-keys (mapconcat 'identity help ""))))
273 (with-electric-help
274 (function
275 (lambda ()
276 (insert text)))
277 mh-help-buffer)))
278
279;;;###mh-autoload
280(defun mh-prefix-help ()
281 "Display cheat sheet for the commands of the current prefix in minibuffer."
282 (interactive)
283 ;; We got here because the user pressed a "?", but he pressed a prefix key
b4dc7d98 284 ;; before that. Since the key vector starts at index 0, the index of the
dda00b2c
BW
285 ;; last keystroke is length-1 and thus the second to last keystroke is at
286 ;; length-2. We use that information to obtain a suitable prefix character
287 ;; from the recent keys.
288 (let* ((keys (recent-keys))
289 (prefix-char (elt keys (- (length keys) 2)))
290 (help (cdr (assoc prefix-char (assoc major-mode mh-help-messages)))))
291 (mh-help help)))
a1506d29 292
dda00b2c
BW
293\f
294
295;;; Message Number Utilities
296
297;;;###mh-autoload
298(defun mh-coalesce-msg-list (messages)
299 "Given a list of MESSAGES, return a list of message number ranges.
300This is the inverse of `mh-read-msg-list', which expands ranges.
301Message lists passed to MH programs should be processed by this
302function to avoid exceeding system command line argument limits."
303 (let ((msgs (sort (copy-sequence messages) 'mh-greaterp))
304 (range-high nil)
305 (prev -1)
306 (ranges nil))
307 (while prev
308 (if range-high
309 (if (or (not (numberp prev))
310 (not (equal (car msgs) (1- prev))))
311 (progn ;non-sequential, flush old range
312 (if (eq prev range-high)
313 (setq ranges (cons range-high ranges))
314 (setq ranges (cons (format "%s-%s" prev range-high) ranges)))
315 (setq range-high nil))))
316 (or range-high
317 (setq range-high (car msgs))) ;start new or first range
318 (setq prev (car msgs))
319 (setq msgs (cdr msgs)))
320 ranges))
321
322(defun mh-greaterp (msg1 msg2)
323 "Return the greater of two message indicators MSG1 and MSG2.
324Strings are \"smaller\" than numbers.
325Valid values are things like \"cur\", \"last\", 1, and 1820."
326 (if (numberp msg1)
327 (if (numberp msg2)
328 (> msg1 msg2)
329 t)
330 (if (numberp msg2)
331 nil
332 (string-lessp msg2 msg1))))
333
334;;;###mh-autoload
335(defun mh-lessp (msg1 msg2)
336 "Return the lesser of two message indicators MSG1 and MSG2.
337Strings are \"smaller\" than numbers.
338Valid values are things like \"cur\", \"last\", 1, and 1820."
339 (not (mh-greaterp msg1 msg2)))
340
341;;;###mh-autoload
bdcfe844
BW
342(defun mh-get-msg-num (error-if-no-message)
343 "Return the message number of the displayed message.
2dcf34f9
BW
344If the argument ERROR-IF-NO-MESSAGE is non-nil, then complain if
345the cursor is not pointing to a message."
bdcfe844
BW
346 (save-excursion
347 (beginning-of-line)
dda00b2c 348 (cond ((looking-at (mh-scan-msg-number-regexp))
e495eaec
BW
349 (string-to-number (buffer-substring (match-beginning 1)
350 (match-end 1))))
c3d9274a
BW
351 (error-if-no-message
352 (error "Cursor not pointing to message"))
353 (t nil))))
bdcfe844 354
dda00b2c 355(add-to-list 'debug-ignored-errors "^Cursor not pointing to message$")
924df208
BW
356
357\f
358
dda00b2c 359;;; Folder Cache and Access
c26cf6c8 360
3d7ca223 361(defvar mh-sub-folders-cache (make-hash-table :test #'equal))
924df208 362(defvar mh-current-folder-name nil)
a66894d8
BW
363(defvar mh-flists-partial-line "")
364(defvar mh-flists-process nil)
365
dda00b2c
BW
366;;;###mh-autoload
367(defun mh-clear-sub-folders-cache ()
368 "Clear `mh-sub-folders-cache'."
369 (clrhash mh-sub-folders-cache))
370
a66894d8
BW
371;; Initialize mh-sub-folders-cache...
372(defun mh-collect-folder-names ()
11db987f 373 "Collect folder names by running \"folders\"."
a66894d8
BW
374 (unless mh-flists-process
375 (setq mh-flists-process
376 (mh-exec-cmd-daemon "folders" 'mh-collect-folder-names-filter
377 "-recurse" "-fast"))))
378
379(defun mh-collect-folder-names-filter (process output)
380 "Read folder names.
2dcf34f9
BW
381PROCESS is the flists process that was run to collect folder
382names and the function is called when OUTPUT is available."
a66894d8 383 (let ((position 0)
dda00b2c
BW
384 (prevailing-match-data (match-data))
385 line-end folder)
a66894d8 386 (unwind-protect
dda00b2c
BW
387 (while (setq line-end (string-match "\n" output position))
388 (setq folder (format "+%s%s"
a66894d8
BW
389 mh-flists-partial-line
390 (substring output position line-end)))
dda00b2c 391 (setq mh-flists-partial-line "")
a66894d8
BW
392 (unless (equal (aref folder 1) ?.)
393 (mh-populate-sub-folders-cache folder))
dda00b2c 394 (setq position (1+ line-end)))
a66894d8
BW
395 (set-match-data prevailing-match-data))
396 (setq mh-flists-partial-line (substring output position))))
397
398(defun mh-populate-sub-folders-cache (folder)
399 "Tell `mh-sub-folders-cache' about FOLDER."
400 (let* ((last-slash (mh-search-from-end ?/ folder))
401 (child1 (substring folder (1+ (or last-slash 0))))
402 (parent (and last-slash (substring folder 0 last-slash)))
403 (parent-slash (and parent (mh-search-from-end ?/ parent)))
404 (child2 (and parent (substring parent (1+ (or parent-slash 0)))))
405 (grand-parent (and parent-slash (substring parent 0 parent-slash)))
406 (cache-entry (gethash parent mh-sub-folders-cache)))
407 (unless (loop for x in cache-entry when (equal (car x) child1) return t
408 finally return nil)
409 (push (list child1) cache-entry)
410 (setf (gethash parent mh-sub-folders-cache)
411 (sort cache-entry (lambda (x y) (string< (car x) (car y)))))
412 (when parent
413 (loop for x in (gethash grand-parent mh-sub-folders-cache)
414 when (equal (car x) child2)
415 do (progn (setf (cdr x) t) (return)))))))
3d7ca223
BW
416
417(defun mh-normalize-folder-name (folder &optional empty-string-okay
c80658b7
BW
418 dont-remove-trailing-slash
419 return-nil-if-folder-empty)
3d7ca223 420 "Normalizes FOLDER name.
3d7ca223 421
2dcf34f9
BW
422Makes sure that two '/' characters never occur next to each
423other. Also all occurrences of \"..\" and \".\" are suitably
424processed. So \"+inbox/../news\" will be normalized to \"+news\".
3d7ca223 425
2dcf34f9
BW
426If optional argument EMPTY-STRING-OKAY is nil then a '+' is added
427at the front if FOLDER lacks one. If non-nil and FOLDER is the
428empty string then nothing is added.
429
430If optional argument DONT-REMOVE-TRAILING-SLASH is non-nil then a
431trailing '/' if present is retained (if present), otherwise it is
c80658b7
BW
432removed.
433
434If optional argument RETURN-NIL-IF-FOLDER-EMPTY is non-nil, then
435return nil if FOLDER is \"\" or \"+\". This is useful when
436normalizing the folder for the \"folders\" command which displays
437the directories in / if passed \"+\". This is usually not
438desired. If this argument is non-nil, then EMPTY-STRING-OKAY has
439no effect."
440 (cond
441 ((if (and (or (equal folder "+") (equal folder ""))
442 return-nil-if-folder-empty)
443 (setq folder nil)))
444 ((stringp folder)
3d7ca223
BW
445 ;; Replace two or more consecutive '/' characters with a single '/'
446 (while (string-match "//" folder)
447 (setq folder (replace-match "/" nil t folder)))
448 (let* ((length (length folder))
449 (trailing-slash-present (and (> length 0)
924df208
BW
450 (equal (aref folder (1- length)) ?/)))
451 (leading-slash-present (and (> length 0)
452 (equal (aref folder 0) ?/))))
453 (when (and (> length 0) (equal (aref folder 0) ?@)
454 (stringp mh-current-folder-name))
455 (setq folder (format "%s/%s/" mh-current-folder-name
456 (substring folder 1))))
c80658b7
BW
457 ;; XXX: Purge empty strings from the list that split-string
458 ;; returns. In XEmacs, (split-string "+foo/" "/") returns
459 ;; ("+foo" "") while in GNU Emacs it returns ("+foo"). In the
460 ;; code it is assumed that the components list has no empty
461 ;; strings.
924df208 462 (let ((components (delete "" (split-string folder "/")))
3d7ca223
BW
463 (result ()))
464 ;; Remove .. and . from the pathname.
465 (dolist (component components)
466 (cond ((and (equal component "..") result)
467 (pop result))
468 ((equal component ".."))
469 ((equal component "."))
470 (t (push component result))))
471 (setq folder "")
472 (dolist (component result)
473 (setq folder (concat component "/" folder)))
474 ;; Remove trailing '/' if needed.
475 (unless (and trailing-slash-present dont-remove-trailing-slash)
476 (when (not (equal folder ""))
924df208
BW
477 (setq folder (substring folder 0 (1- (length folder))))))
478 (when leading-slash-present
479 (setq folder (concat "/" folder)))))
3d7ca223 480 (cond ((and empty-string-okay (equal folder "")))
c80658b7
BW
481 ((equal folder "")
482 (setq folder "+"))
483 ((not (equal (aref folder 0) ?+))
484 (setq folder (concat "+" folder))))))
3d7ca223
BW
485 folder)
486
11db987f
BW
487(defmacro mh-children-p (folder)
488 "Return t if FOLDER from sub-folders cache has children.
489The car of folder is the name, and the cdr is either t or some
490sort of count that I do not understand. It's too small to be the
491number of messages in the sub-folders and too large to be the
492number of sub-folders. XXX"
493 `(if (cdr ,folder)
494 t
495 nil))
496
dda00b2c 497;;;###mh-autoload
11db987f
BW
498(defun mh-folder-list (folder)
499 "Return FOLDER and its descendents.
898dda92
BW
500FOLDER may have a + prefix. Returns a list of strings without the
501+ prefix. If FOLDER is nil, then all folders are considered. For
502example, if your Mail directory only contains the folders +inbox,
503+outbox, +lists, and +lists/mh-e, then
504
505 (mh-folder-list nil)
506 => (\"inbox\" \"lists\" \"lists/mh-e\" \"outbox\")
507 (mh-folder-list \"+lists\")
2044e8f1 508 => (\"lists\" \"lists/mh-e\")
898dda92
BW
509
510Respects the value of `mh-recursive-folders-flag'. If this flag
511is nil, and the sub-folders have not been explicitly viewed, then
512they will not be returned."
11db987f 513 (let ((folder-list))
c80658b7
BW
514 ;; Normalize folder. Strip leading + and trailing slash(es). If no
515 ;; folder is specified, ensure it is nil to avoid adding the
516 ;; folder to the folder-list and adding a slash to it.
11db987f 517 (when folder
d5dc8c56 518 (setq folder (mh-replace-regexp-in-string "^\+" "" folder))
c80658b7
BW
519 (setq folder (mh-replace-regexp-in-string "/+$" "" folder))
520 (if (equal folder "")
521 (setq folder nil)))
898dda92 522 ;; Add provided folder to list, unless all folders are asked for.
c80658b7 523 ;; Then append slash to separate sub-folders.
898dda92 524 (unless (null folder)
c80658b7
BW
525 (setq folder-list (list folder))
526 (setq folder (concat folder "/")))
11db987f 527 (loop for f in (mh-sub-folders folder) do
898dda92
BW
528 (setq folder-list
529 (append folder-list
530 (if (mh-children-p f)
c80658b7
BW
531 (mh-folder-list (concat folder (car f)))
532 (list (concat folder (car f)))))))
11db987f
BW
533 folder-list))
534
dda00b2c 535;;;###mh-autoload
3d7ca223
BW
536(defun mh-sub-folders (folder &optional add-trailing-slash-flag)
537 "Find the subfolders of FOLDER.
2dcf34f9
BW
538The function avoids running folders unnecessarily by caching the
539results of the actual folders call.
3d7ca223 540
2dcf34f9
BW
541If optional argument ADD-TRAILING-SLASH-FLAG is non-nil then a
542slash is added to each of the sub-folder names that may have
543nested folders within them."
c80658b7 544 (let* ((folder (mh-normalize-folder-name folder nil nil t))
3d7ca223
BW
545 (match (gethash folder mh-sub-folders-cache 'no-result))
546 (sub-folders (cond ((eq match 'no-result)
547 (setf (gethash folder mh-sub-folders-cache)
548 (mh-sub-folders-actual folder)))
549 (t match))))
550 (if add-trailing-slash-flag
551 (mapcar #'(lambda (x)
552 (if (cdr x) (cons (concat (car x) "/") (cdr x)) x))
553 sub-folders)
554 sub-folders)))
555
cd35b20a
BW
556;; FIXME: This function does not do well if FOLDER does not exist. It
557;; then changes the context to that folder which causes problems down
558;; the line. Since a folder in the cache could later be deleted, it
559;; would be good for mh-sub-folders-actual to return nil in this case
560;; so that mh-sub-folders could delete it from the cache. This
561;; function could protect itself by using a temporary context.
3d7ca223
BW
562(defun mh-sub-folders-actual (folder)
563 "Execute the command folders to return the sub-folders of FOLDER.
2dcf34f9 564Filters out the folder names that start with \".\" so that
cd35b20a
BW
565directories that aren't usually mail folders are hidden.
566Expects FOLDER to have already been normalized with
567 (mh-normalize-folder-name folder nil nil t)"
3d7ca223
BW
568 (let ((arg-list `(,(expand-file-name "folders" mh-progs)
569 nil (t nil) nil "-noheader" "-norecurse" "-nototal"
570 ,@(if (stringp folder) (list folder) ())))
571 (results ())
572 (current-folder (concat
573 (with-temp-buffer
574 (call-process (expand-file-name "folder" mh-progs)
575 nil '(t nil) nil "-fast")
576 (buffer-substring (point-min) (1- (point-max))))
577 "+")))
578 (with-temp-buffer
579 (apply #'call-process arg-list)
580 (goto-char (point-min))
581 (while (not (and (eolp) (bolp)))
d5dc8c56
BW
582 (goto-char (mh-line-end-position))
583 (let ((start-pos (mh-line-beginning-position))
584 (has-pos (search-backward " has "
585 (mh-line-beginning-position) t)))
3d7ca223
BW
586 (when (integerp has-pos)
587 (while (equal (char-after has-pos) ? )
588 (decf has-pos))
589 (incf has-pos)
f0d73c14
BW
590 (while (equal (char-after start-pos) ? )
591 (incf start-pos))
592 (let* ((name (buffer-substring start-pos has-pos))
3d7ca223
BW
593 (first-char (aref name 0))
594 (last-char (aref name (1- (length name)))))
595 (unless (member first-char '(?. ?# ?,))
596 (when (and (equal last-char ?+) (equal name current-folder))
597 (setq name (substring name 0 (1- (length name)))))
598 (push
599 (cons name
d5dc8c56 600 (search-forward "(others)" (mh-line-end-position) t))
3d7ca223
BW
601 results))))
602 (forward-line 1))))
603 (setq results (nreverse results))
604 (when (stringp folder)
605 (setq results (cdr results))
606 (let ((folder-name-len (length (format "%s/" (substring folder 1)))))
607 (setq results (mapcar (lambda (f)
608 (cons (substring (car f) folder-name-len)
609 (cdr f)))
610 results))))
611 results))
612
dda00b2c 613;;;###mh-autoload
3d7ca223
BW
614(defun mh-remove-from-sub-folders-cache (folder)
615 "Remove FOLDER and its parent from `mh-sub-folders-cache'.
2dcf34f9
BW
616FOLDER should be unconditionally removed from the cache. Also the
617last ancestor of FOLDER present in the cache must be removed as
618well.
619
620To see why this is needed assume we have a folder +foo which has
621a single sub-folder qux. Now we create the folder +foo/bar/baz.
622Here we will need to invalidate the cached sub-folders of +foo,
623otherwise completion on +foo won't tell us about the option
624+foo/bar!"
3d7ca223
BW
625 (remhash folder mh-sub-folders-cache)
626 (block ancestor-found
627 (let ((parent folder)
628 (one-ancestor-found nil)
629 last-slash)
630 (while (setq last-slash (mh-search-from-end ?/ parent))
631 (setq parent (substring parent 0 last-slash))
632 (unless (eq (gethash parent mh-sub-folders-cache 'none) 'none)
633 (remhash parent mh-sub-folders-cache)
634 (if one-ancestor-found
635 (return-from ancestor-found)
636 (setq one-ancestor-found t))))
637 (remhash nil mh-sub-folders-cache))))
638
dda00b2c
BW
639\f
640
641;;; Folder Utilities
642
643;;;###mh-autoload
644(defun mh-folder-name-p (name)
645 "Return non-nil if NAME is the name of a folder.
646A name (a string or symbol) can be a folder name if it begins
647with \"+\"."
648 (if (symbolp name)
649 (eq (aref (symbol-name name) 0) ?+)
650 (and (> (length name) 0)
651 (eq (aref name 0) ?+))))
652
653;;;###mh-autoload
654(defun mh-expand-file-name (filename &optional default)
655 "Expand FILENAME like `expand-file-name', but also handle MH folder names.
656Any filename that starts with '+' is treated as a folder name.
657See `expand-file-name' for description of DEFAULT."
658 (if (mh-folder-name-p filename)
659 (expand-file-name (substring filename 1) mh-user-path)
660 (expand-file-name filename default)))
661
bdcfe844 662(defvar mh-folder-hist nil)
7094eefe
BW
663
664;; Shush compiler.
73e6d1af 665(defvar mh-speed-flists-cache)
924df208
BW
666
667(defvar mh-allow-root-folder-flag nil
668 "Non-nil means \"+\" is an acceptable folder name.
2dcf34f9
BW
669This variable is used to communicate with
670`mh-folder-completion-function'. That function can have exactly
671three arguments so we bind this variable to t or nil.
924df208
BW
672
673This variable should never be set.")
674
3d7ca223 675(defvar mh-folder-completion-map (copy-keymap minibuffer-local-completion-map))
2bd87afb 676(define-key mh-folder-completion-map " " 'minibuffer-complete) ;Why???
3d7ca223 677
a66894d8
BW
678(defvar mh-speed-flists-inhibit-flag nil)
679
dda00b2c 680;;;###mh-autoload
924df208
BW
681(defun mh-speed-flists-active-p ()
682 "Check if speedbar is running with message counts enabled."
683 (and (featurep 'mh-speed)
a66894d8 684 (not mh-speed-flists-inhibit-flag)
924df208
BW
685 (> (hash-table-count mh-speed-flists-cache) 0)))
686
dda00b2c 687;;;###mh-autoload
3d7ca223
BW
688(defun mh-folder-completion-function (name predicate flag)
689 "Programmable completion for folder names.
2dcf34f9 690NAME is the partial folder name that has been input. PREDICATE if
cd35b20a
BW
691non-nil is a function that is used to filter the possible
692choices. FLAG is nil to indicate `try-completion', t for
693`all-completions', or the symbol lambda for `test-completion'.
694See Info node `(elisp) Programmed Completion' for details."
3d7ca223 695 (let* ((orig-name name)
cd35b20a
BW
696 ;; After normalization, name is nil, +, or +something. If a
697 ;; trailing slash is present, it is preserved.
3d7ca223
BW
698 (name (mh-normalize-folder-name name nil t))
699 (last-slash (mh-search-from-end ?/ name))
cd35b20a
BW
700 ;; nil if + or +folder; +folder/ if slash present.
701 (last-complete (if last-slash (substring name 0 (1+ last-slash)) nil))
702 ;; Either +folder/remainder, +remainder, or "".
3d7ca223 703 (remainder (cond (last-complete (substring name (1+ last-slash)))
cd35b20a 704 (name (substring name 1))
3d7ca223
BW
705 (t ""))))
706 (cond ((eq flag nil)
cd35b20a
BW
707 (let ((try-res
708 (try-completion
709 name
710 (mapcar (lambda (x)
711 (cons (concat (or last-complete "+") (car x))
712 (cdr x)))
713 (mh-sub-folders last-complete t))
714 predicate)))
3d7ca223
BW
715 (cond ((eq try-res nil) nil)
716 ((and (eq try-res t) (equal name orig-name)) t)
717 ((eq try-res t) name)
718 (t try-res))))
719 ((eq flag t)
cd35b20a
BW
720 (mapcar (lambda (x)
721 (concat (or last-complete "+") x))
722 (all-completions
723 remainder (mh-sub-folders last-complete t) predicate)))
3d7ca223 724 ((eq flag 'lambda)
cd35b20a
BW
725 (let ((path (concat (unless (and (> (length name) 1)
726 (eq (aref name 1) ?/))
727 mh-user-path)
728 (substring name 1))))
924df208
BW
729 (cond (mh-allow-root-folder-flag (file-exists-p path))
730 ((equal path mh-user-path) nil)
731 (t (file-exists-p path))))))))
732
dda00b2c 733;; Shush compiler.
42f8c37f
BW
734(defvar completion-root-regexp) ; XEmacs
735(defvar minibuffer-completing-file-name) ; XEmacs
dda00b2c 736
924df208
BW
737(defun mh-folder-completing-read (prompt default allow-root-folder-flag)
738 "Read folder name with PROMPT and default result DEFAULT.
2dcf34f9
BW
739If ALLOW-ROOT-FOLDER-FLAG is non-nil then \"+\" is allowed to be
740a folder name corresponding to `mh-user-path'."
3d7ca223 741 (mh-normalize-folder-name
cd35b20a 742 (let ((completion-root-regexp "^[+/]")
f0d73c14 743 (minibuffer-local-completion-map mh-folder-completion-map)
924df208 744 (mh-allow-root-folder-flag allow-root-folder-flag))
3d7ca223
BW
745 (completing-read prompt 'mh-folder-completion-function nil nil nil
746 'mh-folder-hist default))
747 t))
bdcfe844 748
dda00b2c 749;;;###mh-autoload
bdcfe844 750(defun mh-prompt-for-folder (prompt default can-create
3d7ca223 751 &optional default-string allow-root-folder-flag)
bdcfe844 752 "Prompt for a folder name with PROMPT.
2dcf34f9
BW
753Returns the folder's name as a string. DEFAULT is used if the
754folder exists and the user types return. If the CAN-CREATE flag
755is t, then a folder is created if it doesn't already exist. If
756optional argument DEFAULT-STRING is non-nil, use it in the prompt
757instead of DEFAULT. If ALLOW-ROOT-FOLDER-FLAG is non-nil then the
758function will accept the folder +, which means all folders when
759used in searching."
c26cf6c8
RS
760 (if (null default)
761 (setq default ""))
d88a70a0 762 (let* ((default-string (cond (default-string (format " (default %s)" default-string))
f0d73c14 763 ((equal "" default) "")
d88a70a0
RF
764 (t (format " (default %s)" default))))
765 (prompt (format "%s folder%s: " prompt default-string))
924df208 766 (mh-current-folder-name mh-current-folder)
c3d9274a 767 read-name folder-name)
924df208
BW
768 (while (and (setq read-name (mh-folder-completing-read
769 prompt default allow-root-folder-flag))
c3d9274a
BW
770 (equal read-name "")
771 (equal default "")))
3d7ca223
BW
772 (cond ((or (equal read-name "")
773 (and (equal read-name "+") (not allow-root-folder-flag)))
c3d9274a
BW
774 (setq read-name default))
775 ((not (mh-folder-name-p read-name))
776 (setq read-name (format "+%s" read-name))))
a1b4049d
BW
777 (if (or (not read-name) (equal "" read-name))
778 (error "No folder specified"))
c26cf6c8
RS
779 (setq folder-name read-name)
780 (cond ((and (> (length folder-name) 0)
c3d9274a
BW
781 (eq (aref folder-name (1- (length folder-name))) ?/))
782 (setq folder-name (substring folder-name 0 -1))))
924df208
BW
783 (let* ((last-slash (mh-search-from-end ?/ folder-name))
784 (parent (and last-slash (substring folder-name 0 last-slash)))
785 (child (if last-slash
786 (substring folder-name (1+ last-slash))
787 (substring folder-name 1))))
788 (unless (member child
789 (mapcar #'car (gethash parent mh-sub-folders-cache)))
790 (mh-remove-from-sub-folders-cache folder-name)))
bdcfe844 791 (let ((new-file-flag
c3d9274a 792 (not (file-exists-p (mh-expand-file-name folder-name)))))
bdcfe844 793 (cond ((and new-file-flag
13fe29bd 794 can-create
c3d9274a
BW
795 (y-or-n-p
796 (format "Folder %s does not exist. Create it? "
797 folder-name)))
798 (message "Creating %s" folder-name)
bdcfe844 799 (mh-exec-cmd-error nil "folder" folder-name)
3d7ca223 800 (mh-remove-from-sub-folders-cache folder-name)
bdcfe844
BW
801 (when (boundp 'mh-speed-folder-map)
802 (mh-speed-add-folder folder-name))
3d7ca223 803 (message "Creating %s...done" folder-name))
c3d9274a 804 (new-file-flag
13fe29bd 805 (error "Folder %s does not exist" folder-name))
c3d9274a 806 ((not (file-directory-p (mh-expand-file-name folder-name)))
f9c53c97 807 (error "%s is not a directory"
3d7ca223 808 (mh-expand-file-name folder-name)))))
c26cf6c8
RS
809 folder-name))
810
cee9f5c6
BW
811\f
812
dda00b2c 813;;; Message Utilities
c26cf6c8 814
dda00b2c
BW
815;; Functions that would ordinarily be in mh-letter.el that are needed
816;; by mh-show.el are found here in order to prevent the loading of
817;; mh-letter.el until a message is actually composed.
818
819;;;###mh-autoload
820(defun mh-in-header-p ()
821 "Return non-nil if the point is in the header of a draft message."
822 (< (point) (mh-mail-header-end)))
823
824;;;###mh-autoload
825(defun mh-extract-from-header-value ()
826 "Extract From: string from header."
827 (save-excursion
828 (if (not (mh-goto-header-field "From:"))
829 nil
830 (skip-chars-forward " \t")
831 (buffer-substring-no-properties
832 (point) (progn (mh-header-field-end)(point))))))
833
a55f450f
BW
834;;;###mh-autoload
835(defun mh-get-header-field (field)
836 "Find and return the body of FIELD in the mail header.
837Returns the empty string if the field is not in the header of the
838current buffer."
839 (if (mh-goto-header-field field)
840 (progn
841 (skip-chars-forward " \t") ;strip leading white space in body
842 (let ((start (point)))
843 (mh-header-field-end)
844 (buffer-substring-no-properties start (point))))
845 ""))
846
dda00b2c
BW
847;;;###mh-autoload
848(defun mh-goto-header-field (field)
849 "Move to FIELD in the message header.
850Move to the end of the FIELD name, which should end in a colon.
851Returns t if found, nil if not."
f0d73c14 852 (goto-char (point-min))
dda00b2c
BW
853 (let ((case-fold-search t)
854 (headers-end (save-excursion
855 (mh-goto-header-end 0)
856 (point))))
857 (re-search-forward (format "^%s" field) headers-end t)))
858
859;;;###mh-autoload
860(defun mh-goto-header-end (arg)
861 "Move the cursor ARG lines after the header."
c932f02a
BW
862 (if (re-search-forward (concat "^\\(" (regexp-quote mh-mail-header-separator)
863 "\\)?$") nil nil)
dda00b2c
BW
864 (forward-line arg)))
865
866;;;###mh-autoload
867(defun mh-mail-header-end ()
868 "Substitute for `mail-header-end' that doesn't widen the buffer.
869
870In MH-E we frequently need to find the end of headers in nested
871messages, where the buffer has been narrowed. This function works
872in this situation."
873 (save-excursion
874 ;; XXX: The following replaces a call to rfc822-goto-eoh. Occasionally,
875 ;; mail headers that MH-E has to read contains lines of the form:
876 ;; From xxx@yyy Mon May 10 11:48:07 2004
877 ;; In this situation, rfc822-goto-eoh doesn't go to the end of the
878 ;; header. The replacement allows From_ lines in the mail header.
879 (goto-char (point-min))
880 (loop for p = (re-search-forward
881 "^\\([:\n]\\|[^: \t\n]+[ \t\n]\\)" nil 'move)
882 do (cond ((null p) (return))
883 (t (goto-char (match-beginning 0))
884 (unless (looking-at "From ") (return))
885 (goto-char p))))
886 (point)))
887
888;;;###mh-autoload
889(defun mh-header-field-beginning ()
890 "Move to the beginning of the current header field.
891Handles RFC 822 continuation lines."
892 (beginning-of-line)
893 (while (looking-at "^[ \t]")
894 (forward-line -1)))
895
896;;;###mh-autoload
897(defun mh-header-field-end ()
898 "Move to the end of the current header field.
899Handles RFC 822 continuation lines."
900 (forward-line 1)
901 (while (looking-at "^[ \t]")
902 (forward-line 1))
903 (backward-char 1)) ;to end of previous line
904
a55f450f
BW
905;;;###mh-autoload
906(defun mh-letter-hide-all-skipped-fields ()
907 "Hide all skipped fields."
908 (save-excursion
909 (goto-char (point-min))
910 (save-restriction
911 (narrow-to-region (point) (mh-mail-header-end))
912 (while (re-search-forward mh-letter-header-field-regexp nil t)
913 (if (mh-letter-skipped-header-field-p (match-string 1))
914 (mh-letter-toggle-header-field-display -1)
915 (mh-letter-toggle-header-field-display 'long))
916 (beginning-of-line 2)))))
917
918;;;###mh-autoload
919(defun mh-letter-skipped-header-field-p (field)
920 "Check if FIELD is to be skipped."
921 (let ((field (downcase field)))
922 (loop for x in mh-compose-skipped-header-fields
923 when (equal (downcase x) field) return t
924 finally return nil)))
925
926(defvar mh-hidden-header-keymap
927 (let ((map (make-sparse-keymap)))
928 (mh-do-in-gnu-emacs
929 (define-key map [mouse-2] 'mh-letter-toggle-header-field-display-button))
930 (mh-do-in-xemacs
931 (define-key map '(button2)
932 'mh-letter-toggle-header-field-display-button))
933 map))
934
935;;;###mh-autoload
936(defun mh-letter-toggle-header-field-display (arg)
937 "Toggle display of header field at point.
938
939Use this command to display truncated header fields. This command
940is a toggle so entering it again will hide the field. This
941command takes a prefix argument ARG: if negative then the field
942is hidden, if positive then the field is displayed."
943 (interactive (list nil))
944 (when (and (mh-in-header-p)
945 (progn
946 (end-of-line)
947 (re-search-backward mh-letter-header-field-regexp nil t)))
948 (let ((buffer-read-only nil)
949 (modified-flag (buffer-modified-p))
950 (begin (point))
951 end)
952 (end-of-line)
953 (setq end (1- (if (re-search-forward "^[^ \t]" nil t)
954 (match-beginning 0)
955 (point-max))))
956 (goto-char begin)
957 ;; Make it clickable...
958 (add-text-properties begin end `(keymap ,mh-hidden-header-keymap
959 mouse-face highlight))
960 (unwind-protect
961 (cond ((or (and (not arg)
962 (text-property-any begin end 'invisible 'vanish))
d5dc8c56
BW
963 (and (numberp arg)
964 (>= arg 0))
965 (and (eq arg 'long)
966 (> (mh-line-beginning-position 5) end)))
a55f450f 967 (remove-text-properties begin end '(invisible nil))
d5dc8c56 968 (search-forward ":" (mh-line-end-position) t)
a55f450f
BW
969 (mh-letter-skip-leading-whitespace-in-header-field))
970 ;; XXX Redesign to make usable by user. Perhaps use a positive
971 ;; numeric prefix to make that many lines visible.
972 ((eq arg 'long)
973 (end-of-line 4)
974 (mh-letter-truncate-header-field end)
975 (beginning-of-line))
976 (t (end-of-line)
977 (mh-letter-truncate-header-field end)
978 (beginning-of-line)))
979 (set-buffer-modified-p modified-flag)))))
980
981;;;###mh-autoload
982(defun mh-letter-skip-leading-whitespace-in-header-field ()
983 "Skip leading whitespace in a header field.
984If the header field doesn't have at least one space after the
985colon then a space character is added."
986 (let ((need-space t))
987 (while (memq (char-after) '(?\t ?\ ))
988 (forward-char)
989 (setq need-space nil))
990 (when need-space (insert " "))))
991
992(defun mh-letter-truncate-header-field (end)
993 "Replace text from current line till END with an ellipsis.
994If the current line is too long truncate a part of it as well."
995 (let ((max-len (min (window-width) 62)))
996 (when (> (+ (current-column) 4) max-len)
997 (backward-char (- (+ (current-column) 5) max-len)))
998 (when (> end (point))
999 (add-text-properties (point) end '(invisible vanish)))))
1000
dda00b2c
BW
1001;;;###mh-autoload
1002(defun mh-signature-separator-p ()
1003 "Return non-nil if buffer includes \"^-- $\"."
1004 (save-excursion
1005 (goto-char (point-min))
1006 (re-search-forward mh-signature-separator-regexp nil t)))
f0d73c14 1007
c26cf6c8
RS
1008(provide 'mh-utils)
1009
cee9f5c6
BW
1010;; Local Variables:
1011;; indent-tabs-mode: nil
1012;; sentence-end-double-space: nil
1013;; End:
bdcfe844 1014
c26cf6c8 1015;;; mh-utils.el ends here