本文共 15970 字,大约阅读时间需要 53 分钟。
刚接触 Vim 的同学往往因为无法搭建开发环境而“从入门到放弃”,本文旨在帮助这些同学搭建开发环境,聚焦于最核心的开发需求,忽略换配色调字体之类的细枝末节。如果需要开箱即用的 vim 配置(发行版),可以使用 。
本文使用 neovim-nightly,但也适用于 Vim 8.2+,不需要读者有任何 VimL 基础,以 C/C++ 为例,但应该适用于任何语言。
在 Vim 中,插件只是一些脚本,存放在特定的目录中,运行时将它们所在的目录加入到 runtimepath 中。Vim 8 内置了插件管理功能,但不支持高级的插件管理功能。Vimmers 实现了多个插件管理器,可以自动下载、更新、安装插件,还可以延迟加载、按需加载,提高启动速度。
上古时期流行手动或使用 管理插件,以上两种方式已经落伍了,这里介绍目前比较流行的三个插件管理器:
以上三款插件管理器风格各不相同,都有大量用户,功能相当完善,根据自己的喜好选取即可。推荐新手选择 vim-plug,对启动时间特别敏感的同学可以考虑 dein.vim,我的配置在安装 70 余个插件的情况下,启动仅需 60 余秒。
使用 vim-plug 安装插件只需要在 .vimrc 中写入以下代码:
call plug#begin('~/.vim/plugged') " 括号里面是插件目录 " 只能在 plug#begin() 和 plug#end() 之间写安装插件的命令Plug 'junegunn/vim-easy-align' " 用户名/插件名,默认从 github 下载安装Plug 'https://github.com/junegunn/vim-github-dashboard.git' " 从特定 URL 下载安装call plug#end()
使用 dein.vim 安装插件:
set runtimepath+=~/.vim/plugged/repos/github.com/Shougo/dein.vim " 将 dein.vim 添加到 runtimepath " 注意这是 Ex 命令,路径不要加引号if dein#load_state('~/.vim/plugged') " 参数是插件目录 call dein#begin(general#plugin_dir) call dein#add('~/.vim/plugged/repos/github.com/Shougo/dein.vim') " 安装 dein.vim call dein#add('junegunn/vim-easy-align') " 用户名/插件名,默认从 github 下载安装 call dein#end() call dein#save_state()endifif dein#check_install() " 自动安装未安装的插件 call dein#install()endif
在安装插件的代码后加上这两行设置:
filetype plugin indent onsyntax on
这样可以确保特定于文件类型的插件正常工作。
最简单的代码补全方式是利用 生成 tag 文件,补全插件解析 tag 文件进行补全,这种方式有以下两个好处:
基于 tag 的补全不够智能,后来又诞生了 life-changer 级别的补全插件 ,可以提供 IDE 级别的代码补全。但YouCompleteMe 不是开箱即用的,需要下载依赖并编译,并且耦合度比较大,只支持特定语言(主要是 C++)。
目前体验补全体验最好的方式是基于 LSP(Language Server protocal)的方案。LSP 是一套通信协议,遵从 LSP 规范的客户端(各种编辑器/IDE)可以通过众多 LSP 服务端按协议标准进行通信,由客户端完成用户界面相关的事情,由服务端提编程语言相关的:补全,定义引用查找,诊断,帮助文档,重构等服务。架构图如下:
有了 LSP,不同的 IDE/编辑器只需要实现 LSP 客户端,专心改进用户体验,所有补全的工作都交给 LSP 服务器。使用基于 LSP 的方案,用户可以在多种语言间无缝切换,让 Vim 支持所有语言(只要有 LSP 实现),用户只需要做以下两件事:
目前 LSP 客户端有以下几种选择:
coc.nvim 使用 Typescript 开发,是目前最流行、最强大的 LSP 客户端,已经发展成了一个 Vim 插件平台,存在大量基于 coc.nvim 开发的插件(coc 拓展),推荐大家使用 coc.nvim。
coc.nvim 依赖于 node.js,但 node.js 似乎已经不再支持 32 位机,因此最新的 coc.nvim 很可能无法在 32 位机上运行,请考虑其他几种方案。
"选择补全候选inoremap \ pumvisible() ? "\ " : \ check_back_space() ? "\ " : \ coc#refresh()inoremap pumvisible() ? "\ " : "\ "function! s:check_back_space() abort let col = col('.') - 1 return !col || getline('.')[col - 1] =~# '\s'endfunction" gn 跳转到下一个错误,gN 跳转到上一个错误nmap gN (coc-diagnostic-prev)nmap gn (coc-diagnostic-next)" gd 跳转到定义,gs 跳转到引用,gt 跳转到类型定义,gK 显示文档nmap gd (coc-definition)nmap gs (coc-references)nmap gt (coc-type-definition)nnoremap gK :call show_documentation() function! s:show_documentation() if (index(['vim','help'], &filetype) >= 0) execute 'h '.expand(' ') elseif (coc#rpc#ready()) call CocActionAsync('doHover') else execute '!' . &keywordprg . " " . expand(' ') endifendfunction" rv 改名, rf 重构nmap rv (coc-rename)nmap rf (coc-refactor)" 和 滚动悬浮窗口nnoremap coc#float#has_scroll() ? coc#float#scroll(1) : "\ "nnoremap coc#float#has_scroll() ? coc#float#scroll(0) : "\ "inoremap coc#float#has_scroll() ? "\ =coc#float#scroll(1)\ " : "\ "inoremap coc#float#has_scroll() ? "\ =coc#float#scroll(0)\ " : "\ "vnoremap coc#float#has_scroll() ? coc#float#scroll(1) : "\ "vnoremap coc#float#has_scroll() ? coc#float#scroll(0) : "\ "autocmd User CocJumpPlaceholder call CocActionAsync('showSignatureHelp')
coc.nvim 有自己的配置文件,叫做 coc-settings.json,一般存放在 .vim(neovim 的话在 ~/.config/nvim)。一般我们会在 coc-settings.json 中微调 coc.nvim 和配置 LSP。
目前功能最强的 C++ LSP 服务器是 ,在 coc-settings.json 中配置 ccls:
{ "languageserver": { "ccls": { "command": "ccls", "filetypes": ["c", "cc", "cpp", "c++"], "rootPatterns": [".ccls", "compile_commands.json", ".git/", ".root"], "initializationOptions": { "cache": { "directory": ".cache/ccls" }, "highlight": {"lsRanges": true } } } }}
以上配置仅在编辑 C/C++ 文件使用命令ccls
启动 LSP 服务器,将项目根目录设置为包含rootpatterns
中任意文件的目录,索引文件存放在项目根目录的隐藏目录 .cache/ccls 中。highlight
字段指示 ccls 生成高亮信息,基于 LSP 的语法高亮插件会利用 LSP 服务器提供的信息进行精确的语法高亮。
为了让 C++ LSP 服务器知道以程序以何种方法编译,必须要在项目根目录生成 (compile_commands.json)。CMake 内置了对编译数据库的支持,只需要在执行 CMake 时加上-DCMAKE_EXPORT_COMPILE_COMMANDS=1
即可;如果使用 Makefile,可以利用 生成编译数据库,通过bear make
来执行 Makefile。Bear 需要执行 Makefile 才能生成编译数据库,如果项目无法正常构建,将不能生成编译数据库,没法使用 LSP 的功能。
有些 vimmer 还基于 coc.nvim 开发了一些插件,可以在查看完整的拓展列表。这里给两点建议:
coc 基于 coc.nvim,使用 Typescript 编写,有些 coc 拓展仅支持使用 coc.nvim 安装。建议在 .vimrc 中定义列表let g:coc_global_extensions
,把自己想安装的 coc 拓展写进入,coc.nvim 会在第一次打开文件时安装。
let g:coc_global_extensions` = ['coc-vimlsp', 'coc-rust-analyzer']
之类的 LSP coc 拓展通常利用 coc.nvim 实现了更多 LSP 功能,请优先使用这些拓展,只在没有对应语言的 LSP coc 拓展时手动配置 LSP。
使用 ccls ,即使是在 Linux 这种规模的代码仓库中也可以流畅地补全代码。
目前错误检查有两种方案:
如果使用 coc.nvim,不需要额外配置,开箱即用。coc.nvim 使用 LSP 进行错误检查,不够灵活,无法使用 linter 实时检测代码。
基于外部程序的方案非常灵活,比如,可以在基础的错误检测之外同时使用 clang-tidy 等工具进行检测。目前这种方案最好的插件是 ,并且 ale 可以与 coc.nvim 共存,用 ale 做实时错误检查,coc.nvim 做补全。如果没有特殊需求,直接使用 coc.nvim 即可。
LSP 已经提供了符号索引的功能,可以方便地跳转到定义/引用。通常 LSP 的功能已经够用,但 LSP 存在以下缺点:
比如可能存在汇编和 C 混合的项目,汇编定义了一个变量在 C 中读写,LSP 无法理解汇编,找不到变量定义的地方。
LSP 一般不支持跳转到变量赋值的地方,不支持查找包含该头文件的源文件等。
我们可以使用静态代码索引工具,克服 LSP 的以上缺点。目前静态代码索引最好的方案是 ctags 和 混合使用,具体的方法参考韦应笑的深度文章,不再赘述。。
Tips:
建议同时使用 LSP 和静态索引工具,LSP 支持的功能用 LSP,不支持的功能或没有 LSP 时用静态索引工具,由于实现这个功能需要用 VimL 编程,这里不介绍,有兴趣的话可以参考我的配置
ccls 实现了更多的功能,如查看类继承体系,查看调用链,查找类的全部实例等。参考配置如下,部分功能用的比较少,就不创建快捷键了,直接使用命令。
" This comands are defined for ccls(only supports C/C++)command! -nargs=0 Derived :call CocLocations('ccls','$ccls/inheritance',{'derived':v:true})command! -nargs=0 Base :call CocLocations('ccls','$ccls/inheritance')command! -nargs=0 VarAll :call CocLocations('ccls','$ccls/vars')command! -nargs=0 VarLocal :call CocLocations('ccls','$ccls/vars', {'kind': 1})command! -nargs=0 VarArg :call CocLocations('ccls','$ccls/vars', {'kind': 4})command! -nargs=0 MemberFunction :call CocLocations('ccls','$ccls/member', {'kind': 3})command! -nargs=0 MemberType :call CocLocations('ccls','$ccls/member', {'kind': 2})command! -nargs=0 MemberVar :call CocLocations('ccls','$ccls/member', {'kind': 4})nmapgc :call CocLocations('ccls','$ccls/call') nmap gC :call CocLocations('ccls','$ccls/call', {'callee': v:true})
在古老的 Vim 工作流中,项目的构建一直是个老大难的问题,要么手动完成,要么自己写简单的脚本完成,VSCode 引入任务系统解决了这个问题,韦易笑大佬的 和 又将 VSCode 的任务系统引入到了 Vim 中,彻底改变了 Vim 的工作流。这充分体现了 Vim 的优势,Vim 用户非常乐于吸收别的编辑器的优点,让 Vim 变得更好。
asyncrun.vim 让用户可以异步运行 shell 命令,asynctasks 让用户可以将常用的命令写入到配置文件中(~/.vim/tasks.ini 或项目根目录中的 tasks.ini),一次编写多次使用。详细的使用方法请参考插件的中文文档。基本配置如下:
" 将终端放到 tab 中let g:asynctasks_term_pos = 'tab'" 设置 quickfix 大小let g:asyncrun_open = 10" 设置项目根目录标志" 实际上,许多插件都使用这种方法定位根目录,因此可以定一个变量 g:rootmarks," 将所有插件的根目录标志都设置为 g:rootmarkslet g:asyncrun_rootmarks = ['.compile_commands.json', '.ccls', '.git']
以构建 CMake 项目为例,我需要以不同的模式(Debug/Release)执行 CMake,编译项目,可能还会删除二进制目录,利用这两个 life-changer 级别的插件,可以实现一键配置、编译、运行、清理目录。
[project-build]command = cmake --build _builds -- VERBOSE=1cwd=$(VIM_ROOT)notify=echosave=2[project-run]command/linux=_builds/$(VIM_PRONAME)command/win32=_builds\$(VIM_PRONAME).execwd=$(VIM_ROOT)output=terminal[project-clean]command/linux=rm -rf _buildscommand/win32=rd/s/q _buildsnotify=echocwd=$(VIM_ROOT)[project-configure]command/linux=cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -DCMAKE_BUILD_TYPE=Debug -S. -B_builds && ln -sf _builds/compile_commands.json .command/win32=cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -DCMAKE_BUILD_TYPE=Debug -G "MinGW Makefiles" -S. -B_builds && copy _builds\compile_commands.json .cwd=$(VIM_ROOT)notify=echosave=2
我的 tasks.ini 中写好常用的任务命令,您可以直接将我的直接复制到自己的配置中。
在 .vimrc 中映射几个快捷键:
" 编辑全局 tasks.ini,随时优化工作流command! TaskEdit exec 'vsp ' .. general#vimfiles .. '/tasks.ini'" 由于的码值和 相同,映射这些快捷键后 会变慢nnoremap 5 :AsyncTask file-build nnoremap 6 :AsyncTask file-run nnoremap 7 :AsyncTask project-configure nnoremap 8 :AsyncTask project-build nnoremap 9 :AsyncTask project-run nnoremap 0 :AsyncTask project-clean
再回头看前面提到的错误检测,我们可以将执行 linter 的命令写成一个任务,在代码没有语法错误后调用。
[clang-tidy]command=find . -type d -name '.cache' -prune -o -type d -name '_builds' -prune -o -name '*.cpp' -print | xargs clang-tidy -checks=level2 -p default/compile_commands.jsoncwd=(VIM_ROOT)notify=echo
可能您还想统计本项目的代码量,也可以通过 asynctask.vim 完成:
[cloc]command=cloc --exclude-dir=_builds,.* .cwd=(VIM_ROOT)notify=echo
只有想象力丰富,asyncrun.vim 几乎没有完不成的工作!
您可能会有这样的疑问,假如我定义了 100 个任务,平时只用其中的少数几个任务,岂不是要经常打开 tasks.ini 查询?这样的困扰根本不存在,Vim 的另一个强大之处就是插件可以配合工作,我们会在后面介绍解决这个问题的办法。
基于正则表达式的语法高亮在 C++ 这种语法非常复杂的语言上表现的很差,2021 年可以彻底抛弃掉这种老掉牙的高亮方案了。请使用 ,它是目前最好的高亮方案(只支持 neovim-nightly),如果用 Vim 的话请使用 。
vim-lsp-cxx-highlight 基于 LSP 实现精确的高亮,但存在性能问题,打开文件时有点晃眼,前面 coc-settings.json 中已经配置好了 vim-lsp-cxx-highlight。
nvim-treesitter 是 neovim 移植的 treesitter(是的,从别的编辑器超过来的),基于语义高亮代码,性能强,容错好。
配置代码如下:
lua <
Tip:您可以在 vimrc 中进行判断,在 Vim 中使用 vim-lsp-cxx-highlight,在 neovim-nightly 中使用 nvim-treesitter,可以参考我配置中的 init.vim 和 autoload/tools.vim。
许多 Vim 外的编辑器用户喜欢使用文件树定位项目文件,但 Vimmer 更喜欢使用模糊查找插件定位文件。尽管如此,文件树也并非一无用处,在浏览自己不熟悉的项目时,文件树插件可以帮助我们了解项目结构。Vim 自带文件树插件,也有许多 vimmer 编写的插件,这里介绍最经典的 。
NERDtree 虽然是最经典的文件树插件,但在许多介绍 Vim 的文章中被骂的狗血临头。许多人批评 NERDtree 性能差,在 Linux 这种规模的项目中会直接卡死,但应付中小型项目绰绰有余。
是国人开发的一款模糊查找插件,性能最强,并且支持许多插件。配置如下:
let g:Lf_PreviewResult = { \ 'File': 0, \ 'Buffer': 0, \ 'Mru': 0, \ 'Tag': 1, \ 'BufTag': 1, \ 'Function': 1, \ 'Line': 0, \ 'Colorscheme': 0, \ 'Rg': 1, \ 'Gtags': 1 \}let g:Lf_PreviewInPopup = 1 " 在 popup 窗口中预览结果let g:Lf_PreviewCode = 1 " 预览代码let g:Lf_RootMarkers = ['.root', 'compile_command.json', '.git'] "你的根目录标志let g:Lf_WorkingDirectoryMode = 'A' " 设置 LeaderF 工作目录为项目根目录,如果不在项目中,则为当前目录。let g:Lf_ShortcutF = "f"let g:Lf_ShortcutB = " bl"nnoremap p :LeaderfFunctionAll " 搜索函数nnoremap l :LeaderfBufTagAll " 搜索缓冲区中的 tagnnoremap d :LeaderfTag " 搜索项目中的 tagnnoremap h :LeaderfHelp " 搜索 vim helpnnoremap rg :Leaderf rg " 调用 ripgrep 查找字符串
现在,只要按下 <Leader>f ,即使是 Linux 这种级别的项目,也能在一瞬间切换到目标文件。
既然 LeaderF 的模糊搜索功能如此强大,能不能让 LeaderF 搜索我们定义的 asynctask.vim 任务?答案当然是可以的!
function! s:lf_task_source(...) let rows = asynctasks#source(&columns * 48 / 100) let source = [] for row in rows let name = row[0] let source += [name . ' ' . row[1] . ' : ' . row[2]] endfor return sourceendfunctionfunction! s:lf_task_accept(line, arg) let pos = stridx(a:line, '<') if pos < 0 return endif let name = strpart(a:line, 0, pos) let name = substitute(name, '^\s*\(.\{-}\)\s*$', '\1', '') if name != '' exec "AsyncTask " . name endifendfunctionfunction! s:lf_task_digest(line, mode) let pos = stridx(a:line, '<') if pos < 0 return [a:line, 0] endif let name = strpart(a:line, 0, pos) return [name, 0]endfunctionfunction! s:lf_win_init(...) setlocal nonumber setlocal nowrapendfunctionlet g:Lf_Extensions = get(g:, 'Lf_Extensions', {})let g:Lf_Extensions.task = { \ 'source': string(function('s:lf_task_source'))[10:-3], \ 'accept': string(function('s:lf_task_accept'))[10:-3], \ 'get_digest': string(function('s:lf_task_digest'))[10:-3], \ 'highlights_def': { \ 'Lf_hl_funcScope': '^\S\+', \ 'Lf_hl_funcDirname': '^\S\+\s*\zs<.*>\ze\s*:', \ }, \ }nnoremapT :Leaderf task " T 模糊搜索任务
Tips:使用 和 可以在 LeaderF、NERDtree 等插件中显示漂亮的文件图标。
调试一直是 Vim 的弱点,最近 DAP(Debug Adapter Protocol)的提出带来了一些改变。YouCompleteMe 的主要开发者 puremourning 创建了 ,这是目前最强的 Vim 调试插件,仍处于开发阶段,您如果有兴趣的话可以参考我的博客 。
Tpope 的 让 Git 工作流在 Vim 中顺畅无比,使用 在侧边栏展示 Git 状态。
command! -bang -nargs=* -complete=file Make AsyncRun -program=make @" 异步 git push" git-gutterlet g:gitgutter_map_keys = 0nmap ghp (GitGutterPreviewHunk) " 预览修改(diff)nmap ghs (GitGutterStageHunk) " 暂存修改nmap ghu (GitGutterUndoHunk) " 丢弃修改nmap [c (GitGutterPrevHunk) " 上一处修改nmap ]c (GitGutterNextHunk) " 下一处修改
Tip:vim-fugitve 还可以用来处理 git conflict,这里不介绍。
注释请使用 ,它易于拓展,可以支持所有文件类型。vim-format 会根据文件类型执行对应的格式化命令,C/C++ 默认使用 clang-format,所以您只需要将 .clang-format 放到项目根目录即可。
定义一个快捷键快速格式化代码。
nnoremapbf :Autoformat
Tip:您还可以利用自动命令在写入文件时自动格式化,利用替换命令在写入文件时自动清除行尾空白。
目前最流行的注释/反注释是 和 。nerdcommenter 相比于 vim-commentary 功能更强,拓展性更好,因此推荐使用 nerdcommenter。
" Add spaces after comment delimiters by defaultlet g:NERDSpaceDelims = 1" Align line-wise comment delimiters both sideslet g:NERDDefaultAlign = 'both'" Allow commenting and inverting empty lines (useful when commenting a region)let g:NERDCommentEmptyLines = 1" Enable trimming of trailing whitespace when uncommentinglet g:NERDTrimTrailingWhitespace = 1" Enable NERDCommenterToggle to check all selected lines is commented or notlet g:NERDToggleCheckAllLines = 1" Usefull when comment argumentlet g:NERDAllowAnyVisualDelims = 0let g:NERDAltDelims_asm = 1
nerdcommenter 默认的快捷键请参考文档。请不要再蜗牛一样地用:help
命令查看文档,用<Leader>h
模糊搜索!
Tip:您会发现注释/反注释后光标仍停留在原来的位置,如果您希望光标停留在可视区域结尾,可以添加上以下代码:
let g:NERDCreateDefaultMappings = 0" It is impossible to determine execute mode in hooks. Thus, I wrap raw NERDComment()" to pass mode infomation to hooks and create mappings manually."" NERDCommenterAltDelims is not wrapped and it would execute hooks. So I" delete variable g:NERDCommenter_mode in NERDCommenter_after() to disable" hooks executed by NERDCommenterAltDelimsfunction! s:NERDCommenter_wrapper(mode, type) range let g:NERDCommenter_mode = a:mode execute a:firstline .. ',' .. a:lastline 'call NERDComment(' .. string(a:mode) .. ',' .. string(a:type) .. ')'endfunction" modes: a list of mode(n - normal, x - visual)function! s:create_commenter_mapping(modes, map, type) for l:mode in split(a:modes, '\zs') execute l:mode .. 'noremap' .. a:map .. ' :call NERDCommenter_wrapper(' .. string(l:mode) .. ', ' .. string(a:type) .. ') ' endforendfunctionfunction! CreateCommenterMappings() " All mappings are equal to standard NERDCommenter mappings. call s:create_commenter_mapping('nx', 'cc', 'Comment') call s:create_commenter_mapping('nx', 'cu', 'Uncomment') call s:create_commenter_mapping('n', 'cA', 'Append') call s:create_commenter_mapping('nx', 'c ', 'Toggle') call s:create_commenter_mapping('nx', 'cm', 'Minimal') call s:create_commenter_mapping('nx', 'cn', 'Nested') call s:create_commenter_mapping('n', 'c$', 'ToEOL') call s:create_commenter_mapping('nx', 'ci', 'Invert') call s:create_commenter_mapping('nx', 'cs', 'Sexy') call s:create_commenter_mapping('nx', 'cy', 'Yank') call s:create_commenter_mapping('n', 'cA', 'Append') call s:create_commenter_mapping('nx', 'cl', 'AlignLeft') call s:create_commenter_mapping('nx', 'cb', 'AlignBoth') call s:create_commenter_mapping('nx', 'cu', 'Uncomment') call s:create_commenter_mapping('n', 'ca', 'AltDelims') nmap ca NERDCommenterAltDelimsendfunction" NERDCommenter hooksfunction! NERDCommenter_before() let g:nerdcommmenter_visual_flag = v:false if get(g:, 'NERDCommenter_mode', '') =~# '[vsx]' " executed in visual mode let l:marklist = getmarklist('%') for l:mark in l:marklist if l:mark['mark'] =~ "'>" let g:nerdcommmenter_cursor = l:mark.pos let g:nerdcommmenter_visual_flag = v:true break endif endfor endifendfunctionfunction! NERDCommenter_after() if g:nerdcommmenter_visual_flag call setpos('.', g:nerdcommmenter_cursor) endif let g:nerdcommmenter_visual_flag = v:false unlet! g:NERDCommenter_modeendfunction
本文介绍了用 Vim 搭建开发环境的思路,但 Vim 的魅力不在于“千篇一律”,而在于“各不相同”,每个 Vimmer 都有自己的 Vim,根据自己的习惯不断改进工作流。总之,希望本文可以帮助大家走进 Vim 的世界。
转载地址:http://imdyz.baihongyu.com/