把 Rime 输入法装进你的代码编辑器

作品

项目地址: https://github.com/wlh320/rime-ls

前言

这是上一篇文章的后续. 折腾完 neovim 后, 我刚好看到 ds-pinyin-lsp 这个项目, 作者将拼音输入与 LSP 的补全功能结合, 自己实现了拼音打字的逻辑, 将候选作为 LSP 补全的结果返回给编辑器.

这个想法感觉很好也很新鲜, 解决了 vim 这种多模式编辑器与中文输入法的不兼容性. 之前面向系统输入法的解决方案好像没有完全跨平台的, 而且, 专注于实现 LSP 而不是局限于某个编辑器也提供了很强的通用性, 相当于任何一个支持 vim 模式和 LSP 的编辑器都解决了这个问题 (这就是协议的重要性, 也是计算机网络爱好者喜闻乐见的. 看到 VSCode 介绍 LSP 的 那张图 马上就让人想起来 IP 协议了).

我想, 程序员群体里比较流行的 Rime 输入法提供了开源的核心库 librime, 如果能按照思路这个为 Rime 实现 LSP 协议, 通用的输入法框架 + 通用的编辑器支持, 岂不是更通用了?

起步

我去简单找了找有没有类似的项目, 好像还真没有 (等我写完我大概知道为啥没有了, 这是后话). 此外, 我之前学 Rust 刚好只剩 FFI 这块没有实践过了. 感觉这是个很好的机会, 我就参照之前那位作者的实现搞出了第一个能用的版本.

能用是很简单, 怎么让它好用一点才是个大坑. 现在虽然相对好一点了, 但还是有很多问题没有解决.

完善

我遇到的坑主要有两个: 主要是 Rime 相关, 其次是 LSP 相关.

Rime 相关的坑

最主要的坑就是文档太少了, 每个 API 干什么用的要靠自己不断去猜和试, 用起来就像是盲人摸象, 对新手太不友好了.

比较印象深刻的是我发现很多 API 其实是后台开多线程的, 从外面却完全看不出来. 我在获取候选项的时候总是出现"打印 debug 信息时就正常运行, 什么都不输出就段错误", 才醒悟过来, 给数组加了锁.

促使我开始项目的主要原因就是发现作者已经提供了一个 librime-sys 的 Rust crate, 同时好像还没有一个 librime 的 Rust wrapper.

我本来其实是打算写一个 wrapper 的, 后面发现对我这样一个既是 Rust 新手也是 Rime 新手的人 来说还是太困难了, 最后目标就成了能用就行.

LSP 相关

主要问题就是 LSP 本来就是为了写代码设计的, 这种歪门邪道肯定会有一些需求上的不兼容.

对补全来说, 一般编辑器在实现时会默认只有在写标识符或者 ‘foo.bar’ 到中间点的时候才去自动请求补全, 我打算实现输入标点符号时, 发现就需要手动触发补全, 而且因为我是只从 rime 获取候选, 直接上屏的标点符号也暂时没法打出来 (已实现, 利用 RimeCommit 获取到直接提交的结果). 后面我准备利用 Code Action 功能, 提供把中文后的英文标点符号全改成中文.

此外还有一个坑是 LSP 默认的 Position 表示竟然是 UTF-16, 我好像至今还没见过哪个文档是 UTF-16 的. 最新的 3.17 标准对此进行了修改, 变成了客户端与服务端协商, 但目前应该还没推开? 我用的库还没有加入这个选项.

目前的特性

观看演示

  • 用 rime 能输入的东西按理说都能输入 ( 汉字, 标点, emoji …)
  • 支持按数字选择补全项
  • 支持候选词翻页
  • 多种触发方式
    • 默认开启, 随时补全, 用快捷键控制关闭 (写大量汉字)
    • 平时关闭, 检测到配置的特殊字符或光标前有非英文字符时触发补全 (写少量汉字)
  • 可以按配置其他 rime 输入法的方式去配置 (只有能影响候选项的配置是有用的)
  • 可以同步系统中已有 rime 输入法的词频
  • 可以通过 TCP 远程使用 (无任何加密,谨慎使用) (since v0.1.3)

