给(Neo-)VIM装配Language Server Protocol(配合deoplete)

renyuneyun 2021年05月23日(周日) 1 mins

作为一个长期(Neo-)VIM用户,我对代码补全等活计还是有很大需求的。之前在试用各种插件的时候就在想,为什么不搞个通用的,非要每个插件自己搞一套,然后还需要互相配套。最近两年听说微软搞了个Language Server Protocol,而且还和RedHat等合作最终将其标准化并开放出来,我对此还是十分好奇的。

然而我之前刚切换到deoplete不久,配合any-jump还是挺好用的,于是就没有动力去继续切换——毕竟我看到许多个LSP插件,但每个的文档都语焉不详的,而且也考虑LSP可能会改,它们的完成度也需要时间来提升。而且貌似实现得最好的是CoC——嗯,NodeJS的,不想用。

直到到了昨天,我发现了一个大问题(见后文),导致我不得不去修改我的流程。于是我又去试了LSP支持插件,最终用了LanguageClient-neovim这一插件,和deoplete配合很好,而且迁移过程十分简单。于是就有了此文,叙述一下我之前的方案问题在哪,以及切换的过程,以帮助潜在的类似需求者。

之前的方案和问题

我最早用的是YouCompleteMe,但因为种种因素(慢,每次更新薛定谔地需要重新编译且耗时很久,并且出问题),我于是去寻找其他方案。恰好当时我切换到NeoVim不久,于是就顺手找了一下使用相关异步接口的插件,就用起来了deoplete

但deoplete只提供自动补全的机制,还需要相应的语言插件。但好在它的仓库里有维护得不错的列表,于是我就在其中寻找自己需要的语言,安装即可。对于相当多语言,只要用最常见的就好,比如Python用jedi,go直接用go等等。

另外,为了可以进行跳转(deoplete不支持该功能),我又找到了any-jump这个插件。虽然并没有语义探测,会有一些多余的结果,但整体来说使用还是比较顺利的,而且架不住它快啊。于是我就这么用了很久。

然而在我将我的某一个Python程序(包)改用绝对导入后,any-jump再也找不到函数定义了,而且也不会在其他文件中搜索引用了!这可就要了命了,毕竟跳转是很重要的功能。更令我郁闷的是,在另一个Python程序(包)里,它明明工作得十分正常。无论直接搜索还是去any-jump的仓库issue中找,都没有相关信息,于是我只能怀疑是它的机制在这里有问题。

寻找新解决方案

为了减少麻烦,我期待新的解决方案可以让我尽量少地需要配置,并且最好可以和我之前的配置共同工作。毕竟我目前的最大问题是Python的部分状况下any-jump不工作,而不是完全没有可用的工作环境。

我之前也找过LSP方案,而且上次还看到说NeoVim打算在0.5版正式引入对LSP的支持。于是CoC这种「完整」的方案似乎意义不大,我更倾向于模块化的方案。

于是我在一个回答中找到了指引,发现了LanguageClient-neovim这个插件。他们宣称这个插件和deoplete可以很好地联合工作,并且我试用后发现的确如此。

打开仓库发现上次更新是去年12月。但翻了一下分支列表,就发现dev分支还在维护,于是就放下心来。我切换的部分目的还是临时使用,等待NeoVim 0.5版基础上的支持。

安装与配置

那么理所当然地,照着说明,我直接在插件部分新增了它,并且进行了非常简单的配置。

首先是安装并启用该插件。我正好也在用vim-plug来管理插件,于是直接复制:

Plug 'autozimu/LanguageClient-neovim', {
    \ 'branch': 'next',
    \ 'do': 'bash install.sh',
    \ }

" (Optional) Multi-entry selection UI.
Plug 'junegunn/fzf'

然后还需要进行简单配置。文档中有例子,于是我就拿来稍作修改:

" Required for operations modifying multiple buffers like rename.
set hidden

let g:LanguageClient_serverCommands = {
                        \ 'rust': ['rustup', 'run', 'stable', 'rls'],
                        \ 'python': ['pyls'],
                        \ }

" note that if you are using Plug mapping you should not use `noremap` mappings.
nmap <F5> <Plug>(lcn-menu)
nmap <leader>l <Plug>(lcn-menu)
" Or map each action separately
nmap <silent>K <Plug>(lcn-hover)
nmap <silent> gd <Plug>(lcn-definition)
nmap <silent> <F2> <Plug>(lcn-rename)

可以看到,和示例相比,我主要进行了两处改动:

  1. 对应的Language Server实现不用绝对路径,而用相对路径。我的考虑主要是万一需要在virtual environment中工作,使用绝对路径大概会出错。不过我还没有仔细测试过,不清楚到底怎样。
  2. 我新增了一个<leader>l到它的调用菜单的映射。这是因为之前用any-jump时我就映射<leader>j为调用any-jump的搜索,这里想用类似的方法。

当然,配置完后,另外执行nvim +PlugInstall +UpdateRemotePlugins +qa(或者打开nvim直接执行对应的命令)来真正安装它。另外,我选择了使用系统的包管理器来安装对应的Language Server实现,这也是Arch Linux下的通常倾向。

注意,(在Arch上)pyls对应的包其实叫python-language-server,但提供的命令倒确实是pyls

到这里,其实安装和配置已经完成了。之后就只需要和以前一样调用补全即可,来自Language Server的结果会自动加到deoplete给出的列表中。各项快捷键也都很好理解,尤其是F5(和<leader>l)会叫出主选单。

其他

在此之外,我还进行了一些微调。其中最主要的就是在哪里存留显示函数签名。我用echodoc来提示函数签名。本来的设置是显示在底栏中的,为了避免遮挡。但现在我将其调整到显示在旁边,使用floating text/window机制:

let g:echodoc#enable_at_startup = 1
let g:echodoc#type = 'floating'

我不知道这是得益于deoplete还是LanguageClient-neovim的机制,但至少它和我目前的配置配合良好。

另外简单解释一下我为什么还是没用CoC:CoC很骄傲地宣称自己基于NodeJS,这让我有点不喜欢。而除此之外,CoC的安装说明也不怎么清晰,我还是看了前面提到的那个回答才大概明白怎么装。然后看到CoC文档里说它是一个平台,我就彻底放弃了。

当然这不是说CoC不好——我没用过,也没深入了解过,所以没法做这种评论。但在我已经知道NeoVim打算在下一个版本中原生支持LSP的前提下,我自然是不会想要去再搞一个很有可能之后会被大规模改组/重构的平台上的平台的。

最后,我完整的vim配置在这里,可以随意参考使用。


Related posts:

您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。