2

I wrote a function to iterate over all of an org file entries, and remove specific properties from drawer.

It works well, and does what I need, but to make things cleaner, I would like to delete the property drawer after the removal of properties if the drawer is left empty.

This is my current function:

(defun my/org-roam-de-nodify-buffer-entries ()
  (interactive)
  (when (and (eq major-mode 'org-mode)
             (eq buffer-read-only nil)
             (string-match (expand-file-name org-roam-directory) (buffer-file-name)))
    (org-map-entries (lambda ()
                       (org-entry-delete (point) "ID")
                       (org-entry-delete (point) "AREA")
                       (org-entry-delete (point) "NAMESPACE")
                       ;; TODO Check if there are any properties left
                       ;; If not, delete the empty drawer
                       ))))

I tried messing with org-element-drawer-parser, but I am fairly new to Emacs and even newer to Elisp, and I did not get any interesting result.

Thanks for your time.

3
  • 1
    Maybe org-remove-empty-drawer-at? You have to give it a position or a marker.
    – NickD
    Commented May 24 at 18:33
  • 1
    Thanks, it works with adding (org-remove-empty-drawer-at (point)). The function is well named I am surprised I missed it while searching through callables. I cannot mark a comment as an accepted answer though...
    – lyndhurst
    Commented May 25 at 8:03
  • Yes, sometimes when I don't have time to write an answer immediately, I write a quick comment that might help, then go back and fill out an answer with more details. You can vote up a comment BTW if it is useful (although there is no reputation change involved).
    – NickD
    Commented May 25 at 13:30

1 Answer 1

2

The function org-remove-empty-drawer-at will do that. C-h v org-remove-empty-drawer-at says:

(org-remove-empty-drawer-at POS)

Remove an empty drawer at position POS.
POS may also be a marker.

As long as your cursor is somewhere "in" the drawer, (org-remove-empty-drawer-at (point)) will work. So for a drawer like this:

:EMPTY:
:END:




Next para

your cursor (or the POS argument) can be anywhere from the initial : to one before the N of Next para.

I found it using completion: C-h f org--drawer TAB gave me 32 choices and, with a quick scan, I was able to zero-in to it fairly easily.

BTW, your idea about using the org-element parser was correct: look at the source code of the function (by clicking on the filename at the end of the first line of the doc string when you use C-h f). That is always a useful exercise if you want to learn more and go deeeper into programming these things.


Response to comment: What does POS mean?

POS is the conventional name for an argument that should be a buffer position. Every character in a buffer has a position: an integer from 1 to the length of the buffer (in characters). You can move your cursor and do C-x = to find out what position the cursor is on.

Read the Point section in the Emacs manual with C-h i g(emacs) RET i point RET for more info.

You can programmatically change point by calling (goto-char POS) where POS is any value from 1 to the length of the buffer.

Another way to change point is by searching in the buffer for something: if the search succeeds, point will be either just after (if you search forwards) or just before (if you search backwards) the first match.

In general, the second method (searching) is much more common than the first method (jumping to a given point directly), but both can come in handy. For example, when I want to test a function which is supposed to be called with point at the leftmost start of a headline (a common situation when you are using org-map-entries e.g.), then I set up an Org mode file like this:

...
** A second level headline

<stuff>

* Code

First position the cursor on the leftmost asterisk above and do
`C-x =` to get the value of `point` - let us says it is 37. Then I
write two code blocks like this:

#+begin_src elisp :results drawer
(defun my-function (...)
   "This function assumes that it is called with `point`
at the beginning of a headline. It does something."
   ...)
#+end_src

#+begin_src elisp :results drawer
(save-excursion
   (goto-char 37)
   (my-function)
#+end_src

I do C-c C-c on the first block to define the function and then C-c C-c on the second block to test it: the goto-char guarantees that when the function is called, point will be at the beginning of a headline (the headline at position 37 in the file) and the precondition on the function is satisfied. The function will do something (which will probably be wrong), so I iterate by changing the definition of the function, doing C-c C-c on the first block to redefine it with the changes and then C-c C-c on the second block again to retest. Lather-rinse-repeat until I'm satisfied.

But searching will be much more common. For example, if you know the name of the drawer, you can do something like this:

    ...
    ;; Search for `:<name>:` - it's more specific than just `<name>'.
    ;; You want to specify `bound` to be the end of this entry
    ;; (that's just another buffer position). That will stop
    ;; the search when it reaches that point, since you don't want
    ;; to find a drawer of the same name in some later entry.
    (if (search-forward (concat ":" drawer-name ":") bound t)
        (org-remove-empty-drawer-at (point)))
    

If the search succeeds, point will be just after the match, so it is going to be "inside" the drawer and you can call org-remove-empty-drawer-at at that point: if the drawer is indeed empty, it will be deleted, otherwise nothing will happen. If you want to make sure that point will not be disturbed by this shenanigan, you wrap the whole thing in a (save-excursion ...) form. All these manipulations are very common, so they will become second-nature very quickly.

This sideways excursion has grown too long already, but I did want to mention a couple of other things:

  • POM stands for position-or-marker: markers are a data structure holding both a buffer and a position within that buffer, so they allow you to go to a particular place in another buffer. Many functions are polymorphic in that sense: they can take as argument either a simple position (with the buffer implicitly specified to be the current buffer) or a marker - they can then go do their thing either at the specified position of the current buffer or they can use marker-buffer to get a buffer and marker-position to get the position in that buffer to go do their thing.

  • The org-element parser allows you to parse an Org mode file "locally" and get information about the element there. You can then use the parse tree that the parser generates to get useful information. For example, if point is currently at the beginning of a headline (on the leftmost asterisk), you can use org-element-at-point to generate the parse tree for that element and use org-element-property to retrieve information from the parse tree - e.g. the position of the end of the section that starts with that headline, so you can limit a search (as in the example above). A typical code example goes like this:

    ...
    (let* ((headline (org-element-at-point))
             (end (org-element-property :contents-end headline)))
       ;; check to make sure that we are *really* at a headline
       (when (eq (org-element-type headline) 'headline)
           (if (search (concat ":" drawer-name ":") end t)
               ;; we found it
               (org-remove-empty-drawer-at (point)))))
    ...

Hope this helps, but there is no substitute for reading code and then trying things out: that's how you gain experience.

6
  • Thanks for adding some context about how POS works exactly. I got lucky that in the function I wrote, adding (point) as an argument works. After checking the info, I was a bit worried about this argument because I would have no idea how to move the point to the right place, or pass a POS. I do not even understand from the info what POS is exactly, a line/column type of coordinate I would imagine.
    – lyndhurst
    Commented May 26 at 13:21
  • As for the org-element-drawer-parser, I found using more or less the same technique as you describe,I went back to the info pages, and I kind of understood that I could parse the property drawer, and evaluate if an empty list was returned, or something in that vein. The main problem after trying it a few times in the eval prompt is that I do not understand the arguments expected, so I get a wrong argument error each time.
    – lyndhurst
    Commented May 26 at 13:26
  • 1
    That is indeed a problem and the only solution I know is "experience". You can always ask here: it's fairly easy to cobble up a short piece of code illustrating the usage. For core Emacs functions, the Elisp Reference manual (do C-h i g(elisp) RET to get to it in Emacs) is indispensable, but it too uses these conventional names that you need to understand. For the rest, looking at the code helps sometimes, but it's better to write a short piece of code illustrating the usage and modify it until it works. I'll add a note about POS to the answer.
    – NickD
    Commented May 26 at 16:09
  • 1
    I added a (long) note on POS/POM - hope it helps.
    – NickD
    Commented May 27 at 1:21
  • 1
    It does help a lot, it is like the fish VS fishing stick paradigm. I have a lot of new material to enrich my notes on Elisp, Thank you very much for taking the time to be that thorough and clear in your exxplanations.
    – lyndhurst
    Commented May 27 at 7:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.