没解决的问题

  • 更友好的触发条件: 像写这种文章的时候肯定是全部时间都开着, 而写代码的时候还开着肯定会影响正常补全. 目前我简单实现了一个特定字符起始时触发, 但一个词补全后就结束了, 还需要再触发. 实际上需要光标在中文后也触发补全, 但我发现我的实现在 vim 和 neovim 里表现不太一致, 具体是因为客户端的实现问题还是我对协议理解有问题就不太清楚了. (更新:已解决,方式是再对输入做一次正则匹配判断是否符合触发条件,这样不用交给编辑器做就没问题了)

  • 没和 Rime 同步: 现在只是获取候选项, 翻页和提交实现起来有些困难(翻页常用键会打断补全), 目前没有调用 rime API, 作出的选择对用户词频的积累不能产生影响 (更新: 已解决,翻页早已通过将翻页键添加为触发补全的字符来实现,提交自 v0.2.0 之后支持, 现在完全与 Rime 的 API 保持同步。 感谢 yao-weijie 这位网友提醒我 Rime 有一个获取原始输入的接口, 这个接口并没有以 RimeXxx 的 C 函数的方式提供,之前被我忽略掉了。 支持提交后,我用它来判断这此输入补全的起始位置,防止吃掉光标前面无需补全的英文符号。)

总结

  1. Rust 应该算是对调包侠很友好的现代语言了. 虽然 LSP 是通过 json-rpc 通信, 得益于各种 rust 库, 我写这个项目的过程中一个 json 串都没有手动组过. 我还了解到了 ropey 这种针对 UTF-8 文本编辑的库. 此外, 写完很容易就 linux / windows 都能用了. 如果让我用 C/C++ 来写, 以我的水平肯定是写不出来的.
  2. LSP 协议在处理编程语言之外文本的潜力, 感觉还没有完全挖掘出来.

更新 2023.2.9

过去了近一个月,本来以为没什么能做的了,结果还是又更了几个版本,不仅重构了部分代码,还加了些功能。 目前是完全足够我自己日常用的了,甚至在内网搞了个输入法服务器,让每个服务器的 vim 拥有相同的输入体验。 希望之后也能帮到更多有类似需求的人。

同时也学到了很多新知识:

  • Rust 的自引用结构体。不知道我的设计是不是有问题,但我目前觉得在我这个场景下用这种方式 parse 输入是合理的, 我觉得这太酷了,很符合我对零开销的想象。
  • 学会了用 once_cell 实现一个全局单例,但是感觉对需要额外析构的类型不太友好,需要在调用者那里大费周章。
  • 发现了之前代码中的一些内存泄漏。主要还是我没看文档,其实已经在 into_raw 这个函数的文档里警告过了。
  • 我的 C 语言基础还是太弱了。尝试在 linux 静态编译 librime 方便分发,但总是不能把 libboost 包进去。 这个之后有空闲时间再整整。

这个软件的早期开发我看就到此为止了,下次再更新估计要等几个月之后看有没有空了, 理智提醒我已经不能在这上面继续花时间了。

更新 2023.3.2

之前与一个同样对在 vim 中使用输入法感兴趣的网友交流时,获得了实现一直拖着没有实现的提交功能的线索, 于是又开始了一轮更新,终于实现了与 Rime API 的完全同步。

同时也因为新特性引入了一批新的 Bug,现在我感觉应该是修得差不多了。 至少我自己用没再觉得有啥影响了。不知道用其他配置的用户会不会还有什么问题。

之后再发个版本,我这边感觉就没啥再需要更新的地方了。 除了依赖的 librime-sys 这个库的依赖项好像需要更新了,不然要影响正常编译了。

See Also

过程中调研到的其他思路不同但也很有意思的项目: