输入法的奇妙冒险:不灭 coc

01 Jan 2025 3373 words 12 minutes BY-SA 4.0
develop ime

在上一篇文章中我们实现了一个命令行输入法。老实说,笔者在一开始也没有绝对的把握可以写出那个代码,但还是抱着一往无前的勇气去做这件事了。有道是:

人类的赞歌是勇气的赞歌,人类的伟大是勇气的伟大。

– 威廉·安东尼奥·齐贝林《JoJo 的奇妙冒险》

好了,本篇中我们来实现了一个编辑器输入法。这并不是一个新概念,笔者甚至专门在 ime.nvim 中总结了一下:

对于拥有多模式的编辑器(例如 Vim ),我们必须要在退出插入模式后也退出输入法的输入汉字的模式,才能保证 ASCII 按键被接收到。

IME outside Vim

一种简单的思路就是直接在 Vim 中实现一个输入法,从而轻松实现这一功能。

IME inside Vim

我们开始吧。

Vim 插件开发

(Neo)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 语言开发一个 nodejs 模块。有 3 种方法:

一通操作猛如虎后我们总算有了:

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 插件。

那么两种开发最大的区别是什么呢?

从某种意义上, coc.nvim 插件开发更像 npm 包开发。开发者往往会倾向于使用更多 npm 包开发的常见技术,诸如用 esbuild 转译打包缩小体积等等。

coc.nvim 并不是唯一使用 js 开发 Vim 插件的技术。还有:

回到 Vim 这边。笔者以为 Vim 和 VS Code 在设计上最大的区别其实不是:

而是 Vim 倾向于让用户界面所有的元素都是一个缓冲区,窗口和标签页也只是缓冲区的内容呈现。文件浏览器、 quickfix 窗口、终端模拟器也不过是特定的只读缓冲区的窗口。所有的缓冲区都被统一管理。

而 VS Code 所有的元素都是容器。容器与容器是互相独立的。文件浏览器、终端模拟器、编辑器都是不同的容器,快捷键并不统一。难以做到像 Vim 一样灵活的窗口布局(例如一个屏幕上多个文件浏览器)或者一个命令统一修改所有容器的快捷键。换言之,每个容器是一个自己的“独立王国”。

考虑到无论用哪种语言开发插件,使用的 (Neo)Vim API 都是一样的,笔者将在下一篇文章中详细介绍相关内容。

本文代码开源于 coc-rime 。考虑到 coc.nvim 的 API 和 VS Code 的相似性,有兴趣的 VS Code 友友们也可以试着移植该插件哟(笔者会考虑把与 Vim 无关的 rime 代码单独封装成一个 npm 模块滴)

https://user-images.githubusercontent.com/32936898/199681341-1c5cfa61-4411-4b67-b268-7cd87c5867bb.png https://user-images.githubusercontent.com/32936898/199681363-1094a0be-85ca-49cf-a410-19b3d7965120.png https://user-images.githubusercontent.com/32936898/199681368-c34c2be7-e0d8-43ea-8c2c-d3e865da6aeb.png