- 1 课程回顾
- 2 Org mode代码块概述
- 3 Org mode代码块基本配置
- 4 emacs-lisp代码块设置
- 5 Python代码块设置
- 6 shell代码块设置
- 7 限定我们代码块的结果行数
- 8 关于括号显示的增强
- 8.1 高亮匹配的括号
- 8.2 不同层级的括号用不同的颜色显示
- 9 结语
1 课程回顾 🔗
我们在上节课学习了如何在Emacs里通过 magit
插件来对我们的文件做版本管理,今后我们就可以通过 magit
+ github
(甚至你可以自己搭建一个 git 服务器)来管理你想管理的任何文件,其中强烈建议大家将配置文件管理起来。
这节课,我们来聊一聊Org mode里的代码块。
2 Org mode代码块概述 🔗
我们在 第2课:Org mode初识 - Org mode能干嘛 - 文学编程 这个章节已经简单介绍过Org mode里的代码块。它非常的强大!
今天,我们对代码块进行一些个性化的配置,让它更加好用。
Org mode的代码块着实强大,不同的后端(backend)能提供不同的功能,有的可以让你在代码块里画图,有的可以让你在代码块里写Python,有的可以让你在代码块里写Shell,有的甚至可以让你在代码块里实现API请求的测试。
我们今天的课程,不涉及第三方的代码块,所有第三方的代码块后端,将在后续课程里按照场景的方式进行介绍。
3 Org mode代码块基本配置 🔗
我们先对代码块做一个基本配置:
(use-package org-src
:ensure nil
:hook (org-babel-after-execute . org-redisplay-inline-images)
:bind (("s-l" . show-line-number-in-src-block)
:map org-src-mode-map
("C-c C-c" . org-edit-src-exit))
:init
;; 设置代码块的默认头参数
(setq org-babel-default-header-args
'(
(:eval . "never-export") ; 导出时不执行代码块
(:session . "none")
(:results . "replace") ; 执行结果替换
(:exports . "both") ; 导出代码和结果
(:cache . "no")
(:noweb . "no")
(:hlines . "no")
(:wrap . "results") ; 结果通过#+begin_results包裹
(:tangle . "no") ; 不写入文件
))
:config
;; ==================================
;; 如果出现代码运行结果为乱码,可以参考:
;; https://github.com/nnicandro/emacs-jupyter/issues/366
;; ==================================
(defun display-ansi-colors ()
(ansi-color-apply-on-region (point-min) (point-max)))
(add-hook 'org-babel-after-execute-hook #'display-ansi-colors)
;; ==============================================
;; 通过overlay在代码块里显示行号,s-l显示,任意键关闭
;; ==============================================
(defvar number-line-overlays '()
"List of overlays for line numbers.")
(defun show-line-number-in-src-block ()
(interactive)
(save-excursion
(let* ((src-block (org-element-context))
(nlines (- (length
(s-split
"\n"
(org-element-property :value src-block)))
1)))
(goto-char (org-element-property :begin src-block))
(re-search-forward (regexp-quote (org-element-property :value src-block)))
(goto-char (match-beginning 0))
(cl-loop for i from 1 to nlines
do
(beginning-of-line)
(let (ov)
(setq ov (make-overlay (point) (point)))
(overlay-put ov 'before-string (format "%3s | " (number-to-string i)))
(add-to-list 'number-line-overlays ov))
(next-line))))
;; now read a char to clear them
(read-key "Press a key to clear numbers.")
(mapc 'delete-overlay number-line-overlays)
(setq number-line-overlays '()))
;; =================================================
;; 执行结果后,如果结果所在的文件夹不存在将自动创建
;; =================================================
(defun check-directory-exists-before-src-execution (orig-fun
&optional arg
info
params)
(when (and (assq ':file (cadr (cdr (org-babel-get-src-block-info))))
(member (car (org-babel-get-src-block-info)) '("mermaid" "ditaa" "dot" "lilypond" "plantuml" "gnuplot" "d2")))
(let ((foldername (file-name-directory (alist-get :file (nth 2 (org-babel-get-src-block-info))))))
(if (not (file-exists-p foldername))
(mkdir foldername)))))
(advice-add 'org-babel-execute-src-block :before #'check-directory-exists-before-src-execution)
;; =================================================
;; 自动给结果的图片加上相关属性
;; =================================================
(setq original-image-width-before-del "400") ; 设置图片的默认宽度为400
(setq original-caption-before-del "") ; 设置默认的图示文本为空
(defun insert-attr-decls ()
"insert string before babel execution results"
(insert (concat "\n#+CAPTION:"
original-caption-before-del
"\n#+ATTR_ORG: :width "
original-image-width-before-del
"\n#+ATTR_LATEX: :width "
(if (>= (/ (string-to-number original-image-width-before-del) 800.0) 1)
"1.0"
(number-to-string (/ (string-to-number original-image-width-before-del) 800.0)))
"\\linewidth :float nil"
"\n#+ATTR_HTML: :width "
original-image-width-before-del
)))
(defun insert-attr-decls-at (s)
"insert string right after specific string"
(let ((case-fold-search t))
(if (search-forward s nil t)
(progn
;; (search-backward s nil t)
(insert-attr-decls)))))
(defun insert-attr-decls-at-results (orig-fun
&optional arg
info
param)
"insert extra image attributes after babel execution"
(interactive)
(progn
(when (member (car (org-babel-get-src-block-info)) '("mermaid" "ditaa" "dot" "lilypond" "plantuml" "gnuplot" "d2"))
(setq original-image-width-before-del (number-to-string (if-let* ((babel-width (alist-get :width (nth 2 (org-babel-get-src-block-info))))) babel-width (string-to-number original-image-width-before-del))))
(save-excursion
;; `#+begin_results' for :wrap results, `#+RESULTS:' for non :wrap results
(insert-attr-decls-at "#+begin_results")))
(org-redisplay-inline-images)))
(advice-add 'org-babel-execute-src-block :after #'insert-attr-decls-at-results)
;; 再次执行时需要将旧的图片相关参数行删除,并从中头参数中获得宽度参数,参考
;; https://emacs.stackexchange.com/questions/57710/how-to-set-image-size-in-result-of-src-block-in-org-mode
(defun get-attributes-from-src-block-result (&rest args)
"get information via last babel execution"
(let ((location (org-babel-where-is-src-block-result))
;; 主要获取的是图示文字和宽度信息,下面这个正则就是为了捕获这两个信息
(attr-regexp "[:blank:]*#\\+\\(ATTR_ORG: :width \\([0-9]\\{3\\}\\)\\|CAPTION:\\(.*\\)\\)"))
(setq original-caption-before-del "") ; 重置为空
(when location
(save-excursion
(goto-char location)
(when (looking-at (concat org-babel-result-regexp ".*$"))
(next-line 2) ; 因为有个begin_result的抽屉,所以往下2行
;; 通过正则表达式来捕获需要的信息
(while (looking-at attr-regexp)
(when (match-string 2)
(setq original-image-width-before-del (match-string 2)))
(when (match-string 3)
(setq original-caption-before-del (match-string 3)))
(next-line) ; 因为设置了:wrap,所以这里不需要删除这一行
)
)))))
(advice-add 'org-babel-execute-src-block :before #'get-attributes-from-src-block-result)
:custom
;; 代码块语法高亮
(org-src-fontify-natively t)
;; 使用编程语言的TAB绑定设置
(org-src-tab-acts-natively t)
;; 保留代码块前面的空格
(org-src-preserve-indentation t)
;; 代码块编辑窗口的打开方式:当前窗口+代码块编辑窗口
(org-src-window-setup 'reorganize-frame)
;; 执行前是否需要确认
(org-confirm-babel-evaluate nil)
;; 代码块默认前置多少空格
(org-edit-src-content-indentation 0)
;; 代码块的语言模式设置,设置之后才能正确语法高亮
(org-src-lang-modes '(("C" . c)
("C++" . c++)
("bash" . sh)
("cpp" . c++)
("elisp" . emacs-lisp)
("python" . python)
("shell" . sh)
("mysql" . sql)
))
;; 在这个阶段,只需要加载默认支持的语言
(org-babel-load-languages '((python . t)
(awk . t)
(C . t)
(calc . t)
(emacs-lisp . t)
(eshell . t)
(shell . t)
(sql . t)
(css . t)
))
)
上面的配置里有几个要点:
- 我个人经常使用代码块画图,如
plantuml
,d2
等,这些代码块都会通过:file
头参数指定一个输出的图片文件路径。因此,配置里有关于这部分的优化,主要有- 如果输出的图片不在当前目录,如放在
xxxx.assets
文件夹里时,自动创建该目录; - 自动给图片加上
#+attr_org:
,#+attr_html:
,#+attr_latex:
等属性,并根据头参数里的:width
参数自动设置,这样在导出时会按照设定的宽度导出;
- 如果输出的图片不在当前目录,如放在
- 想对所有代码块默认生效的头参数,可以统一设置在
org-babel-default-header-args
这个变量里; - 代码块的语法高亮需要在
org-src-lang-modes
里设置,如jupyter-python
的语法高亮需要设置成python
; - Org mode默认启动的代码块语言仅仅有
emacs-lisp
,我们需要将我们想用到的代码块语言加到这个列表中去;
配置完后,我们可以在org文件里写一些代码块,光标可以在代码块内,按下 C-c C-c
执行这个代码块,Org mode会自动执行代码块并输出答案。这里因为设置了 :wrap
参数默认为 results
,所以它会把结果自动包裹在 #+begin_results
和 #+end_results
的区域里:
经过这个基本配置,我们修改了代码块默认空格2个字符的行为,所有的代码都会顶格,这样看上去舒服多了。
下面这张图是我们用 d2
代码块画图的一个例子,可以先看下效果,先不用急,后面我会专门写一篇如何通过Org mode的纯文本来画图的教程:
4 emacs-lisp代码块设置 🔗
在Emacs里, emacs-lisp
当然是最常用的代码块了,我们所有的配置文件都是通过 emacs-lisp
的代码块来管理的。下面我们对 emacs-lisp
语言进行一些设置:
(use-package elisp-mode
:ensure nil
:after org
:bind (:map emacs-lisp-mode-map
("C-c C-b" . eval-buffer)
("C-c C-c" . eval-to-comment)
:map lisp-interaction-mode-map
("C-c C-c" . eval-to-comment)
:map org-mode-map
("C-c C-;" . eval-to-comment)
)
:init
;; for emacs-lisp org babel
(add-to-list 'org-babel-default-header-args:emacs-lisp
'(:results . "value pp"))
:config
(defconst eval-as-comment-prefix " ⇒ ")
(defun eval-to-comment (&optional arg)
(interactive "P")
;; (if (not (looking-back ";\\s*"))
;; (call-interactively 'comment-dwim))
(call-interactively 'comment-dwim)
(progn
(search-backward ";")
(forward-char 1))
(delete-region (point) (line-end-position))
(save-excursion
(let ((current-prefix-arg '(4)))
(call-interactively 'eval-last-sexp)))
(insert eval-as-comment-prefix)
(end-of-line 1))
)
设置完 emacs-lisp
后,我们可以在代码块里按下 C-c C-;
来执行一个s表达式,通过上面设置的 eval-to-comment
函数,会自动在这个s表达式的后添加一个注释,并输出执行的结果,非常方便:
5 Python代码块设置 🔗
产品经理经常也会需要写一些简单的Python脚本,因此,我们对Python的代码块也做一个配置,其中我们安装了一个插件 py-autopep8.el ,它可以让我们在编写Python保存文件的时候,自动以 PEP8
标准做格式美化(需要提前 brew install autopep8
)。
这里有一个小的函数 add-list-to-list
,这个函数可以让我们不用一行一行的通过 add-to-list
将元素添加到列表,非常方便,我们设置 org-babel-default-header-args:python
这个变量的时候就会用到这个函数,我们可以把这个函数放到 init-base.el
这个标题行下,我们后续有一些非常便利的小函数都可以放到那里。
;; 将列表加入到列表的函数
(defun add-list-to-list (dst src)
"Similar to `add-to-list', but accepts a list as 2nd argument"
(set dst
(append (eval dst) src)))
(use-package python
:ensure nil
:mode ("\\.py\\'" . python-mode)
:hook ((inferior-python-mode . my/buffer-auto-close))
:init
(add-list-to-list 'org-babel-default-header-args:python '((:results . "output pp")
(:noweb . "yes")
(:session . "py")
(:async . "yes")
(:exports . "both")
))
:config
(setq python-indent-offset 4)
;; Disable readline based native completion
(setq python-shell-completion-native-enable nil)
(setq python-indent-guess-indent-offset-verbose nil)
(setq python-shell-interpreter "python3"
python-shell-prompt-detect-failure-warning nil)
;; disable native completion
(add-to-list 'python-shell-completion-native-disabled-interpreters "python3")
;; Env vars
(with-eval-after-load 'exec-path-from-shell
(exec-path-from-shell-copy-env "PYTHONPATH"))
)
;; need to pip install autopep8 first
(use-package py-autopep8
:ensure t
:hook (python-mode . py-autopep8-mode)
)
可以看到,我们的Python代码块,设置默认开启了 :session
这个功能,这个功能可以让我们在不同的Python代码块里共享变量等定义:
我们也默认开启了 :noweb
功能,通过这个功能,我们可以在某个代码块里,引用另外一个代码块的代码,非常灵活:
为什么要用Emacs来写Python代码呢?因为他可以像 Jupyter notebook
一样,让你一个代码块一个代码块的写,跟着你的思路一步一步的实现你想要的功能。这对于产品经理而言,尤为重要,因为产品经理不要求代码写的多么漂亮,多么鲁棒,产品经理需要的是快速有效的实现。
6 shell代码块设置 🔗
shell脚本是我们经常会用到的另外一种代码块,下面我们设置一下:
(use-package sh-script
:ensure nil
:mode (("\\.sh\\'" . sh-mode)
("zshrc" . sh-mode)
("zshenv" . sh-mode)
("/PKGBUILD\\'" . sh-mode))
:hook (sh-mode . sh-mode-setup)
:bind (:map sh-mode-map
("C-c C-e" . sh-execute-region))
:init
;; for org babel
(add-list-to-list 'org-babel-default-header-args:shell
'((:results . "output")
(:tangle . "no")))
:config
(defun sh-mode-setup ()
(add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p nil t))
:custom
(sh-basic-offset 2)
(sh-indentation 2))
然后我们可以就可以愉快的在Org mode里写shell脚本或执行shell命令啦:
7 限定我们代码块的结果行数 🔗
我们有的时候通过代码块执行一些 shell 命令,常常会发现太长了,几百行的结果,此时我们需要对代码块的结果进行一个限制(当然,你也可以不用限制),下面这段代码能实现这个需求,代码来自于 twlz0ne 大佬在这篇贴子的回复:
;; limit the babel result length
(defvar org-babel-result-lines-limit 40)
(defvar org-babel-result-length-limit 6000)
(defun org-babel-insert-result@limit (orig-fn result &rest args)
(if (not (member (car (org-babel-get-src-block-info)) '("jupyter-python"))) ; not for jupyter-python etc.
(if (and result (or org-babel-result-lines-limit org-babel-result-length-limit))
(let (new-result plines plenght limit)
(with-temp-buffer
(insert result)
(setq plines (if org-babel-result-lines-limit
(goto-line org-babel-result-lines-limit)
(point-max)))
(setq plenght (if org-babel-result-length-limit
(min org-babel-result-length-limit (point-max))
(point-max)))
(setq limit (min plines plenght))
(setq new-result (concat (buffer-substring (point-min) limit)
(if (< limit (point-max)) "..."))))
(apply orig-fn new-result args))
(apply orig-fn result args))
(apply orig-fn result args)))
(advice-add 'org-babel-insert-result :around #'org-babel-insert-result@limit)
设置完这个后,我们看到当我们 ls -al ~/
时,我们的home目录下有1488个文件和文件夹,但代码块的执行结果只会输出40行,非常方便:
8 关于括号显示的增强 🔗
在 emacs-lisp 里,所有的语句都是通过括号括起来的,括号嵌套一多,我们很多时候就就很难区分了,我们可以看下Emacs在默认设置下括号嵌套多的时候代码块里的效果:
我们来设置优化一下括号的显示。
8.1 高亮匹配的括号 🔗
(use-package paren
:ensure nil
:hook (after-init . show-paren-mode)
:custom
(show-paren-when-point-inside-paren t)
(show-paren-when-point-in-periphery t))
8.2 不同层级的括号用不同的颜色显示 🔗
rainbow-delimiters 插件将多彩显示括号等分隔符。
(use-package rainbow-delimiters
:ensure t
:hook (prog-mode . rainbow-delimiters-mode))
设置完这两个插件后,我们可以看到,所有不同层级的括号,都用了不同的颜色区分,光标放在括号里和括号外,都会自动高亮匹配的括号对,增强了括号显示的视觉效果:
9 结语 🔗
经过今天的学习,我们了解了在Org mode里写代码块的一些知识,我们可以愉快的在Org mode里写emacs-lisp、shell、python等代码了,而且我们对代码块相关的设置也做了一些强化,如括号的高亮和多彩显示等。这对于我们通过Org mode来构建自己的知识体系是非常有帮助的。
这节课的配置文件的快照见:emacs-config-l14.org
你也可以在 这里 查看最新的配置文件。