在上一篇NeoVim开发环境配置中记录了NeoVim的基本插件配置,随着了解的深入,这里对NeoVim的插件配置进行一步优化处理。

首先对配置进行切分,避免配置都混杂在init.vim中,不利于管理,下面是我的配置文件结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$HOME/.config/nvim/
├── after
│  └── plugin
│     ├── coq_nvim.rc.vim
│     ├── defx.rc.vim
│     ├── fugitive.rc.vim
│     ├── lspconfig.rc.vim
│     └── treesitter.rc.vim
├── init.vim
├── macos.vim
├── maps.vim
└── plug.vim

init.vim: 根配置文件

macos.vim: mscOS特殊配置

plug.vim :Vim-plug插件配置

maps.vim:快捷键配置

after/plugin: 放置插件的配置脚本,这里面的文件也会在vim每次启动的时候加载,不过是等待plugin加载完成之后才加载after里的内容,所以叫做after。VIM USER MANUAL runtimepath

1、插件管理器:vim-plug

安装 vim-plug$HOME/.local/share/nvim/site/autoload/plug.vim:

1
2
sh -c 'curl -fLo $HOME/.local/share/nvim/site/autoload/plug.vim --create-dirs \
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

我在$HOME/.config/nvim/plug.vim管理配置插件:

1
2
3
4
5
6
7
8
9
if has("nvim")
  	 let g:plug_home = stdpath('data') . '/plugged'
endif
  
call plug#begin()
  
  ...
  
call plug#end()

为了使plug.vim配置生效,需要在init.vim中进行配置加载:

1
runtime ./plug.vim

此时,重启NeoVim,运行:PlugInstall进行插件安装。

此外其他几个配置文件:maps.vim,macos.vim 也需要手动配置加载:

1
2
3
4
5
6
7
8
if has("unix")
    let s:uname = system("uname -s")
    " Do mac stuff
    if s:uname == "Darwin\n"
       runtime ./macos.vim
    endif
endif
runtime ./maps.vim

后续安装的插件,分个单独配置,放置到after/plugin目录下。

至此root配置(init.vim)、插件管理、快捷键管理、各插件独立进行配置,配置分离便于管理。

2、目录文件插件

NERDTree是Vim最常用的插件之一,可以在Vim运行时显示目录和文件结构,类似TextMate左侧的文件浏览器,但操作起来更为方便。

目前还有另外一个目录和文件结构管理插件defx.nvim

  • 不依赖于 denite.nvim
  • Vim8/neovim 兼容(Vim8 需要 nvim-yarp)
  • 由 Python3 实现
  • 没有双重过滤器功能
  • 列功能
  • 类似于 denite.nvim 一样支持 source
  • 支持选项
  • 突出显示由列定义
  • 很少的命令(仅:Defx 命令?)
  • 扩展重命名
  • 支持标记

安装

1
Plug 'Shougo/defx.nvim', { 'do': ':UpdateRemotePlugins' }

配置使用

安装完成后,使用:Defx命令来使用:

img

每次使用时Buffer会充满整个窗口,并不是我们常用形态,此时需要再进行配置,指定显示的位置以及大小等:

1
2
3
4
5
6
7
8
9
call defx#custom#option('_', {
   \ 'winwidth': 38,
   \ 'direction': 'topleft',
   \ 'split': 'vertical',
   \ 'show_ignored_files': 0,
   \ 'buffer_name': '',
   \ 'toggle': 1,
   \ 'resume': 1,
   \ })

效果如下:

Defx -split=vertical

设置快捷键

使用时通过输入Defx命令太过繁琐效率不高,可以通过设置快捷键来改善,maps.vim中添加:

1
2
3
nnoremap <silent>sf :<C-u>Defx
   \ -auto-cd
   \ -columns=mark:indent:icon:icons:filename:type:git<CR>

后续通过键入sf即可唤出文件目录管理。

