reddit / emacs

Did you know that org-mode's source code contains more than 5000 examples?

tonyaldon · 2022-03-11 08:35
(This post in Org format -> [https://github.com/tonyaldon/posts](https://github.com/tonyaldon/posts)) Hey org-mode lovers, Have you ever been in the following situation? You have tried to understand a specific aspect of org-mode (say a command) and you have done the following: 1. you have played with the command (writing your own examples), 2. you have read all the parts in the manual dealing with this command, 3. you have read the docstring of the command, 4. you have looked for explanations on the web, 5. you even have read the source code of the command, but you still haven't figured it out after all the efforts. I've been in that situation and I thought it would be easier if there were more examples. BUT THERE ARE MORE EXAMPLES. We just have to change our lenses to see them. What if we looked at org-mode tests as examples of org-mode? BOOM we have our 5000 examples. Before going any further, let's make it clear that we don't need to understand or know how to use `ert` (the built-in package used for testing) to benefit from org-mode testing. And this post is not about `ert` testing but about the information that we can get from the org-mode test suite. In this post, we use the example of the command `org-insert-heading` (bound by default to `M-<RET>`, that allows to insert a new heading or item with the same depth at point) to "demonstrate" that the tests are indeed examples. Let's assume, we are already familiar with `org-insert-heading`, we use it every day, but sometimes we don't understand why it behaves the way it does. What can we do to find out the truth about `org-insert-heading`? Table of Contents 1. Org manual 2. Help buffer 3. The source code 4. The Tests (aka. "The Examples") 1. `test-org/insert-heading` 2. Example 1 3. `ert` and `org-test-with-temp-text` 4. `should`, `should-not`, `should-error` 5. Example 2 6. Example 3 7. Example 4 5. Where the truth lives ## Org manual First of all, we can take a look in the manual of org. In the info node (`M-x eval-expression RET (info "(org)Plain Lists")`) we can read: ‘M-<RET>’ (‘org-insert-heading’) Insert new item at current level. With a prefix argument, force a new heading (see Structure Editing). If this command is used in the middle of an item, that item is _split_ in two, and the second part becomes the new item(5). If this command is executed _before item’s body_, the new item is created _before_ the current one. ... (5) If you do not want the item to be split, customize the variable ‘org-M-RET-may-split-line’. That might be enough, but let's say it isn't, and we continue our investigation to find the truth. ## Help buffer Secondly, we can have more information about `org-insert-heading` by reading its docstring by running: C-h f org-insert-heading RET which pops up this help buffer: org-insert-heading is an interactive compiled Lisp function. (org-insert-heading &optional ARG INVISIBLE-OK TOP) Insert a new heading or an item with the same depth at point. If point is at the beginning of a heading, insert a new heading or a new headline above the current one. When at the beginning of a regular line of text, turn it into a heading. If point is in the middle of a line, split it and create a new headline with the text in the current line after point (see ‘org-M-RET-may-split-line’ on how to modify this behavior). As a special case, on a headline, splitting can only happen on the title itself. E.g., this excludes breaking stars or tags. With a ‘C-u’ prefix, set ‘org-insert-heading-respect-content’ to a non-nil value for the duration of the command. This forces the insertion of a heading after the current subtree, independently on the location of point. With a ‘C-u C-u’ prefix, insert the heading at the end of the tree above the current heading. For example, if point is within a 2nd-level heading, then it will insert a 2nd-level heading at the end of the 1st-level parent subtree. When INVISIBLE-OK is set, stop at invisible headlines when going back. This is important for non-interactive uses of the command. When optional argument TOP is non-nil, insert a level 1 heading, unconditionally. Now we not only know what the command does but also how to call it. There is also some information about its non-interactive use. Reading the Org manual and its docstring may have given us the information we wanted, but let's say we want to know more and continue our investigation to find the truth. ## The source code The truth resides in the source code! Isn't that right? Ok, let's take a look at the command `org-insert-heading` defined in the file `lisp/org.el` as follow: (defun org-insert-heading (&optional arg invisible-ok top) ;; HERE WAS THE DOCSTRING (interactive "P") (let* ((blank? (org--blank-before-heading-p (equal arg '(16)))) (level (org-current-level)) (stars (make-string (if (and level (not top)) level 1) ?*))) (cond ((or org-insert-heading-respect-content (member arg '((4) (16))) (and (not invisible-ok) (invisible-p (max (1- (point)) (point-min))))) ;; Position point at the location of insertion. Make sure we ;; end up on a visible headline if INVISIBLE-OK is nil. (org-with-limited-levels (if (not level) (outline-next-heading) ;before first headline (org-back-to-heading invisible-ok) (when (equal arg '(16)) (org-up-heading-safe)) (org-end-of-subtree))) (unless (bolp) (insert "\n")) (when (and blank? (save-excursion (backward-char) (org-before-first-heading-p))) (insert "\n") (backward-char)) (when (and (not level) (not (eobp)) (not (bobp))) (when (org-at-heading-p) (insert "\n")) (backward-char)) (unless (and blank? (org-previous-line-empty-p)) (org-N-empty-lines-before-current (if blank? 1 0))) (insert stars " ") ;; When INVISIBLE-OK is non-nil, ensure newly created headline ;; is visible. (unless invisible-ok (pcase (get-char-property-and-overlay (point) 'invisible) (`(outline . ,o) (move-overlay o (overlay-start o) (line-end-position 0))) (_ nil)))) ;; At a headline... ((org-at-heading-p) (cond ((bolp) (when blank? (save-excursion (insert "\n"))) (save-excursion (insert stars " \n")) (unless (and blank? (org-previous-line-empty-p)) (org-N-empty-lines-before-current (if blank? 1 0))) (end-of-line)) ((and (org-get-alist-option org-M-RET-may-split-line 'headline) (org-match-line org-complex-heading-regexp) (org-pos-in-match-range (point) 4)) ;; Grab the text that should moved to the new headline. ;; Preserve tags. (let ((split (delete-and-extract-region (point) (match-end 4)))) (if (looking-at "[ \t]*$") (replace-match "") (org-align-tags)) (end-of-line) (when blank? (insert "\n")) (insert "\n" stars " ") (when (org-string-nw-p split) (insert split)))) (t (end-of-line) (when blank? (insert "\n")) (insert "\n" stars " ")))) ;; On regular text, turn line into a headline or split, if ;; appropriate. ((bolp) (insert stars " ") (unless (and blank? (org-previous-line-empty-p)) (org-N-empty-lines-before-current (if blank? 1 0)))) (t (unless (org-get-alist-option org-M-RET-may-split-line 'headline) (end-of-line)) (insert "\n" stars " ") (unless (and blank? (org-previous-line-empty-p)) (org-N-empty-lines-before-current (if blank? 1 0)))))) (run-hooks 'org-insert-heading-hook)) You can get org-mode's source code by running the following command: git clone https://git.savannah.gnu.org/git/emacs/org-mode.git Ok... The source code helps, but now we need to know a lot more about the implementations of `org-mode` and `emacs/elisp` than we want to spend time on. Indeed, if we want to understand the implementation of `org-insert-heading` we have to understand: 1. (org API) `org--blank-before-heading-p`, `org-current-level`, `org-with-limited-levels`, `outline-next-heading`, `org-back-to-heading`, `org-up-heading-safe`, `org-end-of-subtree`, `org-before-first-heading-p`, `org-N-empty-lines-before-current`, etc., 2. (emacs/elisp API) `make-string`, `member`, `invisible-p`, `bolp`, `save-excursion`, `eobp`, `bobp`, `pcase`, `get-char-property-and-overlay`, etc. So, what can we do? Maybe we can look at the test of the command `org-insert-heading`. # The Tests (aka. "The Examples") ## test-org/insert-heading In the directory `testing` of `org-mode` repository, doing a search for `org-insert-heading` using `grep` (or `ripgrep`) shows that the command `org-insert-heading` is tested in the `ert` test `test-org/insert-heading` defined in the file `testing/lisp/test-org.el` as follow (we reproduce only the first 4 `should` forms - it contains 28 `should` forms and 1 `should-not` form): (ert-deftest test-org/insert-heading () "Test `org-insert-heading' specifications." ;; In an empty buffer, insert a new headline. (should (equal "* " (org-test-with-temp-text "" (org-insert-heading) (buffer-string)))) ;; At the beginning of a line, turn it into a headline. (should (equal "* P" (org-test-with-temp-text "<point>P" (org-insert-heading) (buffer-string)))) ;; In the middle of a line, split the line if allowed, otherwise, ;; insert the headline at its end. (should (equal "Para\n* graph" (org-test-with-temp-text "Para<point>graph" (let ((org-M-RET-may-split-line '((default . t)))) (org-insert-heading)) (buffer-string)))) (should (equal "Paragraph\n* " (org-test-with-temp-text "Para<point>graph" (let ((org-M-RET-may-split-line '((default . nil)))) (org-insert-heading)) (buffer-string)))) ;; ... ) What can we observe from those 4 `should` forms? 1. they have the same "shape", so we can deduce that if we understand how the first one works, we will understand the others, 2. they use only a few symbols (much less than in the source code of `org-insert-heading`): * `should` from `ert` package, * `equal`, `let` and `buffer-string` from Emacs/Elisp, * `org-test-with-temp-text` specific to Org test suite and * `org-insert-heading` and `org-M-RET-may-split-line`, the command and the variable we are testing. Now let's describe these `should` forms in words as best we can. After doing this, perhaps we will think of these `should` forms as small examples, each describing a behavior of the command `org-insert-heading`. ## Example 1 The first `should` form: (should (equal "* " (org-test-with-temp-text "" (org-insert-heading) (buffer-string)))) could be translated like this: 1. in a temporary buffer in org-mode (`org-test-with-temp-text`), 2. keep the buffer empty (`""`), 3. keep the point at the beginning, 4. call `org-insert-heading`, 5. return the string from the temporary buffer (`buffer-string`), 6. compare this string to the string `"* "` (`equal`), 7. if they are equal, return `t`, if not, return the error `ert-test-failed` (`should`). ## ert and org-test-with-temp-text To evaluate the preceding form we need to have `ert` loaded (for the macro `should`) which can be achieve by evaluating the following form: (require 'ert) and we need the macro `org-test-with-temp-text` (defined in the file `testing/org-test.el`) to be defined which can be done by evaluating its definition: (defmacro org-test-with-temp-text (text &rest body) (declare (indent 1)) `(let ((inside-text (if (stringp ,text) ,text (eval ,text))) (org-mode-hook nil)) (with-temp-buffer (org-mode) (let ((point (string-match "<point>" inside-text))) (if point (progn (insert (replace-match "" nil nil inside-text)) (goto-char (1+ (match-beginning 0)))) (insert inside-text) (goto-char (point-min)))) (font-lock-ensure (point-min) (point-max)) ,@body))) ## should, should-not, should-error Our examples only use the macro `should` but if we want to read more tests it is useful to know the macros `should-not` and `should-error` that `ert` package provides. In the following Elisp snippet, we give some examples of the use of `should`, `should-not` and `should-error`: (should 3) ; 3 (should t) ; t (should nil) ;; Debugger entered--Lisp error: (ert-test-failed ((should nil) :form nil :value nil)) (should-not nil) ; nil (should-not t) ;; Debugger entered--Lisp error: (ert-test-failed ((should-not t) :form t :value t)) (should-error (= "1" 1)) ; (wrong-type-argument number-or-marker-p "1") (should-error t) ;; Debugger entered--Lisp error: (ert-test-failed ((should-error t) :form t :value t :fail-reason "did not signal an error")) ## Example 2 The second `should` form: (should (equal "* P" (org-test-with-temp-text "<point>P" (org-insert-heading) (buffer-string)))) could be translated like this: 1. in a temporary buffer in org-mode (`org-test-with-temp-text`), 2. add the string `P` and place the point before `P` (`"<point>P"`), 3. call `org-insert-heading`, 4. return the string from the temporary buffer (`buffer-string`), 5. compare this string to the string `"* P"` (`equal`), 6. if they are equal, return `t`, if not, return the error `ert-test-failed` (`should`). ## Example 3 The third `should` form: (should (equal "Para\n* graph" (org-test-with-temp-text "Para<point>graph" (let ((org-M-RET-may-split-line '((default . t)))) (org-insert-heading)) (buffer-string)))) could be translated like this: 1. in a temporary buffer in org-mode (`org-test-with-temp-text`), 2. add the string `Paragraph` and place the point between the character `a` and `g` (`"Para<point>graph"`), 3. locally set the variable `org-M-RET-may-split-line` to the alist `'((default . t))` and call `org-insert-heading` with this binding (`(let ...)`), 4. return the string from the temporary buffer (`buffer-string`), 5. compare this string to the string `"Para\n* graph"` (`equal` and note that `\n` is the newline), 6. if they are equal, return `t`, if not, return the error `ert-test-failed` (`should`). ## Example 4 The fourth `should` form: (should (equal "Paragraph\n* " (org-test-with-temp-text "Para<point>graph" (let ((org-M-RET-may-split-line '((default . nil)))) (org-insert-heading)) (buffer-string)))) could be translated like this: 1. in a temporary buffer in org-mode (`org-test-with-temp-text`), 2. add the string `Paragraph` and place the point between the character `a` and `g` (`"Para<point>graph"`), 3. locally set the variable `org-M-RET-may-split-line` to the alist `'((default . nil))` and call `org-insert-heading` with this binding (`(let ...)`), 4. return the string from the temporary buffer (`buffer-string`), 5. compare this string to the string `"Paragraph\n* "` (`equal` and note that `\n` is the newline), 6. if they are equal, return `t`, if not, return the error `ert-test-failed` (`should`). # Where the truth lives By now you should be convinced (that's what I hope) that the org-mode test suite is a gold mine for us (org-mode users) and that is where part of THE ORG-MODE TRUTH lives. ... More than 5000 examples ... WE ARE DONE!
Walheimat · 2022-03-11 11:27
Thank you for the work you put into all of your posts. Always learning something new.
tonyaldon · 2022-03-11 12:24
This super cool to hear that you've read and learned new stuff from the posts. I also learn new things when I write them. This is a Win-Win :) Have a nice day
thevoidzero · 2022-03-11 13:01
I've started to write ert tests for my project so it's becoming lot easier to manage my own package. Seeing this post in such a convenient timing is really great, I didn't even think of looking at other packages's tests to learn it better.
oceanlessfreediver · 2022-03-11 13:53
I don't have the time to read the post (although I definitely will), I am just here to say than having a github repo to store an org version of your social media comments is completely insool, really cool, but also totally insane.
AlchemicRez · 2022-03-11 14:59
This is incredibly useful. Thank you! Org-table is what usually gets me. If I could just be as good with it as I am with excel then it would improve my speed considerably.
tonyaldon · 2022-03-11 16:33
Timing is everything :) I don't remember when I started to do this, but now when I look at a "project", I do 2 things (almost always before reading the documentation or even try the "project"): 1. I randomly read some files in the source code, 2. and look for the corresponding tests. And at some point, maybe I try the "project".
tonyaldon · 2022-03-11 16:36
This way I can read my own posts with Emacs :)
tonyaldon · 2022-03-11 16:39
Really thank you!!! I made a video series on org-table :) You might be interested in watching the episodes on YouTube: \-> [Inside Emacs #6 - org-table playlist](https://www.youtube.com/watch?v=pRXRwQ1GGr4&list=PLGMx7bOKMJTw4p7vs1kTGBAnW81NB57Wv&ab_channel=TonyAldon)
tomas_krulis · 2022-03-11 19:01
Your posts are awesome and really helpful, thank you, sir! I also cloned your repo to even get your knowledge for offline browsing/reading ;)
tonyaldon · 2022-03-12 06:07
I'm glad to hear that the posts are helpful. Thank you. Cool that you cloned the repo. While the online experience with Reddit offers all the features that Reddit implements (which is great), you'll definitely love reading the posts inside emacs for at least 3 reasons: 1. Almost all the variables, functions and commands in the posts are links to their definitions in the source code, 2. Mention to info nodes are link to the info nodes, 3. you can run the code and directly try the examples. I hope you'll enjoy the offline experience (provided by EMACS). Have a nice day