此前将自己的 Emacs Org 相关函数用 AI 重新实现了一遍,其中有个功能是统计一天的计时情况。以前的函数只能统计当天的,后来借助 AI 将其扩展了一下,做成了可以统计任意一天时间的函数,同时还有看板以及 TODO 的函数,这些函数统一需要展示一个 org buffer 界面,因此就把这些显示封装了一个函数,具体如下。

(defun my/render-view-buffer (buffer-name title refresh-fn body-fn &optional skip-footer)
  "创建并显示一个只读的 org-mode 视图缓冲区。
BUFFER-NAME: 缓冲区名称
TITLE: 标题(包含emoji图标)
REFRESH-FN: 刷新函数符号(用于 'g' 键)
BODY-FN: 无参函数,负责插入缓冲区主体内容
SKIP-FOOTER: 可选,为 t 时跳过自动添加底部提示(由 body-fn 自行处理)"
  (with-current-buffer (get-buffer-create buffer-name)
    (let ((inhibit-read-only t))
      (erase-buffer)
      (org-mode)
      (insert (format "#+TITLE: %s\n" title))
      (insert (format "#+DATE: %s\n\n" (format-time-string "%Y-%m-%d %A")))

      ;; 调用主体内容生成函数
      (funcall body-fn)

      ;; 统一的底部提示(除非明确跳过)
      (unless skip-footer
        (insert (make-string my/view-separator-width ?) "\n")
        (insert (my/--styled-text "Press 'g' to Refresh | 'q' to Quit" 
                            'font-lock-comment-face)))

      ;; 统一的按键绑定
      (local-set-key (kbd "g") refresh-fn)
      (local-set-key (kbd "q") #'quit-window)

      (goto-char (point-min))
      (setq buffer-read-only t)))
  (pop-to-buffer buffer-name))

这个函数会挂两个函数,分别绑定到 g 和 q,作为刷新和退出的函数。使用起来是这样的。

(defun my/org-clock-report (&optional _match)
    (interactive)
    (let ((date (org-read-date)))
      (my/render-view-buffer
       "*Org Timeline Report*"
       (format "Report of [%s]" date)
       ...)))

(defun my/org-kanban-view (&optional _match)
  (interactive)
  (my/render-view-buffer
   "*Kanban*"
   "📊 Kanban View"
   #'my/kanban-view
   (lambda ()
     (my/render-task-sections (my/get-kanban-tasks)))))

最近发现一个问题,过一段时间,org-mode 里面就不能正常敲 g 了,会变成 my/org-clock-report 的调用,说是绑定到了 org-mode-map, 检查了半天没找到哪里绑定的,然后就重启了,一开始是好的,过一会就又绑定了,特别奇怪。实在找不到了,心想,用 AI 试试,反正这几个函数本来就是 AI 写出来的。于是就把 my/org-clock-report 发给 AI 看,并告诉他:=org-mode= 中的 g 变成了这个函数,无法输入正常的 g 了,可能是什么原因。 AI 回复的第一句就把我惊到了:

这个问题发生的根源,几乎可以肯定隐藏在你的 my/render-view-buffer 这个函数的实现里。

于是我把 my/render-view-buffer 发给 AI,马上他就指出问题了:

找到了!问题出在 local-set-key 这个函数的“陷阱”上。

为什么 g 会在所有 Org 文件里失效?

在 Emacs 中,local-set-key 虽然名字叫“本地”,但它的实现逻辑是:修改当前缓冲区所使用的 Keymap。

重点来了:大多数 Major Mode(包括 org-mode)为了节省内存,所有的 Org 缓冲区都共用同一个 org-mode-map。 当你在这个渲染函数里执行 (local-set-key (kbd “g”) …) 时,你实际上是直接修改了全局的 org-mode-map。结果就是:所有的 Org 文件里的 g 都变成了刷新按钮,导致你无法正常输入字符 g。

;; — 核心修复:创建一个本地 map 副本,防止污染全局 — (use-local-map (copy-keymap org-mode-map))

也就是在做 local-set-key 之前,复制一份本地的 keymap 即可。加上,果然就好了。

不禁特别感慨,不了解这个知识点,可能找 bug 需要找好久好久,也很难知道如何来 debug 。而 AI 在这些问题上的经验已经远远大于人们了,我们确实生在了一个好时代。