[gnu.emacs] generic help functions/ `database' mode

davis@scr.slb.com (Paul Davis) (04/26/89)

Next batch: 

Code to do provide `popup help' from a file, and a simple, multi-key
database command.

Enclosed are: generic-help.el (elisp code)
	      db-mode.texinfo (texinfo `manual' for db)
	      some of ~/db (a sample `database' file)

You may notice an overlap between the idea of this and the rolo-mode I
recently posted. Its not an accident - I prefer rolo for simple reference
card stuff, but the generic help function provides a way into some
very useful facilites (RMS: this is the basis of the help-on-thing
system I mentioned quite a while back). I began developing this for C, 
and LaTeX, but gave up. If anyone wants the bits I have ...

I have NO doubt that someone can clean up my code A LOT ....

enjoy:

Paul
                             Paul Davis at Schlumberger Cambridge Research
                                <davis%scr.slb.com@relay.cs.net>

                              "to shatter tradition makes us feel free ..."

=====> generic-help.el
;; GNU Emacs help/database facility
;; Paul Davis <davis%scr.slb.com@relay.cs.net>, May & July 1988

;; this file is subject to the conditions of the GNU Emacs general
;; public license.

;; Not an interface to dbm, and not much use with BIG databases, but
;; for on-line (non-Emacs) help, and quick database queries, this e-lisp code seems quite
;; adequate.  Again, brought about due to an inferiority complex over
;; Unipress, although this does regexp's, and will handle any number
;; of keys. It does lose a bit in speed however, since Emacs has to
;; read each database visited into a buffer first (!!).

(require 'elib)

;;----------------------------------------------------------
;; `global variables' - use set-default to set your default
;; help path 
;; 
;;   eg; (set-default 'help-default-path "/a /b/c /d/e/f")
;;
;;-----------------------------------------------------------

(defvar help-last-file nil
  "File last visited by \[help]")
(defvar help-last-start 0
  "Start of line where last match occured in this buffer")
(defvar help-last-regexp nil
  "previous regexp passed to help in this buffer")
(defvar help-default-path nil
  "*default file used by help")
(defvar help-regexp-prefix "^\\(\\S +.*"
  "*regexp used by help to filter key lines from a help file. This
should always incude an open alternative specifier \\(")
(defvar help-keyline-identifier-regexp "^\\S "
  "*Regexp used for locating a key line in a help file")
(defvar help-buffer-to-add-to nil
  "Holds name of file from which help-add-item
is called")
(defvar help-case-fold-search nil
  "*If non-nil, help/db commands ignore case when searching")
(defvar help-modeline-prefix-string " GNU-DBASE Database:[%b]%3 current target: "
  "Used for modeline format in help-mode, suffixed by help-last-regexp")

;; --------------------------------------------------------------------
;; local variables - this is so that each buffer (and each mode ?) can
;; have their own search path and their own previous items searched for.
;; We don't really want to have to make require-final-newline global
;; so its recast here as a local variable. We need it on when adding
;; new entries to the database files.
;; --------------------------------------------------------------------

(make-local-variable 'help-last-regexp)
(make-local-variable 'help-last-start)
(make-local-variable 'help-default-path)
(make-local-variable 'require-final-newline)

;; ------------------------------------------------
;; help - the function
;; ------------------------------------------------

(defun help (key path &optional repeat reverse)

  "Display text on KEY from a file in PATH. KEY is a regexp
combined with help-regexp-prefix to filter out key lines from text.

Optional third argument non-nil specifies repeated search for KEY from
current position in PATH.  Optional fourth argument if non-nil
specifies reverse direction for search.

The format of FILE is simple: keys are on a line which begins with a
non-space character, text is on lines beginning with a whitespace
character. An entry runs from the start of its key line to the start
of the next key line. The file *must* have newlines both *before* the
first and *after* the last entry, if these are not to be lost.

This function is really designed for building other, more specific
help and db functions , and not for calling via M-x."
  
  (interactive "sKey (regexp): \nsFile (search path): ")
  (setq path (expand-path-names (resolve-string path)))
  (if repeat
      (setq path (pop-this-file path help-last-file)))           ;; rotate current file to front of path
  (cond ((null path)                                             ;; empty search path
	 (message "no further matching entries for \"%s\"" key)  ;; tell us with words
	 (ding))			 			 ;; tell us with sound
	((setq entry (help-entry key (car path) repeat reverse)) ;; search for it
	 (show-help entry key (car path)))                       ;; found it - now display it
	(t (help key (cadr path)))))                             ;; otherwise, recurse down the search path

;; -------------------------------------------------------
;; second order functions (not to be called interactively)
;; -------------------------------------------------------

(defun show-help (entry key file)
  "Do some formatting of a help file entry."
  (if (string-match "\n" entry)
      (progn
	(setq key-list (substring entry 0 (match-beginning 0)))
	(setq entry (substring entry (match-end 0))))
    (setq key-list nil))
  (if (and key-list (string-match "{" key-list))
      (progn
	(setq primary-key (substring key-list 0 (match-beginning 0)))
	(setq key-list (substring key-list (match-end 0) (1- (length
							      key-list)))))
    (setq primary-key (eval key-list))
    (setq key-list nil))
  (with-output-to-temp-buffer "*DBASE*"
    (pop-to-buffer "*DBASE*")
    (help-mode)
    (insert "  " primary-key "\n\n")
    (insert entry)
    (if key-list
	(insert "\n\n Cross references: " key-list))
    (setq mode-line-format
	(list (concat "Emacs-DBASE database:[" (file-name-nondirectory
						file) "]   current target: " key)))))
  
(defun help-entry (help-regexp file repeat reverse)
  "Return a strng containing an entry with a key matching
HELP-REGEXP found in FILE. Third and fourth arguments are as for \[help].
Returns NIL if no match is found."
  (save-window-excursion
    (pop-to-buffer (find-file-noselect file))
    (help-locate-search-start repeat reverse)
    (if help-case-fold-search
	(setq case-fold-search t))
    (if (help-search)
	(progn
	  (beginning-of-line)
	  (setq help-last-file (buffer-file-name))
	  (setq help-last-regexp help-regexp)
	  (setq help-last-start (point))
	  (buffer-to-next-key))
      nil)))

(defun help-search ()
  "Encapsulation of search commands for help-mode"
  (if reverse
      (re-search-backward (concat help-regexp-prefix help-regexp
				  "\\|" help-regexp "\\)") (point-min) t)
    (re-search-forward (concat help-regexp-prefix help-regexp
			       "\\|" help-regexp "\\)") (point-max) t)))

(defun help-locate-search-start (repeat reverse)
  "Set up position of point for search"
  (widen)
  (if  repeat
      (progn
	(goto-char help-last-start)
	(if reverse
	    (beginning-of-line 0)
	  (beginning-of-line 2)))
    (if reverse
	(goto-char (point-max))
      (goto-char (point-min)))))
  
(defun buffer-to-next-key ()
  "Return current buffer from point to the start of the next key line or
EOB if there are no futher key lines in the buffer"
  (setq start (save-excursion (beginning-of-line) (point)))
  (save-excursion
    (beginning-of-line 2)
    (if (null
	 (re-search-forward help-keyline-identifier-regexp (point-max) t))
	(goto-char (point-max)))
    (beginning-of-line 1)
    (buffer-substring start (point))))

;; ---------------------------
;; interactive "key" commands
;; ---------------------------

(defun db (key)
  "Master help function"
  (interactive "sKey (regexp): ")
  (help key help-default-path))

(defun help-next-match ()
  "Display the next entry in the current help file having a key
matching the current target"
  (interactive)
  (help help-last-regexp help-default-path 1 nil))

(defun help-previous-match ()
  "Display the previous entry in the current help file having a key
matching the current target"
  (interactive)
  (help help-last-regexp help-default-path 1 t))

(defun help-this-file (item)
  (interactive "sItem (regexp): ")
  (help item (buffer-file-name)))

;; -------------------------------------
;; adding new entries to a database file
;; -------------------------------------

(defun help-edit-this-item ()
  "Edit the current help/db item being viewed."
  (interactive)
  (pop-to-buffer (file-name-nondirectory help-last-file))
  (beginning-of-line 1)
  (narrow-to-region (point)
		    (save-excursion 
		      (forward-char 1)
		      (if (null (re-search-forward
				 help-keyline-identifier-regexp (point-max) t))
			  (goto-char (point-max))
			(beginning-of-line))
			(point))))

(defun help-add-item (key xref)
  "Add a new entry to the file from which the current entry came.
This function simply sets up a buffer for the user to add the entry -
the actual save is done by \[help-save-item]."
  (interactive "sStore under keys: \nsCross-reference by: ")
  (setq help-buffer-to-add-to (file-name-nondirectory help-last-file))
  (with-output-to-temp-buffer "help-item"
    (set-buffer "help-item")
    (indented-text-mode)
    (local-set-key "\C-x\C-s" 'help-save-item)
    (local-set-key "\C-x\C-q" 'help-quit-add)
    (message "C-x C-s to add the entry, C-x C-q to quit")
    (insert (concat key " {" xref "}\n")))
  (pop-to-buffer "help-item")
  (goto-char (point-min))
  (forward-line 2)
  (narrow-to-region (point) (point-max)))

(defun help-save-item ()
  "append the current buffer (assumed to have been created using
\[help-add-item]) to that of the help file from which this was
created, and save the buffer, ensuring the prescence of a final
newline."
  (interactive)
  (indent-region (point-min) (point-max) 4)
  (widen)
  (save-window-excursion
    (pop-to-buffer help-buffer-to-add-to)
    (widen)
    (goto-char (point-max)))
  (append-to-buffer help-buffer-to-add-to (point-min) (point-max))
  (save-window-excursion
    (pop-to-buffer help-buffer-to-add-to)
    (setq require-final-newline t)
    (save-buffer 1)
    (bury-buffer))
  (kill-buffer "help-item"))

(defun help-quit-add ()
  "Exit from an item creating session by killing the temporary
buffer in which its going on."
  (interactive)
  (kill-buffer "help-item"))

(defun help-add-key (key)
  (interactive "sKey to add: ")
  (save-window-excursion
    (pop-to-buffer (file-name-nondirectory help-last-file))
    (beginning-of-line)
    (cond ((re-search-forward "}" (save-excursion (end-of-line) (point)) t)
	   (backward-char 1)
	   (insert " " key))
	  (t
	   (insert "{" key "}")))))

(defun help-set-default-path (path)
  "Set help-default-path to FILE so that future
calls to help use FILE instead of current default."
  (interactive (list (read-string "Help search path: " help-default-path)))
  (setq help-default-path path))

(defun help-mode ()
  "Mode for Emacs help/database mode

\\{help-mode-map}."
  (interactive)
  (kill-all-local-variables)
  (use-local-map help-mode-map)
  (define-abbrev-table 'text-mode-abbrev-table ())
  (set-syntax-table text-mode-syntax-table)
  (setq major-mode 'help-mode)
  (setq mode-name "HELP")
  (run-hooks 'help-mode-hook))
  
(defvar help-mode-map (make-sparse-keymap)
  "*Keymap for help mode")

(define-key help-mode-map "\e[B" 'help-next-match)       ;; ANSI arrow key DOWN
(define-key help-mode-map "\e[A" 'help-previous-match)   ;; ANSI arrow key UP
(define-key help-mode-map "f" 'db)
(define-key help-mode-map "n" 'help-next-match)
(define-key help-mode-map "p" 'help-previous-match)
(define-key help-mode-map "c" 'help-this-file)
(define-key help-mode-map "a" 'help-add-item)
(define-key help-mode-map "s" 'help-set-default-path)
(define-key help-mode-map "k" 'help-add-key)
(define-key help-mode-map "e" 'help-edit-this-item)

;; ----------------------------------------------------------
;; this one's for doing Norton Guide type pop-up info
;; on something in a buffer. Needs a good hyperkey to be
;; bound to and mode-specific setting of the help search path
;; ----------------------------------------------------------

(defun help-on-thing ()
  "Provide help on thing before point, using current buffer's
help-default-path"
  (interactive)
  (if (eobp) (open-line 1))
  (let* ((char (char-after (point)))
         (syntax (char-syntax char)))
    (cond
     ((eq syntax ?\ )
      (backward-sexp 1)
      (set-mark (point))
      (forward-sexp 1))
     ((eq syntax ?w)			; word.
      (forward-word 1)
      (set-mark (point))
      (forward-word -1))
     ((eq syntax ?\( )			; open paren.
      (mark-sexp 1))
     ((eq syntax ?\) )			; close paren.
      (forward-char 1)
      (mark-sexp -1)
      (exchange-point-and-mark))
     ((eolp)				; mark line if at end.
      (set-mark (1+ (point)))
      (beginning-of-line 1))
     (t					; mark character
      (set-mark (1+ (point)))))
    (help (buffer-substring (region-beginning) 
			    (region-end)) help-default-path)))

;; BibTeX/rolodex database


(defun bibtex-db (bibtex-database key)
  (interactive "Fbibtex database: \nskey: ")
  (help key bibtex-database)
  (pop-to-buffer "*DBASE*")
  (goto-char (point-min))
  (replace-regexp "^\\|}$" "")
  (setq old-regexp help-regexp-prefix
	 help-xref-string "Cite code: "
	 help-default-path bibtex-database
	 show-help-hook '(lambda ()
			   (goto-char (point-min))
			   (replace-regexp "^\\|}$" ""))))

(provide 'generic-help)


===> db-mode.texinfo
\input texinfo
@tex
\global\chapno=29
@end tex
@chapter Databases in GNU Emacs

The command @code{M-x db} is the entry to the world of simple databases
within Emacs. The function @code{db} searches each file in
@code{help-default-path} for lines containing matches for a regular
expression given as input. If it finds a match, it will display the entry
in a special buffer called "*DBASE*". This buffer is in @code{dbase-mode},
and has a special modeline to tell you what's going on: its looks like this:

@example
Emacs-DBASE database:[@var{name}]  current target: @var{target}
@end example

where @var{name} tells you the name of the file in which the current match
has been found, and @var{target} describes the regular expression that
@code{db} is currently using (ie; the one used to find this match).

The *DBASE* buffer also has a number of useful commands for finding
subsequent and previous matches of the same target, and for altering the
information held in your databases.

Note that this database system is a multi-key one, unlike @code{dbm}-based
systems. This feature can be used to create a very flexible means of
searching through a database of, for example, names and addresses. Each
entry can have additional keys besides that person's name describing things
with which they are involved or interested: then, to find someone who might
be able help us on a particular topic, just search using that topic, and
then step through each match in turn. Alternatively, you can just find
entries based on name alone.

[Technical aside: you may notice that many of the functions described below
begin with the word ``help''. This is because Emacs' database commands are
based on a more wide-ranging facility for providing rapid ``pop-up'' help
whilst editing.]

@section Keys and Entries

Your database entries are defined by a series of one or more @var{keys}.
These are simply words you wish to be able to use in order to retrieve the
entry. If the database was just a list of names and addresses, then
typically, the keys would consist of the name of each person, and the entry
would be the address.

However, you can also include cross-references on the key line by
surrounding them with braces. These are not actually functional, but will
provide hints on what other keys someone might wish to use to find other,
related entries.

@section What a Database file looks like

Each file in the search path defined by the variable
@code{help-search-path} should have the following format:

Keys should be listed on a line starting with a non-white space character.
Cross-reference hints are surrounded by braces ({ and }), although they
still count as keys for the search function. Each key line should have at
least one `primary' key not enclosed by braces.

The entry for a given set of keys should consist of a number of lines beginning
with at least one  white space character (a space or a tab).

Here a sample item from a database file:

@smallexample
Richard Stallman @{fsf gnu emacs MIT@}
   c/o Free Software Foundation
   675 Mass Ave
   Cambridge, MA 02139
   USA
@end smallexample

This entry could be retrieved using any of the following topics, amongst others:

@smallexample
Stallman, gnu, MIT, Rich, stall\|emacs
@end smallexample

@section variables affecting database operations

@code{help-default-search-path} is a string containing a list of
space-separated filenames to be used by @code{db} in its hunt for matching
entries. It is local to each dbase-mode buffer, so that if you wish you can
have different *DBASE* buffers accessing different databases. You should
set this in your @var{.emacs} file using @code{set-default}. Here is an
example:

@smallexample
(set-default 'help-default-path "/usr/davis/db /usr/mygroup/db")
@end smallexample

@code{help-case-fold-search} determines whether or not case should be
ignored when looking for database entries. Its default value, @code{nil},
means that database searches are case-insensitive; you can reset to
@code{t} in this in your @var{.emacs} file if you want them to be
case-sensitive.

@section Commands to find database entries

@table @kbd
@item M-x db @var{key}
find an entry filed under @var{key} and display it in a buffer called "*DBASE*".
@end table

The following commands are avaiable only within the ``*DBASE*'' buffer:

@table @kbd
@item c
find another item within current database file only (@code{help-this-file})
@item f
find an entry filed under a different key (@code{db})
@item n
find the next match for the current target key(@code{help-next-match})
@item p
find the previous match for the current target key(@code{help-previous-match})
@end table

On keyboards whose arrow (cursor) keys generate ANSI-standard sequences,
the UP arrow is equivalent to "p" and the DOWN arrow to "n".

@section Commands to alter or add to a database

@table @kbd
@item a
add a new entry to the current database file (@code{help-add-item})
@item e
edit the entry currently being viewed (@code{help-edit-this-item})
@item k
add a key to the current database entry (@code{help-add-key})
@end table

@code{help-add-item} prompts for a primary key and cross-references, then
creates a temporary buffer for you to create a new entry in the current
database file. It looks after all formatting for you, so you can put
whatever you want in this buffer without worrying about white-spaces or key
entry formats. When you're finished creating the entry, just save as normal
using @code{C-x C-s} and the item will be formatted, added to the database
and written to a file.

@code{help-edit-this-item} switches to a buffer containing the current
database file. Point will be on the key line of the entry, and the buffer
will be narrowed to the text of this entry. You can carry out all
normal editing operations on this buffer -- its a good idea to save it
after you're finished though.

@code{help-add-key} allows you to add an extra key to the current entry.

@bye


=====> ~/db (sample)

Wolfram Research {mathematica Steven Wolfram}
    PO Box 6059
    Champaign
    IL 61821
 
    tel: 217 398 0700

Charles Lachlan {dentist}
    4 Cross Street
    Cambridge
    CB1 2HW
 
    tel: 315680

Cooperative Bank {}
    134-138 The Grove
    Stratford E15 1NX
 
    01-555-7241

Travel Store {usa flights barry}
    Suite 6
    Strode House
    46-48 Osnaburgh St.
    London Nw1 3ND

Maria Tuck {Independent sun operations}
    work: 40 City Road
    London EC1
 
    tel:01 956 1677

Harlow Ski School {}
    tel: 0279 21792

Travel Corona {Mark travel flight}
    81 George St
    London W1H 5PL

    tel: 01-935-7433/7355

Chas Roberts {mountain bikes}
    tel: 01-684-3370

Bromwich Cycles {mountain bikes}
    tel: 0203 664459

Egg {chainrings bicycles}
    tel: 0272 770626

Ben Hayward {bicycles}
    69 Trumpington Street
    Cambridge CB2 1RJ
 
    Tel: 0223 352294
    Contact: robin

Spanish Tourist Board {spain}
    tel: 01 499 0901

Karrimor International {backpacks panniers}
	tel: 0254 385911

Citadel {Bike locks}
	tel: 01-208-2106

STA Travel {travel flights}
	tel: 01-581-1022 (intercontinental)

London Student Travel {travel flights}
	tel: 01-730-3402