;;; finder.el --- topic & keyword-based code finder -*- lexical-binding: t -*- ;; Copyright (C) 1992, 1997-1999, 2001-2022 Free Software Foundation, ;; Inc. ;; Author: Eric S. Raymond ;; 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 . ;;; 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-library-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-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-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