Subject: Re: bug#60587: Patch for adding links to symbols' help documentation

From: "H. Dieter Wilhelm" <[email-masked]>
Date: Sun, 05 Feb 2023 01:48:13 +0100

Tags: patch

Drew Adams <[email-masked]> writes:

>> > 4. Or instead of having an option, maybe just have
>> > a minor mode.
>> 
>> That's also a nice idea.  (Maybe for later.)
>
> See Stefan's reply...

I rewrote the former toggle as a minor mode and also wanted to implement
the mode's variable as a defcustom but I couldn't figure out to avoid
then that the mode is assigned to every buffer!  (By the way, the
information in the manual (info "(elisp) Minor Modes") is not very
enlightening in this respect because the mentioned Diff Auto Refine mode
-- specific to a major mode -- is since Emacs-27 obsolete and, it seems,
now changed from it's original implementation..)

Please check a further iteration of patches for buttonising Info.

Thank you

>From 963e018503b7e1703480c527d48c2b119842f8f0 Mon Sep 17 00:00:00 2001
From: Dieter Wilhelm <[email-masked]>
Date: Wed, 1 Feb 2023 22:01:19 +0100
Subject: [PATCH] Documentation of Info providing buttons on quoted symbols
 (bug#60587)

* doc/emacs/help.texi
* etc/NEWS
---
 doc/emacs/help.texi | 15 +++++++++++++++
 etc/NEWS            | 10 ++++++++++
 2 files changed, 25 insertions(+)

diff --git a/doc/emacs/help.texi b/doc/emacs/help.texi
index 2513e6be271..8aeb4068319 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-button-mode
+  The minor mode @code{info-button-mode} makes quoted symbol names
+into buttons which show 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 regular 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-button-mode} or deactivating it with
+@code{(info-button-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 5b8ab06086c..96637797358 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -195,6 +195,16 @@ 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 minor mode 'info-button-mode'
+This mode provides buttons on symbols showing their documentation
+string in *Help* buffers (in another window) when typing `RET' or
+clicking `mouse-2'.  These buttons are working independently of the
+regular Info cross references and can be toggled with `M-x
+info-button-mode' or disabled with `(info-button-mode -1)'.
+
 
 * New Modes and Packages in Emacs 30.1
 
-- 
2.34.1

>From 16453b420ca272b9139568d54c7b47e551ced8e8 Mon Sep 17 00:00:00 2001
From: Dieter Wilhelm <[email-masked]>
Date: Wed, 25 Jan 2023 22:00:57 +0100
Subject: [PATCH] Info providing buttons on quoted symbols (bug#60587)

*lisp/info.el
---
 lisp/info.el | 401 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 401 insertions(+)

diff --git a/lisp/info.el b/lisp/info.el
index 035dff66e75..0496259353b 100644
--- a/lisp/info.el
+++ b/lisp/info.el
@@ -5510,6 +5510,407 @@ 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)
+
+(define-minor-mode info-button-mode
+  "The mode creates buttons on symbols linking to their help documentation.
+
+"
+  :init-value nil
+  :lighter " B"
+  :group 'info
+  :version "30.1"
+  (if (string= major-mode "Info-mode")
+      (if info-button-mode
+          (progn
+            (add-hook 'Info-selection-hook 'info-make-button)
+            (info-make-button))               ;for the current buffer
+        (remove-hook 'Info-selection-hook 'info-make-button)
+        (setq inhibit-read-only t)
+        (remove-list-of-text-properties (point-min) (point-max)
+                                        '(button category info-args))
+        (set-buffer-modified-p nil))
+    (setq info-button-mode nil)))
+;; No defcustom for i-b-m since it activates it then for all buffers
+
+;; Activate the mode for all Info buffers
+(add-hook 'Info-mode-hook 'info-button-mode)
+
+(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
+  "<mouse-2>" #'push-button
+  "<follow-link>" 'mouse-face
+  "<mode-line> <mouse-2>" #'push-button
+  "<header-line> <mouse-2>" #'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-button'.  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-button' 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-button'."
+  (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-button (&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")
+  (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 fofllowing 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.
+                        ;; "\\<M-x\\s-+\\(\\sw\\(\\sw\\|\\s_\\)*\\sw\\)" nil t)
+                        ;; include M-x and quotes
+                        "['`‘]?M-x\\s-*\n?\\(\\sw\\(\\sw\\|\\s_\\)*\\sw\\)['’]?" nil t)
+                  (let ((sym (intern-soft (match-string 1))))
+                    (if (fboundp sym)
+                        (info-button 1 'info-function sym))))))))))))
+
 (provide 'info)
 
 ;;; info.el ends here
-- 
2.34.1

-- 
Best wishes
H. Dieter Wilhelm
Zwingenberg, Germany