Auto-show latest heading state in org-mode links
I just added some features to my org-mode setup to easily update links to other org headings, to pull in the current keyword and headline from the linked item.
The idea here is that I want to treat links to org items more like a current reference to the original heading item, instead of a stale duplicate. Org-agenda is a good example of doing this: you run the agenda command and it shows an up-to-date view of the current items, their keyword state and tags, and you can easily jump from the item to the original source. Links on the other hand, have no awareness of their target item's state, and even if I copy the name of the headline item when I create the link, it will became stale as soon as I edit the original headline.
The reason I want this is because sometimes I want to build ad-hoc, informal collections of org items regardless of their current state. Eg. maybe I want to pull in some items that I need on hand this week, without having to formalise what those items are with keywords or tags, and without having to keep editing the link description to always be in sync with the original item.
To make it easier to understand, here it is in action:
4. What I'm implementing
The main thing I want is a function which, if my cursor is on a link, will
update the description/contents of the link to reflect the current headline and
its current todo keyword, overwriting whatever the previous description was. I
want to somehow hook this into
C-c C-c, because I'm used to pressing that to
update different things in org.
Additionally, I want configuration and bindings to:
- Use org
:ID:links automatically instead of the default text search method.
- Easily store an
:ID:link in org-mode and org-agenda-mode.
- Easily insert the stored
:ID:link in org-mode.
Using IDs instead of text match
This part is easy - I just have to do:
(setq org-id-link-to-org-use-id 'create-if-interative)
(org-store-link) will automatically generate and use
I could set also this to
t. The documentation suggests there are circumstances
where it might not be desirable to always do this though - something to do with
(org-capture), so I'm starting out by keeping it interactive-only.
Handling narrowed buffers
The one complication here is narrowed/restricted buffers. If you've narrowed the
buffer that the link points to, then
(org-open-at-point) will open the right
buffer but won't jump to the right place because the buffer contents will be
(org-get-heading) will then return the wrong
information. AFAIK org just doesn't handle following links to a narrowed buffer.
Emacs does provide a
(save-restriction) macro, which works like
(save-window-excursion) but for restoring any current
buffer restrictions. So the goal here is that we'll need to jump to the buffer
that the link points to, save the restriction, widen that buffer, then go back
to the original buffer, and call
(org-open-at-point) to follow the link - and
it should always hit the correct heading because it will have access to the full
widened buffer. And then then the
(save-restriction) macro should exit and
restore any restrictions in the linked buffer.
The ordering of these operations is a bit awkward, because
operates on the current buffer at time of calling. And so I encapsulated it in a
(md/with-widened-buffer), which accepts a buffer object (or name of the
buffer) that you want to widen and restore.
(defmacro md/with-widened-buffer (buffer-or-name &rest body) "Widen the given BUFFER-OR-NAME, execute BODY in the context of your current buffer, and restore restrictions on the given buffer. This allows the calling code to not have to worry about manually handling narrowed vs widened state." (let ((orig-buffer (gensym "orig-buffer"))) `(let ((,orig-buffer (current-buffer))) (with-current-buffer ,buffer-or-name (save-restriction (save-excursion (widen) (with-current-buffer ,orig-buffer ,@body)))))))
One detail here is that we use
(gensym) to ensure that our
variable is unique and doesn't leak into the outer code.
The final detail is that we need to grab the source file from the org link
before we call
(org-open-at-point), so we can jump to that buffer and widen
org-id provides the
(org-id-find-id-file) function to grab the
file path associated with a particular UUID. We then need to convert the
returned file path to a buffer object in order to pass it to our macro. The
(md/find-file-buffer) helper function handles this:
(defun md/find-file-buffer (path) "Get or create a buffer visiting PATH without affecting current windows. This is useful in situations where you have functions that accept a buffer object but you only have the file path." (save-window-excursion (find-file path) (current-buffer)))
Arguably this could just be inlined somewhere but I figured it won't be the only
time I need to do this and I don't like having to manually manage the restore
(save-window-excursion) in the calling code.
Hooking into C-c C-c
With the function in place, how do we call it? I want to hook into
which feels intuitive because it's the binding you hit in org-mode to update
various different elements.
I thought this would require advising
(org-ctrl-c-ctrl-c), but org actually
provides a hook named
org-ctrl-c-ctrl-c-hook, which is designed exactly for
this - it lets you extend
C-c C-c to support your own behaviour. The function
has to lookup the current org element/context, do whatever it wants to do, and
t if it did something, and
nil if it didn't.
(defun md/org-ctrl-c-ctrl-c () "I use this to add custom handlers and behaviour to C-c C-c. For example, C-c- C-c is often used to update the state of org elements, and so it feels like a natural way for me to call md/org-link-sync, because that function updates the state of a ID link to be in sync with the target heading." (condition-case nil (let* ((link-context (org-element-context)) (type (org-element-property :type link-context))) (cond ((and (eq (car link-context) 'link) (equal type "id")) (md/org-link-sync) t) ; Returning t tells org-ctrl-c-ctrl-c that we did something (t nil))) ; Tell org-ctrl-c-ctrl-c there was no match (error nil))) ; Catch any errors in case org-element-context failed (add-hook 'org-ctrl-c-ctrl-c-hook 'md/org-ctrl-c-ctrl-c)
The implementation looks similar to the link update function - we use
(org-element-property) to detect if we're on a
supported element, and if so we call
(md/org-link-sync). Now if I hit
C-c on an ID link, it will call the function and update the link to show the
latest keyword and headline.
The last thing I wanted was some bindings - these aren't very interesting. I
(org-store-link) to be accessible via
C-c y in both org-mode and
org-agenda-mode, and I mapped
C-c L to run
which I find nicer than
(org-insert-link) as I never actually need the menu of
(org-insert-link) forces you to choose from.
6. Next steps - informal org agendas?
This all seems to work well and I think it's a nice improvement that makes links more useful for me. There are a few things I could look into next:
- You could take the "mini informal org-agenda" idea further, and update keyword
or tag state from the link in the same way you can with org agenda. Eg. maybe
if I press
C-c C-ton an org link, it could update the keyword state on the linked item.
- You could automatically update multiple links at once, or update links on save, rather than requiring the links to be updated manually - this way they become live references to other heading items.
(org-insert-last-stored-link)inserts a newline after the link, and doesn't include the keyword. It would be nice if I could insert the link and automatically call
(md/org-link-sync)to see the keyword, instead of having to insert the link and immediately call the function.
(md/org-link-sync)is inserting the target headline into the mark ring and posting a message about it - not sure that I really need this.
:CUSTOM_ID:links could be useful.
You can find the code I'm actually using in my dotfiles.