Emacs, scripting and anything text oriented.

Splitting an Org block into two

Kaushal Modi

I ventured out to start writing about a 100+ line Emacs Lisp snippet in my config, and then I thought — Wouldn’t it be nice if I can quickly split out that huge snippet into smaller Org Src blocks?

And so this blog post happened.

Mention org-babel-demarcate-block, tweak the org-meta-return advice.
Use M-return instead of C-return for splitting blocks and support upper-case blocks (though I don’t prefer those!).

Problem #

If I have a huge Org Src block, I’d like to split it into multiple Org Src blocks so that I can write my explanations in-between.

So I’d like to quickly split up:

#+begin_src emacs-lisp
(message "one")
(message "two")


#+begin_src emacs-lisp
(message "one")

#+begin_src emacs-lisp
(message "two")

.. like this.

Click for animation.

Action Plan #

  1. Write a function to return non-nil if point is in any Org block – Not just “src”, “example”, “export” or any of the inbuilt Org blocks.. but also any Org Special block like #+begin_foo .. #+end_foo.
  2. Write a function that does this imagined block splitting.
  3. Overload the M-return binding so that this block splitting function gets called only when the point is inside an Org block (detected using that first function).

<2018-08-26 Sun> Thanks to the comment by reader Mankoff, I learnt about the org-babel-demarcate-block function (bound by default to C-c C-v d and C-c C-v C-d).

This function varies from the solution in this post in at least two ways:

  1. It works only for Org Src blocks.
  2. It splits the block exactly at where the point is, whereas I would like to always split only at EOL or BOL.

But I can see that org-babel-demarcate-block can cover most of the block splitting use cases.

Am I in an Org block? #

Before venturing into writing this function, I looked at these existing ones, but none did what I exactly wanted:

