Refiling Trees to Files
As I was explaining in Part 2 of this essay series, shuffling org entries (subtrees) from one file to another is common, but sometimes an entry grows and deserves its own file. Org can refile or archive a subtree to a new file, however, these functions simply creates a new file and copies that subtree. Personally, what I really want, is to have a subtree become a proper org file.
For instance, take a headline title like:
* Foobar Bling
And convert it to a file’s title like:
#+TITLE: Foobar Bling
And convert tags too:
* Foobar Bling :first:second:
To become:
#+tags: first, second
Same with properties in a drawer. Other aspects of a subtree, like its priority, clock logs, and DONE state, doesn’t really have an equivalent at the file-level, and personally, I’m not sure if I really care to keep that information.
Do you need this too? Do you want this? For the last year, I’ve been simply archiving, and then editing the resulting file manually, and I’ll admit that the amount of times I need this feature, editing manually isn’t too bad. But I started writing some simple helper functions, which grew to cover more and more use (or edge) cases (the following sections show the resulting functions and hacking enjoyment).
Note, You could create a wrapper function that simply calls
org-archive-subtree
to move a subtree to a new file (ask me if you want the
details)1.
Get Subtree Components
Org has the ability to parse a file, and return its parts. When looking at a
subtree, we have two primary functions, org-entry-properties
and
org-element-context
. The first of these gives a nice summary of values that
is often what we want. However, the org-element-context
gives a lot of
details, including buffer positions, allowing us to walk through its
sub-parts.
Calling this context function returns a tuple, with the first element a
symbol of the element’s type, and the second is a plist
of its attributes
(see the Emacs Lisp manual for details). For instance, acquiring the context
while the point is on a header returns headline
as the type, and something
like this list of attributes:
(:raw-value "Best Header Ever" :begin 221 :end 522 :pre-blank 0 :contents-begin 301 :contents-end 522 :level 2 :priority nil :tags "foo" :todo-keyword "DONE" :todo-type done :post-blank 0 :footnote-section-p nil :archivedp nil :commentedp nil :closed ... :deadline ... :scheduled ... :title "Best Header Ever")
Why yes, the ellipses do show where I trimmed it for brevity. Keep in mind,
attributes like :deadline
return a timestamp
element, and others, like the
:todo-keyword
return a string with font properties.
The following org-subtree-metadata
function uses the results of calling this
org-element-context
function, and call some helper functions to return a
plist
of the parts that I need in order to create a new Org file from the
subtree.
(defun org-subtree-metadata () "Return a list of key aspects of an org-subtree. Includes the following: header text, body contents, list of tags, region list of the start and end of the subtree." (save-excursion ;; Jump to the parent header if not already on a header (when (not (org-at-heading-p)) (org-previous-visible-heading 1)) (let* ((context (org-element-context)) (attrs (second context)) (props (org-entry-properties))) (list :region (list (plist-get attrs :begin) (plist-get attrs :end)) :header (plist-get attrs :title) :tags (org-get-subtree-tags props) :properties (org-get-subtree-properties attrs) :body (org-get-subtree-content attrs)))))
Get the Tags
As mentioned above, a colon-separated list of tags at the end of a section’s
headline, needs to become a space-separated list at the end of the #+tags:
entry in a file. Question, do we want to copy the inherited tags? In other
words, does the refiling the third headline in this example:
* First Headline :foo: ** Second Headline :bar: *** Third Headline :baz:
In other words, do we get just the baz
tag, or all three? Perhaps we need a
customization variable to determine whether we retrieve ALLTAGS
or just TAGS
from a call to org-entry-properties
.
(defun org-get-subtree-tags (&optional props) "Given the properties, PROPS, from a call to `org-entry-properties', return a list of tags." (unless props (setq props (org-entry-properties))) (let ((tag-label (if org-get-subtree-tags-inherited "ALLTAGS" "TAGS"))) (-some->> props (assoc tag-label) cdr substring-no-properties (s-split ":") (--filter (not (equalp "" it)))))) (defvar org-get-subtree-tags-inherited t "Returns a subtree's tags, and all tags inherited (from tags specified in parents headlines or on the file itself). Defaults to true.")
Let me mention that weird function, -some->>
which, like the more familiar
threading macro, ->>
, will take the value of props
and put it as the last
value in the call to assoc
, and then take the results of that expression, and
pass it as the last value to cdr
, etc. As soon as one of the values is nil
,
however, this function stops and returns nil
, saving us from throwing an
exception if no tag exists for the subtree.
Get the Properties from the Drawer
Look at a subtree’s list of properties in the property drawer, e.g.
* Some Headline :PROPERTIES: :hello: world :DESCRIPTION: This could be pretty good :END:
The function, org-element-context
returns these properties when called on a
headline, but spread out with all the other properties, for instance:
( ... :todo-type done :DESCRIPTION "This could be pretty good" :HELLO "world" :title "Best Header Ever")
Note that properties we add in a drawer are uppercase, while standard properties are not. Let’s write a function to iterate over the context attributes, looking for uppercase properties, and gather them into a more useful list of lists:
(defun org-get-subtree-properties (attributes) "Return a list of tuples of a subtrees properties where the keys are strings." (defun symbol-upcase? (sym) (let ((case-fold-search nil)) (string-match-p "^:[A-Z]+$" (symbol-name sym)))) (defun convert-tuple (tup) (let ((key (first tup)) (val (second tup))) (list (substring (symbol-name key) 1) val))) (->> attributes (-partition 2) ; Convert plist to list of tuples (--filter (symbol-upcase? (first it))) ; Remove lowercase tuples (-map 'convert-tuple)))
Get the Subtree Contents
Org offers functions for getting key aspects of your file (like
org-heading-components
), however, it does not offer a function for acquiring
the contents of a subtree (without gathering the property drawer and whatnot).
The following function returns what I consider a subtree’s contents by walking
through a parsed version of the Org elements.
When given the headline
attributes, we jump to what it considers the
beginning of its contents (which I expect is just the next line). Next, we
call org-element-context
to walk down the org elements until we get to the
real content.
But as we see each component element, how do we decide what element belongs to a
subtree’s header (obviously headline
is) as opposed to the body (for instance,
paragraph
or list-item
)? The org-element-all-elements
variables lists everything,
and here I place header-specific elements in a list, header-components
, and
use the member
function to see determine if I need to skip over it (by
jumping to that element’s :end
location).
After skipping over all header elements, I’m left at the beginning of the
subtree’s contents, and use buffer-substring
to return those contents:
(defun org-get-subtree-content (attributes) "Return the contents of the current subtree as a string." (let ((header-components '(clock diary-sexp drawer headline inlinetask node-property planning property-drawer section))) (goto-char (plist-get attributes :contents-begin)) ;; Walk down past the properties, etc. (while (let* ((cntx (org-element-context)) (elem (first cntx)) (props (second cntx))) (when (member elem header-components) (goto-char (plist-get props :end))))) ;; At this point, we are at the beginning of what we consider ;; the contents of the subtree, so we can return part of the buffer: (buffer-substring-no-properties (point) (org-end-of-subtree))))
Create the Destination File
The function org-refile-subtree-to-file
takes a directory destination and
moves the subtree section to a new file there.
(defun org-refile-subtree-to-file (dir) "Archive the org-mode subtree and create an entry in the directory folder specified by DIR. It attempts to move as many of the subtree's properties and other features to the new file." (interactive "DDestination: ") (let* ((props (org-subtree-metadata)) (head (plist-get props :header)) (body (plist-get props :body)) (tags (plist-get props :tags)) (properties (plist-get props :properties)) (area (plist-get props :region)) (filename (org-filename-from-title head)) (filepath (format "%s/%s.org" dir filename))) (apply #'delete-region area) (org-create-org-file filepath head body tags properties)))
However, the heavy lifting of the previous function is actually done by
org-create-org-file
, which given all the information it needs, creates a new
org file. Note: This doesn’t get rid of auto-insert, so while the file is new,
we still need to update the file.
(defun org-create-org-file (filepath header body tags properties) "Create a new Org file by FILEPATH. The contents of the file is pre-populated with the HEADER, BODY and any associated TAGS." (find-file-other-window filepath) (org-set-file-property "TITLE" header t) (when tags (org-set-file-property "tags" (s-join " " tags))) ;; Insert any drawer properties as #+PROPERTY entries: (when properties (goto-char (point-min)) (or (re-search-forward "^\s*$" nil t) (point-max)) (--map (insert (format "#+PROPERTY: %s %s" (first it) (second it))) properties)) ;; My auto-insert often adds an initial headline for a subtree, and in this ;; case, I don't want that... Yeah, this isn't really globally applicable, ;; but it shouldn't cause a problem for others. (when (re-search-forward "^\\* [0-9]$" nil t) (replace-match "")) (delete-blank-lines) (goto-char (point-max)) (insert "\n") (insert body))
File Name from Title
I need to programmatically choose a good filename to place the contents. Sure,
I would like to take the section’s header, however, what should I do with
something like, Let's go Shopping!
… in this case, I’ll convert all the
non-alphanumeric characters to dashes and lowercase everything:
(defun org-filename-from-title (title) "Creates a useful filename based on a header string, TITLE. For instance, given the string: What's all this then? This function will return: whats-all-this-then" (let* ((no-letters (rx (one-or-more (not alphanumeric)))) (init-try (->> title downcase (replace-regexp-in-string "'" "") (replace-regexp-in-string no-letters "-")))) (string-trim init-try "-+" "-+")))
Set Properties
Using auto-insert
to pre-populate a file is great, so I need a way to make
sure certain lines are set correctly, and if not, insert them:
(defun org-set-file-property (key value &optional spot) "Make sure file contains a top-level, file-wide property. KEY is something like `TITLE' or `tags'. This function makes sure that the property contains the contents of VALUE, and if the file doesn't have the property, it is inserted at either SPOT, or if nil,the top of the file." (save-excursion (goto-char (point-min)) (let ((case-fold-search t)) (if (re-search-forward (format "^#\\+%s:\s*\\(.*\\)" key) nil t) (replace-match value nil nil nil 1) (cond ;; if SPOT is a number, go to it: ((numberp spot) (goto-char spot)) ;; If SPOT is not given, jump to first blank line: ((null spot) (progn (goto-char (point-min)) (re-search-forward "^\s*$" nil t))) (t (goto-char (point-min)))) (insert (format "#+%s: %s\n" (upcase key) value))))))
I think I spent a bit too much time worry about text that can be easily manipulated afterwards. Well, now I can finish my workflow.
Projects
Good, big, chewy projects may start life as a single idea in incubate
but
when they outgrow that file, they need to be moved to their own org file in
the projects
directory.
(defun org-refile-to-projects-dir () "Move the current subtree to a file in the `projects' directory." (interactive) (org-refile-subtree-to-file org-default-projects-dir))
Completed projects need to be moved out of the projects
directory. This could be:
technical
: where complicated notes can help with ongoing maintenancepersonal
: need to remember personal information about the projectprojects/trophies
: seems like an apt name for adone
directory
Sounds like a job for dired,
eh?
Technical Folder
The technical
folder contains any notes on non-work, non-personal
information. The idea with this box is that I can share it publicly.
(defun org-refile-to-technical-dir () "Move the current subtree to a file in the `technical' directory." (interactive) (org-refile-subtree-to-file org-default-technical-dir))
Personal Folder
Any thing remembered or referenced goes into a file in the personal
folder.
Each of these files end with a .txt
extension so that Dropbox can display it
on my mobile device. However, I still want Emacs to render it as an org file,
so my Yasnippet template for files in this directory looks like:
--org-- #+TITLE: $1 #+AUTHOR: Howard Abrams #+EMAIL: howard.abrams@gmail.com $0
We need an auto insert for anything in that directory to expand that snippet.
(define-auto-insert "/personal/*\\.org" ["personal.org" ha/autoinsert-yas-expand])
Where ha/autoinsert-yas-expand
deletes any existing file contents and then
calls yas-expand-snippet
:
(defun ha/autoinsert-yas-expand() "Replace text in yasnippet template. This compensates for a bug(?) in the `yas-expand-snippet' function where it doesn't delete the current contents of the specified region (like the entire buffer that was inserted)." (let ((template (buffer-string))) (delete-region (point-min) (point-max)) (yas-expand-snippet template)))
Now, we just need a helper function for throwing subtrees into new files in that directory:
(defun org-refile-to-personal-dir () "Move the current subtree to a file in the `personal' directory." (interactive) (org-refile-subtree-to-file org-default-personal-dir))
Summary
Whew. Is automatically polishing the archiving/refiling of a subtree to its own file worth this amount of code? I think I will keep it as something extra and use it with:
(require 'boxes-extras)
Footnotes:
Since you asked, here is the code that I used earlier, before refactoring to the code you see in this essay.
The trick is simply to define the destination by setting the value of the
variable, org-archive-location
, and then calling org-archive-subtree
:
(defun ha/org-refile-subtree-as-file (dir) "Archive the org-mode subtree and create an entry in the directory folder specified by DIR. The formatting, since it is an archive, isn't quite what I want,but it gets it going." (let* ((header (substring-no-properties (org-get-heading))) (title (if (string-match ": \\(.*\\)" header) (match-string 1 header) header)) (filename (replace-regexp-in-string "\s+" "-" (downcase title))) (filepath (format "%s/%s.org" dir filename)) (org-archive-location (format "%s::" filepath))) (org-archive-subtree) (find-file-other-window filepath)))