*** 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
6e24ad22 3;; Copyright (C) 1992, 1994, 1997 Free Software Foundation, Inc.
0fc37e7d
ER
4
5;; Author: Eric S. Raymond <esr@snark.thyrsus.com>
6;; Maintainer: Eric S. Raymond <esr@snark.thyrsus.com>
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
68756f1f 116(require 'emacsbug)
0fc37e7d 117
d5eead69
RS
118;;; Variables:
119
666b9413
SE
120(defgroup lisp-mnt nil
121 "Minor mode for Emacs Lisp maintainers."
122 :prefix "lm-"
123 :group 'maint)
124
125(defcustom lm-header-prefix "^;;*[ \t]+\\(@\(#\)\\)?[ \t]*\\([\$]\\)?"
d5eead69 126 "Prefix that is ignored before the tag.
ca4bd734
RS
127For example, you can write the 1st line synopsis string and headers like this
128in your Lisp package:
d5eead69
RS
129
130 ;; @(#) package.el -- pacakge description
131 ;;
132 ;; @(#) $Maintainer: Person Foo Bar $
133
134The @(#) construct is used by unix what(1) and
666b9413
SE
135then $identifier: doc string $ is used by GNU ident(1)"
136 :type 'regexp
137 :group 'lisp-mnt)
138
139(defcustom lm-comment-column 16
140 "Column used for placing formatted output."
141 :type 'integer
142 :group 'lisp-mnt)
143
144(defcustom lm-commentary-header "Commentary\\|Documentation"
145 "Regexp which matches start of documentation section."
146 :type 'regexp
147 :group 'lisp-mnt)
148
149(defcustom lm-history-header "Change Log\\|History"
150 "Regexp which matches the start of code log section."
151 :type 'regexp
152 :group 'lisp-mnt)
d5eead69
RS
153
154;;; Functions:
155
0fc37e7d
ER
156;; These functions all parse the headers of the current buffer
157
d5eead69 158(defsubst lm-get-header-re (header &optional mode)
6e24ad22 159 "Return regexp for matching HEADER.
ca4bd734
RS
160If called with optional MODE and with value `section',
161return section regexp instead."
162 (cond ((eq mode 'section)
163 (concat "^;;;;* " header ":[ \t]*$"))
164 (t
165 (concat lm-header-prefix header ":[ \t]*"))))
d5eead69
RS
166
167(defsubst lm-get-package-name ()
6e24ad22 168 "Return package name by looking at the first line."
d5eead69
RS
169 (save-excursion
170 (goto-char (point-min))
171 (if (and (looking-at (concat lm-header-prefix))
172 (progn (goto-char (match-end 0))
173 (looking-at "\\([^\t ]+\\)")
174 (match-end 1)))
6e24ad22 175 (buffer-substring-no-properties (match-beginning 1) (match-end 1))
d5eead69
RS
176 )))
177
178(defun lm-section-mark (header &optional after)
179 "Return the buffer location of a given section start marker.
ca4bd734
RS
180The HEADER is the section mark string to search for.
181If AFTER is non-nil, return the location of the next line."
0fc37e7d
ER
182 (save-excursion
183 (let ((case-fold-search t))
184 (goto-char (point-min))
d5eead69 185 (if (re-search-forward (lm-get-header-re header 'section) nil t)
0fc37e7d
ER
186 (progn
187 (beginning-of-line)
64e07c7b 188 (if after (forward-line 1))
0fc37e7d
ER
189 (point))
190 nil))))
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
204(defun lm-header (header)
ca4bd734
RS
205 "Return the contents of the header named HEADER."
206 (goto-char (point-min))
207 (let ((case-fold-search t))
208 (if (and (re-search-forward (lm-get-header-re header) (lm-code-mark) t)
209 ;; RCS ident likes format "$identifier: data$"
210 (looking-at "\\([^$\n]+\\)")
211 (match-end 1))
6e24ad22 212 (buffer-substring-no-properties (match-beginning 1) (match-end 1))
ca4bd734 213 nil)))
0fc37e7d 214
d5eead69 215(defun lm-header-multiline (header)
ca4bd734 216 "Return the contents of the header named HEADER, with continuation lines.
d5eead69 217The returned value is a list of strings, one per line."
0fc37e7d
ER
218 (save-excursion
219 (goto-char (point-min))
d5eead69 220 (let ((res (lm-header header)))
be961cd5 221 (when res
d5eead69
RS
222 (setq res (list res))
223 (forward-line 1)
224
225 (while (and (looking-at (concat lm-header-prefix "[\t ]+"))
226 (progn
227 (goto-char (match-end 0))
228 (looking-at "\\(.*\\)"))
229 (match-end 1))
6e24ad22 230 (setq res (cons (buffer-substring-no-properties
d5eead69
RS
231 (match-beginning 1)
232 (match-end 1))
233 res))
234 (forward-line 1))
be961cd5 235 )
d5eead69
RS
236 res
237 )))
0fc37e7d
ER
238
239;; These give us smart access to the header fields and commentary
240
be961cd5
SM
241(defmacro lm-with-file (file &rest body)
242 (let ((filesym (make-symbol "file")))
243 `(save-excursion
244 (let ((,filesym ,file))
245 (if ,filesym (set-buffer (find-file-noselect ,filesym)))
246 (prog1 (progn ,@body)
247 (if (and ,filesym (not (get-buffer-window (current-buffer) t)))
248 (kill-buffer (current-buffer))))))))
249(put 'lm-with-file 'lisp-indent-function 1)
250(put 'lm-with-file 'edebug-form-spec t)
251
0fc37e7d 252(defun lm-summary (&optional file)
ca4bd734 253 "Return the one-line summary of file FILE, or current buffer if FILE is nil."
be961cd5 254 (lm-with-file file
0fc37e7d 255 (goto-char (point-min))
be961cd5
SM
256 (if (and
257 (looking-at lm-header-prefix)
258 (progn (goto-char (match-end 0))
259 (looking-at "[^ ]+[ \t]+--+[ \t]+\\(.*\\)")))
260 (let ((summary (buffer-substring-no-properties (match-beginning 1)
261 (match-end 1))))
262 ;; Strip off -*- specifications.
263 (if (string-match "[ \t]*-\\*-.*-\\*-" summary)
264 (substring summary 0 (match-beginning 0))
265 summary)))))
0fc37e7d 266
68756f1f 267(defun lm-crack-address (x)
6e24ad22 268 "Split up an email address X into full name and real email address.
ca4bd734 269The value is a cons of the form (FULLNAME . ADDRESS)."
68756f1f
ER
270 (cond ((string-match "\\(.+\\) [(<]\\(\\S-+@\\S-+\\)[>)]" x)
271 (cons (substring x (match-beginning 1) (match-end 1))
272 (substring x (match-beginning 2) (match-end 2))))
273 ((string-match "\\(\\S-+@\\S-+\\) [(<]\\(.*\\)[>)]" x)
274 (cons (substring x (match-beginning 2) (match-end 2))
275 (substring x (match-beginning 1) (match-end 1))))
276 ((string-match "\\S-+@\\S-+" x)
277 (cons nil x))
278 (t
279 (cons x nil))))
280
0fc37e7d 281(defun lm-authors (&optional file)
ca4bd734
RS
282 "Return the author list of file FILE, or current buffer if FILE is nil.
283Each element of the list is a cons; the car is the full name,
284the cdr is an email address."
be961cd5 285 (lm-with-file file
68756f1f 286 (let ((authorlist (lm-header-multiline "author")))
be961cd5 287 (mapcar 'lm-crack-address authorlist))))
0fc37e7d
ER
288
289(defun lm-maintainer (&optional file)
ca4bd734
RS
290 "Return the maintainer of file FILE, or current buffer if FILE is nil.
291The return value has the form (NAME . ADDRESS)."
be961cd5
SM
292 (lm-with-file file
293 (let ((maint (lm-header "maintainer")))
294 (if maint
295 (lm-crack-address maint)
296 (car (lm-authors))))))
0fc37e7d
ER
297
298(defun lm-creation-date (&optional file)
ca4bd734 299 "Return the created date given in file FILE, or current buffer if FILE is nil."
be961cd5
SM
300 (lm-with-file file
301 (lm-header "created")))
0fc37e7d
ER
302
303
304(defun lm-last-modified-date (&optional file)
ca4bd734 305 "Return the modify-date given in file FILE, or current buffer if FILE is nil."
be961cd5
SM
306 (lm-with-file file
307 (goto-char (point-min))
308 (when (re-search-forward
309 "\\$[I]d: [^ ]+ [^ ]+ \\([^/]+\\)/\\([^/]+\\)/\\([^ ]+\\) "
310 (lm-code-mark) t)
311 (format "%s %s %s"
312 (buffer-substring (match-beginning 3) (match-end 3))
313 (nth (string-to-int
314 (buffer-substring (match-beginning 2) (match-end 2)))
315 '("" "Jan" "Feb" "Mar" "Apr" "May" "Jun"
316 "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"))
317 (buffer-substring (match-beginning 1) (match-end 1))))))
0fc37e7d
ER
318
319(defun lm-version (&optional file)
ca4bd734
RS
320 "Return the version listed in file FILE, or current buffer if FILE is nil.
321This can befound in an RCS or SCCS header to crack it out of."
be961cd5
SM
322 (lm-with-file file
323 (or
324 (lm-header "version")
325 (let ((header-max (lm-code-mark)))
326 (goto-char (point-min))
327 (cond
328 ;; Look for an RCS header
329 ((re-search-forward "\\$[I]d: [^ ]+ \\([^ ]+\\) " header-max t)
330 (buffer-substring-no-properties (match-beginning 1) (match-end 1)))
331
332 ;; Look for an SCCS header
333 ((re-search-forward
334 (concat
335 (regexp-quote "@(#)")
336 (regexp-quote (file-name-nondirectory (buffer-file-name)))
337 "\t\\([012345679.]*\\)")
338 header-max t)
339 (buffer-substring-no-properties (match-beginning 1) (match-end 1)))
340
341 (t nil))))))
0fc37e7d
ER
342
343(defun lm-keywords (&optional file)
ca4bd734 344 "Return the keywords given in file FILE, or current buffer if FILE is nil."
be961cd5
SM
345 (lm-with-file file
346 (let ((keywords (lm-header "keywords")))
347 (and keywords (downcase keywords)))))
0fc37e7d
ER
348
349(defun lm-adapted-by (&optional file)
ca4bd734
RS
350 "Return the adapted-by names in file FILE, or current buffer if FILE is nil.
351This is the name of the person who cleaned up this package for
352distribution."
be961cd5
SM
353 (lm-with-file file
354 (lm-header "adapted-by")))
0fc37e7d 355
64e07c7b 356(defun lm-commentary (&optional file)
ca4bd734 357 "Return the commentary in file FILE, or current buffer if FILE is nil.
e7b377f0
DL
358The value is returned as a string. In the file, the commentary starts
359with the tag `Commentary' or `Documentation' and ends with one of the
360tags `Code', `Change Log' or `History'."
be961cd5
SM
361 (lm-with-file file
362 (let ((commentary (lm-commentary-mark))
363 (change-log (lm-history-mark))
364 (code (lm-code-mark)))
365 (when (and commentary (or change-log code))
366 (buffer-substring-no-properties
367 commentary (min (or code (point-max)) (or change-log (point-max))))))))
0fc37e7d
ER
368
369;;; Verification and synopses
370
d5eead69 371(defun lm-insert-at-column (col &rest strings)
6e24ad22 372 "Insert, at column COL, list of STRINGS."
d5eead69 373 (if (> (current-column) col) (insert "\n"))
b4d73c71 374 (move-to-column col t)
d5eead69 375 (apply 'insert strings))
0fc37e7d 376
e7b377f0 377(defun lm-verify (&optional file showok verb)
0fc37e7d 378 "Check that the current buffer (or FILE if given) is in proper format.
ca4bd734 379If FILE is a directory, recurse on its files and generate a report in
0fc37e7d 380a temporary buffer."
d5eead69
RS
381 (interactive)
382 (let* ((verb (or verb (interactive-p)))
383 ret
384 name
385 )
386 (if verb
387 (setq ret "Ok.")) ;init value
388
389 (if (and file (file-directory-p file))
390 (setq
391 ret
392 (progn
393 (switch-to-buffer (get-buffer-create "*lm-verify*"))
394 (erase-buffer)
395 (mapcar
396 '(lambda (f)
397 (if (string-match ".*\\.el$" f)
398 (let ((status (lm-verify f)))
399 (if status
0fc37e7d
ER
400 (progn
401 (insert f ":")
d5eead69
RS
402 (lm-insert-at-column lm-comment-column status "\n"))
403 (and showok
404 (progn
405 (insert f ":")
406 (lm-insert-at-column lm-comment-column "OK\n")))))))
407 (directory-files file))
408 ))
be961cd5 409 (lm-with-file file
d5eead69
RS
410 (setq name (lm-get-package-name))
411
412 (setq
413 ret
be961cd5
SM
414 (cond
415 ((null name)
416 "Can't find a package NAME")
417
418 ((not (lm-authors))
419 "Author: tag missing.")
420
421 ((not (lm-maintainer))
422 "Maintainer: tag missing.")
423
424 ((not (lm-summary))
425 "Can't find a one-line 'Summary' description")
426
427 ((not (lm-keywords))
428 "Keywords: tag missing.")
429
430 ((not (lm-commentary-mark))
431 "Can't find a 'Commentary' section marker.")
432
433 ((not (lm-history-mark))
434 "Can't find a 'History' section marker.")
435
436 ((not (lm-code-mark))
437 "Can't find a 'Code' section marker")
438
439 ((progn
440 (goto-char (point-max))
441 (not
442 (re-search-backward
443 (concat "^;;;[ \t]+" name "[ \t]+ends here[ \t]*$"
444 "\\|^;;;[ \t]+ End of file[ \t]+" name)
445 nil t
446 )))
447 (format "Can't find a footer line for [%s]" name))
448 (t
449 ret))
450 )))
d5eead69
RS
451 (if verb
452 (message ret))
453 ret
454 ))
0fc37e7d
ER
455
456(defun lm-synopsis (&optional file showall)
457 "Generate a synopsis listing for the buffer or the given FILE if given.
ca4bd734
RS
458If FILE is a directory, recurse on its files and generate a report in
459a temporary buffer. If SHOWALL is non-nil, also generate a line for files
0fc37e7d 460which do not include a recognizable synopsis."
d5eead69
RS
461 (interactive
462 (list
463 (read-file-name "Synopsis for (file or dir): ")))
464
0fc37e7d
ER
465 (if (and file (file-directory-p file))
466 (progn
467 (switch-to-buffer (get-buffer-create "*lm-verify*"))
468 (erase-buffer)
469 (mapcar
470 '(lambda (f)
471 (if (string-match ".*\\.el$" f)
472 (let ((syn (lm-synopsis f)))
473 (if syn
474 (progn
475 (insert f ":")
f9636fb2 476 (lm-insert-at-column lm-comment-column syn "\n"))
0fc37e7d
ER
477 (and showall
478 (progn
479 (insert f ":")
f9636fb2 480 (lm-insert-at-column lm-comment-column "NA\n")))))))
0fc37e7d
ER
481 (directory-files file))
482 )
be961cd5
SM
483 (lm-with-file file
484 (lm-summary))))
0fc37e7d
ER
485
486(defun lm-report-bug (topic)
487 "Report a bug in the package currently being visited to its maintainer.
6e24ad22 488Prompts for bug subject TOPIC. Leaves you in a mail buffer."
68756f1f 489 (interactive "sBug Subject: ")
d5eead69
RS
490 (let ((package (lm-get-package-name))
491 (addr (lm-maintainer))
492 (version (lm-version)))
3082623d
RS
493 (mail nil
494 (if addr
495 (concat (car addr) " <" (cdr addr) ">")
6d38633e 496 report-emacs-bug-address)
3082623d 497 topic)
0fc37e7d
ER
498 (goto-char (point-max))
499 (insert "\nIn "
500 package
ef92f83b 501 (if version (concat " version " version) "")
0fc37e7d 502 "\n\n")
d5eead69 503 (message
0fc37e7d
ER
504 (substitute-command-keys "Type \\[mail-send] to send bug report."))))
505
506(provide 'lisp-mnt)
507
508;;; lisp-mnt.el ends here
d5eead69 509