home *** CD-ROM | disk | FTP | other *** search
- ;;;; info.jl -- Info browser
- ;;; Copyright (C) 1994 John Harper <jsh@ukc.ac.uk>
-
- ;;; This file is part of Jade.
-
- ;;; Jade 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.
-
- ;;; Jade 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 Jade; see the file COPYING. If not, write to
- ;;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
-
- (provide 'info)
-
- ;;; Limitations:
- ;;; - Depends wholly on tag tables --- does no searching for nodes just looks
- ;;; up their position (except in the dir file).
- ;;; - No support for `*' node name.
- ;;; - Doesn't work 100% with info files formatted by emacs. For best results
- ;;; makeinfo has to be used.
- ;;; - No editing of nodes.
-
- (defvar info-directory-list
- (if (amiga-p) '("INFO:") '("/usr/info" "/usr/local/info/" "~/info"))
- "List of directories to search for info files if they can't be found as-is.")
-
- (defvar info-keymap (make-keytab)
- "Keymap for Info.")
-
- (defvar info-buffer (make-buffer "*Info*")
- "Buffer in which Info nodes are displayed.")
- (set-buffer-special info-buffer t)
-
- (defvar info-tags-buffer (make-buffer "*Info tags*")
- "Buffer for storing the current Info file's tag table.")
- (set-buffer-special info-tags-buffer t)
-
- (defvar info-history '()
- "List of `(FILE NODE POS)' showing how we got to the current node.")
-
- (defvar info-file-name nil
- "The true name (in the filesystem) of the current Info file.")
-
- (defvar info-node-name nil
- "The name of the current Info node.")
-
- (defvar info-file-modtime nil
- "The modtime of file `info-file-name' last time we read something from it.")
-
- (defvar info-indirect-list nil
- "List of `(START-OFFSET . FILE-NAME)' saying where the current Info file
- is split.")
-
- (defvar info-has-tags-p nil
- "t when we were able to load a tag table for this Info file.")
-
- (defvar info-initialised nil
- "Protection against being loaded multiple times.")
-
- (unless info-initialised
- (setq info-initialised t)
- (put 'info-error 'error-message "Info")
- (bind-keys info-keymap
- "SPC" 'next-screen
- "BS" 'prev-screen
- "1" 'info-menu-nth
- "2" 'info-menu-nth
- "3" 'info-menu-nth
- "4" 'info-menu-nth
- "5" 'info-menu-nth
- "6" 'info-menu-nth
- "7" 'info-menu-nth
- "8" 'info-menu-nth
- "9" 'info-menu-nth
- "b" 'goto-buffer-start
- "d" '(info "(dir)Top")
- "f" 'info-follow-ref
- "h" '(info "(info)Help")
- "g" 'info-goto-node
- "l" 'info-last
- "m" 'info-menu
- "n" 'info-next
- "p" 'info-prev
- "q" 'bury-buffer
- "u" 'info-up
- "?" 'describe-mode
- "HELP" 'describe-mode
- "RET" 'info-goto-link
- "LMB-CLICK2" 'info-goto-link
- "TAB" 'info-next-link
- "Meta-TAB" 'info-prev-link
- "Shift-TAB" 'info-prev-link)
- (with-buffer info-buffer
- (setq keymap-path (cons 'info-keymap keymap-path)
- major-mode 'info-mode
- buffer-record-undo nil)
- (set-buffer-read-only info-buffer t))
- (with-buffer info-tags-buffer
- (setq buffer-record-undo nil)))
-
- ;; Read the indirect list (if it exists) and tag table from the file FILENAME.
- ;; Indirect list ends up in `info-indirect-list', tag table is read into the
- ;; `info-tags-buffer' buffer. `info-has-tags-p' is set to t if a tags table
- ;; was loaded.
- (defun info-read-tags (filename)
- (let
- ((file (open filename "r"))
- (dir (file-name-directory filename))
- str)
- (unless file
- (signal 'info-error (list "Can't open info file" filename)))
- (unwind-protect
- (with-buffer info-tags-buffer
- (clear-buffer)
- (setq info-indirect-list nil
- info-file-name nil
- info-has-tags-p nil)
- ;; Read until we find the tag table or the indirect list.
- (setq str (read-file-until file "^(Tag Table:|Indirect:) *\n$" t))
- (when (and str (regexp-match "Indirect" str t))
- ;; Parse the indirect list
- (while (and (setq str (read-line file))
- (/= (aref str 0) ?\^_))
- (setq info-indirect-list
- (cons
- (cons
- (read-from-string (regexp-expand "^.*: ([0-9]+)\n$" str "\\1"))
- (concat dir (regexp-expand "^(.*): [0-9]+\n$" str "\\1")))
- info-indirect-list)))
- (setq info-indirect-list (nreverse info-indirect-list))
- ;; Now look for the tag table
- (setq str (read-file-until file "^Tag Table: *\n$" t)))
- (when (and str (regexp-match "Tag Table" str t))
- (read-buffer file)
- (setq info-has-tags-p t))
- (setq info-file-name filename
- info-file-modtime (file-modtime filename))
- t)
- (close file))))
-
- ;; Read the `dir' file, if multiple `dir' files exist concatenate them
- (defun info-read-dir ()
- (let
- ((read-dir nil)
- (path info-directory-list))
- (clear-buffer)
- (while path
- (let
- ((name (file-name-concat (expand-file-name (car path)) "dir")))
- (when (file-exists-p name)
- (if read-dir
- (let
- ((spos (cursor-pos)))
- (insert (read-file name))
- ;; lose all text from the beginning of the file to the
- ;; first menu item
- (when (find-next-regexp "^\\* Menu:" spos nil t)
- (delete-area spos (next-line 1 (match-start)))))
- (read-buffer name)
- ;; try to delete the file's preamble
- (when (find-next-regexp "^File:" (buffer-start) nil t)
- (delete-area (buffer-start) (match-start)))
- (goto-buffer-end)
- (setq read-dir t))
- (unless (equal (cursor-pos) (line-start))
- (split-line))))
- (setq path (cdr path)))
- (unless read-dir
- (signal 'info-error '("Can't find `dir' file")))
- (setq info-file-name "dir"
- info-file-modtime 0
- info-node-name "Top"
- mode-name "(dir)")
- (goto-buffer-start)
- t))
-
- ;; Record the file, node and cursor-position in the `info-history' list
- ;; for the `info-last' command.
- (defun info-remember ()
- (when (and info-file-name info-node-name)
- (setq info-history (cons (list info-file-name
- info-node-name
- (cursor-pos))
- info-history))))
-
- ;; Find the actual file for the info-file FILENAME
- (defun info-locate-file (filename)
- (if (and info-file-name (or (not filename) (equal filename "")))
- info-file-name
- (let*
- ((filename-and-info (concat filename ".info"))
- (lcase-name (translate-string (copy-sequence filename)
- downcase-table))
- (lcase-and-info (concat lcase-name ".info")))
- (cond
- ((file-exists-p filename)
- filename)
- ((file-exists-p filename-and-info)
- filename-and-info)
- ((file-exists-p lcase-name)
- lcase-name)
- ((file-exists-p lcase-and-info)
- lcase-and-info)
- (t
- (catch 'foo
- (let
- ((dir info-directory-list)
- real)
- (while dir
- (setq real (expand-file-name (car dir)))
- (cond
- ((file-exists-p (file-name-concat real filename))
- (throw 'foo (file-name-concat real filename)))
- ((file-exists-p (file-name-concat real filename-and-info))
- (throw 'foo (file-name-concat real filename-and-info)))
- ((file-exists-p (file-name-concat real lcase-name))
- (throw 'foo (file-name-concat real lcase-name)))
- ((file-exists-p (file-name-concat real lcase-and-info))
- (throw 'foo (file-name-concat real lcase-and-info))))
- (setq dir (cdr dir)))
- (signal 'info-error (list "Can't find file" filename)))))))))
-
- ;; Display the node NODENAME. NODENAME can contain a file name. If no node
- ;; is specified go to `Top' node.
- ;; This depends on some magic for locating the node text. It only works 100%
- ;; with `makeinfo' generated files.
- (defun info-find-node (nodename)
- (let
- ((filename (regexp-expand "^\\((.*)\\).*$" nodename "\\1"))
- (inhibit-read-only t)
- offset)
- (when filename
- (unless (setq nodename (regexp-expand "^\\(.*\\)(.+)$" nodename "\\1"))
- (setq nodename "Top")))
- (if (member filename '("dir" "DIR" "Dir"))
- (info-read-dir)
- (setq filename (info-locate-file filename))
- (when (or (not (equal info-file-name filename))
- (> (file-modtime filename) info-file-modtime))
- (info-read-tags filename))
- (if (not info-has-tags-p)
- (progn
- (clear-buffer)
- (read-buffer info-file-name info-buffer)
- (goto-buffer-start)
- (setq info-node-name ""
- mode-name (concat ?( (file-name-nondirectory info-file-name) ?))))
- (let
- ((regexp (concat "^Node: " (regexp-quote nodename) ?\^?))
- subfile text)
- (if (find-next-regexp regexp (buffer-start) info-tags-buffer t)
- (progn
- (setq offset (read (cons info-tags-buffer (match-end))))
- (if (null info-indirect-list)
- (setq offset (+ offset 2)
- subfile info-file-name)
- (catch 'info
- (let
- ((list info-indirect-list))
- (while (cdr list)
- (when (< offset (car (car (cdr list))))
- (setq subfile (car list))
- (throw 'info))
- (setq list (cdr list)))
- (setq subfile (car list))))
- ;; Use some magic to calculate the physical position of the
- ;; node. This seems to work?
- (if (eq subfile (car info-indirect-list))
- (setq offset (+ offset 2))
- (setq offset (+ (- offset (car subfile))
- (car (car info-indirect-list)) 2)))
- (setq subfile (cdr subfile)))
- (if (setq text (read-file-from-to subfile offset ?\^_))
- (progn
- (clear-buffer)
- (insert text)
- (goto-buffer-start)
- (setq info-node-name nodename
- mode-name (concat ?( (file-name-nondirectory info-file-name)
- ?) info-node-name)))
- (signal 'info-error (list "Can't read from file" filename))))
- (signal 'info-error (list "Can't find node" nodename))))))))
-
- ;; Return a list of all node names matching START in the current tag table
- (defun info-list-nodes (start)
- (let
- ((regexp (concat "^Node: (" (regexp-quote start) ".*)\^?"))
- (list ()))
- (with-buffer info-tags-buffer
- (goto-buffer-start)
- (while (find-next-regexp regexp nil nil t)
- (goto-char (match-end))
- (setq list (cons (regexp-expand-line regexp "\\1" nil nil t) list))))
- list))
-
- ;; `prompt2' variant. LIST-FUN is a function to call the first time a list
- ;; of possible completions is required.
- (defun info-prompt (list-fun &optional title default start)
- (unless title
- (setq title "Select node"))
- (when default
- (setq title (concat title " (default: " default ")")))
- (unless start
- (setq start ""))
- (let*
- ((prompt-completion-function #'(lambda (w)
- (unless prompt-list
- (with-buffer info-buffer
- (setq prompt-list (funcall list-fun))))
- (prompt-complete-from-list w)))
- (prompt-validate-function 'prompt-validate-from-list)
- (prompt-word-regexps prompt-def-regexps)
- (prompt-list '())
- (res (prompt2 title start)))
- (if (equal res "")
- default
- res)))
-
- ;;;###autoload
- (defun info (&optional start-node)
- "Start the Info viewer. If START-NODE is given it specifies the node to
- show, otherwise the current node is used (or `(dir)' if this is the first
- time that `info' has been called)."
- (interactive)
- (goto-buffer info-buffer)
- (cond
- (start-node
- (info-remember)
- (info-find-node start-node))
- ((and info-file-name info-node-name)
- (when (> (file-modtime info-file-name) info-file-modtime)
- (info-find-node info-node-name)))
- (t
- (info-find-node "(dir)"))))
-
- ;; The *Info* buffer has this function as its major-mode so that `Ctrl-h m'
- ;; displays some meaningful text
- (defun info-mode ()
- "Info mode:\n
- This mode is used to browse through the Info tree of documentation, special
- commands are,\n
- `SPC' Next screen of text
- `BS' Previous screen
- `b' Move to the start of this node
- `1' to `9' Go to the Nth menu item in this node
- `d' Find the `(dir)' node -- the root of Info
- `f' Find the node of the next cross-reference in this node
- `g NODE RET' Go to the node called NODE
- `h' Display the Info tutorial, the node `(info)Help'
- `l' Backtrack one node
- `m' Choose a menu item from this node
- `n' Find the `next' node
- `p' Go to the `previous' node
- `u' Display the parent node of this one
- `q' Quit Info
- `?', `HELP' Display this command summary
- `RET',
- `LMB-CLICK2' Go to the link (menu item or xref) on this line
- `TAB' Put the cursor on the next link in this node
- `Meta-TAB' Move to the previous link in this node")
-
- ;; Prompt for the name of a node and find it.
- (defun info-goto-node (node)
- (interactive "sGoto node: ")
- (when node
- (info-remember)
- (info-find-node node)))
-
- ;; Returns the node name of the menu item on the current line
- (defun info-parse-menu-line ()
- (or (regexp-expand-line "^\\* (.+)::" "\\1")
- (regexp-expand-line "^\\* .+:[\t ]*((\\([^ ]+\\)|)([^,.]+|))\\." "\\1")))
-
- ;; Return a list of the names of all menu items. Starts searching from
- ;; the cursor position.
- (defun info-list-menu-items ()
- (let
- ((list ())
- (opos (cursor-pos)))
- (while (find-next-regexp "^\\* [a-zA-Z0-9]+.*:")
- (goto-char (match-end))
- (setq list (cons (regexp-expand-line "^\\* ([^:.]+)" "\\1") list)))
- list))
-
- ;; Position the cursor at the start of the menu.
- (defun info-goto-menu-start ()
- (when (or (find-prev-regexp "^\\* Menu:" nil nil t)
- (find-next-regexp "^\\* Menu:" nil nil t))
- (goto-char (next-line 1 (match-start)))))
-
- ;; Goto the ITEM-INDEX'th menu item.
- (defun info-menu-nth (item-index)
- (interactive (list (- (strtoc (current-event-string)) ?0)))
- (unless (info-goto-menu-start)
- (signal 'info-error (list "Can't find menu")))
- (while (and (> item-index 0) (find-next-regexp "^\\* .*:"))
- (goto-char (match-end))
- (setq item-index (1- item-index)))
- (when (/= item-index 0)
- (signal 'info-error (list "Can't find menu node")))
- (goto-line-start)
- (let
- ((nodename (info-parse-menu-line)))
- (if nodename
- (progn
- (info-remember)
- (info-find-node nodename))
- (signal 'info-error (list "Menu line malformed")))))
-
- ;; Prompt for the name of a menu item (with a default) and find it's node.
- (defun info-menu ()
- (interactive)
- (let
- ((menu-name (regexp-expand-line "^\\* ([^:.]+)" "\\1")))
- (when (info-goto-menu-start)
- (let
- ((opos (cursor-pos)))
- (setq menu-name (info-prompt 'info-list-menu-items
- "Menu item:" menu-name))
- (goto-char opos)))
- (when menu-name
- (if (find-next-regexp (concat "^\\* " (regexp-quote menu-name) ?:))
- (progn
- (goto-char (match-start))
- (let
- ((node-name (info-parse-menu-line)))
- (if node-name
- (progn
- (info-remember)
- (info-find-node node-name))
- (signal 'info-error (list "Menu line malformed")))))
- (signal 'info-error (list "Can't find menu" menu-name))))))
-
- ;; Retrace our steps one node.
- (defun info-last ()
- (interactive)
- (if info-history
- (progn
- (let
- ((hist (car info-history)))
- (setq info-history (cdr info-history))
- (when (info-find-node (concat ?( (car hist) ?) (nth 1 hist)))
- (goto-char (nth 2 hist))
- t)))
- (message "No more history")
- (beep)))
-
- (defun info-next ()
- (interactive)
- (info-find-link "Next"))
-
- (defun info-prev ()
- (interactive)
- (info-find-link "Prev"))
-
- (defun info-up ()
- (interactive)
- (info-find-link "Up"))
-
- (defun info-find-link (link-type)
- (let*
- ((regexp (concat link-type ": ([^,]*)(,|[\t ]*$)"))
- (new-node (regexp-expand-line regexp "\\1" (buffer-start) nil t)))
- (if new-node
- (progn
- (info-remember)
- (info-find-node new-node))
- (message (concat "No " link-type " node"))
- (beep))))
-
- ;; Check this line for a menuitem of an xref, if one exists find its node
- (defun info-goto-link ()
- (interactive)
- (let
- (node)
- (unless (setq node (cdr (info-parse-ref)))
- (goto-line-start)
- (unless (setq node (info-parse-menu-line))
- (signal 'info-error '("Nothing on this line to go to"))))
- (info-remember)
- (info-find-node node)))
-
- ;; Move the cursor to the next menuitem or xref
- (defun info-next-link ()
- (interactive)
- (let
- ((pos (find-next-regexp "(^\\* |\\*Note)" (next-char) nil t)))
- (while (and pos (looking-at "\\* Menu:" pos nil t))
- (setq pos (find-next-regexp "(^\\* |\\*Note)" (next-char 1 pos) nil t)))
- (goto-char pos)))
-
- ;; Move the cursor to the previous menuitem or xref
- (defun info-prev-link ()
- (interactive)
- (let
- ((pos (find-prev-regexp "(^\\* |\\*Note)" (prev-char) nil t)))
- (while (and pos (looking-at "\\* Menu:" pos nil t))
- (setq pos (find-prev-regexp "(^\\* |\\*Note)" (prev-char 1 pos) nil t)))
- (goto-char pos)))
-
- ;; Parse the cross-reference under the cursor into a cons-cell containing
- ;; its title and node. This is fairly hairy since it has to cope with refs
- ;; crossing line boundarys.
- (defun info-parse-ref ()
- (when (looking-at "\\*Note *" nil nil t)
- (let
- ((pos (match-end))
- end ref-title ref-node)
- (if (setq end (find-next-regexp "[\t ]*:"))
- (progn
- (while (> (pos-line end) (pos-line pos))
- (let
- ((bit (copy-area pos (find-next-regexp "[\t ]*$" pos))))
- (unless (equal bit "")
- (setq ref-title (cons ?\ (cons bit ref-title)))))
- (setq pos (find-next-regexp "[^\t ]" (match-end)))
- (unless pos
- (signal 'info-error '("Malformed reference"))))
- (setq ref-title (apply 'concat (nreverse (cons (copy-area pos end)
- ref-title)))
- pos (next-char 1 end))
- (if (= (get-char pos) ?:)
- (setq ref-node ref-title)
- (when (looking-at " +" pos)
- (setq pos (match-end)))
- (if (setq end (find-next-regexp "[\t ]*[:,.]" pos))
- (progn
- (while (> (pos-line end) (pos-line pos))
- (let
- ((bit (copy-area pos (find-next-regexp "[\t ]*$"
- pos))))
- (unless (equal bit "")
- (setq ref-node (cons ?\ (cons bit ref-node))))
- (setq pos (find-next-regexp "[^\t ]" (match-end))))
- (unless pos
- (signal 'info-error '("Malformed reference"))))
- (setq ref-node (apply 'concat (nreverse (cons (copy-area
- pos end)
- ref-node)))))
- (signal 'info-error '("Malformed reference")))))
- (signal 'info-error '("Malformed reference")))
- (when (and ref-title ref-node)
- (cons ref-title ref-node)))))
-
- ;; This should give you a prompt with all xrefs in the node to complete from,
- ;; currently it just finds the node of the next xref
- (defun info-follow-ref ()
- (interactive)
- (unless (looking-at "\\*Note" nil nil t)
- (goto-char (find-next-regexp "\\*Note" nil nil t)))
- (let
- ((ref (info-parse-ref)))
- (when ref
- (info-remember)
- (info-find-node (cdr ref)))))
-