解析 neovim 的 remote plugin 機制
來自專欄 Vim19 人贊了文章
neovim 添加的主要功能例如: 非同步任務,自帶 terminal 都在 vim 中實現了,儘管實現有所差別,但是使用上並沒有太大問題。本文所要介紹的是 neovim 的獨有功能: remote plugin。
主要內容:
- 什麼是 remote plugin
- remote plugin 主要優勢
- remote plugin 存在的問題
- 解決 remote plugin 的問題
- 讓 vim 支持 remote plugin
什麼是 remote plugin
在 neovim 中使用 :h remote-plugin
可以查看官方文檔,簡單的說就是 neovim 在啟動時開啟了一個基於 named pipes 的服務(使用 :echo $NVIM_LISTEN_ADDRESS
查看),開發者可以用其它編程語言開發的插件連接這個服務,進而與 neovim 交互。
為了幫助用戶避免處理底層的一些問題,neovim 對一些常見語言提供了 host 支持,目前默認支持 python,nodejs 和 ruby。
Host 可以幫助開發者降低開發的難度,例如可以這樣在 python 文件聲明一個 vim 的函數:
@neovim.function(Func) def function_handler(self, args): self._increment_calls() self.vim.current.line = ( Function: Called %d times, args: %s % (self.calls, args))
執行::UpdateRemotePlugins
後再重啟 neovim,你就可以使用 python 裡面定義的 vim 函數了。
remote plugin 主要優勢
- 不需要交叉編譯,因為是基於 client server 的方式,所以不用像 vim 一樣需要編譯時配置。
- 容易編寫和維護。
vimscript 做為一個古老的腳本語言,很容易不小心就掉坑裡,儘管寫起來比較方便,但是一旦插件變得複雜,其編寫和維護的成本都是比較高的。個人比較推薦基於 typescript 的 node-client,藉助 tsserver 的智能支持,可以降低很多開發和重構的成本。
- 按需載入,啟動更快。
remote plugin 的 host 只有在第一次調用相關方法時才會啟動,所以理論上不會增加 vim 的啟動時間,同時也沒有按需載入的必要。
- 更好的 io 效率,藉助底層的 libuv,neovim 的 io 效率要比 vim 的 job 好不少。
remote plugin 存在的問題
- 大幅增加 vim 啟動時間。
如果你的 vim 插件在啟動時調用了 python 就很可能碰到啟動時間大幅增加的問題,主要的原因是 neovim 完成 host 的初始化相當耗時,這個過程主要包含以下幾步:
- 從環境變數或者命令調用等方式找出 host 啟動文件
- 啟動 host 服務
- 客戶端調用
nvim_get_api_info
方法獲取 channelId 以及 api 的 meta - nvim 調用一個 `rpcrequrest("poll")` 方法確保 host 服務正常
第一個和最後一步通常都會非常耗時。
為了避免第一步的查找,可以在 init.vim 中將 host 的位置寫死,例如:
let g:python_host_prog = /usr/local/bin/pythonlet g:python3_host_prog = /usr/local/bin/python3let g:ruby_host_prog = exepath(neovim-ruby-host)let g:node_host_prog = /usr/local/lib/node_modules/neovim/bin/cli.js
為了解決 rpcrequest 調用慢的問題,可以把它去掉,參考 neovim/issues/5728#issuecomment-406475616,但是會有錯誤無法及時發現的副作用。
理想的情況是不要在 vim 啟動時初始化 remote plugin 的 host。
某些插件例如 fcitx.vim 會在 plugin 的腳本裡面直接調用 pyfile
命令, 所以以上辦法無法完全解決載入慢的問題,這種插件可以考慮將函數定義在 autoload 文件裡面,然後在 autoload 文件里執行 pyfile
, 這樣就避免了啟動時載入 python host。
- API 更新後用戶需要執行
:UpdateRemotePlugins
來更新插件的介面,因為某些插件管理插件並不支持更新後自動執行,所以可能導致 bug - 用戶需要額外安裝 remote host 模塊,例如為了使用 node 插件,用戶需要使用
npm i -g neovim
來安裝 host。如果開發者想使用新的功能,還需要讓用戶更新這個全局的 remote host 模塊。
- 不同插件存在互相影響,儘管 node-client 使用了 sandbox 方式隔離不同 node 插件的運行環境,它們還是共用了 process 模塊,所以開發者也不能在插件中捕獲
uncaughtException
以及unhandledRejection
,因為這可能會捕獲到其它插件的錯誤。
解決 remote plugin 的問題
現在的問題都是 host 造成的,所以通過避免使用 host 就可以解決以上問題,其實現也不是非常麻煩。
以 node 為例, 實現監聽 neovim 的 rpc 服務 server.js
:
const attach = require(neovim).attachconst nvim = attach({ socket: process.env.NVIM_LISTEN_ADDRESS})nvim.on(notification, (method, args) => { switch (method) { case VimEnter: // TODO dosomething onEnter return }})nvim.on(request, (method, args, resp) => { switch (method) { case Action: // TODO dosomthing on Action return }})nvim.channelId.then(channelId => { // assign channel id to vim global variable nvim.setVar(my_channel_id, channelId)})
啟動 rpc 服務:
let s:job_opts = {}let s:std_err = []call jobstart([node, /path/to/server.js], s:job_opts)function! s:job_opts.on_stderr(chan_id, data, event) dict call extend(s:std_err, a:data)endfunctionfunction! s:job_opts.on_exit(chan_id, code, event) dict let g:my_channel_id = 0 if a:code != 0 for msg in s:std_err echoerr msg endfor endifendfunction
調用 rpc 服務:
autocmd VimEnter * call s:notify(VimEnter, [])call s:request(Action, [])function s:notify(method, args) if !get(g:, my_channel_id, 0) | return | endif call call(rpcnotify, [g:my_channel_id, a:method] + a:args)endfunctionfunction s:request(method, args) if !get(g:, my_channel_id, 0) | return | endif call call(rpcrequest, [g:my_channel_id, a:method] + a:args)endfunction
讓 vim 支持 remote plugin
可以使用 vim-hug-neovim-rpc 或者 vim-node-rpc 。前者只能支持 python 插件而且需要 vim 有 python 支持,後者性能更好,而且可以支持 node, 但是目前沒有 host 相關功能。
對比數據:
如果你想體驗一個實用的 node 插件,可以了解一下
neoclide/coc.nvim推薦閱讀:
※理解使用 vim 中的正則表達式
※Vim操作記錄
※11 個超棒的 Vi 技巧和竅門
※VIM學習筆記 縮進 (Indent)
※Linux基礎--vim以及bash