Split up shortdoc functions and groups, fix their format

Move shortdoc group definitions from shortdoc.el to a separate file
shortdoc-doc.el.  Document shortdoc group format in a future-proof way
and guide package authors on how to use shortdoc groups across past
Emacs versions.

* lisp/emacs-lisp/shortdoc-doc.el: New file.
* lisp/emacs-lisp/shortdoc.el: Document shortdoc group format in a
future-proof way.  Require 'shortdoc-doc'.
(shortdoc--keyword-plist-p): New function.
(shortdoc--check): Update to check the documented shortdoc group format.
(shortdoc--groups, define-short-documentation-group): Pull out of
autoloaded 'progn'.
(define-short-documentation-group): Report errors in terms of byte
compiler warnings.
(alist, map, string, file-name, file, hash-table, list, symbol)
(comparison, vector, regexp, sequence, buffer, overlay, process, number)
(text-properties, keymaps): Move group to shortdoc-doc.el.
(shortdoc): Move alias to after function.
(shortdoc-add-function): Add argument checks.
* doc/lispref/tips.texi (Documentation Group Tips): New section.
* doc/lispref/elisp.texi (Top):
* doc/lispref/tips.texi (Tips): Add references to it.
* doc/lispref/help.texi (Documentation Groups): Ditto, and add some
concept index entries.  (bug#80297)
This commit is contained in:
Jens Schmidt 2026-03-25 18:05:10 +01:00
parent c29971b6fa
commit 82882db8ed
5 changed files with 229 additions and 1959 deletions

View file

@ -1661,6 +1661,7 @@ Tips and Conventions
* Compilation Tips:: Making compiled code run fast.
* Warning Tips:: Turning off compiler warnings.
* Documentation Tips:: Writing readable documentation strings.
* Documentation Group Tips:: Writing useful documentation groups.
* Comment Tips:: Conventions for writing comments.
* Library Headers:: Standard headers for library packages.

View file

@ -828,14 +828,16 @@ if the user types the help character again.
@cindex documentation groups
@cindex groups of functions
@cindex function groups
@cindex shortdoc groups
Emacs can list functions based on various groupings. For instance,
@code{string-trim} and @code{mapconcat} are ``string'' functions, so
@kbd{M-x shortdoc RET string RET} will give an overview
of functions that operate on strings.
@kbd{M-x shortdoc RET string RET} will give an overview of these and
other functions that operate on strings.
The documentation groups are created with the
@code{define-short-documentation-group} macro.
@code{define-short-documentation-group} macro. @xref{Documentation
Group Tips}, for how to write good documentation groups.
@defmac define-short-documentation-group group &rest functions
Define @var{group} as a group of functions, and provide short
@ -846,6 +848,7 @@ summaries of using those functions. The optional argument
(@var{func} [@var{keyword} @var{val}]@dots{})
@end lisp
@cindex documentation group keywords
The following keywords are recognized:
@table @code

View file

@ -35,6 +35,7 @@ in batch mode, e.g., with a command run by @kbd{@w{M-x compile
* Compilation Tips:: Making compiled code run fast.
* Warning Tips:: Turning off compiler warnings.
* Documentation Tips:: Writing readable documentation strings.
* Documentation Group Tips:: Writing useful documentation groups.
* Comment Tips:: Conventions for writing comments.
* Library Headers:: Standard headers for library packages.
@end menu
@ -934,6 +935,89 @@ If you do not anticipate anyone editing your code with older Emacs
versions, there is no need for this work-around.
@end itemize
@node Documentation Group Tips
@section Tips for Documentation Groups
@cindex documentation groups, tips
@cindex tips for documentation groups
@cindex documentation groups, compatibility
Documentation groups, available since Emacs 28, are useful to document
functions of Lisp packages based on various groupings
(@pxref{Documentation Groups}). This section gives some tips on how you
can define documentation groups in your Lisp package in a way such that
users of different Emacs versions can equally well use these groups.
@itemize @bullet
@item
To define documentation groups for your own Lisp package across
different Emacs versions, you can use a boilerplate template along the
lines of the following to make your package compile and load without
errors:
@smallexample
@group
;;; well-doc.el --- a well-documented package -*- lexical-binding: t; -*-
@dots{} package header and contents @dots{}
@end group
@group
;; Explicitly require shortdoc for Emacs 28, which does not have an
;; autoload for macro `define-short-documentation-group'. And for
;; Emacs 30, so that we can redefine `shortdoc--check' later.
(require 'shortdoc nil t)
(eval-when-compile
;; Default macro `define-short-documentation-group' for Emacs 27
;; and older, which do not have the shortdoc feature at all.
(unless (fboundp 'define-short-documentation-group)
(defmacro define-short-documentation-group (&rest _)))
;; Disable too rigid shortdoc checks for Emacs 30, which let it
;; error out on newer shortdoc keywords.
(when (eq emacs-major-version 30)
(fset 'shortdoc--check #'ignore)))
@end group
@group
(define-short-documentation-group well-doc
@dots{})
;;; well-doc.el ends here
@end group
@end smallexample
@findex define-short-documentation-group
If you do not intend to support some of the Emacs versions mentioned
above, you can safely omit the corresponding forms from the template.
If you intend to support only Emacs 31 and newer, you do not need any
of the above and can just use @code{define-short-documentation-group}.
@item
@cindex documentation group keywords, compatibility
Newer Emacs versions might introduce newer documentation group features
and keywords. However, these features or keywords will never break the
display of a documentation group in older Emacs versions. Suppose you
use a hypothetical group keyword @code{:super-pretty-print}, available
in some future Emacs version, like this in your Lisp package
@file{well-doc.el}:
@smallexample
@group
(define-short-documentation-group well-doc
(well-doc-foo
:eval (well-doc-foo)
:super-pretty-print t))
@end group
@end smallexample
That future Emacs version will then supposedly super-pretty-print the
example for function @code{well-doc-foo}. Older Emacs versions will
silently ignore keyword @code{:super-pretty-print} and show the example
according to their regular display rules.
@end itemize
@node Comment Tips
@section Tips on Writing Comments
@cindex comments, Lisp convention for

View file

@ -1,4 +1,4 @@
;;; shortdoc.el --- Short function summaries -*- lexical-binding: t -*-
;;; shortdoc-doc.el --- Builtin shortdoc groups -*- lexical-binding: t -*-
;; Copyright (C) 2020-2026 Free Software Foundation, Inc.
@ -22,119 +22,16 @@
;;; Commentary:
;; This package lists functions based on various groupings.
;; This file defines builtin Emacs shortdoc groups.
;;
;; For instance, `string-trim' and `mapconcat' are `string' functions,
;; so `M-x shortdoc RET string RET' will give an overview of functions
;; that operate on strings.
;;
;; The documentation groups are created with the
;; `define-short-documentation-group' macro.
;; If a shortdoc group describes builtin functions, functions from
;; subr.el or simple.el or otherwise preloaded files, or functions from
;; different files, then you should probably define it in this file.
;; Otherwise, you might as well define the shortdoc group in the file
;; where the documented functions live, like treesit.el does it.
;;; Code:
(require 'seq)
(require 'text-property-search)
(eval-when-compile (require 'cl-lib))
(defgroup shortdoc nil
"Short documentation."
:group 'lisp)
(defface shortdoc-heading
'((t :inherit variable-pitch :height 1.3 :weight bold))
"Face used for a heading."
:version "28.1")
(defface shortdoc-section
'((t :inherit variable-pitch))
"Face used for a section.")
;;;###autoload
(defun shortdoc--check (group functions)
(let ((keywords '( :no-manual :args :eval :no-eval :no-value :no-eval*
:result :result-string :eg-result :eg-result-string :doc)))
(dolist (f functions)
(when (consp f)
(dolist (x f)
(when (and (keywordp x) (not (memq x keywords)))
(error "Shortdoc %s function `%s': bad keyword `%s'"
group (car f) x)))))))
;;;###autoload
(progn
(defvar shortdoc--groups nil)
(defmacro define-short-documentation-group (group &rest functions)
"Add GROUP to the list of defined documentation groups.
FUNCTIONS is a list of elements on the form:
(FUNC
:no-manual BOOL
:args ARGS
:eval EVAL
:no-eval EXAMPLE-FORM
:no-value EXAMPLE-FORM
:no-eval* EXAMPLE-FORM
:result RESULT-FORM
:result-string RESULT-STRING
:eg-result RESULT-FORM
:eg-result-string RESULT-STRING)
FUNC is the function being documented.
NO-MANUAL should be non-nil if FUNC isn't documented in the
manual.
ARGS is optional list of function FUNC's arguments. FUNC's
signature is displayed automatically if ARGS is not present.
Specifying ARGS might be useful where you don't want to document
some of the uncommon arguments a function might have.
While the `:no-manual' and `:args' property can be used for
any (FUNC ..) form, all of the other properties shown above
cannot be used simultaneously in such a form.
Here are some common forms with examples of properties that go
together:
1. Document a form or string, and its evaluated return value.
(FUNC
:eval EVAL)
If EVAL is a string, it will be inserted as is, and then that
string will be `read' and evaluated.
2. Document a form or string, but manually document its evaluation
result. The provided form will not be evaluated.
(FUNC
:no-eval EXAMPLE-FORM
:result RESULT-FORM) ;Use `:result-string' if value is in string form
Using `:no-value' is the same as using `:no-eval'.
Use `:no-eval*' instead of `:no-eval' where the successful
execution of the documented form depends on some conditions.
3. Document a form or string EXAMPLE-FORM. Also manually
document an example result. This result could be unrelated to
the documented form.
(FUNC
:no-eval EXAMPLE-FORM
:eg-result RESULT-FORM) ;Use `:eg-result-string' if value is in string form
A FUNC form can have any number of `:no-eval' (or `:no-value'),
`:no-eval*', `:result', `:result-string', `:eg-result' and
`:eg-result-string' properties."
(declare (indent defun))
(shortdoc--check group functions)
`(progn
(setq shortdoc--groups (delq (assq ',group shortdoc--groups)
shortdoc--groups))
(push (cons ',group ',functions) shortdoc--groups))))
(define-short-documentation-group alist
"Alist Basics"
(assoc
@ -1626,335 +1523,6 @@ A FUNC form can have any number of `:no-eval' (or `:no-value'),
(keymap-lookup
:eval (keymap-lookup (current-global-map) "C-x x g")))
;;;###autoload
(defun shortdoc-display-group (group &optional function same-window)
"Pop to a buffer with short documentation summary for functions in GROUP.
Interactively, prompt for GROUP.
If FUNCTION is non-nil, place point on the entry for FUNCTION (if any).
If SAME-WINDOW, don't pop to a new window."
(interactive (list (completing-read
"Group of functions for which to show summary: "
(mapcar #'car shortdoc--groups))))
(when (stringp group)
(setq group (intern group)))
(unless (assq group shortdoc--groups)
(error "No such documentation group %s" group))
(let ((buf (get-buffer-create (format "*Shortdoc %s*" group))))
(shortdoc--insert-group-in-buffer group buf)
(funcall (if same-window
#'pop-to-buffer-same-window
#'pop-to-buffer)
buf))
(goto-char (point-min))
(when function
(text-property-search-forward 'shortdoc-function function t)
(beginning-of-line)))
(provide 'shortdoc-doc)
(defun shortdoc--insert-group-in-buffer (group &optional buf)
"Insert a short documentation summary for functions in GROUP in buffer BUF.
BUF defaults to the current buffer if nil or omitted."
(with-current-buffer (or buf (current-buffer))
(let ((inhibit-read-only t)
(prev nil))
(erase-buffer)
(shortdoc-mode)
(button-mode)
(mapc
(lambda (data)
(cond
((stringp data)
(setq prev nil)
(unless (bobp)
(insert "\n"))
(insert (propertize
(substitute-command-keys data)
'face 'shortdoc-heading
'shortdoc-section t
'outline-level 1))
(insert (propertize
"\n\n"
'face 'shortdoc-heading
'shortdoc-section t)))
;; There may be functions not yet defined in the data.
((fboundp (car data))
(when prev
(insert (make-separator-line)
;; This helps with hidden outlines (bug#53981)
(propertize "\n" 'face '(:height 0))))
(setq prev t)
(shortdoc--display-function data))))
(cdr (assq group shortdoc--groups))))))
;;;###autoload
(defalias 'shortdoc #'shortdoc-display-group)
(defun shortdoc--display-function (data)
(let ((function (pop data))
(start-section (point))
arglist-start)
;; Function calling convention.
(insert (propertize "(" 'shortdoc-function function 'outline-level 2))
(if (plist-get data :no-manual)
(insert-text-button
(symbol-name function)
'face 'button
'action (lambda (_)
(describe-function function))
'follow-link t
'help-echo "mouse-1, RET: describe function")
(insert-text-button
(symbol-name function)
'face 'button
'action (lambda (_)
(info-lookup-symbol function 'emacs-lisp-mode))
'follow-link t
'help-echo "mouse-1, RET: show \
function's documentation in the Info manual"))
(setq arglist-start (point))
(insert ")\n")
;; Doc string.
(insert " "
(or (plist-get data :doc)
(car (split-string (or (documentation function)
"Error: missing docstring.")
"\n"))))
(insert "\n")
(add-face-text-property start-section (point) 'shortdoc-section t)
(let ((print-escape-newlines t)
(double-arrow (if (char-displayable-p ?⇒)
""
"=>"))
(single-arrow (if (char-displayable-p ?→)
""
"->"))
(start-example (point)))
(cl-loop for (type value) on data by #'cddr
do
(cl-case type
(:eval
(insert " ")
(if (stringp value)
(insert value)
(prin1 value (current-buffer)))
(insert "\n " double-arrow " ")
(let ((expr (if (stringp value)
(car (read-from-string value))
value)))
(prin1 (eval expr) (current-buffer)))
(insert "\n"))
(:no-eval*
(if (stringp value)
(insert " " value "\n")
(insert " ")
(prin1 value (current-buffer)))
(insert "\n " single-arrow " "
(propertize "[it depends]"
'face 'shortdoc-section)
"\n"))
(:no-value
(if (stringp value)
(insert " " value)
(insert " ")
(prin1 value (current-buffer)))
(insert "\n"))
(:no-eval
(if (stringp value)
(insert " " value)
(insert " ")
(prin1 value (current-buffer)))
(insert "\n"))
(:result
(insert " " double-arrow " ")
(prin1 value (current-buffer))
(insert "\n"))
(:result-string
(insert " " double-arrow " ")
(princ value (current-buffer))
(insert "\n"))
(:eg-result
(insert " e.g. " double-arrow " ")
(prin1 value (current-buffer))
(insert "\n"))
(:eg-result-string
(insert " e.g. " double-arrow " ")
(princ value (current-buffer))
(insert "\n"))))
(add-text-properties start-example (point) `(shortdoc-example ,function)))
;; Insert the arglist after doing the evals, in case that's pulled
;; in the function definition.
(save-excursion
(goto-char arglist-start)
(dolist (param (or (plist-get data :args)
(help-function-arglist function t)))
(insert " " (symbol-name param)))
(add-face-text-property arglist-start (point) 'shortdoc-section t))))
(defun shortdoc-function-examples (function)
"Return all shortdoc examples for FUNCTION.
The result is an alist with items of the form (GROUP . EXAMPLES),
where GROUP is a shortdoc group where FUNCTION appears, and
EXAMPLES is a string with the usage examples of FUNCTION defined
in GROUP. Return nil if FUNCTION is not a function or if it
doesn't has any shortdoc information."
(let ((groups (and (symbolp function)
(shortdoc-function-groups function)))
(examples nil))
(mapc
(lambda (group)
(with-temp-buffer
(shortdoc--insert-group-in-buffer group)
(goto-char (point-min))
(let ((match (text-property-search-forward
'shortdoc-example function t)))
(push `(,group . ,(string-trim
(buffer-substring-no-properties
(prop-match-beginning match)
(prop-match-end match))))
examples))))
groups)
examples))
(defun shortdoc-help-fns-examples-function (function)
"Insert Emacs Lisp examples for FUNCTION into the current buffer.
You can add this function to the `help-fns-describe-function-functions'
hook to show examples of using FUNCTION in *Help* buffers produced
by \\[describe-function]."
(let* ((examples (shortdoc-function-examples function))
(num-examples (length examples))
(times 0))
(dolist (example examples)
(when (zerop times)
(if (> num-examples 1)
(insert "\n Examples:\n\n")
;; Some functions have more than one example per group.
;; Count the number of arrows to know if we need to
;; pluralize "Example".
(let* ((text (cdr example))
(count 0)
(pos 0)
(end (length text))
(double-arrow (if (char-displayable-p ?⇒)
""
" =>"))
(double-arrow-example (if (char-displayable-p ?⇒)
" e.g. ⇒"
" e.g. =>"))
(single-arrow (if (char-displayable-p ?→)
""
" ->")))
(while (and (< pos end)
(or (string-match double-arrow text pos)
(string-match double-arrow-example text pos)
(string-match single-arrow text pos)))
(setq count (1+ count)
pos (match-end 0)))
(if (> count 1)
(insert "\n Examples:\n\n")
(insert "\n Example:\n\n")))))
(setq times (1+ times))
(insert " ")
(insert (cdr example))
(insert "\n\n"))))
(defun shortdoc-function-groups (function)
"Return all shortdoc groups FUNCTION appears in."
(cl-loop for group in shortdoc--groups
when (assq function (cdr group))
collect (car group)))
(defun shortdoc-add-function (group section elem)
"Add ELEM to shortdoc GROUP in SECTION.
If GROUP doesn't exist, it will be created.
If SECTION doesn't exist, it will be added.
ELEM is a Lisp form. See `define-short-documentation-group' for
details.
Example:
(shortdoc-add-function
\\='file \"Predicates\"
\\='(file-locked-p :no-eval (file-locked-p \"/tmp\")))"
(let ((glist (assq group shortdoc--groups)))
(unless glist
(setq glist (list group))
(push glist shortdoc--groups))
(let ((slist (member section glist)))
(unless slist
(setq slist (list section))
(nconc glist slist))
(while (and (cdr slist)
(not (stringp (cadr slist))))
(setq slist (cdr slist)))
(setcdr slist (cons elem (cdr slist))))))
(defvar-keymap shortdoc-mode-map
:doc "Keymap for `shortdoc-mode'."
"n" #'shortdoc-next
"p" #'shortdoc-previous
"N" #'shortdoc-next-section
"P" #'shortdoc-previous-section
"C-c C-n" #'shortdoc-next-section
"C-c C-p" #'shortdoc-previous-section
"w" #'shortdoc-copy-function-as-kill)
(define-derived-mode shortdoc-mode special-mode "shortdoc"
"Mode for shortdoc."
:interactive nil
(setq-local outline-search-function #'outline-search-level
outline-level (lambda ()
(get-text-property (point) 'outline-level))))
(defun shortdoc--goto-section (arg sym &optional reverse)
(unless (natnump arg)
(setq arg 1))
(while (> arg 0)
(funcall
(if reverse 'text-property-search-backward
'text-property-search-forward)
sym nil t)
(setq arg (1- arg))))
(defun shortdoc-next (&optional arg)
"Move point to the next function.
With prefix numeric argument ARG, do it that many times."
(interactive "p" shortdoc-mode)
(shortdoc--goto-section arg 'shortdoc-function))
(defun shortdoc-previous (&optional arg)
"Move point to the previous function.
With prefix numeric argument ARG, do it that many times."
(interactive "p" shortdoc-mode)
(shortdoc--goto-section arg 'shortdoc-function t)
(backward-char 1))
(defun shortdoc-next-section (&optional arg)
"Move point to the next section.
With prefix numeric argument ARG, do it that many times."
(interactive "p" shortdoc-mode)
(shortdoc--goto-section arg 'shortdoc-section))
(defun shortdoc-previous-section (&optional arg)
"Move point to the previous section.
With prefix numeric argument ARG, do it that many times."
(interactive "p" shortdoc-mode)
(shortdoc--goto-section arg 'shortdoc-section t)
(forward-line -2))
(defun shortdoc-copy-function-as-kill ()
"Copy name of the function near point into the kill ring."
(interactive)
(save-excursion
(goto-char (pos-bol))
(when-let* ((re (rx bol "(" (group (+ (not (in " )"))))))
(string
(and (or (looking-at re)
(re-search-backward re nil t))
(match-string 1))))
(set-text-properties 0 (length string) nil string)
(kill-new string)
(message string))))
(provide 'shortdoc)
;;; shortdoc.el ends here
;;; shortdoc-doc.el ends here

File diff suppressed because it is too large Load diff