Future-proof your Emacs Lisp customizations!
- TL;DR
- Installation
- Why does it exist
- Basic usage
- Patch directives
- Defining patches
- Inspecting patches
- Validating patches
- Removing patches
- Lazy-loading packages
- Validating patches that are not loaded yet
- Integration with
use-package
- Usage with byte-compiled init-file
- But how does it work?
- But how does it actually work?
- But does it actually work?
From MELPA, using your package manager of choice.
See Installation. Emacs 25 and later is supported,
please submit an issue if you want el-patch
to support Emacs 24.
Like the advice system, el-patch
provides a way to
customize the behavior of Emacs Lisp functions that do not provide
enough variables and hooks to let you make them do what you want. The
advantage of using el-patch
is that you will be notified if the
definition of a function you are customizing changes, so that you are
aware your customizations might need to be updated.
Using the same mechanism, el-patch
also
provides a way to make lazy-loading packages much more
easy, powerful, and robust.
el-patch
is available on MELPA. It is easiest to install it
using straight.el
:
(straight-use-package 'el-patch)
However, you may install using any other package manager if you prefer.
Emacs provides a comprehensive set of customizable variables and hooks as well as a powerful advice system. Sometimes, however, these are not enough and you must override an entire function in order to change a detail of its implementation.
Such a situation is not ideal, since the original definition of the function might change when you update Emacs or one of its packages, and your overridden version would then be outdated. This could prevent you from benefitting from bugfixes made to the original function, or introduce new bugs into your configuration. Even worse, there is no way to tell when the original definition has changed! The correctness of your configuration is basically based on faith.
el-patch
introduces another way to override Emacs Lisp functions.
You can provide a patch which simutaneously specifies both the
original and modified definitions of the function. When Emacs starts
up, your patches act just like you had overridden the functions they
are modifying. However, you can later ask el-patch
to validate
your patches—that is, to make sure that the original function
definitions have not changed since you created the patches. If they
have, el-patch
will show you the difference using Ediff.
Of course, in an ideal world, el-patch
would not be necessary,
because user options and hooks could be made configurable enough to
satisfy everyone's needs. Unfortunately, that will never truly be
possible (or, arguably, desirable), so—like the advice
system—el-patch
offers a concession to the practical needs of your
Emacs configuration.
Consider the following function defined in
the company-statistics
package:
(defun company-statistics--load ()
"Restore statistics."
(load company-statistics-file 'noerror nil 'nosuffix))
Suppose we want to change the third argument from nil
to
'nomessage
, to suppress the message that is logged when
company-statistics
loads its statistics file. We can do that by
placing the following code in our init.el
:
(el-patch-feature company-statistics)
(with-eval-after-load 'company-statistics
(el-patch-defun company-statistics--load ()
"Restore statistics."
(load company-statistics-file 'noerror
(el-patch-swap nil 'nomessage)
'nosuffix)))
Simply calling el-patch-defun
instead of defun
defines a no-op
patch: that is, it has no effect (well, not
quite—see later). However, by including patch
directives, you can make the modified version of the function
different from the original.
In this case, we use the el-patch-swap
directive. The
el-patch-swap
form is replaced with nil
in the original definition
(that is, the version that is compared against the "official"
definition in company-statistics.el
), and with 'nomessage
in the
modified definition (that is, the version that is actually evaluated
in your init-file).
Note that it is important to cause the patch to be loaded after
company-statistics
is loaded. Otherwise, when company-statistics
is loaded, the patch will be overwritten!
You may also be wondering what el-patch-feature
does. The patch will
still work without it; however, until company-statistics
is actually
loaded, el-patch
will not be aware that you have defined the patch
(since the code has not been run yet). Telling el-patch
that you
define a patch inside a with-eval-after-load
for
company-statistics
allows M-x el-patch-validate-all
to make sure to validate all your patches, and not just the ones
currently defined. See
also Validating patches that are not loaded yet.
-
(el-patch-add ARGS...)
Insert forms. In the original definition, the entire form is removed, and in the modified definition, each of the
ARGS
is spliced into the surrounding form. For example, the following patch:(foo (el-patch-add bar baz) quux)
resolves to this in the modified definition:
(foo bar baz quux)
-
(el-patch-remove ARGS...)
Remove forms. This is just like
el-patch-add
, except that the roles of the original and modified definitions are exchanged. -
(el-patch-swap OLD NEW)
Replace one form with another. In the original definition, the entire form is replaced with
OLD
, and in the modified definition, the entire form is replaced withNEW
. -
(el-patch-wrap [TRIML [TRIMR]] ARGS...)
Wrap forms in a list, optionally prepending or postpending additional forms. This is the most complicated directive, so an example will probably be helpful. The following patch:
(el-patch-wrap 1 1 (or (eq (get-text-property (point) 'face) 'font-lock-doc-face) (eq (get-text-property (point) 'face) 'font-lock-string-face)))
resolves to this in the original definition:
(eq (get-text-property (point) 'face) 'font-lock-doc-face)
and this in the modified definition:
(or (eq (get-text-property (point) 'face) 'font-lock-doc-face) (eq (get-text-property (point) 'face) 'font-lock-string-face)))
That is, the original
eq
call has been wrapped in an additional list, and also it has had forms inserted before and after it. The first1
in the call toel-patch-wrap
is the number of forms to insert before it, and the second1
is the number of forms to insert after it.What you provide to
el-patch-wrap
forARGS
is the fully wrapped form, so you can think ofTRIML
andTRIMR
as the number of forms to trim from each end of theARGS
before removing the surrounding parentheses.You can omit both
TRIML
andTRIMR
; each defaults to zero. Notice thatARGS
is always a list, so the number of arguments is either one, two, or three—thus eliminating any ambiguity about which argument is which. -
(el-patch-splice [TRIML [TRIMR]] ARGS...)
Splice forms into their containing form, optionally removing some from the beginning and end first. This is just like
el-patch-wrap
, except that the roles of the original and modified definitions are exchanged. -
(el-patch-let VARLIST ARGS...)
Sometimes you need to restructure a form in an inconvenient way. For example, suppose you need to turn the following form:
(if $cond $then $else)
into the following form:
(cond ($cond $then) ($new-cond $new-then) (t $else))
where
$cond
,$then
,$new-cond
,$new-then
, and$else
are all long forms with many sub-expressions. You could do it in the following way:(el-patch-swap (if $cond $then $else) (cond ($cond $then) ($new-cond $new-then) (t $else)))
However, this is not ideal because you have repeated the forms and violated DRY.
You could achieve the patch without any repetition by using the basic patch directives, but that would be hard to read. Wouldn't it be great if you could just do the following?
(el-patch-let (($cond (... long form ...)) ($then (... another form ...)) ($else (... more code ...)) ($new-cond (... even more ...)) ($new-then (... lots more code ...))) (el-patch-swap (if $cond $then $else) (cond ($cond $then) ($new-cond $new-then) (t $else))))
Well, you can. Welcome to
el-patch
. -
(el-patch-literal ARGS...)
Hopefully this will never happen, but you might need to use
el-patch
to modify functions that use symbols likeel-patch-add
. In this case, you can wrap a form inel-patch-literal
to prevent anything within from being interpreted byel-patch
. For example, the following form:(foo (el-patch-literal (el-patch-add bar baz)) quux)
will be replaced with:
(foo (el-patch-add bar baz) quux)
in both the original and modified definitions. Thus, you can happily write
el-patches
that patch otherel-patch
definitions :) -
(el-patch-concat ARGS...)
This patch directive lets you concatenate strings. It is useful for modifying long string literals. For example, let's say that you have a string
"Pretend this is a very long string we only want to write once"
in a function you are patching. To change just a small part of this string, you could use
el-patch-swap
directly:(el-patch-swap "Pretend this is a very long string we only want to write once" "Pretend this is a really long string we only want to write once")
But this repeats the rest of the string, violating DRY. Imagine if you just want to add a sentence to a 40-line docstring! Here's an alternative:
(el-patch-concat "Pretend this is a " (el-patch-swap "very" "really") " long string we only want to write once")
Basically,
el-patch-concat
just resolves all of its arguments, which may contain arbitrary patch directives, and then concatenates them as strings and splices the result into both the original and modified definition.
To patch a function, start by copying its definition into your
init-file, and replace defun
with el-patch-defun
. Then modify the
body of the function to use patch directives, so that the modified
definition is what you desire.
You can also patch other types of definitions using:
el-patch-defmacro
el-patch-defsubst
el-patch-defvar
el-patch-defconst
el-patch-defcustom
el-patch-define-minor-mode
Some warnings:
-
Patching
defmacro
,defsubst
, anddefconst
forms will not affect usages of them in already-defined functions, due to macroexpansion and byte-compilation. You may need to define no-op patches of client functions to get your changes to show up. Or take a different strategy—figuring out the best way to make a particular change to an internal function is often a complex process. You may also consider using advice, dynamic binding, and just plain forking the package. -
Patching
defvar
,defconst
, anddefcustom
forms will not affect the value of the variable, if it has already been defined. Thus, they are only useful for lazy-loading by default. To override this behavior and force the patches to reset the value of the variable, even if it is already defined, setel-patch-use-aggressive-defvar
.
You can patch any definition form, not just those above. To register
your own definition types, use the el-patch-deftype
macro. For
example, the el-patch-defun
function is defined as follows:
(el-patch-deftype defun
:classify el-patch-classify-function
:locate el-patch-locate-function
:declare ((doc-string 3)
(indent defun)))
See the docstrings on the macro el-patch-deftype
and the variable
el-patch-deftype-alist
for more detailed information. See also the
source code of el-patch
for examples of how to use
el-patch-deftype
.
Sometimes you want to define a slightly modified version of a function, so that you can use the patched version in your own code but you can still use the original version under its original name. This is easy to do:
(el-patch-defun (el-patch-swap my-old-fn my-new-fn) ...)
Be sure to include patch directives in the function body showing how your modified function is derived from the original, just like in any other patch.
You can run Ediff on a patch (original vs. modified definitions) by
running M-x el-patch-ediff-patch
and selecting the desired patch.
Note that in this context, the "original" definition is the one
specified by the patch, not the actual definition that is checked when
you validate the patch (see below).
To validate a patch, run M-x el-patch-validate
and select the
desired patch. A warning will be printed if there is a difference
between what the patch definition asserts the original definition of
the function is and the actual original definition of the function.
If there is a difference, you can visualize it using Ediff with M-x el-patch-ediff-conflict
.
You can validate all the patches that have been defined so far using
M-x el-patch-validate-all
.
Use M-x el-patch-unpatch
. Note that this does not technically remove
the patch: instead, it sets the function or variable definition to the
"original" definition as specified by the patch. These two actions
will, however, be equivalent as long as the patch is not outdated
(i.e., it is validated without errors by M-x el-patch-validate
).
el-patch
does not mind if you patch a function that is not yet
defined. You can therefore use el-patch
to help lazy-load a package.
As an example, consider the Ivy package. Ivy provides a minor
mode called ivy-mode
that sets completing-read-function
to
ivy-completing-read
. The idea is that you call this function
immediately, so that when a completing-read
happens, it calls into
the Ivy code.
Now, ivy-completing-read
is autoloaded. So Ivy does not need to be
loaded immediately: as soon as ivy-completing-read
is called, Ivy
will be loaded automatically. However, calling ivy-mode
will trigger
the autoload for Ivy, so we can't do that if we want to lazy-load the
package. The natural thing to do is to copy the definition of
ivy-mode
into our init-file, but what if the original definition
changes? That's where el-patch
comes in. The code from Ivy looks
like this:
(defvar ivy-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [remap switch-to-buffer]
'ivy-switch-buffer)
(define-key map [remap switch-to-buffer-other-window]
'ivy-switch-buffer-other-window)
map)
"Keymap for `ivy-mode'.")
(define-minor-mode ivy-mode
"Toggle Ivy mode on or off.
Turn Ivy mode on if ARG is positive, off otherwise.
Turning on Ivy mode sets `completing-read-function' to
`ivy-completing-read'.
Global bindings:
\\{ivy-mode-map}
Minibuffer bindings:
\\{ivy-minibuffer-map}"
:group 'ivy
:global t
:keymap ivy-mode-map
:lighter " ivy"
(if ivy-mode
(progn
(setq completing-read-function 'ivy-completing-read)
(when ivy-do-completion-in-region
(setq completion-in-region-function 'ivy-completion-in-region)))
(setq completing-read-function 'completing-read-default)
(setq completion-in-region-function 'completion--in-region)))
To enable ivy-mode
while still lazy-loading Ivy, simply copy those
definitions to your init-file before the call to ivy-mode
, replacing
defvar
with el-patch-defvar
and replacing define-minor-mode
with
el-patch-define-minor-mode
. That is:
(el-patch-defvar ivy-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [remap switch-to-buffer]
'ivy-switch-buffer)
(define-key map [remap switch-to-buffer-other-window]
'ivy-switch-buffer-other-window)
map)
"Keymap for `ivy-mode'.")
(el-patch-define-minor-mode ivy-mode
"Toggle Ivy mode on or off.
Turn Ivy mode on if ARG is positive, off otherwise.
Turning on Ivy mode sets `completing-read-function' to
`ivy-completing-read'.
Global bindings:
\\{ivy-mode-map}
Minibuffer bindings:
\\{ivy-minibuffer-map}"
:group 'ivy
:global t
:keymap ivy-mode-map
:lighter " ivy"
(if ivy-mode
(progn
(setq completing-read-function 'ivy-completing-read)
(when ivy-do-completion-in-region
(setq completion-in-region-function 'ivy-completion-in-region)))
(setq completing-read-function 'completing-read-default)
(setq completion-in-region-function 'completion--in-region)))
(ivy-mode 1)
(featurep 'ivy) ;; => ivy is still not loaded!
It's really that simple!
If you want to define a patch for a function provided by an unloaded
feature, it is likely that you will just put the patch in a
with-eval-after-load
for the feature. But then el-patch-validate
and el-patch-validate-all
will not be able to validate your patch,
because it is not yet defined.
To get around this problem, you can add functions to
el-patch-pre-validate-hook
in order to make sure all your patches
are defined (for instance, you might need to require some features or
even enable a custom minor mode). This hook is run before
el-patch-validate-all
, and also before el-patch-validate
when you
provide a prefix argument.
Since defining some patches after a feature is loaded is such a common
operation, el-patch
provides a convenience macro for it:
el-patch-feature
. You can call this macro with an (unquoted) feature
name, and it will create a function that loads that feature, and add
it to el-patch-pre-validate-hook
for you.
If you don't want all of your patches to be defined all the time, you
can put some functions in el-patch-post-validate-hook
to disable
them again. For some examples of how to use these hooks, check out
Radian Emacs.
You can enable the use-package
integration of el-patch
by toggling
the global minor mode el-patch-use-package-mode
, but it is more
convenient to set the variable
el-patch-enable-use-package-integration
(defaults to non-nil) and
then the mode will be toggled appropriately once el-patch
and
use-package
have both been loaded.
The use-package
integration defines two new use-package
keywords,
:init/el-patch
and :config/el-patch
. They are analogous to :init
and :config
, but each top-level form is converted into an el-patch
form: for example, a defun
will be turned into an el-patch-defun
,
and so on. (Definition forms that have no corresponding el-patch
macro are left as is.) The resulting code is prepended to the code in
:init
or :config
, respectively. Also, if you provide either
keyword, then a call to el-patch-feature
is inserted into the
:init
section.
el-patch
does not need to be loaded at runtime just to define
patches. This means that if you byte-compile your init-file, then
el-patch
will not be loaded when you load the compiled code.
For this to work, you will need to stick to defining patches with
el-patch-def*
and declaring features with el-patch-feature
.
Anything else will cause el-patch
to be loaded at runtime.
If you do not byte-compile your init-file, then all of this is immaterial.
Magic.
The basic idea is simple. When a patch is defined, the patch
definition is resolved to figure out the modified definition is. Then
that definition is installed by evaluating it (but using
el-patch--stealthy-eval
, so that looking up the function definition
will return the original location rather than the el-patch
invocation location, and also using makunbound
to override a
previous variable definition if el-patch-use-aggressive-defvar
is
non-nil).
The patch definition is also recorded in the hash el-patch--patches
.
This allows for the functionality of M-x el-patch-ediff-patch
.
Obtaining the actual original definition of a function is done using a
modified version of find-function-noselect
, which provides for M-x el-patch-validate
and M-x el-patch-ediff-conflict
.
When you call M-x el-patch-unpatch
, the patch definition is resolved
again and the original version is installed by evaluating it.
It doesn't seem to crash my Emacs, at least.