after/plugin/defx.rc.vim 添加快捷键配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
autocmd FileType defx call s:defx_my_settings()
	function! s:defx_my_settings() abort
	  " Define mappings
	  nnoremap <silent><buffer><expr> <CR>
	  \ defx#do_action('open')
	  nnoremap <silent><buffer><expr> c
	  \ defx#do_action('copy')
	  nnoremap <silent><buffer><expr> m
	  \ defx#do_action('move')
	  nnoremap <silent><buffer><expr> p
	  \ defx#do_action('paste')
	  nnoremap <silent><buffer><expr> l
	  \ defx#do_action('open')
	  nnoremap <silent><buffer><expr> E
	  \ defx#do_action('open', 'vsplit')
	  nnoremap <silent><buffer><expr> P
	  \ defx#do_action('preview')
	  nnoremap <silent><buffer><expr> o
	  \ defx#do_action('open_tree', 'toggle')
	  nnoremap <silent><buffer><expr> K
	  \ defx#do_action('new_directory')
	  nnoremap <silent><buffer><expr> N
	  \ defx#do_action('new_file')
	  nnoremap <silent><buffer><expr> M
	  \ defx#do_action('new_multiple_files')
	  nnoremap <silent><buffer><expr> C
	  \ defx#do_action('toggle_columns',
	  \                'mark:indent:icon:filename:type:size:time')
	  nnoremap <silent><buffer><expr> S
	  \ defx#do_action('toggle_sort', 'time')
	  nnoremap <silent><buffer><expr> d
	  \ defx#do_action('remove')
	  nnoremap <silent><buffer><expr> r
	  \ defx#do_action('rename')
	  nnoremap <silent><buffer><expr> !
	  \ defx#do_action('execute_command')
	  nnoremap <silent><buffer><expr> x
	  \ defx#do_action('execute_system')
	  nnoremap <silent><buffer><expr> yy
	  \ defx#do_action('yank_path')
	  nnoremap <silent><buffer><expr> .
	  \ defx#do_action('toggle_ignored_files')
	  nnoremap <silent><buffer><expr> ;
	  \ defx#do_action('repeat')
	  nnoremap <silent><buffer><expr> h
	  \ defx#do_action('cd', ['..'])
	  nnoremap <silent><buffer><expr> ~
	  \ defx#do_action('cd')
	  nnoremap <silent><buffer><expr> q
	  \ defx#do_action('quit')
	  nnoremap <silent><buffer><expr> <Space>
	  \ defx#do_action('toggle_select') . 'j'
	  nnoremap <silent><buffer><expr> *
	  \ defx#do_action('toggle_select_all')
	  nnoremap <silent><buffer><expr> j
	  \ line('.') == line('$') ? 'gg' : 'j'
	  nnoremap <silent><buffer><expr> k
	  \ line('.') == 1 ? 'G' : 'k'
	  nnoremap <silent><buffer><expr> <C-l>
	  \ defx#do_action('redraw')
	  nnoremap <silent><buffer><expr> <C-g>
	  \ defx#do_action('print')
	  nnoremap <silent><buffer><expr> cd
	  \ defx#do_action('change_vim_cwd')
	endfunction

添加图标

设置文件图标可以让文件目录使用起来更加舒适,此时需要使用任意一种Nerd Fonts字体,并配置好iTerm,以便在iTerm2中可以正常显示字体图标:

1
2
brew tap homebrew/cask-fonts
brew cask install font-hack-nerd-font

iTerm2配置:

image-20220121123108917

这里有一点需要注意的是,如果勾选了Use a different font for non-ASSII text,那么第二种字体也需要使用Nerd Fonts中的一种,不然还是无法正常显示图标。

配置好iTerm2后,还需要安装defx-icons插件,才能使defx.nvim显示出文件图标,plug.vim添加:

1
Plug 'kristijanhusak/defx-icons'

:PlugInstall安装重启后,就可以看到:

image-20220121123639182

此时发现文件图片和文件名重叠,添加下面配置即可:

1
let g:defx_icons_column_length = 2

此时效果如下:

image-20220121123849035

从最近关闭的窗口打开文件

使用官方文档中的默认快捷键配置后,发现总是从当前窗口中,打开文件,很不符合习惯:

image-20220121124512001

如果想要和vscode一样,从右侧窗口打开文件,可以使用drop替代open来配置快捷键:

1
2
nnoremap <silent><buffer><expr> <CR> defx#do_action('drop')
nnoremap <silent><buffer><expr> l defx#do_action('drop')

文件名太长

文件名太长导致显示问题时,可以通过下面的方式,限制文件名可以显示的最大长度:

1
2
3
call defx#custom#column('filename', {
   \ 'max_width': 26,
   \})

3、启动屏

mhinz/vim-startify 启动屏可以记录最近编辑的文件,使用对应数字编号就可以快速打开文件,使用起来非常方便。

image-20220121125121988

4、内置LSP配置gopls

Neovim已经内置了语言服务器协议 (LSP)。LSP时一种开放的、基于JSON- RPC的协议,用于源代码编辑器和语言服务器之间的通信,可以提供特定于编程语言的功能,如:

  1. 调转到定义
  2. 自动完成
  3. 代码操作(自动格式化、包导入…)
  4. 显示方法签名
  5. 显示、转到参考
  6. 代码片段

