hsingko


一个增强 abbrev-mode 在中文环境下体验的构想

最近 prot 发了一个关于 abbrev-mode 的视频。介绍的东西很基础,主要是如何通过 elisp 代码定义 abbrev 。所谓的 abbrev 相当于一些输入法的“快速短语”,比如输入“xnkl”就可以替换为“新年快乐”。所以实际上如果使用的是一些能够组词造句的输入法的话, abbrev-mode 并不是那么必须。

然而问题是我使用的是 rime 输入法,并且还是一个组词比较麻烦的音形码,每次想要往词库里添加名词或者短语都很麻烦,我也不懂 rime 的 lua 脚本写法,所以在 emacs 输入中文一直有些卡手,尤其是遇到专用名词比较多的情况。一种常见的情形是看完一本小说之后我想要记笔记,这时就会涉及到书中人物、地点和其它只对于这本书有意义的词语,这些词语几乎不可能出现在默认的输入法词库中。一个个地手打又很麻烦,对于我在使用的“星空键道输入法”来说,长处在于输入常用词语,而在输入专用名词时短板就会很明显了,这一点对于其它非纯拼音的输入法来说都是一样的。

这时 abbrev-mode 就显得非常有价值了。但问题是中文对于英文来说处理起来比较麻烦:

  • 中文并不使用空格分词,而 abbrev-mode 会多出一个空格
  • 英文使用 abbrev 主要是为了快速输入一些常用的短语,而在中文环境中,场景变成了在特定场合使用的名词。换句话说英文环境中 abbrev 比较通用,而中文环境中的 abbrev 最需要的是单一文件定义的 abbrev 。然而问题是,虽然 emacs 中确实有 local-abbrev-table 但使用起来其实是 mode abbrev table ,在这个表中定义的 abbrev 会自动覆盖到其它相同的 mode 中,这显然不是我们想要的

通过查阅资料,我搜集到了以下连接:

第一个链接实现了“真 buffer local abbrev”,第二个链接解决了自动插入空格的问题。既然技术上我想要的中文 abbrev 在理论上可行,那么我就可以逐步来进行实现了。

按照 sicp 中提倡的按愿望思维,我想要的 abbrev 能够支持如下功能:

  • 定义 buffer-local 的 abbrev 表,并且可以支持持久化到对应文件,下次打开文件可以寻找对对应的数据并进行加载
  • 获得 marked region 字符串的拼音首字母,自动生成对应的 abbrev

技术细节:

  • 调用外部程序获得词语的首字母,我选择的是 go-pinyin
  • 具体到 emacs-rime 输入法,中文的特点是词语是紧挨着的没有空格,如果单纯地用字母作为缩写,那么还不得不切换到英文模式。一个相对巧妙的解决方法是统一上数字前缀,比如“1mks”来替代“马克思”,这样会自动触发 emacs-rime 的 ascii 临时模式,自动将缩写变成中文后,输入法又会自动变回中文模式,全程无需切换输入法,非常方便
  • buffername 和 abbrev table 之间的对应关系参考第一个链接,使用 md5 截短的方法,对于文件名来说,碰撞的概率应该比较小,作为 demo 实现足够了
  • 持久化的代码参考 write-abbrev-file ,核心其实就是 insert-abbrev-table-description 会将相应 abbrev table 插入到 buffer 中
  • 之后加载其实就相当于 load 一个 elisp 文件,没有什么难度

完整代码如下:

(add-hook 'text-mode-hook 'abbrev-mode)

(defun dont-insert-expansion-char ()  t)    ;; this is the "hook" function
(put 'dont-insert-expansion-char 'no-self-insert t)   ;; the hook should have a "no-self-insert"-property set

(defun pinyin-get-initial-string (str)
  (replace-regexp-in-string
   "[ \t\n]" ""
   (shell-command-to-string
    (format "pinyin -s z %s" str))))

(defcustom quick-abbrev-prefix "1"
  "quick abbrev prefix")

