4

The built-in help functions like describe-{function,variable,face,...} all reuse one *Help* buffer.

I would like it to instead create separate help buffers for each Help topic I visit, with clear and distinct names, such as

  • *Help (function: kill-buffer)*,
  • *Help (variable: undo-limit)*,

and so on - the exact format doesn't matter, but at least the name of the thing being described should be in there, and ideally two different kinds of thing with the same name wouldn't share the same buffer.

Because when I'm using the help system heavily I start to feel some limitations:

  1. I can't have more than one help page on the screen at a time. Sometimes I want to compare or keep referring to multiple help pages.

  2. The built-in "back" and "forward" functionality can end up forgetting pages I visited (if I go back and then elsewhere, the forward history is lost).

  3. There's no better way to quickly see, search, or revisit help pages that I've visited than to just cycle back/forward.

Having multiple well-named help buffers would solve all of this for me:

  • I could put the different buffers in their own windows,

  • I'd have a (deduplicated) "history" of visited help topics in my buffer list, and

  • I'd be able to manage, search, and navigate recently visited help topics with the full power we have for buffers, such as narrowing search with Vertico/Helm/etc.

1
  • 2
    FWIW, I think that github.com/Wilfred/helpful does that as standard (along with a whole heap of other things). If you like the look of it, then it has you covered for this purpose as well.
    – phils
    Feb 22 at 1:07

1 Answer 1

3

It turns out we just need to advise help-buffer to use a different buffer name.

Here's what I have in my config right now, minus differences of style and personal helper macros, plus comments:

(defun new-help-buffer (name)
    (get-buffer-create name)
    ;; To match the interface of `help-buffer`,
    ;; we must return the name of the buffer,
    ;; not the buffer itself:
    name)

;; When doing invasive monkey-patching with
;; advice, best practice is to apply the advice
;; during just the code that needs it, and then
;; ensure it's removed right after.
(defmacro with-advice (symbol where function &rest body)
    `(unwind-protect
        (progn
            (advice-add ,symbol ,where ,function)
            ,@body)
        (advice-remove ,symbol ,function)))

(defun independent-help (type formatter function &rest arguments)
    ;; TYPE is a string, like "function", added
    ;; after the "*Help (" in the buffer name.
    ;;
    ;; FORMATTER is a helper function which will
    ;; format the arguments received by FUNCTION.
    ;; This should take `(&rest arguments)` and
    ;; return either the string you want or
    ;; something that 'format with "%s" would
    ;; turn into the string you want.
    ;;
    ;; (TYPE and FORMATTER come first so
    ;; that they can be partially applied.)
    ;;
    ;; FUNCTION is a function which will create
    ;; a help buffer, like 'describe-function.
    ;;
    ;; ARGUMENTS forwards any arguments that
    ;; FUNCTION might take. Examples:
    ;; * `describe-variable` takes one argument,
    ;;   the function being described.
    ;; * `describe-variable` takes 1-3 arguments,
    ;;   the variable and then also optionally a
    ;;   buffer and frame in which to look up
    ;;   that variable's value.
    ;; * `describe-mode` takes no arguments.
    ;; FORMATTER will also be called with
    ;; ARGUMENTS.
    (let ((name (if arguments
                    (format "*Help (%s: %s)*" type (apply formatter arguments))
                    (format "*Help (%s)*" type))))
        (with-advice 'help-buffer :override (apply-partially 'new-help-buffer name)
            (apply function arguments))))

With that done, we can start applying independent-help with the type and formatter arguments as advice to the help functions that we want to change this way.

For simple functions like describe-function, we can just pass identity as the formatter:

(advice-add 'describe-function :around
    (apply-partially 'independent-help "function" 'identity))

For a function like describe-variable, I find it nicer to omit the extra arguments (buffer and frame):

(advice-add 'describe-variable :around
    (apply-partially 'independent-help "variable"
        (lambda (variable &rest _)
            variable)))

Functions like describe-face are built to show one-or-more help entries at a time, so their first argument will sometimes be a list. So if you describe the face "default", your buffer will get named *Help (face: (default))*. If you're a stickler for aesthetic, you can use a formatter like this to trim off those extea parentheses:

(advice-add 'describe-face :around
    (apply-partially 'independent-help "face"
        (lambda (face &rest _)
            (if (listp face)
                (substring (format "%s" face) 1 -1)
                face))))

For other help functions, it gets messier - for example, describe-package will sometimes just receive a simple package name as its package argument, but sometimes, such as when invokes by clicking on a package in the list-packages buffer, it receives a package-desc struct. So I use this:

(advice-add 'describe-package :around
    (apply-partially 'independent-help "package"
        (lambda (package)
            (if (package-desc-p package)
                (package-desc-name package)
                package))))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.