From 96ebac084acf21af46980ea6686dd474e4e8974a Mon Sep 17 00:00:00 2001 From: Dieter Wilhelm Date: Wed, 25 Jan 2023 22:00:57 +0100 Subject: [PATCH] Info providing buttons on quoted symbols (bug#60587) *lisp/info.el Implement as global minor mode Adjust NEWS for global minor mode Explain the usage of Info-next-reference And improve some other documentation Adjust emacs.texi to the minor mode name --- doc/emacs/help.texi | 15 ++ etc/NEWS | 11 ++ lisp/info.el | 449 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 475 insertions(+) diff --git a/doc/emacs/help.texi b/doc/emacs/help.texi index 2513e6be271..777adf9bce5 100644 --- a/doc/emacs/help.texi +++ b/doc/emacs/help.texi @@ -653,6 +653,21 @@ Misc Help K @var{key}}, described above, enter Info and go straight to the documentation of @var{function} or @var{key}. +@vindex info-links-to-help-mode + In Info, by default, the global minor mode +@code{info-links-to-help-mode} makes quoted symbol names into buttons +which link to the symbols' documentation string when typing @key{RET} +or clicking the mouse (@pxref{Mouse References}). For example, the +quoted name @code{info-other-window} is made into a button which shows +the function's documentation string in another window, in the +@file{*Help*} buffer. Info highlights such quoted symbols (variables, +functions and face names) by a distinct face and these can be reached, +as the Info manual references, with @key{TAB} and @kbd{S-Tab}. + + If you want to remove these buttons, you can toggle the mode with +typing @kbd{M-x info-links-to-help-mode} or deactivating it with +@code{(info-links-to-help-mode -1)} in Emacs' initialisation file. + @kindex C-h S @findex info-lookup-symbol When editing a program, if you have an Info version of the manual diff --git a/etc/NEWS b/etc/NEWS index 31fb22fc1e2..5a58f27726e 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -201,6 +201,17 @@ This command adds a docstring comment to the current defun. If a comment already exists, point is only moved to the comment. It is bound to 'C-c C-d' in 'go-ts-mode'. +** Info + ++++ +*** New global minor mode 'info-links-to-help-mode'. +In Info, by default, the mode provides buttons on quoted symbols +showing their documentation string in the *Help* buffer when typing +`RET' or clicking `mouse-2'. These links are working independently of +the Info cross references and can be toggled with 'M-x +info-links-to-help-mode' or disabled with '(info-links-to-help-mode +-1)'. + * New Modes and Packages in Emacs 30.1 diff --git a/lisp/info.el b/lisp/info.el index 035dff66e75..540e84f3f0f 100644 --- a/lisp/info.el +++ b/lisp/info.el @@ -4503,6 +4503,8 @@ Info-mode (setq-local revert-buffer-function #'Info-revert-buffer-function) (setq-local font-lock-defaults '(Info-mode-font-lock-keywords t t)) (Info-set-mode-line) + (when info-links-to-help-mode + (info-links-to-help-mode)) (setq-local bookmark-make-record-function #'Info-bookmark-make-record) (unless search-default-mode (isearch-fold-quotes-mode))) @@ -5510,6 +5512,453 @@ info--manual-names Info-directory-list (mapcar #'car Info-suffix-list)))))))) + +;;; Commentary: + +;; All code below provides buttons of symbols (functions, variables, +;; and faces) within Emacs' Info viewer linking to their builtin help +;; documentation. This linking is done when symbol names in texinfo +;; documentation (like the Emacs- and Elisp manual) are: + +;; 1. Quoted symbol names like `symbol-name'. + +;; 2. Function names which are prefixed by M-x, for example M-x +;; function-name or are quoted and prefixed, like `M-x function-name'. + +;; 3. Function names appearing behind the following forms, which +;; occur, for example, in the Elisp manual: (info "(elisp) Eval") + +;; -- Special Form: function-name +;; -- Command: ... +;; -- Function: ... +;; -- Macro: ... + +;; 4. And variables names behind the following text: + +;; -- User Option: variable-name +;; -- Variable: ... + +;; In any case all symbol names must be known to Emacs, which means it +;; is either a built-in, or its Lisp package is loaded for the current +;; Emacs session, or the symbol is auto-loaded. + +;; You can follow the additional links with the usual Info +;; keybindings. The customisation variable +;; `mouse-1-click-follows-link' is influencing the clicking behavior +;; (and tooltips) of the links, the variable's default is 450 (milli +;; seconds) setting it to nil means only clicking with mouse-2 is +;; following the link (hint: Drew Adams). + +;; The link color of symbols - referencing their builtin documentation +;; - is distinct from links which are referencing further Info +;; documentation. + +;; Below code is checking if Info documents are relevant Elisp and +;; Emacs related files to avoid false positives. Please see the +;; customization variable `info-none-emacs-or-elisp-documents'. + +;; The code uses mostly mechanisms from Emacs' lisp/help-mode.el file. + + +;;; Code: + +(require 'button) +(require 'cl-lib) +(require 'help-mode) +(require 'cl-seq) +(require 'subr-x) + +;; Below mode can reuse the functions 'Info-next-reference' and +;; 'Info-prev-reference' since these functions are (also) finding the +;; text properties of the buttons which are applied for the linking to +;; help. + +;;;###autoload +(define-minor-mode info-links-to-help-mode + "The mode creates buttons on symbols linking to their documentation string. +It shows their documentation in the *Help* buffer (in another +window) when typing or clicking `mouse-2' on the buttons. +These can be followed, as the Info manual references, with +\\[Info-next-reference] and \\[Info-prev-reference]. + +For details about which symbols are considered and the linking +process itself please see the function `info-make-links-to-help'." + :global t + :init-value t + :group 'info + :version "30.1" + (if info-links-to-help-mode + (progn + (when (eq major-mode 'Info-mode) + ;; we need this under Info-mode because of the narrowed + ;; Info file + (add-hook 'Info-selection-hook 'info-make-links-to-help)) + (info-make-links-to-help)) ;for the current buffer + (with-silent-modifications + (when (eq major-mode 'Info-mode) + (remove-hook 'Info-selection-hook 'info-make-links-to-help)) + ;; TODO: for Info mode we need to remove buttons from the whole + ;; (unnarrowed) file! Actually for all buffers under this + ;; mode.. + (remove-list-of-text-properties (point-min) (point-max) + '(button category info-args))))) + +(defvar info-emacs-info-dir-content nil + "List of file names in Emacs' info directories. +It is used to check if the current info file `Info-current-file' +belongs to the Emacs and Elisp context. This variable will be +initialised when opening the first info file.") + +(defcustom info-none-emacs-or-elisp-documents + '("aarm2012" ; Ada manual from package "ada-ref-man", 2023 on Elpa + "aarm2020" ; Ada manual from package "ada-ref-man", 2023 on Elpa + "arm2012" ; Ada manual from package "ada-ref-man", 2023 on Elpa + "arm2020" ; Ada manual from package "ada-ref-man", 2023 on Elpa + "sicp" ; Structure and Interpretation of Computer Programs, + ; package "sicp" 2023 on Melpa archive + ) + "List of documentation which is not related to GNU Emacs or Elisp. +As well as documents which should not be searched for linking to +help documentation, for more details please see +`info-check-docu-p'. The list must contain only the base name of +info files, e.g. without the file extension \".info\"." + :type '(repeat string) + :version "30.1" + :group 'info) + +(defun info-check-docu-p () + "Check if the current info file is relevant to Emacs or Elisp. +That means `Info-current-file' is either found in Emacs' info/ +directory or in `package-user-dir' and is not included in the +`info-none-emacs-or-elisp-documents' list." + ;; When initialising Info and `Info-mode-hook' is running there is + ;; not yet `Info-current-file' initialised + (when Info-current-file + (unless info-emacs-info-dir-content + (info-compile-emacs-info-dir-content)) + (let* ((ifile Info-current-file) ;I-c-f doesn't yield file suffices! + (ifi (file-name-nondirectory ifile)) + ;; Verify that checking pdir is redundant because Package + ;; adds info package folders to Info-directory-list + (pdir (when (boundp 'package-user-dir) + (expand-file-name + package-user-dir))) + (ifiles info-emacs-info-dir-content) + (ndocu info-none-emacs-or-elisp-documents) + (sufs (mapcar 'car Info-suffix-list)) + (vars (mapcar #'(lambda (x)(concat ifi x)) sufs)) + (emacsy (and ifile + (or (cl-intersection vars ifiles :test #'string=) + (when pdir (string-match pdir ifile))) + (not (assoc-string ifi ndocu))))) + (if emacsy + t)))) + +(defvar describe-symbol-backends) ;from help-mode.el +(defvar help-xref-following) ;dito + +(defvar-keymap info-button-map + :doc "Keymap used by buttons in Info buffers." + "RET" #'push-button + "" #'push-button + "" 'mouse-face + " " #'push-button + " " #'push-button) + +;; +;; Button types +;; + +;; Below parent button type `info' inherits Info's quoted symbol face +;; `Info-quoted'. Otherwise the buttons would inherit from +;; button.el's `link' face which looks identical to Info's links. But +;; I think it is helpful to distinguish between button link and cross +;; reference types because they react differently. +(define-button-type 'info + 'link t + 'follow-link t + 'face 'Info-quoted + 'keymap info-button-map + 'action #'info-button-action) + +(define-button-type 'info-function + :supertype 'info + 'info-function 'describe-function + 'help-echo (purecopy "mouse-2, RET: describe this function")) + +(define-button-type 'info-variable + :supertype 'info + 'info-function 'describe-variable + 'help-echo (purecopy "mouse-2, RET: describe this variable")) + +(define-button-type 'info-face + :supertype 'info + 'info-function 'describe-face + 'help-echo (purecopy "mouse-2, RET: describe this face")) + +(define-button-type 'info-symbol + :supertype 'info + 'info-function #'describe-symbol + 'help-echo (purecopy "mouse-2, RET: describe this symbol")) + +(define-button-type 'info-function-def + :supertype 'info + 'info-function (lambda (fun &optional file type) + (or file + (setq file (find-lisp-object-file-name fun type))) + (if (not file) + (message "Unable to find defining file") + (require 'find-func) + (when (eq file 'C-source) + (setq file + (help-C-file-name (indirect-function fun) 'fun))) + ;; Don't use find-function-noselect because it follows + ;; aliases (which fails for built-in functions). + (let ((location + (find-function-search-for-symbol fun type file))) + (pop-to-buffer (car location)) + (run-hooks 'find-function-after-hook) + (if (cdr location) + (goto-char (cdr location)) + (message "Unable to find location in file"))))) + 'help-echo (purecopy "mouse-2, RET: find function's definition")) + +(defun info-compile-emacs-info-dir-content () + "Build a list of file names from Emacs' info directories. +This function fills `info-emacs-info-dir-content' with files from +`Info-directory-list'." + (setq info-emacs-info-dir-content + (mapcar 'file-name-nondirectory + (directory-files + (car + ;; search for the main Emacs' info/ directory, when this + ;; function is called Info-directory-list is already + ;; initialised + (cl-member "[^.]emacs" Info-directory-list :test + 'string-match-p)) + ;; don't list "." and ".." + t "[^.]$")))) + +(defun info-button-action (button) + "Call BUTTON's help function." + (info-do-xref nil + (button-get button 'info-function) + (button-get button 'info-args))) + +(defun info-do-xref (_pos function args) + "Call the help cross-reference function FUNCTION with args ARGS. +Things are set up properly so that the resulting `help-buffer' has +a proper [back] button." + ;; There is a reference at point. Follow it. + (let ((help-xref-following nil)) + (apply + function (if (eq function 'info) + (append args (list (generate-new-buffer-name "*info*")))args)))) + +(defun info-button (match-number type &rest args) + "Make a hyperlink for cross-reference text previously matched. +MATCH-NUMBER is the subexpression of interest in the last matched +regexp. TYPE is the type of button to use. Any remaining +arguments are passed to the button's info-function when it is +invoked. See `info-make-links-to-help'. Don't forget ARGS." + ;; Don't munge properties we've added, especially in some instances. + (unless (button-at (match-beginning match-number)) + (make-text-button (match-beginning match-number) + (match-end match-number) + 'type type 'info-args args))) + +(defvar info-symbol-context + '((variable . "variable\\|option") + (function . "function\\|command\\|call") + (face . "face") + ;; ignore symbols following this context type + (ignore . "symbol\\|program\\|property") + ;; function definitions in files + (definition . "source \\(?:code \\)?\\(?:of\\|for\\)")) + "This list helps to distinguish symbol types. +Words in info documentation preceding a (quoted) symbol are used +to distinguish variables, functions, faces and symbols. The +context information can also be used to ignore symbols because +there is no help documentation for them. The strings of the the +list are becoming part of `info-symbol-regexp'.") + +(defvar info-symbol-regexp + ;; better use purecopy? + (concat + "\\(" ; Context start + "\\<\\(" ; Contex type definition + (string-remove-suffix + "\\|" + (mapconcat + (lambda (x) (concat "\\(" (cdr x) "\\)\\|")) + info-symbol-context "")) + "\\)" ; Context type definition end + "[ \t\n]+" ; Separators to quoted symbols + "\\)?" ; End of context + ;; quoted symbol + "['`‘]" ; opening quotes + ;; Note: Symbol starting with word-syntax character: + "\\(\\sw\\(\\sw\\|\\s_\\)+\\|`\\)" ; The symbol itself + "['’]" ; End quotes + ) + "The regular expression for matching symbols to their help documentation. +It is comprised of the symbol's context and the (quoted) symbol +name. The various groups of context regular expressions are +matched in `info-make-links-to-help' to distinct info buttons.") + +(defun info-check-type( type) + "Check if TYPE corresponds to the current search result. +The function is used in `info-make-links-to-help'." + (let* ((isc info-symbol-context) + (n 3) ;embedded within 2 groups + (l (+ 3 (length isc)))) + (while (and (not (eq type (caar isc) ) ) (< n l) ) + (setq n (1+ n)) + (setq isc (cdr isc))) + (match-string n))) + + +;;;###autoload +(defun info-make-links-to-help (&optional buffer) + "Parse and hyperlink quoted symbols in the given BUFFER. +BUFFER defaults to the current buffer if omitted or nil. Find +symbols and their context in an Info buffer and activate buttons +for linking to their documentation string with `info-button'. + +Symbols have the canonical (quoted) form `symbol-name' and the +type of reference may be disambiguated by the preceding word(s) +as compiled in `info-symbol-regexp'. For example: Symbol names +are receiving distinct variable buttons when preceeded by the +words \"variable\" or \"option\". + +Variables are also detected when their names follow below forms: + + -- User Option: variable-name + -- Variable: ... + +Function names are detected when prefixed by `M-x', for example +`M-x function-name' or are quoted and prefixed like `M-x +function-name'. + +Function names are detected, as well, when appearing behind the +following forms, which occur - for example - in the Elisp manual: + + -- Special Form: function-name + -- Command: ... + -- Function: ... + -- Macro: ... + +The linking is similar to mechanisms from lisp/help.el." + (interactive "b") ;asking for buffer name.. + (if (eq major-mode 'Info-mode) + (when (info-check-docu-p) + (with-current-buffer (or buffer (current-buffer)) + (save-excursion + (goto-char (point-min)) + (with-silent-modifications + (let ((case-fold-search t) + (inhibit-read-only t)) + (with-syntax-table emacs-lisp-mode-syntax-table + ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;; The following is for finding "quoted symbols" + (save-excursion + (while (re-search-forward info-symbol-regexp nil t) + ;; sym-group is the regexp group for the symbol name + (let* ((sym-group (+ 3 (length info-symbol-context))) + (data (match-string sym-group)) + (sym (intern-soft data))) + (if sym + (cond + ((info-check-type 'variable) + (and (or (boundp sym) + (get sym 'variable-documentation)) + (info-button sym-group 'info-variable sym))) + ((info-check-type 'function) + (and (fboundp sym) + (info-button sym-group 'info-function sym))) + ((info-check-type 'face) + (and (facep sym) + (info-button sym-group 'info-face sym))) + ((info-check-type 'ignore)) + ((info-check-type 'definition) + (info-button sym-group 'info-function-def sym)) + ;; for symbols + ((cl-some (lambda (x) (funcall (nth 1 x) sym)) + describe-symbol-backends) + (info-button sym-group 'info-symbol sym))))))) + ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;; See e.g.: (info "(elisp) Eval") + ;; Elisp manual -- Special Form: + ;; -- Command: + ;; -- Function: function-name function + ;; -- Macro: + (save-excursion + (while (re-search-forward + "-- \\(Special Form:\\|Command:\\|Function:\\|Macro:\\) " + nil t) + (looking-at "\\(\\sw\\|\\s_\\)+") + (let ((sym (intern-soft (match-string 0)))) + (if (fboundp sym) + (info-button 0 'info-function sym))))) + ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;; -- User Option: + ;; -- Variable: variable-name + (save-excursion + (while (re-search-forward + "-- \\(User Option:\\|Variable:\\) " + nil t) + (looking-at "\\(\\sw\\|\\s_\\)+") + (let ((sym (intern-soft (match-string 0)))) + (if (boundp sym) + (info-button 0 'info-variable sym))))) + ;; M-x prefixed functions + (save-excursion + (while (re-search-forward + ;; Assume command name is only word and symbol + ;; characters to get things like `use M-x foo->bar'. + ;; Command required to end with word constituent + ;; to avoid `.' at end of a sentence. + ;; "\\