view lisp/=word-help.el @ 16811:df5765f5f6fd

Initial revision
author Richard M. Stallman <rms@gnu.org>
date Thu, 02 Jan 1997 07:19:58 +0000
parents
children 56bf42e4fb29
line wrap: on
line source

;;; word-help.el --- keyword help for any language doc'd in TeXinfo.

;; Copyright (c) 1996 Free Software Foundation, Inc.

;; Maintainer: Jens T. Berger Thielemann, <jensthi@ifi.uio.no>
;; Keywords: help, keyword, languages

;; This file is part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;; This package provides a rather general interface for doing keyword
;; help in most languages.  In short, it'll determine which Texinfo
;; file which is relevant for the current mode; cache the index and
;; use regexps to give you help on the keyword you're looking at.

;; Installation
;; ************

;; For the default setup to work for all supported modes, make sure
;; the Texinfo files from the following packages are installed:

;; Texinfo file   | Available in archive or URL | Notes
;; autoconf.info  | autoconf-2.10.tar.gz        | -
;; bison.info     | bison-1.25.tar.gz           | -
;; libc.info      | glibc-1.09.1.tar.gz         | -
;; elisp.info     | elisp-manual-19-2.4.tar.gz  | -
;; latex.info     | ftp://ftp.dante.de/pub/tex/info/latex2e-help-texinfo/latex2e.texi
;; groff.info     | groff-1.10.tar.gz           | -
;; m4.info        | m4-1.4.tar.gz               | -
;; make.info      | make-3.75.tar.gz            | -
;; perl.info      | http://www.perl.com/CPAN/doc/manual/info/
;; simula.info    | Mail bjort@ifi.uio.no       | Written in Norwegian
;; texinfo.info   | texinfo-3.9.tar.gz          | -

;; BTW: We refer to Texinfo files by just their last component, not
;; with an absolute file name.  You must thus set up
;; `Info-directory-list' and `Info-default-directory-list' so that
;; these can automatically be located.

;; Usage
;; *****
;;
;; Place the cursor over the function/variable/type/whatever you want
;; help on.  Type "C-h C-i".  `word-help' will then make a suggestion
;; to an index topic; press return to accept this.  If not, you may use
;; tab-completion to find the topic you're interested in.

;; Usually, `word-help' is able to determine the relevant Texinfo
;; file from looking at the buffer's `mode-name'; if not, you can use
;; the interactive function `set-help-file' to set this.

;; Customizing
;; ***********
;;
;; User interface
;; --------------
;;
;; Two variables control the behaviour of the user-interface of
;; `word-help': `word-help-split-window' and
;; `word-help-magic-index'.  Do C-h v to get more information on
;; these.

;; Adding more Texinfo files
;; -------------------------
;;
;; Associations between mode-names and Texinfo files can be done
;; through the `word-help-mode-alist' variable, which defines an
;; `alist' making `set-help-file' able to initialize the necessary
;; variable.

;; Contacting the author
;; *********************
;;
;; If you wish to contact me for any reason, please feel free to write
;; to:

;; Jens Berger
;; Spektrumveien 4
;; N-0666 Oslo
;; Norway
;;
;; E-mail: <jensthi@ifi.uio.no>

;; Have fun.

;;
;;; Code:
;;

