将Org块一分为二
问题描述
假设我有一个巨大的Org代码块,我想把它拆分成多个Org代码块,以便在之间填写解释内容。
也就是说,我想快速地把 所以我想快速地分解一下:
#+begin_src emacs-lisp (message "one") (message "two") #+end_src
拆分成
#+begin_src emacs-lisp (message "one") #+end_src #+begin_src emacs-lisp (message "two") #+end_src
行动计划
- 写一个函数,当光标处于 任意 Org块中(不仅仅是“src”, “example”, “export” 或任何一个内建的Org块,还包括类似 #+begin_foo .. #+end_foo这样特殊的Org块。)返回非nil值
- 编写一个函数来对块进行拆分。
- 重载 M-return 键绑定,只有当光标在Org块中时才会调用该块拆分函数(使用第一个函数进行检测)。
感谢读者 Mankoff 的评论,我学到了 org-babel-demarcate-block 函数(默认绑定到 C-c C-v d 和 C-c C-v C-d).
这个函数至少在两个方面与本文中的解决方案有所不同:
- 它只对Org Src块有效。
- 它在光标处将块进行拆分,而我希望只在行末或行首处进行拆分
不过我可以看出 org-babel-demarcate-block 可以覆盖大部分块分割的情况。
我在Org 块中吗?
在开始编写这个函数之前,我查看了下面这些现有函数,但没有一个是我真正想要的:
- org-in-src-block-p
- 仅当光标位于 #+begin_src .. #+end_src块时返回非nil; 其他Org块则无效.
- org-in-block-p
- 仅当光标位于预先定义好的块名(一个列表,例如 '("src" "example" "quote" ..))时才返回非nil. 因此这个也不行,因为我无法预先定义好所有的特殊块.
因此我定义了下面这个 modi/org-in-any-block-p 函数,它在光标处于任何 #+begin_FOOBAR .. #+end_FOOBAR 之间时,返回非nil。
好在我可以重用 org-between-regexps-p 函数中的大量逻辑(org-in-block-p 内部使用该函数).
(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'." (save-match-data (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) (save-excursion ;; 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))))))
检查光标是否在Org块中的函数
- (case-fold-search t)确保- #+BEGIN_ ..或- #+begin_ ..都能够匹配。
- 正则表达式 block-begin-re匹配"#+begin_src foo"," #+begin_src foo","#+BEGIN_EXAMPLE","#+begin_FOOBAR"等等
- limit-up和- limit-down设置为上一个和下一个Org标题在buffer中的位置. 为了性能,下面的正则搜索局限在这个范围内.
- block-end-re基于- block-begin-re的匹配字符串动态组建. 因此若首先找到的是- "#+begin_quote", 那么它对应的块结束符是- "#+end_quote"而不是- "#+end_src".
- 如果光标不是在 #+begin_FOOBAR .. #+end_FOOBAR之间,则返回nil.
- 我没有对嵌套块的情况做支持,尤其是当光标在最内块和最外层块之间的情况。
#+begin_src org ▮ #+begin_src emacs-lisp (message "hello!") #+end_src #+end_src
如果是,则拆分block
"光标在Org块中"的检测完成后,我需要准照如下规则进行拆分
- 如果光标不在某行的行首(BOL),
- 跳转到行末, 然后将块拆分.
因此如果光标在第一个 message 之后, 或者在第一个 message 行的末尾,那么:
#+begin_src emacs-lisp (message "one")▮ (message "two") #+end_src
将块会在 (message "one") 之后 拆分,并将光标移动到拆分后的块之间:
#+begin_src emacs-lisp (message "one") #+end_src ▮ #+begin_src emacs-lisp (message "two") #+end_src
- 否则(如果光标在行首),
- 在光标处拆分块.
因此,如果光标位于第二个 message 行的开头:
#+begin_src emacs-lisp (message "one") ▮(message "two") #+end_src
会在 (message "two") 之前 拆分块,并将光标移动到拆分后的块之间:
#+begin_src emacs-lisp (message "one") #+end_src ▮ #+begin_src emacs-lisp (message "two") #+end_src
下面是遵循该规范的代码:
(defun modi/org-split-block () "Sensibly split the current Org block at point." (interactive) (if (modi/org-in-any-block-p) (save-match-data (save-restriction (widen) (let ((case-fold-search t) (at-bol (bolp)) block-start block-end) (save-excursion (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") block-end "nn" block-start (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")))
以合理的方式分割当前的Org块的函数
- 抽取 block-start的正则表达式与代码快 1 中的block-begin-re一样, 只是子组不同而已.
- block-end字符串脱胎于- block-start字符串中的1号子组 -- 只是把 “begin_” 替换成 “end_”而已.
- 然后基于光标是否在行首 (at-bol), 执行对应的换行插入和光标移动.
让 M-return 执行工作
运行两个函数后,就可以直接通过 M-x modi/org-split-block 完成工作了.
但是这样有什么意思呢?
我需要一个直观的快捷键来拆分块 -- 比如 M-return.
- 默认情况下, M-return 根据上下文环境执行创建新标题,插入项目,将区域转换成表格等工作. 查看 org-meta-return的doc-string(默认该快捷键绑定的函数) 来获取更多信息.
- 但是它的上下文不包括 “光标在Org块中”. 因此这时它会插入一个标题,显得莫名其妙.
- 因此我们需要通过添加上下文环境来解决这个问题.
因此我 建议 org-meta-return 在光标位于Org块中时调用 modi/org-split-block.
建议函数 modi/org-meta-return 与被建议函数 org-meta-return 一样, 除了添加了一个新的上下文 (modi/org-in-any-block-p).
你可以通过将 ((modi/org-in-any-block-p) #'modi/org-split-block) 移动到 cond 中来添加这个新的上下文.
(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 `org-insert-heading'." (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 为 org-meta-return 添加光标处于Org块中时的上下文
建议函数添加光标处于Org块中时的上下文
现在当光标位于Org块中时, 按下 M-return 就可以了!
完整的代码
请查看我Emacs配置 setup-org.el 文件 中 modi/org-split-block (及其依赖函数)的源代码。