从 0.5 版本开始,NeoVim 原生支持该协议。NeoVim在nvim-lspconfig 插件中维护了一个配置列表。该存储库包含设置和排除许多服务器故障的说明。

如果需要为自己刚兴趣的编程语言,需要为其安装和配置相应的LSP服务器,这些可以通过nvim-lspconfig插件来处理。

安装插件

1
Plug 'neovim/nvim-lspconfig'

需要注意的是这个插件只是配置管理,我们还需要单独为相应的编程语言安装LSP服务器,具体详见server_configurations

gopls

安装Google的 golang lsp server见:

https://github.com/golang/tools/tree/master/gopls

激活LSP服务,以及完整配置如下, lspconfig.rc.vim:

1
require'lspconfig'.gopls.setup{}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
if !exists('g:lspconfig')
  finish
endif

lua << EOF
vim.lsp.set_log_level("debug")
EOF

lua << EOF
local nvim_lsp = require('lspconfig')

nvim_lsp.pyright.setup{}
nvim_lsp.gopls.setup{}

local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.completion.completionItem.snippetSupport = true

-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
  local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end
  local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end

  -- Enable completion triggered by <c-x><c-o>
  buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')

  -- Mappings.
  local opts = { noremap=true, silent=true }
  -- See `:help vim.lsp.*` for documentation on any of the below functions
  buf_set_keymap('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', opts)
  buf_set_keymap('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
  buf_set_keymap('n', 'ga', '<Cmd>lua vim.lsp.buf.code_action()<CR>', opts)
  buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
  buf_set_keymap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
  buf_set_keymap('n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>', opts)
  buf_set_keymap('n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
  buf_set_keymap('n', '<space>wa', '<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>', opts)
  buf_set_keymap('n', '<space>wr', '<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>', opts)
  buf_set_keymap('n', '<space>wl', '<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>', opts)
  buf_set_keymap('n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>', opts)
  buf_set_keymap('n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<CR>', opts)
  buf_set_keymap('n', '<space>e', '<cmd>lua vim.diagnostic.open_float()<CR>', opts)
  buf_set_keymap('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<CR>', opts)
  buf_set_keymap('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<CR>', opts)
  buf_set_keymap('n', '<space>q', '<cmd>lua vim.diagnostic.setloclist()<CR>', opts)
  buf_set_keymap('n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)

end

-- Use a loop to conveniently call 'setup' on multiple servers and
-- map buffer local keybindings when the language server attaches
local servers = { 'pyright', 'rust_analyzer', 'tsserver' }
for _, lsp in ipairs(servers) do
  nvim_lsp[lsp].setup {
    on_attach = on_attach,
    flags = {
      debounce_text_changes = 150,
    }
  }
end

nvim_lsp.gopls.setup{
	cmd = {'gopls'},
	-- for postfix snippets and analyzers
	capabilities = capabilities,
	settings = {
	    gopls = {
		    experimentalPostfixCompletions = true,
		    analyses = {
		        unusedparams = true,
		        shadow = true,
		    },
		    staticcheck = true,
		    },
	    },
	on_attach = on_attach,
}

function goimports(timeoutms)
    local context = { source = { organizeImports = true } }
    vim.validate { context = { context, "t", true } }

    local params = vim.lsp.util.make_range_params()
    params.context = context

    -- See the implementation of the textDocument/codeAction callback
    -- (lua/vim/lsp/handler.lua) for how to do this properly.
    local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout_ms)
    if not result or next(result) == nil then return end
    local actions = result[1].result
    if not actions then return end
    local action = actions[1]

    -- textDocument/codeAction can return either Command[] or CodeAction[]. If it
    -- is a CodeAction, it can have either an edit, a command or both. Edits
    -- should be executed first.
    if action.edit or type(action.command) == "table" then
      if action.edit then
        vim.lsp.util.apply_workspace_edit(action.edit)
      end
      if type(action.command) == "table" then
        vim.lsp.buf.execute_command(action.command)
      end
    else
      vim.lsp.buf.execute_command(action)
    end
end

EOF

配置gopls服务,提供跳转、查看定义、重命名等:

1
2
3
4
5
6
7
8
  -- Mappings.
  local opts = { noremap=true, silent=true }
  -- See `:help vim.lsp.*` for documentation on any of the below functions
  buf_set_keymap('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', opts)
  buf_set_keymap('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
  buf_set_keymap('n', 'ga', '<Cmd>lua vim.lsp.buf.code_action()<CR>', opts)
  buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
  buf_set_keymap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)

格式化代码

format img

格式化代码快捷键配置:

1
buf_set_keymap('n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)

保存文件时自动格式化:

1
2
" 保存代码前进行自动格式化
autocmd BufWritePre *.go lua vim.lsp.buf.formatting()

包导入

触发包导入有两种方式来解决,一种时通过code action来手动触发:

1
buf_set_keymap('n', 'ga', '<Cmd>lua vim.lsp.buf.code_action()<CR>', opts)

也可以通过保存文件时,自动触发:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function goimports(timeoutms)
    local context = { source = { organizeImports = true } }
    vim.validate { context = { context, "t", true } }

    local params = vim.lsp.util.make_range_params()
    params.context = context

    -- See the implementation of the textDocument/codeAction callback
    -- (lua/vim/lsp/handler.lua) for how to do this properly.
    local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout_ms)
    if not result or next(result) == nil then return end
    local actions = result[1].result
    if not actions then return end
    local action = actions[1]

    -- textDocument/codeAction can return either Command[] or CodeAction[]. If it
    -- is a CodeAction, it can have either an edit, a command or both. Edits
    -- should be executed first.
    if action.edit or type(action.command) == "table" then
      if action.edit then
        vim.lsp.util.apply_workspace_edit(action.edit)
      end
      if type(action.command) == "table" then
        vim.lsp.buf.execute_command(action.command)
      end
    else
      vim.lsp.buf.execute_command(action)
    end
  end

并配置:

1
autocmd BufWritePre *.go lua goimports(1000)

自动完成

完成是开箱即用的。我们只需要将它映射到 vimomnifunc即可使我们的Ctrl+x,Ctrl+o工作:

1
  buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')

完成

但是,每次触发都需要通过Ctrl+x,Ctrl+o来触发,十分不方便,为了自动触发,可以在通过安装新的插件ms-jpq/coq_nvim来完成,如:

1
2
3
4
5
" 增强代码自动完成
Plug 'ms-jpq/coq_nvim', {'branch': 'coq'}
" 9000+ Snippets
Plug 'ms-jpq/coq.artifacts', {'branch': 'artifacts'}
Plug 'ms-jpq/coq.thirdparty', {'branch': '3p'}

5、其他优化配置

复制到自动剪贴板

始终将所有操作复制到系统剪贴板:

1
set clipboard+=unnamedplus

优化NeoVim内置lsp

目前基于内置lsp的功能已经比较完善,但是缺乏一个良好的操作UI,如重命名时,输入新名字的地方在窗口底部:

image-20220121173142281

使用async-lsp-finder后:

img

安装

1
Plug 'glepnir/lspsaga.nvim'

快捷键配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
" 异步lsp查找
nnoremap <silent> gh :Lspsaga lsp_finder<CR>
" Code Action
nnoremap <silent><leader>ca :Lspsaga code_action<CR>
vnoremap <silent><leader>ca :<C-U>Lspsaga range_code_action<CR>
" 悬停文档
nnoremap <silent>K :Lspsaga hover_doc<CR>
" scroll down hover doc or scroll in definition preview
nnoremap <silent> <C-f> <cmd>lua require('lspsaga.action').smart_scroll_with_saga(1)<CR>
" scroll up hover doc
nnoremap <silent> <C-b> <cmd>lua require('lspsaga.action').smart_scroll_with_saga(-1)<CR>
" help
nnoremap <silent> gs :Lspsaga signature_help<CR>
" 重命名
nnoremap <silent>gr :Lspsaga rename<CR>
" 预览定义
nnoremap <silent> gd :Lspsaga preview_definition<CR>
" 浮动终端
nnoremap <silent> <A-d> :Lspsaga open_floaterm<CR>
tnoremap <silent> <A-d> <C-\><C-n>:Lspsaga close_floaterm<CR>

参考

  1. neovim-lsp
  2. nvim-treesitter
  3. nvim-lspconfig
  4. defx.nvim
  5. defx.txt
  6. How to set up Neovim 0.5 + Modern plugins (LSP, Treesitter, Fuzzy finder, etc)
  7. dotfiles-public
  8. programming-go-in-neovim
  9. go-vim
  10. awesome-neovim
  11. VIM USER MANUAL
  12. runtimepath
  13. 如何配置 Vim 的 Golang 开发环境