输入法的奇妙冒险:不灭 coc
develop ime
在上一篇文章中我们实现了一个命令行输入法。老实说,笔者在一开始也没有绝对的把握可以写出那个代码,但还是抱着一往无前的勇气去做这件事了。有道是:
人类的赞歌是勇气的赞歌,人类的伟大是勇气的伟大。
– 威廉·安东尼奥·齐贝林《JoJo 的奇妙冒险》
好了,本篇中我们来实现了一个编辑器输入法。这并不是一个新概念,笔者甚至专门在 ime.nvim 中总结了一下:
对于拥有多模式的编辑器(例如 Vim ),我们必须要在退出插入模式后也退出输入法的输入汉字的模式,才能保证 ASCII 按键被接收到。
一种简单的思路就是直接在 Vim 中实现一个输入法,从而轻松实现这一功能。
我们开始吧。
Vim 插件开发
(Neo)Vim 的插件开发大概有以下几个流派:
- Vim script: 纯正的 Vim 老玩家
- lua/teal: 纯正的 NeoVim 用户
- nodejs/ts: 从 VS Code 社区转的,国人居多
- denojs/ts: 日本 vim-jp 社区很喜欢
- fennel: 从 Emacs 社区转的
- C/Rust: 邪教,走 FFI, 人数非常少
- python/ruby/…: 人数不多, ruby 之父就很喜欢用 ruby 写 Vim 插件
如何查看你的 Vim 到底用了那些语言开发的插件呢?一个方法就是 psutils 的 pstree:
$ pstree
...
│ ├─tmux: server─┬─zsh───nvim─┬─nvim─┬─node─┬─TabNine─┬─TabNine─┬─TabNine-deep-lo───47*[{TabNine-deep-lo}]
│ │ │ │ │ │ │ └─62*[{TabNine}]
│ │ │ │ │ │ ├─WD-TabNine───18*[{WD-TabNine}]
│ │ │ │ │ │ └─31*[{TabNine}]
│ │ │ │ │ ├─marksman───16*[{marksman}]
│ │ │ │ │ ├─2*[node───9*[{node}]]
│ │ │ │ │ ├─node───14*[{node}]
│ │ │ │ │ └─22*[{node}]
│ │ │ │ ├─python3.12
│ │ │ │ ├─xsel
│ │ │ │ └─9*[{nvim}]
│ │ │ └─2*[{nvim}]
│ │ └─zsh───pstree
...
neovim 启动了一个子进程 nvim --embed 负责 msgpack-rpc 通信,然后又和 nodejs,
python, xsel 通信以确保用 nodejs 和 python 写的插件和剪切板可以正常工作。之后
nodejs 又和 TabNine 和 marksman 两个语言服务器工作,分别是基于神经网络的补全预测和专门对 markdown 的 LSP 支持。(因为笔者正在写 markdown )。 python 是因为用了
Ultisnips 这个插件的缘故。除此之外用 lua 写的插件全部由 neovim 再开子线程来服务了。
Nodejs 模块
我们尝试用 nodejs 开发一个输入法插件。原因如下:
- 该语言必须支持用 C 编写模块才可以使用 librime
- nodejs 有一个框架 coc.nvim 移植了大量 VS Code 的 API 到 Vim 上,开发者迁移的学习成本很低
- 笔者当初也只会用这个语言写 Vim 插件 QAQ
和第二篇文章一样,我们需要用 C 语言开发一个 nodejs 模块。有 3 种方法:
- node.h: nodejs 自带, API 不稳定。不建议
- node_api.h: nodejs 自带,适用于 C 语言
- napi.h: 适用于 C++ 语言
一通操作猛如虎后我们总算有了:
import { default as binding } from 'coc-rime/lib/binding'
另外与 python, lua 不一样, npm 不支持直接托管二进制格式的包,所以还得用 prebuildify 来打包二进制模块。
Coc.nvim 插件
什么是 coc.nvim ?
coc.nvim 是一个将 VS Code API 移植到 (Neo)Vim 端的 nodejs 库。换句话说,以前为 VS Code 开发插件,代码是这样写的:
import { ExtensionContext, window } from 'vscode';
export async function activate(context: ExtensionContext): Promise<void> {
window.showInformationMessage("hello, world!")
}
现在为 Vim 开发插件,代码就变成了:
import { ExtensionContext, window } from 'coc.nvim';
export async function activate(context: ExtensionContext): Promise<void> {
window.showInformationMessage("hello, world!")
}
coc.nvim 的设计最大的便利其实是允许 Vim 开发者快速将 VS Code 插件移植到 Vim 端。笔者就亲自进行了一些尝试。
所以要想弄明白基于 coc.nvim 的 Vim 插件开发方式,就需要先对 VS Code 插件开发有个大概的了解。 为此,笔者鼓励从 VS Code 官方教程开始,先了解相关基本概念。然后再使用 create-coc-extension 来自己创建 coc.nvim 插件。
那么两种开发最大的区别是什么呢?
- 大部分 API 相似而非所有。有一些 VS Code API 是 coc.nvim 到现在因为难度还没有添加的或结构的不同不能添加的。比如 VS Code 的命令 vscode.terminal 等等。同样也有一些 API 是 Vim 具有的,比如自动事件、多模式的快捷键等等。还有一些 API 是有差异的。例如 VSCode 的 webview API 是在编辑器窗口分割一个并排的窗口显示网页,例如 LaTeX workshop 显示编译后的 pdf 、 markdown-preview 显示渲染后的 markdown 、 graphviz 显示编译后的流程图等等。而换成 coc.nvim 的对应插件,它们使用的 coc-webview 就是另开一个网页浏览器显示这些内容了。
- 分发方式不同。 VS Code 插件通过微软闭源的 VS Code Marketplace 和开源的 openvsx 分发。而 coc.nvim 插件直接使用 npm 分发。类似的 coc-marketplace 也只是从 npm 过滤了所有包含 coc.nvim 关键词的 npm 插件而已。
- 使用 npm 也导致了一些开发方式的不同。比如 VS Code 插件打包时使用语法形如 .gitignore 的 .vscodeignore 。而 coc.nvim 使用 .npmignore 。 VS Code 插件需要 vsce package 打包和 vsce publish 发布。 coc.nvim 插件则需要 npm package 和 npm publish 。
从某种意义上, coc.nvim 插件开发更像 npm 包开发。开发者往往会倾向于使用更多 npm 包开发的常见技术,诸如用 esbuild 转译打包缩小体积等等。
coc.nvim 并不是唯一使用 js 开发 Vim 插件的技术。还有:
- neovim: coc.nvim 实际上是在此之上的一层封装。
- denops.vim: vim-jp 日本社区模仿 coc.nvim 开发出的一套新的基于 deno 的 Vim 插件开发框架。
回到 Vim 这边。笔者以为 Vim 和 VS Code 在设计上最大的区别其实不是:
- 多模态编辑
- 可以使用 js 以外的其他语言开发 Vim 插件
- 内置的 vim script (lua) 解释器
而是 Vim 倾向于让用户界面所有的元素都是一个缓冲区,窗口和标签页也只是缓冲区的内容呈现。文件浏览器、 quickfix 窗口、终端模拟器也不过是特定的只读缓冲区的窗口。所有的缓冲区都被统一管理。
而 VS Code 所有的元素都是容器。容器与容器是互相独立的。文件浏览器、终端模拟器、编辑器都是不同的容器,快捷键并不统一。难以做到像 Vim 一样灵活的窗口布局(例如一个屏幕上多个文件浏览器)或者一个命令统一修改所有容器的快捷键。换言之,每个容器是一个自己的“独立王国”。
考虑到无论用哪种语言开发插件,使用的 (Neo)Vim API 都是一样的,笔者将在下一篇文章中详细介绍相关内容。
本文代码开源于 coc-rime 。考虑到 coc.nvim 的 API 和 VS Code 的相似性,有兴趣的 VS Code 友友们也可以试着移植该插件哟(笔者会考虑把与 Vim 无关的 rime 代码单独封装成一个 npm 模块滴)
![]() |
![]() |
![]() |


