Subject: bug#35351: 27.0.50; Enable derived modes to run their own very-early 'change-major-mode-hook' code

From: Phil Sainty <[email-masked]>
Date: Sun, 21 Apr 2019 14:35:35 +1200

The library I'm working on (so-long.el) defines a major mode
which needs to remember various buffer-local values as they were
in the original mode, *before* my major mode takes effect.

I'm currently using `change-major-mode-hook' for this, but it has
occurred to me that it would be nicer if this hook code of mine
only ever ran in the case where it is useful (i.e. the major mode
being changed to is in fact my mode).  `change-major-mode-hook'
has no knowledge of the mode which has just been invoked, so it
must necessarily run for *every* mode change -- which isn't
relevant to my library in the vast majority of cases.

I think `change-major-mode-hook' would more commonly be used by
modes to handle any subsequent 'unloading' needs of that same
mode in case it gets replaced later on (i.e. the mode body could
set a buffer-local hook value), so my scenario of the new mode
wanting to know things about the previous mode is doubtless a bit
of a niche case; but I thought I'd raise it for discussion.

`define-derived-mode' creates the mode function like so:

    (defun ,child ()
      ,docstring
      (interactive)
      ; Run the parent.
      (delay-mode-hooks
        (,(or parent 'kill-all-local-variables))

Where PARENT must likewise `kill-all-local-variables' (which runs
`change-major-mode-hook').

What would be nice is a way for the mode definition to provide
code which would be evaluated before that (delay-mode-hooks...)
form, and consequently acted like a `change-major-mode-hook'
which only ever happened if this mode was called.

(A vaguely analogous facility currently in `define-derived-mode'
is the :after-hook keyword, for running code very *late* in the
proceedings.)

I could always define my mode without using the macro, and then
do whatever I wanted before calling `kill-all-local-variables';
but I *want* to have the benefits of using the macro, so I don't
want to resort to that; I just want the equivalent ability,
while still using the macro.

A hack which works in my case (because my mode was not already
derived from another) is:

    (defun my-mode-parent ()
      (do-my-custom-thing)
      (kill-all-local-variables))

    (define-derived-mode my-mode my-mode-parent ...)

This feels a little ugly, and `derived-mode-make-docstring' will
point out the existence of the parent, so it's not ideal; but I'm
still tempted to use this approach, and simply document it
sufficiently such that the docstring reference won't cause undue
confusion for those who follow it.

That hack obviously can't be used in cases where a parent is
already defined, but my suggested new feature could still work in
those cases.  Something like:

    (defun ,child ()
      ,docstring
      (interactive)
      ,@before-hook
      ; Run the parent.
      (delay-mode-hooks
        (,(or parent 'kill-all-local-variables))

If the new child mode is used directly then things pan out almost
identically to the hack version.

More generally, we end up with:

    ,@child-before-hook
    (delay-mode-hooks
      ,@parent-before-hook
      (delay-mode-hooks
        ,@grandparent-before-hook
        (delay-mode-hooks
          (kill-all-local-variables) ;; runs change-major-mode-hook
          ,@grandparent-body)
        (run-mode-hooks 'grandparent-mode-hook)
        ,@parent-body)
      (run-mode-hooks 'parent-mode-hook)
      ,@child-body)
    (run-mode-hooks 'child-mode-hook)

Which creates the following sequence of events:

    ,@child-before-hook
    ,@parent-before-hook
    ,@grandparent-before-hook
    (kill-all-local-variables) ;; runs change-major-mode-hook
    ,@grandparent-body
    ,@parent-body
    ,@child-body
    (run-hooks 'change-major-mode-after-body-hook)
    (run-hooks 'grandparent-mode-hook)
    (run-hooks 'parent-mode-hook)
    (run-hooks 'child-mode-hook)
    (run-hooks 'after-change-major-mode-hook)

Obviously that sequence of 'before-hook' instances is in the
reverse sequence to the 'body' and 'mode-hook' sequences.  That's
possibly a desirable thing, but I'm not absolutely certain.

A simply way of reversing that sequence would be for each
:before-hook to be added as a buffer-local change-major-mode-hook
entry -- (add-hook 'change-major-mode-hook FOO nil :local) --
rather than evaluating them immediately, which would then build
up a buffer-local hook sequence like:

'(grandparent-before-hook parent-before-hook child-before-hook)

And then `kill-all-local-variables' would run them in that order
(and still ahead of any other pre-existing buffer-local values
for `change-major-mode-hook').

What do people think?

-Phil