(defcustom hsk/abbrev-dir "~/.emacs.d/abbrev/"
  "where do you put those buffer local abbrev tables")

(defun set-local-abbrevs (abbrevs)
  "Add ABBREVS
to `local-abbrev-table' and make it buffer local.
ABBREVS should be a list of abbrevs as passed to `define-abbrev-table'.
The `local-abbrev-table' will be replaced by a copy with the new abbrevs added,
so that it is not the same as the abbrev table used in other buffers with the
same `major-mode'."
  (let* ((bufname (buffer-name))
         (prefix (substring (md5 bufname) 0 (length bufname)))
         (tblsym (intern (concat prefix "-abbrev-table"))))
    (set tblsym (copy-abbrev-table local-abbrev-table))
    (dolist (abbrev abbrevs)
      (define-abbrev (eval tblsym)
        (cl-first abbrev)
        (cl-second abbrev)
        (cl-third abbrev)))
    (setq-local local-abbrev-table (eval tblsym))))


(defun hsk/add-str-to-local-abbrev (str)
  (set-local-abbrevs
   `(
     (,(concat quick-abbrev-prefix (pinyin-get-initial-string str))
      ,str dont-insert-expansion-char))))

(defun hsk/add-mark-word-to-local-abbrev (start end)
  (interactive "r")
  (let (str init-str)
    (setq str (buffer-substring start end))
    (hsk/add-str-to-local-abbrev str)))

(defun hsk/read-input-string-to-local-abbrev ()
  (interactive)
  (let ((str (read-string "Type:")))
    (hsk/add-str-to-local-abbrev str)))


(defun consult-local-abbrev-table ()
  (interactive)
  (consult--read local-abbrev-table))


(defun hsk/get-local-abbrev-store-file ()
  (let* ((bufname (buffer-name))
	 (prefix (substring (md5 bufname) 0 (length bufname))))
    (expand-file-name prefix hsk/abbrev-dir)))

(defun hsk/get-local-abbrev-symbol ()
  (let* ((bufname (buffer-name))
         (prefix (substring (md5 bufname) 0 (length bufname))))
    (intern (concat prefix "-abbrev-table"))))


(defun hsk/save-local-abbrev ()
  (interactive)
  (let (abbrev-file tblsym)
    (setq abbrev-file (hsk/get-local-abbrev-store-file))
    (setq tblsym (hsk/get-local-abbrev-symbol))
    (with-temp-buffer
      (insert-abbrev-table-description tblsym nil)
      (goto-char (point-min))
      (insert (format ";;-*-coding: %s;-*-\n" coding-system-for-write))
      (write-region nil nil abbrev-file)
      )))

(defun hsk/load-local-abbrev ()
  (interactive)
  (let (abbrev-file tblsym)
    (setq abbrev-file (hsk/get-local-abbrev-store-file))
    (setq tblsym (hsk/get-local-abbrev-symbol))
    (if (file-exists-p abbrev-file)
	(progn (load abbrev-file)
	       (setq-local local-abbrev-table (eval tblsym))))))

(provide 'init-abbrev)

初步使用已经没有什么问题了,用法是:

  • 打开文件,执行 hsk/load-local-abbrev 加载已经保存的 abbrev 表
  • 新建 abbrev 可以使用两种方法: hsk/add-mark-word-to-local-abbrev 会将选区中的字符自动生成缩写,或者也可以通过手动的方法 hsk/read-input-string-to-local-abbrev
  • 关闭文件前将缩写表保存到文件中: hsk/save-local-abbrev

整个流程已经跑通了,等我再使用一段时间看看具体效果怎么样。

TODO

  • 处理相同缩写字母的问题
  • 自动处理一块区域内的名词,可以通过约定的形式,比如在 orgmode 中将 * Term 条目下的名词自动录入到缩写表中
  • 用 minor-mode 对代码进行包装,方便自动化处理,不必再手动 save/load