Emacs: a paradigm shift
Recently I read this beginners guide to extend Emacs. The guide is perfect for starting out with elisp and it shows a lot of care in teaching how to interact with Emacs.
To me, the most important bit though is this one, from the section aptly named Emacs Wants You to Extend It.
I haven’t written plugins for other editors extensively, but I can tell you this: emacs doesn’t just make deep customization available, but it actively encourages you to make an absolute customization messes masterpieces. Core editor functions aren’t just documented, but often include tidbits about “you probably want to see this other variable” or “here’s how you should use this”.
Not only that, but emacs happily hands you functions shaped like nuclear warheads like advice-add (that let you override any function) that can absolutely obliterate your editor if you hold it the wrong way. Of course, this also grants you unlimited power.
Remember that emacs is designed to be torn apart and rearranged.
This is the core bit of the argument. Emacs, as a system, wants you to extend it and it gives you all the means to do so. This is in contrast with systems that can be extended through scripting and instead don’t give you all the means to do so!
I think the tutorial is a fantastic example of doing things right. There is a well-thought example, a constructive approach where the solution grows to a full package.
This is problematic. You may get the impression that extending Emacs is only possible if you do things right and that is definitely not true.
To make my point I want to walk you through an example. I will show you how I used standard Emacs extension-points to extend org-mode to sort my reading lists automatically.
What do I want?
The behavior I want is that when I save an org file the entries are ordered automatically. I keep a timeline of the papers I am reading and it is annoying to keep them kind of ordered.
This is the content of an example buffer.
#+TITLE: My tematic reading list
* Paper which is old but not too old
:PROPERTY:
:year: 2002
:END:
* Definitely older paper but unfortunately it's later in the list
:PROPERTY:
:year: 1998
:END:
When I add a paper to my reading list I run org-sort-entries
and
interactively select to order the entries by the value in the property
year
. Initally this was nice to have but now it’s just annoying that
I have to keep doing it. Let’s extend org-mode so that this is done automatically.
A simple solution
The first step is to automate the interactive part. Lucky for me this is easy
as org-sort-entries
is both a function and a command. I can call it in a
script just as I can run it as a command.
(defvar org-sort-option "year")
(defun org-sort-run ()
(when (and (derived-mode-p 'org-mode) org-sort-option)
(let ((case-sensitive nil)
(sorting-type ?r)
(getkey-func nil)
(compare-func nil)
(property org-sort-option)
(interactive? nil))
(org-sort-entries case-sensitive sorting-type getkey-func compare-func property interactive?))))
This solves one part of the problem. Let’s solve the other one, automatically calling
org-sort-run
whenever an org-mode buffer is saved.
Emacs already has support for this use-case through the use of hooks. We can run
org-sort-run
all the times we want to save a buffer.
(add-hook 'before-save-hook #'org-sort-run)
These two together solve the problem but the solution presented is “just more code”. We tapped into the hook extension point but this would be possible in any scriptable system that exposes well-defined extension points such as hooks and commands.
Leveraging Emacs’ extensibility to extend org-mode
I want to show that even if something is not thought with extensibility in mind Emacs allow us to extend it. Most importantly, while we want to extend org-mode’s behavior we would like this not to be an extension to org-mode’s code.
Here’s the updated problem statement. Have the buffer be automatically sorted and have the sorting criteria be in the buffer itself. We will specify the sorting as a in-buffer setting and use Emacs to support this never thought before org-mode behavior.
Our example buffer changes to the following.
#+TITLE: My tematic reading list
+#+SORT: year
* Paper which is old but not too old
:PROPERTY:
:year: 2002
:END:
* Definitely older paper but unfortunately it's later in the list
:PROPERTY:
:year: 1998
:END:
The hard part of this is to find how org-mode reads in-buffer settings from the header. A M-x find-library later we are in org’s sources.
Searching for +STARTUP
(Ctrl+s +STARTUP), one of the
supported settings, leads us to org-startup-folded
and that in turn
(Ctrl+s org-startup-folded) leads us to org-startup-options
.
org-startup-options
is the used by (again Ctrl+s org-startup-option)
org-set-regexps-and-options
.
While the documentation for this function is not very convincing, its code does make sense for what we are after. I copied it here for reference.
(when (derived-mode-p 'org-mode)
(let ((alist (org-collect-keyword
(append '("FILETAGS" "TAGS")
(and (not tags-only)
'("ARCHIVE" "CATEGORY" "COLUMNS" "CONSTANTS"
"LINK" "OPTIONS" "PRIORITIES" "PROPERTY"
"SEQ_TODO" "STARTUP" "TODO" "TYP_TODO")))
'("ARCHIVE" "CATEGORY" "COLUMNS" "PRIORITIES"))))
;; Startup options. Get this early since it does change
;; behavior for other options (e.g., tags).
(let ((startup (cl-mapcan (lambda (value) (split-string value))
(cdr (assoc "STARTUP" alist)))))
...)
Unfortunately this function calls org-collect-keyword
with a list that we cannot
touch. There is no custom variable to set to pass our own keyword.
If this was a “normal programming environment” we would make our changes to this function body and forever maintain a fork of org-mode. As this is elisp instead we have choices.
I think the best choice is to use advice-add
and have Emacs call our
advice code every time org-set-regexps-and-options
is called. We will copy
what we need from the function body but that will be all.
This is what I ended up with.
(defvar org-sort-option nil)
(defun org-sort-set-option (&rest r)
"Read the +SORT: spec value into variable `org-sort-option'."
(when (derived-mode-p 'org-mode)
(let ((alist (org-collect-keywords '("SORT"))))
(let ((sort (cdr (assoc "SORT" alist))))
(let ((sort-spec (car (read-from-string (car sort)))))
(setq-local org-sort-option sort-spec))))))
(advice-add 'org-set-regexps-and-options :after #'org-sort-set-option)
(defun org-sort-run ()
(when (and (derived-mode-p 'org-mode) org-sort-option)
(let ((case-sensitive nil)
(sorting-type ?r)
(getkey-func nil)
(compare-func nil)
(property org-sort-option)
(interactive? nil))
(org-sort-entries case-sensitive sorting-type getkey-func compare-func property interactive?))))
(add-hook 'before-save-hook #'org-sort-run)
We keep a buffer-local variable org-sort-option
around to store the
property name read from #+SORT: property-name
. This variable is initially
nil
and will be set from the property name in #+SORT: property-name
. To do so
we have a function org-sort-set-option
.
But when to call org-sort-set-option
? The easy way out is to have Emacs call it whenever
org-set-regexps-and-options
is called on a file visit. To achieve this we
tap into advice-add
and ask Emacs to run org-sort-set-option
after
org-sort-regexps-and-options
.
We have now succesfully interposed ourselves in the control flow of the org-mode library.
Org-mode did not provide any interposition point for us, there is no thought ahead etension-point or configuration variable we can use to achieve our goal an yet here we are with a sorted buffer.
We succeeded in our effort because Emacs wants you to extend it and it gives you all the means to do so.
Conclusions
I have made a horrible hack and it works. I have learnt nothing about how org-mode works or Emacs’ file-visiting extension-points.