Returns non-nil only if the point is in a #+begin_src .. #+end_src block; not when point is in any other Org block.
Returns non-nil only if the point is in one of the pre-defined block names passed as a list ('("src" "example" "quote" ..)). So this again won’t work as I cannot pre-define all Org Special blocks.

So I define the below modi/org-in-any-block-p function that returns non-nil if the point is in-between any #+begin_FOOBAR .. #+end_FOOBAR. Thankfully, I was able to reuse a lot of logic from the org-between-regexps-p function (org-in-block-p uses that function internally).

(defun modi/org-in-any-block-p ()
  "Return non-nil if the point is in any Org block.

The Org block can be *any*: src, example, verse, etc., even any
Org Special block.

This function is heavily adapted from `org-between-regexps-p'."
    (let ((pos (point))
          (case-fold-search t)
          (block-begin-re "^[[:blank:]]*#\\+begin_\\(?1:.+?\\)\\(?: .*\\)*$")
          (limit-up (save-excursion (outline-previous-heading)))
          (limit-down (save-excursion (outline-next-heading)))
          beg end)
        ;; Point is on a block when on BLOCK-BEGIN-RE or if
        ;; BLOCK-BEGIN-RE can be found before it...
        (and (or (org-in-regexp block-begin-re)
                 (re-search-backward block-begin-re limit-up :noerror))
             (setq beg (match-beginning 0))
             ;; ... and BLOCK-END-RE after it...
             (let ((block-end-re (concat "^[[:blank:]]*#\\+end_"
                                         (match-string-no-properties 1)
                                         "\\( .*\\)*$")))
               (goto-char (match-end 0))
               (re-search-forward block-end-re limit-down :noerror))
             (> (setq end (match-end 0)) pos)
             ;; ... without another BLOCK-BEGIN-RE in-between.
             (goto-char (match-beginning 0))
             (not (re-search-backward block-begin-re (1+ beg) :noerror))
             ;; Return value.
             (cons beg end))))))
Code Snippet 1: Function to check if point is in any Org block
  • (case-fold-search t) ensures that either #+BEGIN_ .. or #+begin_ .. match.
  • The regular expression in block-begin-re matches with "#+begin_src foo" or " #+begin_src foo" or "#+BEGIN_EXAMPLE" or "#+begin_FOOBAR" or ..
  • The limit-up and limit-down are set to the buffer locations of the previous and next Org headings. The following regexp searches are limited to happen in those bounds for better performance.
  • The block-end-re is dynamically constructed based on the string matched using block-begin-re. This is so that if "#+begin_quote" is found initially, it matches the block ending with specifically "#+end_quote" and not something like "#+end_src".
  • nil is returned if the point is not between #+begin_FOOBAR .. #+end_FOOBAR.
I haven’t gone extra lengths to support nested block cases, specifically where the point is outside the inner-most block, but still inside the outer block:
#+begin_src org
#+begin_src emacs-lisp
(message "hello!")

If so, split the block #

With the “point in an Org block” detection working, I now needed the split to happen with these rules:

  1. If the point is anywhere on the line, but not at the beginning of the line (BOL),

    • Go to the end of the line, and then split the block.

      So if the point1 is after the first message identifier, or at the end of that first message line:

      #+begin_src emacs-lisp
      (message "one")
      (message "two")

      Split the block at the point after (message "one") and move the point to between the split blocks:

      #+begin_src emacs-lisp
      (message "one")
      #+begin_src emacs-lisp
      (message "two")
  2. Otherwise (if point is at BOL),

    • Split the block exactly at that point.

      So if the point is at the beginning of the second message line:

      #+begin_src emacs-lisp
      (message "one")
      (message "two")

      Split the block at the point before (message "two") and move the point to between the split blocks:

      #+begin_src emacs-lisp
      (message "one")
      #+begin_src emacs-lisp
      (message "two")

So here’s the code that follows that spec:

(defun modi/org-split-block ()
  "Sensibly split the current Org block at point."
  (if (modi/org-in-any-block-p)
          (let ((case-fold-search t)
                (at-bol (bolp))
              (re-search-backward "^\\(?1:[[:blank:]]*#\\+begin_.+?\\)\\(?: .*\\)*$" nil nil 1)
              (setq block-start (match-string-no-properties 0))
              (setq block-end (replace-regexp-in-string
                               "begin_" "end_" ;Replaces "begin_" with "end_", "BEGIN_" with "END_"
                               (match-string-no-properties 1))))
            ;; Go to the end of current line, if not at the BOL
            (unless at-bol
              (end-of-line 1))
            (insert (concat (if at-bol "" "\n")
                            (if at-bol "\n" "")))
            ;; Go to the line before the inserted "#+begin_ .." line
            (beginning-of-line (if at-bol -1 0)))))
    (message "Point is not in an Org block")))
Code Snippet 2: Function to split the current Org block in sensible fashion
  • The regexp for extracting block-start is the same as block-begin-re in Code Snippet 1, but with different sub-grouping.
  • The block-end string is derived from sub-group 1 of block-start string – just replacing “begin_” with “end_”.
  • And then based on if the point was initially at BOL (at-bol), the insertion of newlines and movement of point is done accordingly.

Now make M-return do that #

With these two functions evaluated, M-x modi/org-split-block will work right away.

    But where’s the fun in that‽

I needed to have the Org block splitting happen with an intuitive binding — like M-return.

  • By default, M-return is used to either create new headings, or do other things like insert an item, wrap a region in table, etc. based on the context. See the doc-string of org-meta-return (function bound to this key by default) for more info.
  • But it doesn’t have a context for “point in an Org block”. So it tries to create a heading when inside a block too, which doesn’t make much sense.
  • So fix that by adding that context.

So I advise org-meta-return to call modi/org-split-block when the point is inside an Org block.

The advising function modi/org-meta-return is the same as the advised function org-meta-return (as of <2018-08-26 Sun>), except that a new context (modi/org-in-any-block-p) is added.

You can tweak the precedence of this new context by moving the ((modi/org-in-any-block-p) #'modi/org-split-block) form in that cond form.

(defun modi/org-meta-return (&optional arg)
  "Insert a new heading or wrap a region in a table.

Calls `org-insert-heading', `org-insert-item',
`org-table-wrap-region', or `modi/org-split-block' depending on
context.  When called with an argument, unconditionally call
  (interactive "P")
  (org-check-before-invisible-edit 'insert)
  (or (run-hook-with-args-until-success 'org-metareturn-hook)
      (call-interactively (cond (arg #'org-insert-heading)
                                ((org-at-table-p) #'org-table-wrap-region)
                                ((org-in-item-p) #'org-insert-item)
                                ((modi/org-in-any-block-p) #'modi/org-split-block)
                                (t #'org-insert-heading)))))
(advice-add 'org-meta-return :override #'modi/org-meta-return)
Code Snippet 3: Advising org-meta-return to add context of point being inside any Org block

Now with the point in any Org block, M-return away!

Full code #

Look for the source of modi/org-split-block (and dependent functions) added to setup-org.el in my Emacs config.

  1. The point is denoted by the BLACK VERTICAL RECTANGLE unicode char (▮). ↩︎

Versions used: emacs 27.0.50 , org release_9.1.13-894-gf79545
Comment by Stacey on Tue Feb 18, 2020 11:56 EST
Thanks for this. I had a half-baked version which didn’t work with QUOTE blocks which yours does. I like too the extra mileage to make M-return do the right thing within a block. Cheers.
Mentioned by [Shreyas Ragavan] on Fri Aug 9, 2019 18:48 EDT
Test post for webmentions with Kaushal Modi
—Published on Fri Aug 9, 2019

This is a test blog post for exploring web mentions.

Kaushal Modi’s blog post : post.

Links to specific parts of the content shows up as an error with webmentions. However, markdown based links to the page content does work.

Comment by Adam Porter on Sun Aug 26, 2018 13:37 EDT
This is great, and I like your optimization, compared to the alternatives, that splits at EOL/BOL. May I suggest that you submit this as a patch to Org? :) Maybe it could be called org-block-split.
Comment by Kaushal Modi on Sun Aug 26, 2018 02:02 EDT


Have a look at the function scimax-split-src-block from John Kitchin

I was not aware of that function. I looked it up, and from its source, it looks like it supports only the Org Src blocks.

As I noted above, I needed a function to split any Org block; Src blocks is one of them. For example, I find myself splitting #+begin_example .. #+end_example blocks a lot too.

Also it might be interesting to see how this function differentiates from the org-babel-demarcate-block function that @Mankoff mentions in his comment.

Best wishes and thanks for the post!

Thanks, and welcome! :)


What about C-c C-v C-d?

I did not know about org-babel-demarcate-block before this!

Though, after going through its doc-string and source, I am thankful that all the exercise in this post did not go to waste :)

org-babel-demarcate-block also deals only with Org Src blocks, and I needed a function that splits any Org block as I note above.

I tried it out, and it also behaves a bit differently than how I would like when point is in-between a line. See my spec about “If the point is anywhere on the line .. Go to the end of the line, and then split the block.”.

That said, now that I know how this function, I’ll find ways to use this vs the one I define in this post.


Comment by Cumol on Sat Aug 25, 2018 04:55 EDT

Have a look at the function scimax-split-src-block from John Kitchin


I guess it is doing the same?

Best wishes and thanks for the post!

Comment by Mankoff on Sat Aug 25, 2018 00:53 EDT
What about C-c C-v C-d?
Mentioned at