用了好几年的 YouCompleteMe,体验还是不错的,最爽的一点是能基于 Token 补全在当前 buffer 中作补全,任何语言用 Vim 就有一个“基本可用”的体验,即使写 git commit message 的时候也很好用。
但是最近玩一些神奇的东西,YouCompleteMe 如果没有支持这种语言的话,就没办法做到一个比较好的体验了。
对于一种语言,YouCompleteMe 都要去实现这种语言的支持,才能起作用。每增加一种语言,YouCompleteMe 都要增加一种工作量。不光 YouCompleteMe,像 Emacs,VS Code,都要去实现每一个语言的自动补全。这就形成了很大的浪费:每一种编辑器要去自己实现每一种语言。如果有人写了一个新的语言想要其他编辑器支持,需要一个一个实现编辑器的补全工具。
这就形成了一种交叉关系,假如有三个语言,三个编辑器存在,就需要9个补全的实现。
所以 VS Code 就提出了 “Language Server” (简称 LSP)这种先进的概念。这是一种 Client-Server 架构,每个语言实现自己的 Language Server,每个编辑器去实现自己对接 Language Server 的前端。这样一个语言只需要实现一次,就可以支持所有的编辑器。上面那种情况,只要有3个语言的补全实现+3个编辑器自己的 Language Server 实现就可以了。
其实 YouCompleteMe 也支持 LSP 的,但是配置上看起来资料比较少,像是不是支持的特别好,所以我索性放弃 YCM 了,转向了 vim-lsp。
配置 vim-lsp
这里以 Python 为例,介绍一下配置方法。
首先需要安装 Python 的 Language Server。推荐使用 pipx 安装。
1 |
$ pipx install python-language-server |
然后修改 .vimrc
的配置修改,以下是一个最小化配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Plugin 'prabirshrestha/async.vim' Plugin 'prabirshrestha/asyncomplete.vim' Plugin 'prabirshrestha/vim-lsp' Plugin 'prabirshrestha/asyncomplete-lsp.vim' inoremap <expr> <Tab> pumvisible() ? "\<C-n>" : "\<Tab>" inoremap <expr> <S-Tab> pumvisible() ? "\<C-p>" : "\<S-Tab>" inoremap <expr> <cr> pumvisible() ? "\<C-y>\<cr>" : "\<cr>" if executable('pyls') au User lsp_setup call lsp#register_server({ \ 'name': 'pyls', \ 'cmd': {server_info->['pyls']}, \ 'whitelist': ['python'], \ }) endif |
安装的插件需要4个,下面分别介绍一下这四个是干什么用的。
async.vim
因为补全是支持 Vim 和 Neovim 的,async.vim 封装了两个 Vim Async 相关的接口。
asyncomplete.vim
利用 async.vim 做的补全,相当于一个补全引擎。(到这里是和 lsp 还没有什么事儿,配置的都是补全插件。)
这个补全只是负责补全引擎,具体的补全还是要交给依赖此引擎的实现。比如我们想要对路径进行自动补全,需要先安装这个插件,再安装基于此插件实现的路径补全插件,最后对补全引擎注册这个插件,才能使用。安装路径补全插件和注册的配置如下。
1 2 3 4 5 6 7 8 |
Plugin 'prabirshrestha/asyncomplete-file.vim' au User asyncomplete_setup call asyncomplete#register_source(asyncomplete#sources#file#get_source_options({ \ 'name': 'file', \ 'whitelist': ['*'], \ 'priority': 10, \ 'completor': function('asyncomplete#sources#file#completor') \ })) |
还有很多挺有意思的补全,比如这个 tmux-completion,是基于 tmux 的窗口 buffer 来补全,在 vim 中抄写 tmux 里面的 git hash,URL 啥的挺有帮助。但是我没找到禁用它的方法,导致它会一直弹出来,太烦人了,就没用。
vim-lsp
是一个与 LSP 交互的插件,类似于一个 Vim 的 SDK。
asyncomplete-lsp
将 lsp 中的内容交给 asyncomplete 做补全。
后面的两个配置,一个是设置用 Tab 触发补全(判断补全窗口有没有打开,有打开的话就将tab
键映射为补全选择键。
1 2 3 |
inoremap <expr> <Tab> pumvisible() ? "\<C-n>" : "\<Tab>" inoremap <expr> <S-Tab> pumvisible() ? "\<C-p>" : "\<S-Tab>" inoremap <expr> <cr> pumvisible() ? "\<C-y>\<cr>" : "\<cr>" |
然后注册 Python 的 LSP。每一个 LSP 都需要注册。
1 2 3 4 5 6 7 8 |
if executable('pyls') " pip install python-language-server au User lsp_setup call lsp#register_server({ \ 'name': 'pyls', \ 'cmd': {server_info->['pyls']}, \ 'whitelist': ['python'], \ }) endif |
效果如下:
配置好这些插件之后,再配置其他语言的 LSP 就简单多了。分成两步:
- 安装该语言的 LSP,这里有一个列表;
- 在 Vim 中注册该 LSP;
以 Bash 为例,我们首先按照仓库的介绍,安装该语言的 LSP:
1 |
$ npm i -g bash-language-server |
然后在 .vimrc
中用以下配置注册:
1 2 3 4 5 6 7 |
if executable('bash-language-server') au User lsp_setup call lsp#register_server({ \ 'name': 'bash-language-server', \ 'cmd': {server_info->[&shell, &shellcmdflag, 'bash-language-server start']}, \ 'whitelist': ['sh'], \ }) endif |
换成其他语言的话,只要将 cmd
换成 LSP 的启动命令,然后通过 whitelist
或者 blacklist
设置生效的文件类型就好了。
Snip 的支持
Language Server 支持补全一段代码模板,比如写 Elixir 的时候自动补全 defmodule
。但是 vim-lsp
本身是不支持的,snip 在 vim-lsp 中会补全成这个样子:
将这个片段正确的显示在 vim 中,并且支持跳转,需要通过插件来解析 Language Server 返回的片段。
这里又涉及到 N 多插件。首先你需要一个代码片段引擎,负责在 Vim 中生成片段,在片段中需要填写的字段中进行来回跳转,补全。之前在博客中介绍过 Ultisnips ,这里就以它为例。
除了 SirVer/ultisnips 本身,还要安装两个插件来将 LSP 中的 snip 补全到 vim 中来。总共需要安装的插件如下:
1 2 3 |
Plugin 'SirVer/ultisnips' Plugin 'thomasfaingnaert/vim-lsp-snippets' Plugin 'thomasfaingnaert/vim-lsp-ultisnips' |
然后有一个小坑需要注意,Snip 必须确认选中才能展开,也就是菜单中将焦点移动到这个 Snip 上去之后,需要按 Ctrl
+ y
来展开。
但是这个快捷键可以更改,比如我换成了 Ctrl
+ o
1 |
inoremap <expr> <C-o> pumvisible() ? "\<C-y>" : "\<C-o>" |