racket-ex-mode + org-src-mode: racket 文学编程中的问题

新人入坑,neovimmer 想趁着学 SICP 的机会入坑 emacs。在费劲千辛万苦把 doomemacs 安装在我的 Windows 之后,我终于可以开始做练习题了。注释 + 源代码写着还是有点不舒服,尤其 SICP 的练习题也不全是敲代码 – 于是我转向了 org-mode。

#+begin_src racket :lang sicp
(define (sum-of-maxes x y z)
  (cond [(> x y) (+ (max y z) x)]
        [(> y z) (+ (max x z) y)]
        [(> z x) (+ (max x y) z)]))
#+end_src

这很棒了,C-c C-c 可以直接运行。C-c ' 也可以直接打开编辑。可是默认打开的时候没有设置 racket-ex-mode,被 LS 惯坏了的我怎么能容忍这种事情发生!

新的问题又出现了。熟悉 racket 的人都知道,这不是 racket 的语法,而打开的 buffer 里面又没有 #lang sicp,这不就报错了嘛。

什么!想让我每次都在里面添加 #lang sicp?那怎么行!我宁可把上面中括号改成小括号都不会干这事的。

最后我拷打 GLM 5.0,写出了自动在 org-src-mode 时添加/删除 #lang 的设置。思路是打开新 buffer 时候用 hook 解析 #+begin_src racket 的参数并添加,用 define-advice org-edit-src-exit hook 关闭 buffer 的时候删除。

(defun my/org-src-header-args ()
  (let* ((el (org-src-do-at-code-block (org-element-context)))
         (params-string (org-element-property :parameters el)))
    (org-babel-parse-header-arguments params-string)))

(defvar-local my/org-src--racket-auto-lang nil
  "The name of the #lang automatically inserted, or nil if none.")

(defun my/racket-src-mode-auto-lang-shebang (lang)
  "Add #lang line in org-src-mode + racket-mode by org-element-context,
when LANG argument matches the `:lang' param in org-element.
Match any language if LANG is empty string.

Set `my/org-src--racket-auto-lang' locally."
  (when (derived-mode-p 'racket-mode)
    (let ((header-lang (alist-get :lang (my/org-src-header-args))))
      (when (and (stringp header-lang) (or (string= lang header-lang) (string= lang "")))
        (save-excursion
          (goto-char (point-min))
          (unless (re-search-forward "^#lang" nil t)
            (insert (format "#lang %s\n" header-lang))
            (setq-local my/org-src--racket-auto-lang header-lang)))
        (racket-xp-mode +1)))))

(define-advice org-edit-src-exit (:before () my-cleanup-auto-lang)
  "Delete #lang line in org-src-mode + racket-mode, which is set by
`my/org-src-header-args'. Do nothing if `my/org-src--racket-auto-lang' is not
set."
  (when (and (stringp my/org-src--racket-auto-lang)
             (derived-mode-p 'racket-mode))
    (save-excursion
      (goto-char (point-min))
      (when (looking-at-p (format "#lang %s\n" my/org-src--racket-auto-lang))
        (delete-line)))))

(after! org
  ;; (add-hook 'org-src-mode-hook (lambda () (my/racket-src-mode-auto-lang-shebang "sicp")))
  (add-hook 'org-src-mode-hook (lambda () (my/racket-src-mode-auto-lang-shebang "")))
)

我舒服多了。

请问各位有没有更好的解决办法?

2 个赞

也许你可以定义一个org-babel-edit-prep:函数,大概这样子:

(defun org-babel-edit-prep:racket (info)
  (when-let ((lang (alist-get ':lang (nth 2 info))))
    (insert "#lang " lang))

我没试过。解析(nth 2 info)应该可以得到你的header params,使用什么key我不确定。

(add-hook 'racket-mode-hook #'racket-ex-mode)
1 个赞

可能是没有体现出来,我想请教的问题是开启了 racket-ex-mode 之后的内容。顺带一提,doomemacs 自动使用 racket-ex-mode 的解决方案是在 $DOOMDIR/init.el 里面把 racket 改成 (racket +xp) 或者 (racket +xp +hash-lang).

C-c ' 打开的 buffer 默认是 racket-mode,没有静态解析所以也不会报错;问题是打开 racket-ex-mode 之后发生的。

顺带一评这个问题就是因为 racket 支持编写自己的 DSL 而且 racket-mode 支持在 org-mode 中使用注释属性声明 #lang 造成的。要是从一开始就不支持 org-mode 属性声明 :lang 我也不会在这里抱怨这事而是像这样乖乖使用 snippets 了:

#+begin_src racket
#lang sicp
$1
#+end_src

这种问题听上去也许挺常见的,我看论坛有个 2019 年的帖子也在讨论用 racket 来做 SICP。所以我想问问有没有人同样遇到过问题,还是说其实这个问题已经被解决。

我发现之前的 my/org-src-header-args 函数不支持 #+property: header-args:racket :lang sicp,于是找到了另一个 API:

(defun my/org-src-header-args ()
  (org-src-do-at-code-block (nth 2 (org-babel-get-src-block-info))))

我发现 bug 了,是 org C-c C-c 执行代码的问题。最后一行不能有行注释,否则就会报错。行为类似内部使用字符串插值拼接执行函数造成的错误。

#+begin_src racket
(define (new-if predicate then-clause else-clause)
  (cond [predicate then-clause]
        [else else-clause]))
(if #t 1 (display 2)) ; evals 1
(new-if #t 1 (display 2)) ; evals 1 and print 2
#+end_src

#+RESULTS:
: c:\Users\xxx\AppData\Local\Temp\org-babel-DUN7dj.rkt:3:7: read-syntax: expected a `)` to close `(`
:   possible cause: indentation suggests a missing `)` before line 5
:   context...:
:    D:\scoop\apps\racket\current\collects\syntax\module-reader.rkt:214:17: body
:    D:\scoop\apps\racket\current\collects\syntax\module-reader.rkt:211:2: wrap-internal
:    .../syntax/module-reader.rkt:76:9: wrap-internal/wrapper

#+begin_src racket
(define (new-if predicate then-clause else-clause)
  (cond [predicate then-clause]
        [else else-clause]))
(if #t 1 (display 2)) ; evals 1
(new-if #t 1 (display 2))
#+end_src

#+RESULTS:
: 21

#+begin_src racket
(define (new-if predicate then-clause else-clause)
  (cond [predicate then-clause]
        [else else-clause]))
(if #t 1 (display 2)) ; evals 1
(new-if #t 1 (display 2)) ; evals 1 and print 2

#+end_src

#+RESULTS:
: 21

(等我哪天有空了就去提个 issue 或者 pr)