;;;;;;;;;;;;;;;;;;;;;;;;;;; -*- Mode: Emacs-Lisp -*- ;;;;;;;;;;;;;;;;;;;;;;;;;;
;; lispdir.el --- Lisp Code Directory formatter and apropos
;; Authors         : Ashwin Ram (Ram-Ashwin@cs.yale.edu)
;;                 ; Dave Sill (de5@ornl.gov)
;;                 ; David Lawrence (tale@pawl.rpi.edu)
;;		   ; Noah Friedman (friedman@ai.mit.edu)
;;		   ; Joe Wells (jbw@cs.bu.edu)
;;                 ; Dave Brennan (brennan@hal.com)
;;		   ; Eric Raymond (eric@snark.thyrsus.com)
;; Created On      : Wed Jan 25, 1989
;; Last Modified By: Dave Brennan
;; Last Modified On: Tue Mar  9 12:47:13 1993
;; Update Count    : 56
;; Status          : No known bugs.
;; Version         : 4.3
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;- Add the following lines to ~/.emacs or an equivalent  (w/o  ";;-"  !):
;;
;;-  (autoload 'format-lisp-code-directory "lispdir" nil t)
;;-  (autoload 'lisp-dir-apropos "lispdir" nil t)
;;-  (autoload 'lisp-dir-retrieve "lispdir" nil t)
;;-  (autoload 'lisp-dir-verify "lispdir" nil t)
;;
;; Other routines of interest to programmers:
;;
;;	insert-lcd-headers
;;	submit-lcd-entry
;;
;;See the doc strings of the individual functions for more documentation.

;; History

;; 09-Mar-1993          Joe Wells <jbw@cs.bu.edu>, Dave Brennan
;;    Added code to break long archive name lines between the hostname and
;;    the filename to avoid exceeding lisp-dir-fill-column characters.

;; 23-Jun-1992		Dave Brennan
;;    Added strategic "(require 'crypt)" statements as suggested by
;;    Martin Boyer.

;; 23-Mar-1992		Dave Brennan
;;    Just use the file name in the variable lisp-code-directory instead
;;    of screwy " *LCD-datafile*" name.  Merged in Eric's changes from
;;    November.  (See next entry.)

;; 26-Nov-1992		Eric Raymond
;;    Added insert-lcd-headers, submit-lcd-entry.  Isolated access-method code.
;;    Introduced lcd-locations variable.  Added progress indication to format
;;    so the hapless luser doesn't just see a frozen screen for 10 minutes.
;;    Added entry point list to header for the enlightenment of newbies.

;; 22-Aug-1991		Dave Brennan
;;    Only load LCD-datafile when it is first referenced to make it faster.
;;    To force a reload kill the buffer " *LCD-datafile*" (note the space).

;; 03-Aug-1991		Dave Brennan
;;    If no matching entry is found print message in minibuffer instead of
;;    displaying an empty apropos buffer.  If the apropos buffer exists
;;    it is not modified.

;; 19-Jul-1991		Dave Sill	
;;    Added lisp-code-retrieve and lisp-code-verify using ange-ftp.

;; 18-Jul-1991		Dave Sill	
;;    Added "Archive" to Ram/Tale header. (Joe Wells <jbw@maverick.uswest.com>)

;; 12-Jul-1991		Noah Friedman (friedman@ai.mit.edu)
;;    Modified format-lisp-code-directory to use find-file-noselect in
;;    another buffer, then insert-buffer.  The reason for this is that
;;    insert-file has no hooks, and so cannot (for example) uncompress
;;    a compressed file.  This loses if I want to grab LCD-datafile.Z
;;    using ange-ftp.

;; 20-Jun-1991		de5
;;    Mostly cosmetic changes in prompts and buffer names.

;; 27-Jun-1989		dsill
;;    Added support for "archive" field containing anonymous FTP location.

;; 28-Feb-1989		dsill	
;;    Changed format-lcd-line-Sill to be smart about GNU-distributed code.
;;    Changed format-lcd-line-Sill to take advantage of 12-char max name.

;; 22-Feb-1989		dsill	
;;    Changed format-lisp-code-directory and lisp-dir-apropos to call the
;;      line formatter indirectly.  The variable 
;;      format-lisp-code-directory-line contains a function to format a single
;;      line, and format-lcd-line-Ram, format-lcd-line-tale, and
;;      format-lcd-line-Sill are the three possibilities at this time.

;; 20-Feb-1989		tale	
;;    changed file's name to lispdir.el
;;    format-lisp-code-directory makes separate buffer
;;    removed lisp-dir-apropos-buffer -- why use more space in memory?
;;    added lisp-dir-[apropos-]hook
;;      (I like (setq lisp-dir-hook 'delete-other-windows))
;;    other aesthetic changes

;; 16-Feb-1989		dsill	
;;    Added lisp-dir-apropos function

(require 'picture)			;provides move-to-column-force

;; *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING ***
;; You'll need ange-ftp and crypt (for uncompressing) to use the name
;; below.  For better performance put the LCD-datafile on a local sytem.
;; Do not use uncompress.el because it (incorrectly) renames a buffer
;; after uncompressing it, which means that each apropos will wind up
;; ftping the LCD-datafile again.  Use Kyle Jones' crypt code which
;; is also available from the archive.
;; *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING ***

;; Some users don't read warnings...
(if (and (featurep 'ange-ftp) (fboundp 'find-compressed-version))
    (progn
      (message
       "Don't use uncompress.el with lispdir.el.  See warning in lispdir.el.")
      (sit-for 5)))

(defvar lisp-code-directory
 "/anonymous@archive.cis.ohio-state.edu:pub/gnu/emacs/elisp-archive/LCD-datafile.Z"
  "*Database of free lisp code.  Entries are in the form:
Name|Author|Contact|Description|Date|Version|Archive")

(defvar format-lisp-code-directory-line 'format-lcd-line-Sill
  "*Function that formats one line of GNU Emacs Lisp Code Directory.\n
Provided as a variable for customizability.  Should not insert
final newline.")

(defvar lisp-code-directory-header 'lcd-header-Sill
  "*Function that inserts header appropriate for 
format-lisp-code-directory-line.")

(defvar elisp-archive-host "archive.cis.ohio-state.edu"
  "*Site with elisp archive available via anonymous ftp.")

(defvar elisp-archive-directory "/pub/gnu/emacs/elisp-archive/"
  "*Root directory of elisp archives on elisp-archive-host.")

(defvar elisp-submission-address "elisp-archive@cis.ohio-state.edu"
  "*Submission mail address for elisp archive.")

(defvar lisp-dir-expand-filename nil
  "*If nil do not expand "~" to the elisp archive.  Non-nil means expand.")

(defvar lisp-dir-fill-column 76
  "*Column beyond which formatted LCD entries are wrapped.  A value of
nil means don't wrap.")

;; Access-method-dependent code begins here
;;
;; Theory: some day soon we'll have a couple of different remote-access
;; methods --- at least, one through FTP and one through an FTP request server
;; accessed by mail (I'd have implemented this already if I could remember
;; the address of the one at Princeton!).  Isolate all that stuff as cases
;; of the implementation of these functions.

(defun remote-exists-p (file)
  ;; Check for the existence of a file at a remote ftp site
  (require 'ange-ftp)
  (require 'crypt)
  (ange-ftp-file-exists-p (concat "/anonymous@" file)))

(defun remote-find-file (file)
  ;; Remote-fetch a file
  (require 'ange-ftp)
  (require 'crypt)
  (find-file-noselect (concat "/anonymous@" file)))

;; Access-method-dependent code ends here

(defun format-lisp-code-directory ()
   "Convert GNU Emacs Lisp Code Directory into something a human could read.
Calls value of lisp-dir-hook with no args if that value is non-nil."
   (interactive)
   (pop-to-buffer "*GNU Emacs Lisp Code Directory*")
   (fundamental-mode)
   (setq buffer-read-only nil)
   (erase-buffer)
   (buffer-flush-undo (current-buffer))
   (lisp-dir-insert-datafile)
   (insert " GNU Emacs Lisp code directory.  " (current-time-string) ".\n\n")
   (message "Formatting %s ..." lisp-code-directory)
   (delete-region (progn (beginning-of-line) (point))
		  (progn (end-of-line) (point)))
   (funcall lisp-code-directory-header)
   (while (re-search-forward
	   "\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)" nil t)
      (let ((name (buffer-substring (match-beginning 1) (match-end 1)))
            (author (buffer-substring (match-beginning 2) (match-end 2)))
            (contact (buffer-substring (match-beginning 3) (match-end 3)))
            (description (buffer-substring (match-beginning 4) (match-end 4)))
            (date (buffer-substring (match-beginning 5) (match-end 5)))
            (version (buffer-substring (match-beginning 6) (match-end 6)))
	    (archive (buffer-substring (match-beginning 7) (match-end 7))))
       (delete-region (progn (beginning-of-line) (point))
	(progn (end-of-line) (point)))
       (funcall format-lisp-code-directory-line
	name author contact description date version archive)))
   (goto-char (point-min))
   (center-line)
   (message "Formatting %s ... done" lisp-code-directory)
   (set-buffer-modified-p nil)
   (run-hooks 'lisp-dir-hook))

(defun lisp-dir-apropos (topic)
  "Display entries in Lisp Code Directory for TOPIC in separate window.
Calls value of lisp-dir-apropos-hook with no args if that value is non-nil."
  (interactive (list
		(read-string
		 (concat "GELCD apropos regexp (" (current-word) "): "))))
  (if (equal "" topic) (setq topic (current-word)))
  (save-excursion
    (let ((lisp-code-directory-tmp-buffer
	   (get-buffer-create "*lcd-working*")))
      (message "Searching for %s ..." topic)
      (set-buffer lisp-code-directory-tmp-buffer)
      (lisp-dir-insert-datafile)
      (delete-non-matching-lines topic)
      (set-buffer-modified-p nil)
      (if (= (point-min) (point-max))
	  (progn
	    (kill-buffer lisp-code-directory-tmp-buffer)
	    (message "No entries matching `%s' were found." topic))
	(set-buffer
	 (get-buffer-create "*GNU Emacs Lisp Code Directory Apropos*"))
	(fundamental-mode)
	(setq buffer-read-only nil)
	(erase-buffer)
	(buffer-flush-undo (current-buffer))
	(insert-buffer lisp-code-directory-tmp-buffer)
	(kill-buffer lisp-code-directory-tmp-buffer)
	(insert "GNU Emacs Lisp Code Directory Apropos -- \"" topic "\"\n")
	(if (not lisp-dir-expand-filename)
	    (insert "\"~/\" refers to "
		    elisp-archive-host ":" elisp-archive-directory))
	(insert "\n\n")
	(backward-char 1)
	(funcall lisp-code-directory-header)
	(while (re-search-forward
	"\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)" nil t)
	  (let ((name (buffer-substring (match-beginning 1) (match-end 1)))
		(author (buffer-substring (match-beginning 2) (match-end 2)))
		(contact (buffer-substring (match-beginning 3) (match-end 3)))
		(description (buffer-substring (match-beginning 4) (match-end 4)))
		(date (buffer-substring (match-beginning 5) (match-end 5)))
		(version (buffer-substring (match-beginning 6) (match-end 6)))
		(archive (buffer-substring (match-beginning 7) (match-end 7))))
	    (delete-region (progn (beginning-of-line) (point))
			   (progn (end-of-line) (point)))
	    (funcall format-lisp-code-directory-line
		     name author contact description date version archive)))
	(goto-char (point-min))
	(center-line)
	(message "Searching for %s ... done" topic)
	(set-buffer-modified-p nil)
	(display-buffer "*GNU Emacs Lisp Code Directory Apropos*")
	(run-hooks 'lisp-dir-apropos-hook)))))

;; Read in lisp code directory file in another buffer (using
;; find-file-noselect, so that usual find-file-hooks will be run,
;; like find-crypt-file-hook). 

(defun lisp-dir-insert-datafile ()
  "Insert the LCD-database in the current buffer.  The datebase is found
in the file named by the lisp-code-directory variable."
  (if (string-match "\\.Z$" lisp-code-directory)
      (require 'crypt))
  (insert-buffer (find-file-noselect lisp-code-directory))
  (setq buffer-read-only nil))

(defun format-lcd-line-Ram
  (name author contact description date version archive)
  "Columnar formatter for Lisp code directory that tries to use as few lines
as possible.  Doesn't fit Contact within first 80 columns."
   (insert-at-column 1  name)
   (insert-at-column 17 description)
   (insert-at-column 49 author)
   (insert-at-column 65 date)
   (insert-at-column 74 "/")
   (insert-at-column 75 version)
   (insert-at-column 84 contact))

(defun format-lcd-line-tale
  (name author contact description date version archive)
  "Multi-line columnar formatter for Lisp Code Directory that tries not
to write anything past column 79."
   (insert-at-column 0  name)
   (insert-at-column 17 description)
   (insert-at-column 56 author)
   (insert-at-column 4  contact)
   (insert-at-column 56 date)
   (insert-at-column 72 version))

(defun format-lcd-line-Sill
  (name author contact description date version archive)
  "Multi-line non-columnar line formatter for Lisp Code Directory."
  (insert-at-column 0 name)
  (if (not (equal version ""))
      (insert " (" version ")"))
  (insert-at-column 22 date)
  (insert "\n")
  (if (and (string-match "[0-9]+\.[0-9]+ dist" contact)
	   (equal author "FSF"))
      (insert-at-column 5 contact)
    (progn
      (insert-at-column 5 author)
      (insert ", <" contact ">\n")
      (if (not (equal archive ""))
	  (progn
	    (if lisp-dir-expand-filename
		(progn
		  (if (and (string-match "~" archive)
			   (= 0 (string-match "~" archive)))
		      (setq archive
			    (concat elisp-archive-host ":"
				    elisp-archive-directory
				    (substring archive 2))))))
	    (insert-at-column 5 archive)
	    ;; jbw: Added code to break long archive name lines between
	    ;; jbw: the hostname and the filename to avoid exceeding 80
	    ;; jbw: characters.
	    (cond ((and lisp-dir-fill-column
			(> (current-column) lisp-dir-fill-column))
		   (move-to-column 5)
		   (cond ((looking-at "[-A-Za-z0-9]+\\(\\.[-A-Za-z0-9]+\\)+:")
			  (goto-char (match-end 0))
			  (insert "\n       ")
			  (end-of-line 1)))))
	    ))))
  (insert-at-column 5 description))

(defun lcd-header-Ram/tale ()
  "Inserts header for column-formatted Lisp Code Directory."
  (funcall format-lisp-code-directory-line
    "Name" "Author" "Contact" "Description" "Date" "Version" "Archive")
  (insert "\n")
  (insert-char ?- 79)
)

(defun lcd-header-Sill ()
  "Inserts empty header for non-columnar Lisp Code Directory"
)

(defun insert-at-column (col string)
   (if (> (current-column) col) (insert "\n"))
   (move-to-column-force col)
   (insert string))

(defun lisp-dir-retrieve (name)
  "Retrieves a copy of the NAMEd package.  The NAME must be an exact match.
Calls value of lisp-dir-retrieve-hook with no args if that value is non-nil."
  (interactive (list
		(read-string
		 (concat "GELCD retrieve (" (current-word) "): "))))
  (if (equal "" name) (setq name (current-word)))
  (save-excursion
    (set-buffer (get-buffer-create (concat "GELCD-" name)))
    (fundamental-mode)
    (setq buffer-read-only nil)
    (erase-buffer)
    (lisp-dir-insert-datafile)
    (message "Searching for %s ..." name)
    (delete-non-matching-lines (concat "^" (regexp-quote name) "|"))
    (let ((matches (count-lines (point-min) (point-max))))
      (cond ((= matches 0)
             (progn
               (message "No match found for %s" name)
               nil))
            ((> matches 1)
             (progn
               (message "Multiple matches found for %s, should be unique" name)
               nil))
            (t
             (re-search-forward
              "\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)" nil t)
             (let ((archive (buffer-substring (match-beginning 7) (match-end 7))))
               (if (not (equal archive ""))
                   (progn
                     (if (and (string-match "~" archive)
                              (= 0 (string-match "~" archive)))
                         (setq archive (concat elisp-archive-host ":"
                                               elisp-archive-directory
                                               (substring archive 2))))))
               (erase-buffer)
               (let ((lisp-code-directory-tmp-buffer
		      (remote-find-file archive)))
                 (insert-buffer lisp-code-directory-tmp-buffer)
                 (kill-buffer lisp-code-directory-tmp-buffer)))
             (goto-char (point-min))
             (display-buffer (concat "GELCD-" name))
             (run-hooks 'lisp-dir-retrieve-hook))))))

(defun lisp-dir-verify (name)
  "Verifies the archive location of the NAMEd package using ange-ftp."
  (interactive (list
		(read-string
		 (concat "GELCD verify (" (current-word) "): "))))
  (if (equal "" name) (setq name (current-word)))
  (save-excursion
    (set-buffer (get-buffer-create "GELCD-verify"))
    (fundamental-mode)
    (setq buffer-read-only nil)
    (erase-buffer)
    (lisp-dir-insert-datafile)
    (message "Searching for %s ..." name)
    (delete-non-matching-lines (concat "^" name "|"))
    (let ((matches (count-lines (point-min) (point-max))))
      (cond ((= matches 0)
             (progn
               (message "No match found for %s" name)
               nil))
            ((> matches 1)
             (progn
               (message "Multiple matches found for %s, should be unique" name)
               nil))
            (t
             (re-search-forward
              "\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)|\\(.*\\)" nil t)
             (let ((archive (buffer-substring (match-beginning 7) (match-end 7))))
               (if (not (equal archive ""))
                   (progn
                     (if (and (string-match "~" archive)
                              (= 0 (string-match "~" archive)))
                         (setq archive (concat elisp-archive-host ":"
                                               elisp-archive-directory
                                               (substring archive 2))))
                     (if (remote-exists-p archive)
                         (message "Package %s is available from: %s" name archive)
                       (message "Package %s is supposed to be available but isn't." name)))
                 (message "Package %s is not archived." name))))))))

;; Snatched from unix-apropos by Henry Kautz
(defun current-word ()
   "Word cursor is over, as a string."
   (save-excursion
      (let (beg end)
	 (re-search-backward "\\w" nil 2)
	 (re-search-backward "\\b" nil 2)
	 (setq beg (point))
	 (re-search-forward "\\w*\\b" nil 2)
	 (setq end (point))
	 (buffer-substring beg end))))
(defun insert-lcd-headers (name version description)
  "Query user for contents of LCD entry.  Insert them in the current buffer"
  (interactive "sPackage name: \nsVersion: \nsDescription: ")
  (beginning-of-line)
  (insert
   ";;\n"
   ";; Purpose of this package:\n;;\n"
   ";; Installation instructions\n;;\n"
   ";; Usage instructions:\n;;\n"
   ";; Known bugs:\n;;\n"
   (format
    ";; LCD Archive Entry:\n;; %s|%s|%s@%s\n;; |%s\n;; %s|%s||\n;;\n"
    name
    (user-full-name)
    (user-login-name) (system-name)
    description
    (current-time-string)
    version)))

(defun submit-lcd-entry (name)
  "Submit code for the LCD archive.  Prompts for package name, version, and  a
one-line description of the package.  Leaves you in a mail buffer."
  (interactive "sPackage name: ")
  (require 'sendmail)
  (mail nil elisp-submission-address name)
  (message (substitute-command-keys "Type \\[mail-send] to send bug report.")))

(provide 'lispdir)