(require 'info)

;;;--------------------
;;;    USER OPTIONS
;;;--------------------

(defvar word-help-split-window t
  "*Non-nil means that the info buffer will pop up in a separate window.
If nil, we will just switch to it.")

(defvar word-help-magic-index t
"*Non-nil means that the keyword will be searched for in the requested node.
This is done by determining whether the line the point is positioned
on after using `Info-goto-node', actually contains the keyword.  If
not, we will search for the first occurence of the keyword.  This may
help when the info file isn't correctly indexed.")

;;; ---- end of user configurable variables

;;;-------------------------
;;;   ADVANCED USER OPTIONS
;;;-------------------------

(defvar word-help-mode-alist
  '(("autoconf"
     (("autoconf" "Macro Index") ("m4" "Macro index"))
     (("AC_\\([A-Za-z0-9_]+\\)" 1)
      ("[a-z]+")))

    ("Bison"
     (("bison" "Index")
      ("libc" "Type Index" "Function Index" "Variable Index"))
     (("%[A-Za-z]+")
      ("[A-Za-z]+")
      ("[A-Za-z_][A-Za-z0-9_]+")))
    ("YACC" . "Bison")

    ("C" (("libc" "Type Index" "Function Index" "Variable Index")))
    ("C++" . "C")

    ("Emacs-Lisp"
     (("elisp" "Index"))
     (("[^][ ()\n\t.\"'#]+")))

    ("LaTeX"
     (("latex" "Command Index"))
     (("\\\\\\(begin\\|end\\){\\([^}\n]+\\)}" 2 0)
      ("\\\\[A-Za-z]+")
      ("\\\\[^A-Za-z]")
      ("[A-Za-z]+")))

    ("Nroff"
     (("groff" "Macro Index" "Register Index" "Request Index"))
     ((".[^A-Za-z]")
      (".[A-Za-z]+")
      (".\\([A-Za-z]+\\)" 1)))
    ("Groff" . "Nroff")

    ("m4" (("m4" "Macro index")))

    ("Makefile"
     (("make" "Name Index" "Concept Index"))
     (("\\.[A-Za-z]+")
      ("\\$[^()]")
      ("\\$([^()= \t]+)")
      ("[A-Za-z]+")))

    ("Perl"
     (("perl" "Variable Index" "Function Index"))
     (("\\$[^A-Za-z^]")
      ("\\$\\^[A-Za-z]?")
      ("\\$[A-Za-z][A-Za-z_0-9]+")
      ("[A-Za-z_][A-Za-z_0-9]+"))
     nil
     (("^\\([^ \t\n]+\\)" 1)))

    ("Simula" (("simula" "Index")) nil t)
    ("Ifi Simula" . "Simula")
    ("SIMULA" . "Simula")

    ("Texinfo"
     (("texinfo" "Command and Variable Index"))
     (("@\\([A-Za-z]+\\)" 1)))

    )
  "Assoc list between `mode-name' and Texinfo files.
The variable should be initialized with a list of elements with the
following form:

\(mode-name (word-help-info-files) (word-help-keyword-regexps)
	   word-help-ignore-case word-help-index-mapper)

where `word-help-info-files', `word-help-keyword-regexps' and so
forth of course are the values which should be put in these variables
for this mode.  Note that `mode-name' doesn't have to be a legal
mode-name; the user may use the call `set-help-file', where
`mode-name' will be used in the `completing-read'.

Example entry (for C):

\(\"C\" ((\"libc\" \"Type Index\" \"Function Index\" \"Variable Index\"))
       ((\"[A-Za-z_][A-Za-z0-9]+\")))

The two first variables must be initialized; the two remaining will
get default values if you omit them or set them to nil.  The default
values are:

word-help-keyword-regexps: (\"[A-Za-z_][A-Za-z0-9]+\")
word-help-ignore-case:     nil

More settings may be defined in the future.

You may also define aliases, if there are several relevant mode-names
to a single entry.  These should be of the form:

\(MODE-NAME-ALIAS . MODE-NAME-REAL)

For C++, you would use the alias

\(\"C++\" . \"C\")

to make C++ mode use the same help files as C files do.  Please note
that you can shoot yourself in the foot with this possibility, by
defining recursive aliases.")

;;; --- end of advanced user options

(defvar word-help-ignore-case nil
  "Non-nil means that case is ignored when doing lookup.")
(make-variable-buffer-local 'word-help-ignore-case)

(defvar word-help-info-files nil
"List of infofiles with respective nodes for the current mode.

This should be a list of the following form:

\((INFO-FILE-1 NODE-NAME-1 NODE-NAME-2 ...)
 (INFO-FILE-1 NODE-NAME-1 NODE-NAME-2 ...)
      :           :           :
 (INFO-FILE-1 NODE-NAME-1 NODE-NAME-2 ...))

An example entry for e.g. C would be:

\((\"/local/share/gnu/info/libc\" \"Function Index\" \"Type Index\"
  \"Variable Index\"))

The files and nodes will be searched/cached in the order specified.
This variable is usually set by the `word-help-switch-help-file'
function, which utilizes  the `word-help-mode-alist'.")
(make-variable-buffer-local 'word-help-info-files)

(defvar word-help-keyword-regexps nil
  "Regexps for finding keywords in the current mode.

This is constructed as a list of the following form:

\((REGEXP SUBMATCH-LOOKUP SUBMATCH-CURSOR)
 (REGEXP SUBMATCH-LOOKUP SUBMATCH-CURSOR)
       :          :          :
 (REGEXP SUBMATCH-LOOKUP SUBMATCH-CURSOR))

The regexps will be searched in order for a match which the cursor is
within.

submatch-lookup is the submatch number which will be looked for in the
index.  May be omitted; defaults to 0 (e.g. the entire pattern).  This is
useful in for instance configure lookup; each command is there prefixed
with 'AC_', which must be ignored when doing a lookup.  Example regexp
entry for this:

\(\"AC_\\\\([A-Za-z0-9]+\\\\)\" 1)

submatch-cursor is the part of the match which the cursor must be within.
May be omitted; defaults to 0 (e.g. the entire pattern).")
(make-variable-buffer-local 'word-help-keyword-regexps)
(set-default 'word-help-keyword-regexps '(("[A-Za-z_][A-Za-z_0-9]*")))

(defvar word-help-index-mapper nil
  "Regexps to use for massaging index-entries into keywords.
This variable should contain a list of regexps with sub-expressions,
where we will only look for the sub-expression in the user text.

The regexp list should be formatted as:

  ((REGEXP SUBEXP) (REGEXP SUBEXP) ... )

If the index entry does not match any of the regexps, it will be ignored.

Example:

Perl has index entries of the following form:

* abs VALUE:                    perlfunc.
* accept NEWSOCKET,GENERICSOCKET: perlfunc.
* alarm SECONDS:                perlfunc.
* atan2 Y,X:                    perlfunc.
* bind SOCKET,NAME:             perlfunc.
         :             :           :

We will thus try to extract the first word in the index entry -
\"abs\" from \"abs VALUE\", etc.  This is done by the following entry:

\((\"^\\\\([^ \\t\\n]+\\\\)\" 1))")
(make-variable-buffer-local 'word-help-index-mapper)

(defvar word-help-main-index nil
"List of all index entries.

See `word-help-process-indexes' for structure formatting.

Minor note: This variable is a list if it is initialized, t if
initializing failed and nil if uninitialized.")
(make-variable-buffer-local 'word-help-main-index)

(defvar word-help-main-obarray nil
"Global work variable for `word-help' system.
Do Not mess with this!")

(defvar word-help-history nil
  "History for `word-help' minibuffer queries.")
(make-local-variable 'word-help-history)

(defvar word-help-current-help-file nil
  "Current help file active for this mode.")

(defvar word-help-index-alist nil
  "An assoc list mapping help files to info indexes.
This means that `word-help-mode-index' can be init'ed faster.")

(defvar word-help-help-mode nil
  "Which mode the help system is bound to for the current mode.")
(make-variable-buffer-local 'word-help-help-mode)


;;; Debugging

;;;###autoload
(defun reset-word-help ()
  "Clear all cached indexes in the `word-help' system.
You should only need this when installing new info files, and/or
adding more Texinfo files to the `word-help' system."
  (interactive)
  (setq word-help-index-alist nil
	word-help-main-index nil))


;;; Changing help file

;;;###autoload
(defun set-help-file ()
  "Change which set of Texinfo files used for word help. 

`word-help' maintains a list over which Texinfo files which are
relevant for each programming language (`word-help-mode-alist').  It
usually selects the correct one, based upon the value of `mode-name'.
If this guess is incorrect, you may also use this function manually to
instruct future `word-help' calls which Texinfo files to use."
  (interactive)
  (let (helpfile helpguess (case-comp completion-ignore-case))
    (setq helpguess (cond
		     (word-help-current-help-file)
		     ((word-help-guess-help-file))))

    (setq completion-ignore-case t
          helpfile (completing-read
		    (if helpguess
			(format "Select help mode (default %s): " helpguess)
		      "Select help mode: ")
		    word-help-mode-alist
		    nil t nil nil))
    (setq completion-ignore-case case-comp)
    (if (equal "" helpfile)
	(setq helpfile helpguess))
    (if helpfile
	(word-help-switch-help-file helpfile))
    )
  )

;;; Main user interface

;;;###autoload
(defun word-help ()
  "Find documentation on the keyword under the cursor.
The determination of which language the keyword belongs to, is based upon
The relevant info file is selected by matching `mode-name' (the major
mode) against the assoc list `word-help-mode-alist'.

If this is not possible, `set-help-file' will be invoked for selecting
the relevant info file.  `set-help-file' may also be invoked
interactively by the user.

If the keyword you are looking at is not available in any index, no
default suggestion will be presented. "
  (interactive)
  (let (helpguess myguess guess index-info case-store)
;; Set necessary variables for later lookup
    (if (not word-help-info-files)
	(if (setq helpguess (word-help-guess-help-file))
	    (word-help-switch-help-file helpguess)
	  (set-help-file)))
;; Have we previously cached datas?
    (word-help-process-indexes)
    (if
	(atom word-help-main-index)
	(message "No help file available for this mode.")
;; First make a guess at what the user is looking for
        (setq myguess (word-help-guess
		       (point)
		       (cond
			((not (atom word-help-main-index))
			 (car word-help-main-index)))
		       word-help-keyword-regexps))
;; Ask the user himself
	(setq case-store completion-ignore-case
	      completion-ignore-case word-help-ignore-case)
	(setq guess (completing-read
					; Format string
		     (if myguess
			 (format "Look up keyword (default %s): " myguess)
  		       "Look up keyword: ")
					; Collection
		     (car word-help-main-index)
		     nil t nil 'word-help-history))
	(setq completion-ignore-case case-store)
	(if (equal guess "")
	    (setq guess myguess))
;; If we've got anything meaningful to lookup, do so
	(if (not guess)
	    (message "Help aborted.")
	    (setq index-info (word-help-find-index-node
			      guess
			      word-help-main-index))
	    (if (not index-info)
		(message "Oops, I could not find \"%s\" anyway! Bug?" guess)
	      (word-help-goto-index-node index-info)
	      )
	    )
	)
    )
  )

;;; Index mappers

(defun word-help-map-index-entries (str re-list)
  (let ((regexp (car (car re-list)))
	(subexp (car (cdr (car re-list))))
	(next (cdr re-list)))
    (cond
     ((string-match regexp str)
      (substring str (match-beginning subexp) (match-end subexp)))
     (next
      (word-help-map-index-entries str next)))))


;;; Mode lookup

(defun word-help-guess-help-file ()
  "Guesses a relevant help file based on mode name.
Returns nil if no guess could be made.  Uses `word-help-mode-alist'."
  (let (guess)
    (cond
     ((setq guess (assoc mode-name word-help-mode-alist))
      (car guess)))))


(defun word-help-switch-help-file (helpfile)
  "Changes the help-file to the mode name given.
Uses `word-help-mode-alist'."
  (if helpfile
      (let (helpdesc)
	(if (not (setq helpdesc (assoc helpfile word-help-mode-alist)))
	    (message "No help defined for \"%s\"." helpfile)
	    (if (stringp (cdr helpdesc))
		(word-help-switch-help-file (cdr helpdesc))
  	        (word-help-make-default-map
		 helpdesc
		 (list 'word-help-help-mode
		       'word-help-info-files
		       'word-help-keyword-regexps
		       'word-help-ignore-case
		       'word-help-index-mapper))))
	    (setq word-help-main-index nil))))

;;; Default mapping

(defun word-help-make-default-map (list vars)
  "Makes a default mapping for `vars', which must be listed in order.
vars is a list of quoted symbols.  If the nth entry in the list is
non-nil, the nth variable will be given this value.  If nil, the var
will be given the global default value."
  (set (car vars) (cond ((car list)) ((default-value (car vars)))))
  (if (cdr vars)
      (word-help-make-default-map (cdr list) (cdr vars))))


;;; Index collection

(defun word-help-extract-index (file-name index-list index-map ignore-case)
  "Extract index from filename and the first node name in index list.
`file-name' is the name of the info file, while `index-list' is a list
of node-names to search."
  (let (cmd1 cmdlow nodename ob-array next)
    (setq nodename (car index-list))
    (setq ob-array (make-vector 211 0))
    (message "Processing \"%s\" in %s..." nodename file-name)
    (save-window-excursion
      (Info-goto-node (concat "(" file-name ")" nodename))
      (end-of-buffer)
      (while (re-search-backward "\\* \\([^\n:]+\\):" nil t)
	(setq cmd1 (buffer-substring (match-beginning 1) (match-end 1)))
	(setq cmdlow (if ignore-case (downcase cmd1) cmd1))
	(if index-map
	    (setq cmdlow (word-help-map-index-entries cmdlow
			      index-map)))
;; We have to do this workaround to support case-insensitive matching
	(cond
	 (cmdlow
	  (put (intern cmdlow ob-array) 'word-help-real-name cmd1)
	  (intern cmdlow word-help-main-obarray)))))
    (setq next (cond
		((cdr index-list)
		 (word-help-extract-index file-name (cdr index-list)
					     index-map ignore-case))))
    (nconc (list (list nodename ob-array)) next)))


(defun word-help-collect-indexes (info-file)
  "Process all the indexes in an info file.

Uses `word-help-extract-index' on each node, and returns an entry
suitable for merging into `word-help-process-indexes'.  `info-file'
is an entry of the form

\(FILE-NAME INDEX-NAME-1 INDEX-NAME-2 ...)"
  (let ((file  (car info-file))
	(nodes (cdr info-file)))
    (nconc (list file) (word-help-extract-index file nodes
						   word-help-index-mapper
                                                   word-help-ignore-case))
    )
  )

(defun word-help-process-indexes ()
  "Process all the entries in the global variable `word-help-info-files'.
Returns a list formatted as follows:

\(all-entries-ob
 (file-name-1 (node-name-1 this-node-entries-ob)
	      (node-name-2 this-node-entries-ob)
	           :          :         :
	      (node-name-n this-node-entries-ob))
 (file-name-2 (node-name-1 this-node-entries-ob)
	      (node-name-2 this-node-entries-ob)
	           :          :         :
	      (node-name-n this-node-entries-ob))
    :    :    :    :    :    :    :    :    :
 (file-name-n (node-name-1 this-node-entries-ob)
	      (node-name-2 this-node-entries-ob)
	           :          :         :
	      (node-name-n this-node-entries-ob)))

The symbols in the obarrays may contain the additional property
`word-help-real-name', which tells the *real* node to go to.

Note that we use `word-help-index-alist' to speed up the process.  Note
that `word-help-switch-help-file' must have been called before this function.

This structure is then later searched by `word-help-find-index-node'."
  (let (index-words old-index)
    (if (not word-help-main-index)
	 (cond
	  ((setq old-index
		 (assoc word-help-help-mode word-help-index-alist))
	   (setq word-help-main-index (nth 1 old-index)))
	  (word-help-info-files
	   (setq word-help-main-obarray (make-vector 307 0)
		 index-words (mapcar 'word-help-collect-indexes
				     word-help-info-files)
		 word-help-main-index
		 (append (list word-help-main-obarray) index-words))
	   (setq word-help-index-alist (cons (list word-help-help-mode
						       word-help-main-index)
						 word-help-index-alist)))
	  (t (setq word-help-main-index t))))))


;;; Keyword lookup

(defun word-help-guess (cur-point cmd-array re-list)
  "Guesses what keyword the user is looking at, and returns that.
CUR-POINT should be the current value of `point', CMD-ARRAY an obarray
of all the keywords which are defined for the current mode, and
RE-LIST a list of regexps use for the hunt.  See also
`word-help-keyword-regexps'."
  (let (guess pre-guess regexp submatch cursmatch end-point)
    (setq regexp (car (car re-list))
	  submatch  (cond ((nth 1 (car re-list))) (0))
	  cursmatch (cond ((nth 2 (car re-list))) (0)))
;; Store where the old point was
    (save-excursion
      (end-of-line)
;; We won't accept matches after the line
      (setq end-point (point))
;; Start at the beginning
      (beginning-of-line)
      (setq guess nil)
      (while (and (not guess) (re-search-forward regexp end-point t))
	(setq pre-guess (buffer-substring (match-beginning submatch)
					  (match-end submatch)))
;; Look whether the cursor is within the match
	(if (and (<= (match-beginning cursmatch) cur-point)
		 (>= (match-end cursmatch) cur-point)
		 (cond
		  (cmd-array (intern-soft (if word-help-ignore-case
					      (downcase pre-guess)
					       pre-guess) cmd-array))
		  (t)))
	    (setq guess pre-guess))
	)
;; If we found anything, return it, else call ourselves again
      (cond
       (guess)
       ((cdr re-list) (word-help-guess cur-point cmd-array (cdr re-list)))
       )
      )
    )
  )

;;; Find an index entry

(defun word-help-find-index-node (node index-reg)
  "Finds the node named `node' in the index-register `index-reg'.
`index-reg' has the format as returned (and documented) by the
`word-help-process-indexes' call.  In most cases, this will be equal to
`word-help-main-index'.

Returns a list with format
  (file-name index-node-name index-entry)
which contains the file and index where the entry can be found.
Returns nil if the entry can't be found."
  (let (file-info node-name)
    (setq node-name (cond (word-help-ignore-case (downcase node)) (node)))
    (if (intern-soft node-name (car index-reg))
      (setq file-info (word-help-index-search-file node-name
						      (cdr index-reg))))
    file-info
    )
  )

(defun word-help-index-search-file (entry file-data)
  "Searches a cached file for the index-entry `entry'."
  (let (this-file next-files file-name node node-infos)
    (setq this-file (car file-data)
	  next-files (cdr file-data)
	  file-name (car this-file)
	  node-infos (cdr this-file)
	  node (word-help-index-search-nodes entry node-infos))
    (cond
     (node
      (cons file-name node))
     (next-files (word-help-index-search-file entry next-files))
     )
    )
  )

(defun word-help-index-search-nodes (entry node-info)
  "Searches a cached list of nodes for the entry `entry'."
  (let (this-node next-nodes node-name node-ob node-sym)
    (setq this-node (car node-info)
	  next-nodes (cdr node-info)
	  node-name (car this-node)
	  node-ob (car (cdr this-node))
	  node-sym (intern-soft entry node-ob))
    (cond
     (node-sym
      (list node-name (get node-sym 'word-help-real-name)))
     (next-nodes (word-help-index-search-nodes entry next-nodes)))))

;;; Switch to a node in an index

(defun word-help-goto-index-node (index-info)
  "Jumps to an index node.
`index-info' should be a list with the following format:

\(FILE-NAME INDEX-NODE-NAME INDEX-ENTRY)"

  (let* ((file-name (car index-info))
	 (node-name (nth 1 index-info))
	 (entry-name (nth 2 index-info))
	 (entry-regexp (concat "\\<" (regexp-quote (nth 2 index-info)) "\\>"))
	 (buffer (current-buffer))
	 end-point)
    (if word-help-split-window
	(pop-to-buffer nil))
    (Info-goto-node (concat "(" file-name ")" node-name))
    (Info-menu entry-name)
;; Do magic keyword search
    (cond
     (word-help-magic-index
      (end-of-line)
      (setq end-point (point))
      (beginning-of-line)
      (cond
       ((not (re-search-forward entry-regexp end-point t))
	(re-search-forward entry-regexp)
	(recenter 0)))))
    (if word-help-split-window
	(pop-to-buffer buffer))))

(provide 'word-help)

;;; word-help.el ends here