一个增强 abbrev-mode 在中文环境下体验的构想
2024-02-09
最近 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