1
0
mirror of https://git.savannah.gnu.org/git/emacs.git synced 2024-12-11 09:20:51 +00:00
emacs/lisp/finder.el
Glenn Morris b66d4616f0 Merge from origin/emacs-27
0aa0410372 (origin/emacs-27, emacs-27) Fix "Finder" description for m...
2021-09-03 07:50:25 -07:00

476 lines
17 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; finder.el --- topic & keyword-based code finder -*- lexical-binding: t -*-
;; Copyright (C) 1992, 1997-1999, 2001-2021 Free Software Foundation,
;; Inc.
;; Author: Eric S. Raymond <esr@snark.thyrsus.com>
;; Created: 16 Jun 1992
;; Keywords: help
;; This file is part of GNU Emacs.
;; GNU Emacs 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 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs 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. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This mode uses the Keywords library header to provide code-finding
;; services by keyword.
;;; Code:
(require 'package)
(require 'lisp-mnt)
(require 'find-func) ;for find-library(-suffixes)
(require 'finder-inf nil t)
;; These are supposed to correspond to top-level customization groups,
;; says rms.
(defvar finder-known-keywords
'((abbrev . "abbreviation handling, typing shortcuts, and macros")
(bib . "bibliography processors")
(c . "C and related programming languages")
(calendar . "calendar and time management tools")
(comm . "communications, networking, and remote file access")
(convenience . "convenience features for faster editing")
(data . "editing data (non-text) files")
(docs . "Emacs documentation facilities")
(emulations . "emulations of other editors")
(extensions . "Emacs Lisp language extensions")
(faces . "fonts and colors for text")
(files . "file editing and manipulation")
(frames . "Emacs frames and window systems")
(games . "games, jokes and amusements")
(hardware . "interfacing with system hardware")
(help . "Emacs help systems")
(hypermedia . "links between text or other media types")
(i18n . "internationalization and character-set support")
(internal . "code for Emacs internals, build process, defaults")
(languages . "specialized modes for editing programming languages")
(lisp . "Lisp support, including Emacs Lisp")
(local . "code local to your site")
(maint . "Emacs development tools and aids")
(mail . "email reading and posting")
(matching . "searching, matching, and sorting")
(mouse . "mouse support")
(multimedia . "images and sound")
(news . "USENET news reading and posting")
(outlines . "hierarchical outlining and note taking")
(processes . "processes, subshells, and compilation")
(terminals . "text terminals (ttys)")
(tex . "the TeX document formatter")
(tools . "programming tools")
(unix . "UNIX feature interfaces and emulators")
(vc . "version control")
(wp . "word processing"))
"Association list of the standard \"Keywords:\" headers.
Each element has the form (KEYWORD . DESCRIPTION).")
(defvar finder-mode-map
(let ((map (make-sparse-keymap)))
(define-key map " " 'finder-select)
(define-key map "f" 'finder-select)
(define-key map [follow-link] 'mouse-face)
(define-key map [mouse-2] 'finder-mouse-select)
(define-key map "\C-m" 'finder-select)
(define-key map "?" 'finder-summary)
(define-key map "n" 'next-line)
(define-key map "p" 'previous-line)
(define-key map "q" 'finder-exit)
(define-key map "d" 'finder-list-keywords)
map)
"Keymap used in `finder-mode'.")
(easy-menu-define finder-mode-menu finder-mode-map
"Menu for `finder-mode'."
'("Finder"
["Select" finder-select
:help "Select item on current line in a finder buffer"]
["List keywords" finder-list-keywords
:help "Display descriptions of the keywords in the Finder buffer"]
["Summary" finder-summary
:help "Summary item on current line in a finder buffer"]
["Quit" finder-exit
:help "Exit Finder mode"]))
(defvar finder-mode-syntax-table
(let ((st (make-syntax-table emacs-lisp-mode-syntax-table)))
(modify-syntax-entry ?\; ". " st)
st)
"Syntax table used while in `finder-mode'.")
(defvar finder-headmark nil
"Internal Finder mode variable, local in Finder buffer.")
;;; Code for regenerating the keyword list.
(defvar finder-keywords-hash nil
"Hash table mapping keywords to lists of package names.
Keywords and package names both should be symbols.")
(defvar generated-finder-keywords-file "finder-inf.el"
"The function `finder-compile-keywords' writes keywords into this file.")
;; Skip autogenerated files, because they will never contain anything
;; useful, and because in parallel builds of Emacs they may get
;; modified while we are trying to read them.
;; https://lists.gnu.org/r/emacs-pretest-bug/2007-01/msg00469.html
;; ldefs-boot is not auto-generated, but has nothing useful.
(defvar finder-no-scan-regexp "\\(^\\.#\\|\\(loaddefs\\|ldefs-boot\\|\
cus-load\\|finder-inf\\|esh-groups\\|subdirs\\|leim-list\\)\\.el$\\)"
"Regexp matching file names not to scan for keywords.")
(autoload 'autoload-rubric "autoload")
(defconst finder--builtins-descriptions
;; I have no idea whether these are supposed to be capitalized
;; and/or end in a full-stop. Existing file headers are inconsistent,
;; but mainly seem to not do so.
'((emacs . "the extensible text editor")
(nxml . "a new XML mode"))
"Alist of built-in package descriptions.
Entries have the form (PACKAGE-SYMBOL . DESCRIPTION).
When generating `package--builtins', this overrides what the description
would otherwise be.")
(defvar finder--builtins-alist
'(("calc" . calc)
("ede" . ede)
("erc" . erc)
("eshell" . eshell)
("gnus" . gnus)
("international" . emacs)
("language" . emacs)
("mh-e" . mh-e)
("semantic" . semantic)
("analyze" . semantic)
("bovine" . semantic)
("decorate" . semantic)
("symref" . semantic)
("wisent" . semantic)
;; This should really be ("nxml" . nxml-mode), because nxml-mode.el
;; is the main file for the package. Then we would not need an
;; entry in finder--builtins-descriptions. But I do not know if
;; it is safe to change this, in case it is already in use.
("nxml" . nxml)
("org" . org)
("srecode" . srecode)
("term" . emacs)
("url" . url))
"Alist of built-in package directories.
Each element should have the form (DIR . PACKAGE), where DIR is a
directory name and PACKAGE is the name of a package (a symbol).
When generating `package--builtins', Emacs assumes any file in
DIR is part of the package PACKAGE.")
(defconst finder-buffer "*Finder*"
"Name of the Finder buffer.")
(defun finder-compile-keywords (&rest dirs)
"Regenerate list of built-in Emacs packages.
This recomputes `package--builtins' and `finder-keywords-hash',
and prints them into the file `generated-finder-keywords-file'.
Optional DIRS is a list of Emacs Lisp directories to compile
from; the default is `load-path'."
;; Allow compressed files also.
(setq package--builtins nil)
(setq finder-keywords-hash (make-hash-table :test 'eq))
(let* ((el-file-regexp "\\`\\([^=].*\\)\\.el\\(\\.\\(gz\\|Z\\)\\)?\\'")
(file-count 0)
(files (cl-loop for d in (or dirs load-path)
when (file-exists-p (directory-file-name d))
append (mapcar
(lambda (f)
(cons d f))
(directory-files d nil el-file-regexp))))
(progress (make-progress-reporter
(byte-compile-info "Scanning files for finder")
0 (length files)))
base-name summary keywords package version entry desc)
(dolist (elem files)
(let* ((d (car elem))
(f (cdr elem))
(package-override
(intern-soft
(cdr-safe
(assoc (file-name-nondirectory
(directory-file-name d))
finder--builtins-alist)))))
(progress-reporter-update progress (setq file-count (1+ file-count)))
(unless (or (string-match finder-no-scan-regexp f)
(null (setq base-name
(and (string-match el-file-regexp f)
(intern (match-string 1 f))))))
;; (memq base-name processed))
;; There are multiple files in the tree with the same
;; basename. So skipping files based on basename means you
;; randomly (depending on which order the files are
;; traversed in) miss some packages.
;; https://debbugs.gnu.org/14010
;; You might think this could lead to two files providing
;; the same package, but it does not, because the duplicates
;; are (at time of writing) all due to files in cedet, which
;; end up with package-override set. FIXME this is
;; obviously fragile. Make the (eq base-name package) case
;; below issue a warning if package-override is nil?
;; (push base-name processed)
(with-temp-buffer
(insert-file-contents (expand-file-name f d))
(setq keywords (mapcar #'intern (lm-keywords-list))
package (or package-override
(let ((str (lm-header "package")))
(if str (intern str)))
base-name)
summary (or (cdr
(assq package finder--builtins-descriptions))
(lm-synopsis))
version (lm-header "version")))
(when summary
(setq version (or (ignore-errors (version-to-list version))
(alist-get package package--builtin-versions)))
(setq entry (assq package package--builtins))
(cond ((null entry)
(push (cons package
(package-make-builtin version summary))
package--builtins))
;; The idea here is that eg calc.el gets to define
;; the description of the calc package.
;; This does not work for eg nxml-mode.el.
((eq base-name package)
(setq desc (cdr entry))
(aset desc 0 version)
(aset desc 2 summary)))
(dolist (kw keywords)
(puthash kw
(cons package
(delq package
(gethash kw finder-keywords-hash)))
finder-keywords-hash))))))
(progress-reporter-done progress))
(setq package--builtins
(sort package--builtins
(lambda (a b) (string< (symbol-name (car a))
(symbol-name (car b))))))
(with-current-buffer
(find-file-noselect generated-finder-keywords-file)
(setq buffer-undo-list t)
(erase-buffer)
(insert (autoload-rubric generated-finder-keywords-file
"keyword-to-package mapping" t))
(search-backward " ")
;; FIXME: Now that we have package--builtin-versions, package--builtins is
;; only needed to get the list of unversioned packages and to get the
;; summary description of each package.
(insert "(setq package--builtins '(\n")
(dolist (package package--builtins)
(insert " ")
(prin1 package (current-buffer))
(insert "\n"))
(insert "))\n\n")
;; Insert hash table.
(insert "(setq finder-keywords-hash\n ")
(prin1 finder-keywords-hash (current-buffer))
(insert ")\n")
(basic-save-buffer)))
(defun finder-compile-keywords-make-dist ()
"Regenerate `finder-inf.el' for the Emacs distribution."
(apply #'finder-compile-keywords command-line-args-left)
(kill-emacs))
;;; Now the retrieval code
(defun finder-insert-at-column (column &rest strings)
"Insert, at column COLUMN, other args STRINGS."
(if (>= (current-column) column) (insert "\n"))
(move-to-column column t)
(apply #'insert strings))
(defvar finder-help-echo nil)
(defun finder-mouse-face-on-line ()
"Put `mouse-face' and `help-echo' properties on the previous line."
(save-excursion
(forward-line -1)
;; If finder-insert-at-column moved us to a new line, go back one more.
(if (looking-at "[ \t]") (forward-line -1))
(unless finder-help-echo
(setq finder-help-echo
(let* ((keys1 (where-is-internal 'finder-select
finder-mode-map))
(keys (nconc (where-is-internal
'finder-mouse-select finder-mode-map)
keys1)))
(concat (mapconcat #'key-description keys ", ")
": select item"))))
(add-text-properties
(line-beginning-position) (line-end-position)
'(mouse-face highlight
help-echo finder-help-echo))))
(defun finder-unknown-keywords ()
"Return an alist of unknown keywords and number of their occurrences.
Unknown keywords are those present in `finder-keywords-hash' but
not `finder-known-keywords'."
(let (alist)
(maphash (lambda (kw packages)
(unless (assq kw finder-known-keywords)
(push (cons kw (length packages)) alist)))
finder-keywords-hash)
(sort alist (lambda (a b) (string< (car a) (car b))))))
;;;###autoload
(defun finder-list-keywords ()
"Display descriptions of the keywords in the Finder buffer."
(interactive)
(if (get-buffer finder-buffer)
(pop-to-buffer finder-buffer)
(pop-to-buffer (get-buffer-create finder-buffer))
(finder-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(dolist (assoc finder-known-keywords)
(let ((keyword (car assoc)))
(insert (propertize (symbol-name keyword)
'font-lock-face 'font-lock-constant-face))
(finder-insert-at-column 14 (concat (cdr assoc) "\n"))
(finder-mouse-face-on-line)))
(goto-char (point-min))
(setq finder-headmark (point)
buffer-read-only t)
(set-buffer-modified-p nil)
(balance-windows)
(finder-summary))))
(defun finder-list-matches (key)
(let* ((id (intern key))
(packages (gethash id finder-keywords-hash)))
(unless packages
(error "No packages matching key `%s'" key))
(let ((package-list-unversioned t))
(package-show-package-list packages))))
(define-button-type 'finder-xref 'action #'finder-goto-xref)
(defun finder-goto-xref (button)
"Jump to a Lisp file for the BUTTON at point."
(let* ((file (button-get button 'xref))
(lib (locate-library file)))
(if lib (finder-commentary lib)
(message "Unable to locate `%s'" file))))
;;;###autoload
(defun finder-commentary (file)
"Display FILE's commentary section.
FILE should be in a form suitable for passing to `locate-library'."
(interactive
(list
(completing-read "Library name: "
(apply-partially 'locate-file-completion-table
(or find-function-source-path load-path)
(find-library-suffixes)))))
(let ((str (lm-commentary (find-library-name file))))
(or str (error "Can't find any Commentary section"))
;; This used to use *Finder* but that would clobber the
;; directory of categories.
(pop-to-buffer "*Finder-package*")
(setq buffer-read-only nil
buffer-undo-list t)
(erase-buffer)
(insert str)
(goto-char (point-min))
(while (re-search-forward "\\<\\([-[:alnum:]]+\\.el\\)\\>" nil t)
(if (locate-library (match-string 1))
(make-text-button (match-beginning 1) (match-end 1)
'xref (match-string-no-properties 1)
'help-echo "Read this file's commentary"
:type 'finder-xref)))
(goto-char (point-min))
(setq buffer-read-only t)
(set-buffer-modified-p nil)
(shrink-window-if-larger-than-buffer)
(finder-mode)
(finder-summary)))
(defun finder-current-item ()
(let ((key (save-excursion
(beginning-of-line)
(current-word))))
(if (or (and finder-headmark (< (point) finder-headmark))
(zerop (length key)))
(error "No keyword or filename on this line")
key)))
(defun finder-select ()
"Select item on current line in a Finder buffer."
(interactive nil finder-mode)
(let ((key (finder-current-item)))
(if (string-match "\\.el$" key)
(finder-commentary key)
(finder-list-matches key))))
(defun finder-mouse-select (event)
"Select item in a Finder buffer with the mouse."
(interactive "e")
(with-current-buffer (window-buffer (posn-window (event-start event)))
(goto-char (posn-point (event-start event)))
(finder-select)))
;;;###autoload
(defun finder-by-keyword ()
"Find packages matching a given keyword."
;; FIXME: Why does this function exist? Should it just be an alias?
(interactive)
(finder-list-keywords))
(define-derived-mode finder-mode nil "Finder"
"Major mode for browsing package documentation.
\\<finder-mode-map>
\\[finder-select] more help for the item on the current line
\\[finder-exit] exit Finder mode and kill the Finder buffer."
:syntax-table finder-mode-syntax-table
:interactive nil
(setq buffer-read-only t
buffer-undo-list t)
(setq-local finder-headmark nil))
(defun finder-summary ()
"Summarize basic Finder commands."
(interactive nil finder-mode)
(message "%s"
(substitute-command-keys
"\\<finder-mode-map>\\[finder-select] = select, \
\\[finder-mouse-select] = select, \\[finder-list-keywords] = to \
finder directory, \\[finder-exit] = quit, \\[finder-summary] = help")))
(defun finder-exit ()
"Exit Finder mode.
Quit the window and kill all Finder-related buffers."
(interactive nil finder-mode)
(quit-window t)
(dolist (buf (list finder-buffer "*Finder-package*"))
(and (get-buffer buf) (kill-buffer buf))))
(defun finder-unload-function ()
"Unload the Finder library."
(with-demoted-errors (unload-feature 'finder-inf t))
;; continue standard unloading
nil)
(provide 'finder)
;;; finder.el ends here