*** empty log message ***
[bpt/emacs.git] / lisp / emacs-lisp / lisp-mnt.el
CommitLineData
0fc37e7d
ER
1;;; lisp-mnt.el --- minor mode for Emacs Lisp maintainers
2
13c40e2f 3;; Copyright (C) 1992, 1994, 1997, 2000, 2001 Free Software Foundation, Inc.
0fc37e7d
ER
4
5;; Author: Eric S. Raymond <esr@snark.thyrsus.com>
877d5b39 6;; Maintainer: FSF
0fc37e7d 7;; Created: 14 Jul 1992
0fc37e7d 8;; Keywords: docs
1894df21 9;; X-Bogus-Bureaucratic-Cruft: Gruad will get you if you don't watch out!
0fc37e7d
ER
10
11;; This file is part of GNU Emacs.
12
13;; GNU Emacs is free software; you can redistribute it and/or modify
14;; it under the terms of the GNU General Public License as published by
7c938215 15;; the Free Software Foundation; either version 2, or (at your option)
0fc37e7d
ER
16;; any later version.
17
18;; GNU Emacs is distributed in the hope that it will be useful,
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
d5eead69 24;; along with GNU Emacs; see the file COPYING. If not, write to
470c5afe
EN
25;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
26;; Boston, MA 02111-1307, USA.
0fc37e7d
ER
27
28;;; Commentary:
29
30;; This minor mode adds some services to Emacs-Lisp editing mode.
31;;
32;; First, it knows about the header conventions for library packages.
33;; One entry point supports generating synopses from a library directory.
34;; Another can be used to check for missing headers in library files.
35;;
36;; Another entry point automatically addresses bug mail to a package's
37;; maintainer or author.
38
39;; This file can be loaded by your lisp-mode-hook. Have it (require 'lisp-mnt)
40
41;; This file is an example of the header conventions. Note the following
42;; features:
43;;
44;; * Header line --- makes it possible to extract a one-line summary of
45;; the package's uses automatically for use in library synopses, KWIC
46;; indexes and the like.
47;;
48;; Format is three semicolons, followed by the filename, followed by
49;; three dashes, followed by the summary. All fields space-separated.
50;;
51;; * Author line --- contains the name and net address of at least
52;; the principal author.
53;;
68756f1f 54;; If there are multiple authors, they should be listed on continuation
0fc37e7d
ER
55;; lines led by ;;<TAB>, like this:
56;;
57;; ;; Author: Ashwin Ram <Ram-Ashwin@cs.yale.edu>
58;; ;; Dave Sill <de5@ornl.gov>
59;; ;; David Lawrence <tale@pawl.rpi.edu>
60;; ;; Noah Friedman <friedman@ai.mit.edu>
61;; ;; Joe Wells <jbw@maverick.uswest.com>
62;; ;; Dave Brennan <brennan@hal.com>
63;; ;; Eric Raymond <esr@snark.thyrsus.com>
64;;
65;; This field may have some special values; notably "FSF", meaning
66;; "Free Software Foundation".
67;;
68;; * Maintainer line --- should be a single name/address as in the Author
69;; line, or an address only, or the string "FSF". If there is no maintainer
70;; line, the person(s) in the Author field are presumed to be it. The example
71;; in this file is mildly bogus because the maintainer line is redundant.
ca4bd734 72;; The idea behind these two fields is to be able to write a Lisp function
0fc37e7d
ER
73;; that does "send mail to the author" without having to mine the name out by
74;; hand. Please be careful about surrounding the network address with <> if
75;; there's also a name in the field.
76;;
77;; * Created line --- optional, gives the original creation date of the
78;; file. For historical interest, basically.
79;;
80;; * Version line --- intended to give the reader a clue if they're looking
68756f1f
ER
81;; at a different version of the file than the one they're accustomed to. This
82;; may be an RCS or SCCS header.
0fc37e7d
ER
83;;
84;; * Adapted-By line --- this is for FSF's internal use. The person named
85;; in this field was the one responsible for installing and adapting the
86;; package for the distribution. (This file doesn't have one because the
87;; author *is* one of the maintainers.)
88;;
89;; * Keywords line --- used by the finder code (now under construction)
c0d79871 90;; for finding Emacs Lisp code related to a topic.
0fc37e7d 91;;
1894df21
NF
92;; * X-Bogus-Bureaucratic-Cruft line --- this is a joke and an example
93;; of a comment header. Headers starting with `X-' should never be used
94;; for any real purpose; this is the way to safely add random headers
95;; without invoking the wrath of any program.
0fc37e7d 96;;
ca4bd734 97;; * Commentary line --- enables Lisp code to find the developer's and
0fc37e7d
ER
98;; maintainers' explanations of the package internals.
99;;
100;; * Change log line --- optional, exists to terminate the commentary
101;; section and start a change-log part, if one exists.
102;;
c0d79871 103;; * Code line --- exists so Lisp can know where commentary and/or
0fc37e7d
ER
104;; change-log sections end.
105;;
106;; * Footer line --- marks end-of-file so it can be distinguished from
107;; an expanded formfeed or the results of truncation.
108
109;;; Change Log:
110
111;; Tue Jul 14 23:44:17 1992 ESR
112;; * Created.
113
114;;; Code:
115
d5eead69
RS
116;;; Variables:
117
666b9413
SE
118(defgroup lisp-mnt nil
119 "Minor mode for Emacs Lisp maintainers."
120 :prefix "lm-"
121 :group 'maint)
122
438cdcde 123(defcustom lm-header-prefix "^;+[ \t]+\\(@(#)\\)?[ \t]*\\$?"
d5eead69 124 "Prefix that is ignored before the tag.
ca4bd734
RS
125For example, you can write the 1st line synopsis string and headers like this
126in your Lisp package:
d5eead69 127
7a3ee510 128 ;; @(#) package.el -- package description
d5eead69
RS
129 ;;
130 ;; @(#) $Maintainer: Person Foo Bar $
131
132The @(#) construct is used by unix what(1) and
666b9413
SE
133then $identifier: doc string $ is used by GNU ident(1)"
134 :type 'regexp
135 :group 'lisp-mnt)
136
13c40e2f
GM
137(defcustom lm-copyright-prefix "^;+[ \t]+Copyright (C) "
138 "Prefix that is ignored before the dates in a copyright."
139 :type 'regexp
140 :group 'lisp-mnt)
141
666b9413
SE
142(defcustom lm-comment-column 16
143 "Column used for placing formatted output."
144 :type 'integer
145 :group 'lisp-mnt)
146
147(defcustom lm-commentary-header "Commentary\\|Documentation"
148 "Regexp which matches start of documentation section."
149 :type 'regexp
150 :group 'lisp-mnt)
151
3d1b88f6 152(defcustom lm-history-header "Change ?Log\\|History"
666b9413
SE
153 "Regexp which matches the start of code log section."
154 :type 'regexp
155 :group 'lisp-mnt)
d5eead69
RS
156
157;;; Functions:
158
0fc37e7d
ER
159;; These functions all parse the headers of the current buffer
160
2505742b 161(defun lm-get-header-re (header &optional mode)
6e24ad22 162 "Return regexp for matching HEADER.
ca4bd734
RS
163If called with optional MODE and with value `section',
164return section regexp instead."
438cdcde
SM
165 (if (eq mode 'section)
166 (concat "^;;;;* " header ":[ \t]*$")
167 (concat lm-header-prefix header "[ \t]*:[ \t]*")))
d5eead69 168
2505742b 169(defun lm-get-package-name ()
6e24ad22 170 "Return package name by looking at the first line."
d5eead69
RS
171 (save-excursion
172 (goto-char (point-min))
173 (if (and (looking-at (concat lm-header-prefix))
174 (progn (goto-char (match-end 0))
175 (looking-at "\\([^\t ]+\\)")
176 (match-end 1)))
2505742b 177 (match-string-no-properties 1))))
d5eead69
RS
178
179(defun lm-section-mark (header &optional after)
180 "Return the buffer location of a given section start marker.
ca4bd734
RS
181The HEADER is the section mark string to search for.
182If AFTER is non-nil, return the location of the next line."
0fc37e7d
ER
183 (save-excursion
184 (let ((case-fold-search t))
185 (goto-char (point-min))
d5eead69 186 (if (re-search-forward (lm-get-header-re header 'section) nil t)
0fc37e7d
ER
187 (progn
188 (beginning-of-line)
64e07c7b 189 (if after (forward-line 1))
2505742b 190 (point))))))
0fc37e7d 191
d5eead69 192(defsubst lm-code-mark ()
ca4bd734 193 "Return the buffer location of the `Code' start marker."
0fc37e7d
ER
194 (lm-section-mark "Code"))
195
d5eead69 196(defsubst lm-commentary-mark ()
ca4bd734 197 "Return the buffer location of the `Commentary' start marker."
d5eead69
RS
198 (lm-section-mark lm-commentary-header))
199
200(defsubst lm-history-mark ()
ca4bd734 201 "Return the buffer location of the `History' start marker."
d5eead69
RS
202 (lm-section-mark lm-history-header))
203
13c40e2f
GM
204(defsubst lm-copyright-mark ()
205 "Return the buffer location of the `Copyright' line."
206 (save-excursion
207 (let ((case-fold-search t))
208 (goto-char (point-min))
209 (if (re-search-forward lm-copyright-prefix nil t)
210 (point))))
211 )
212
d5eead69 213(defun lm-header (header)
ca4bd734
RS
214 "Return the contents of the header named HEADER."
215 (goto-char (point-min))
216 (let ((case-fold-search t))
438cdcde
SM
217 (when (and (re-search-forward (lm-get-header-re header) (lm-code-mark) t)
218 ;; RCS ident likes format "$identifier: data$"
219 (looking-at
220 (if (save-excursion
221 (skip-chars-backward "^$" (match-beginning 0))
222 (= (point) (match-beginning 0)))
223 "[^\n]+" "[^$\n]+")))
224 (match-string-no-properties 0))))
0fc37e7d 225
d5eead69 226(defun lm-header-multiline (header)
ca4bd734 227 "Return the contents of the header named HEADER, with continuation lines.
d5eead69 228The returned value is a list of strings, one per line."
0fc37e7d
ER
229 (save-excursion
230 (goto-char (point-min))
d5eead69 231 (let ((res (lm-header header)))
be961cd5 232 (when res
d5eead69
RS
233 (setq res (list res))
234 (forward-line 1)
438cdcde
SM
235 (while (and (or (looking-at (concat lm-header-prefix "[\t ]+"))
236 (and (not (looking-at
237 (lm-get-header-re "\\sw\\(\\sw\\|\\s_\\)*")))
238 (looking-at lm-header-prefix)))
239 (goto-char (match-end 0))
240 (looking-at ".+"))
241 (setq res (cons (match-string-no-properties 0) res))
2505742b 242 (forward-line 1)))
438cdcde 243 (nreverse res))))
0fc37e7d
ER
244
245;; These give us smart access to the header fields and commentary
246
be961cd5 247(defmacro lm-with-file (file &rest body)
13c40e2f
GM
248 "Make a buffer with FILE current, and execute BODY.
249If FILE isn't in a buffer, load it in, and kill it after BODY is executed."
be961cd5
SM
250 (let ((filesym (make-symbol "file")))
251 `(save-excursion
252 (let ((,filesym ,file))
253 (if ,filesym (set-buffer (find-file-noselect ,filesym)))
254 (prog1 (progn ,@body)
255 (if (and ,filesym (not (get-buffer-window (current-buffer) t)))
256 (kill-buffer (current-buffer))))))))
257(put 'lm-with-file 'lisp-indent-function 1)
258(put 'lm-with-file 'edebug-form-spec t)
259
13c40e2f
GM
260(defun lm-crack-copyright (&optional file)
261 "Return the copyright holder, and a list of copyright years.
262Use the current buffer if FILE is nil.
263Return argument is of the form (\"HOLDER\" \"YEAR1\" ... \"YEARN\")"
264 (lm-with-file file
265 (goto-char (lm-copyright-mark))
266 (let ((holder nil)
267 (years nil)
268 (end (line-end-position)))
269 (while (re-search-forward "\\([0-9]+\\),? +" end t)
270 (setq years (cons (match-string-no-properties 1) years)))
271 (if (looking-at ".*$")
272 (setq holder (match-string-no-properties 0)))
273 (cons holder (nreverse years))
274 )))
275
0fc37e7d 276(defun lm-summary (&optional file)
ca4bd734 277 "Return the one-line summary of file FILE, or current buffer if FILE is nil."
be961cd5 278 (lm-with-file file
0fc37e7d 279 (goto-char (point-min))
2505742b
DL
280 (if (and (looking-at lm-header-prefix)
281 (progn (goto-char (match-end 0))
282 (looking-at "[^ ]+[ \t]+--+[ \t]+\\(.*\\)")))
283 (let ((summary (match-string-no-properties 1)))
be961cd5
SM
284 ;; Strip off -*- specifications.
285 (if (string-match "[ \t]*-\\*-.*-\\*-" summary)
286 (substring summary 0 (match-beginning 0))
287 summary)))))
0fc37e7d 288
68756f1f 289(defun lm-crack-address (x)
6e24ad22 290 "Split up an email address X into full name and real email address.
ca4bd734 291The value is a cons of the form (FULLNAME . ADDRESS)."
68756f1f 292 (cond ((string-match "\\(.+\\) [(<]\\(\\S-+@\\S-+\\)[>)]" x)
2505742b
DL
293 (cons (match-string 1 x)
294 (match-string 2 x)))
68756f1f 295 ((string-match "\\(\\S-+@\\S-+\\) [(<]\\(.*\\)[>)]" x)
2505742b
DL
296 (cons (match-string 2 x)
297 (match-string 1 x)))
68756f1f
ER
298 ((string-match "\\S-+@\\S-+" x)
299 (cons nil x))
300 (t
301 (cons x nil))))
302
0fc37e7d 303(defun lm-authors (&optional file)
ca4bd734
RS
304 "Return the author list of file FILE, or current buffer if FILE is nil.
305Each element of the list is a cons; the car is the full name,
306the cdr is an email address."
be961cd5 307 (lm-with-file file
68756f1f 308 (let ((authorlist (lm-header-multiline "author")))
be961cd5 309 (mapcar 'lm-crack-address authorlist))))
0fc37e7d
ER
310
311(defun lm-maintainer (&optional file)
ca4bd734
RS
312 "Return the maintainer of file FILE, or current buffer if FILE is nil.
313The return value has the form (NAME . ADDRESS)."
be961cd5
SM
314 (lm-with-file file
315 (let ((maint (lm-header "maintainer")))
316 (if maint
317 (lm-crack-address maint)
318 (car (lm-authors))))))
0fc37e7d
ER
319
320(defun lm-creation-date (&optional file)
ca4bd734 321 "Return the created date given in file FILE, or current buffer if FILE is nil."
be961cd5
SM
322 (lm-with-file file
323 (lm-header "created")))
0fc37e7d 324
07d505c1
GM
325(defun lm-last-modified-date (&optional file iso-date)
326 "Return the modify-date given in file FILE, or current buffer if FILE is nil.
327ISO-DATE non-nil means return the date in ISO 8601 format."
be961cd5 328 (lm-with-file file
07d505c1
GM
329 (when (progn (goto-char (point-min))
330 (re-search-forward
331 "\\$[I]d: [^ ]+ [^ ]+ \\([^/]+\\)/\\([^/]+\\)/\\([^ ]+\\) "
332 (lm-code-mark) t))
333 (let ((dd (match-string 3))
334 (mm (match-string 2))
335 (yyyy (match-string 1)))
336 (if iso-date
337 (format "%s-%s-%s" yyyy mm dd)
338 (format "%s %s %s"
339 dd
340 (nth (string-to-int mm)
341 '("" "Jan" "Feb" "Mar" "Apr" "May" "Jun"
342 "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"))
343 yyyy))))))
0fc37e7d
ER
344
345(defun lm-version (&optional file)
ca4bd734 346 "Return the version listed in file FILE, or current buffer if FILE is nil.
2505742b 347This can be found in an RCS or SCCS header."
be961cd5 348 (lm-with-file file
2505742b
DL
349 (or (lm-header "version")
350 (let ((header-max (lm-code-mark)))
351 (goto-char (point-min))
352 (cond
353 ;; Look for an RCS header
354 ((re-search-forward "\\$[I]d: [^ ]+ \\([^ ]+\\) " header-max t)
355 (match-string-no-properties 1))
356 ((re-search-forward "\\$Revision: +\\([^ ]+\\) " header-max t)
357 (match-string-no-properties 1))
358 ;; Look for an SCCS header
359 ((re-search-forward
360 (concat
361 (regexp-quote "@(#)")
362 (regexp-quote (file-name-nondirectory (buffer-file-name)))
363 "\t\\([012345679.]*\\)")
364 header-max t)
365 (match-string-no-properties 1)))))))
0fc37e7d
ER
366
367(defun lm-keywords (&optional file)
ca4bd734 368 "Return the keywords given in file FILE, or current buffer if FILE is nil."
be961cd5
SM
369 (lm-with-file file
370 (let ((keywords (lm-header "keywords")))
371 (and keywords (downcase keywords)))))
0fc37e7d 372
5efa6032
GM
373(defun lm-keywords-list (&optional file)
374 "Return list of keywords given in file FILE."
375 (let ((keywords (lm-keywords file)))
376 (if keywords
377 (split-string keywords ",?[ \t]"))))
378
379(defun lm-keywords-finder-p (&optional file)
380 "Return non-nil if any keywords in FILE are known to finder."
381 (require 'finder)
382 (let ((keys (lm-keywords-list file)))
383 (catch 'keyword-found
384 (while keys
385 (if (assoc (intern (car keys)) finder-known-keywords)
386 (throw 'keyword-found t))
387 (setq keys (cdr keys)))
388 nil)))
389
0fc37e7d 390(defun lm-adapted-by (&optional file)
ca4bd734
RS
391 "Return the adapted-by names in file FILE, or current buffer if FILE is nil.
392This is the name of the person who cleaned up this package for
393distribution."
be961cd5
SM
394 (lm-with-file file
395 (lm-header "adapted-by")))
0fc37e7d 396
64e07c7b 397(defun lm-commentary (&optional file)
ca4bd734 398 "Return the commentary in file FILE, or current buffer if FILE is nil.
e7b377f0
DL
399The value is returned as a string. In the file, the commentary starts
400with the tag `Commentary' or `Documentation' and ends with one of the
401tags `Code', `Change Log' or `History'."
be961cd5 402 (lm-with-file file
2505742b
DL
403 (let ((commentary (lm-commentary-mark))
404 (change-log (lm-history-mark))
405 (code (lm-code-mark)))
406 (cond
407 ((and commentary change-log)
408 (buffer-substring-no-properties commentary change-log))
409 ((and commentary code)
410 (buffer-substring-no-properties commentary code))))))
0fc37e7d
ER
411
412;;; Verification and synopses
413
d5eead69 414(defun lm-insert-at-column (col &rest strings)
6e24ad22 415 "Insert, at column COL, list of STRINGS."
d5eead69 416 (if (> (current-column) col) (insert "\n"))
b4d73c71 417 (move-to-column col t)
d5eead69 418 (apply 'insert strings))
0fc37e7d 419
e7b377f0 420(defun lm-verify (&optional file showok verb)
0fc37e7d 421 "Check that the current buffer (or FILE if given) is in proper format.
13c40e2f
GM
422If FILE is a directory, recurse on its files and generate a report in a
423temporary buffer.
424Optional argument SHOWOK indicates that \"OK\" be displayed in the temp buffer.
425Optional argument VERB specifies verbosity."
d5eead69 426 (interactive)
2505742b
DL
427 (let* ((verb (or verb (interactive-p)))
428 (ret (and verb "Ok."))
429 name)
d5eead69 430 (if (and file (file-directory-p file))
2505742b
DL
431 (setq ret
432 (with-temp-buffer
433 (mapcar
434 (lambda (f)
435 (if (string-match ".*\\.el\\'" f)
436 (let ((status (lm-verify f)))
437 (insert f ":")
438 (if status
439 (lm-insert-at-column lm-comment-column status
440 "\n")
441 (if showok
442 (lm-insert-at-column lm-comment-column
443 "OK\n"))))))
444 (directory-files file))))
be961cd5 445 (lm-with-file file
d5eead69 446 (setq name (lm-get-package-name))
2505742b
DL
447 (setq ret
448 (cond
449 ((null name)
450 "Can't find a package NAME")
451 ((not (lm-authors))
452 "Author: tag missing.")
453 ((not (lm-maintainer))
454 "Maintainer: tag missing.")
455 ((not (lm-summary))
456 "Can't find a one-line 'Summary' description")
457 ((not (lm-keywords))
458 "Keywords: tag missing.")
5efa6032
GM
459 ((not (lm-keywords-finder-p))
460 "Keywords: no valid finder keywords.")
2505742b
DL
461 ((not (lm-commentary-mark))
462 "Can't find a 'Commentary' section marker.")
463 ((not (lm-history-mark))
464 "Can't find a 'History' section marker.")
465 ((not (lm-code-mark))
466 "Can't find a 'Code' section marker")
467 ((progn
468 (goto-char (point-max))
469 (not
470 (re-search-backward
471 (concat "^;;;[ \t]+" name "[ \t]+ends here[ \t]*$"
472 "\\|^;;;[ \t]+ End of file[ \t]+" name)
473 nil t)))
474 (format "Can't find a footer line for [%s]" name))
13c40e2f
GM
475 ((not (and (lm-copyright-mark) (lm-crack-copyright)))
476 "Can't find a valid Copyright")
477 ((not (string-match "Free Software Foundation"
478 (car (lm-crack-copyright))))
479 "Copyright Holder is not the Free Software Foundation.")
2505742b
DL
480 (t
481 ret)))))
d5eead69
RS
482 (if verb
483 (message ret))
2505742b 484 ret))
0fc37e7d
ER
485
486(defun lm-synopsis (&optional file showall)
487 "Generate a synopsis listing for the buffer or the given FILE if given.
ca4bd734
RS
488If FILE is a directory, recurse on its files and generate a report in
489a temporary buffer. If SHOWALL is non-nil, also generate a line for files
0fc37e7d 490which do not include a recognizable synopsis."
d5eead69
RS
491 (interactive
492 (list
493 (read-file-name "Synopsis for (file or dir): ")))
494
0fc37e7d 495 (if (and file (file-directory-p file))
2505742b 496 (with-temp-buffer
0fc37e7d 497 (mapcar
2505742b
DL
498 (lambda (f)
499 (if (string-match "\\.el\\'" f)
500 (let ((syn (lm-synopsis f)))
501 (if syn
502 (progn
503 (insert f ":")
504 (lm-insert-at-column lm-comment-column syn "\n"))
505 (when showall
506 (insert f ":")
507 (lm-insert-at-column lm-comment-column "NA\n"))))))
508 (directory-files file)))
509 (save-excursion
510 (if file
511 (find-file file))
512 (prog1
513 (lm-summary)
514 (if file
515 (kill-buffer (current-buffer)))))))
516
517(eval-when-compile (defvar report-emacs-bug-address))
0fc37e7d
ER
518
519(defun lm-report-bug (topic)
520 "Report a bug in the package currently being visited to its maintainer.
6e24ad22 521Prompts for bug subject TOPIC. Leaves you in a mail buffer."
68756f1f 522 (interactive "sBug Subject: ")
2505742b
DL
523 (require 'emacsbug)
524 (let ((package (lm-get-package-name))
525 (addr (lm-maintainer))
526 (version (lm-version)))
527 (compose-mail (if addr
528 (concat (car addr) " <" (cdr addr) ">")
529 report-emacs-bug-address)
530 topic)
0fc37e7d 531 (goto-char (point-max))
2505742b
DL
532 (insert "\nIn " package)
533 (if version
534 (insert " version " version))
535 (newline 2)
d5eead69 536 (message
0fc37e7d
ER
537 (substitute-command-keys "Type \\[mail-send] to send bug report."))))
538
539(provide 'lisp-mnt)
540
541;;; lisp-mnt.el ends here