Paveltrpn GitHub IO

Posts

About

Todo

Готовим vim (nvim) для golang

Настройка vim (neovim) для работы в качестве IDE для конкретного языка программирования задача довольно нетривиальная, что бы не говорили эксперты в интернете. Сходу трудно разобраться в обилии плагинов, расширений этих плагинов и расширений расширений и т.д. Кто-то реализут LSP, кто-то работает через native LSP, некоторые настолько стары, что вообще обходятся без отдельного сервера, кто-то конкурирует с этими решениями, размывая поляну для настроки, а сверху это ещё приправлено движками автодополнения, сниппетов и пр.
Среди всего этого разнообразия создаётся ощущение, что golang с его экосистемой наиболее подготовлен для интеграции с vim - всё что нужно поставляется вместе с дистрибутивом языка, либо поддерживается самим гуглом, либо легко устанавливается. Такие инструменты как gopls, staticcheck, delve, gofmt (goimports), т.е. весь джентельменский набор находится на расстоянии вытянутой руки, довольно однозначен и активно поддерживается.

Начинаем

Рассказ будет относится как к vim так и к neovim, перечень требований к будущей сборке примерно такой:

Neovim

Первым делом добавим возможность копировать и вставлять текст из системного clipboard

# для wayland
sudo apt-get install wl-clipboard

# для X11
sudo apt-get install xclip

Настройка neovim будет идти с использованием нативного LSP сервера и движка автодополнения coq.nvim. Тут существует возможности фактически дублирования конфига vim, но нативное решение кажется более подходящим.
Подготовим файлы конфигурации и разместим их на своих местах (если это ещё не сделано):

# основной файл конфигурации
touch ~/.config/nvim/init.lua

# директория со скриптами инициализации
# и насторойки плагинов. Подгружается
# автоматически
mkdir ~/.config/nvim/lua
touch ~/.config/nvim/lua/nvim-lspconfig.lua

Скачиваем и устанавливаем менеджер пакетов packer.nvim

curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

Далее показана та часть init.lua, которая отвечает за подключение собственно packer.nvim и плагинов, которые сдклают из ванильного neovim подобие golang IDE


local use = require('packer').use
require('packer').startup(function()
  -- Boilerplate для поддержки нативного LSP 
  -- сервера
  use 'neovim/nvim-lspconfig'
  -- Автодополнение    
  use 'ms-jpq/coq_nvim'
  -- Сниппеты
  use 'ms-jpq/coq.artifacts'

end)

Дале по порядку, neovim/nvim-lspconfig - основной плагин всей этой истории. Boilerplate для нативного LSP сервера. Имеет поддержку для многих серверов, но нас интересует только gopls (и, возможно, sumneko/lua-language-server, но тут не о нём). Файл конфигурации для golang будет переведён ниже. Все привязки клавиш видны из него и повторять их отдельно смысла не вижу. В целом подключается он без проблем, единственное замечание - подключение файла в init.lua должно быть после всех остальных, иначе в моём конфиге не работали хоткей, видимо их перетирал какой-то другой плагин

 -- init.lua
...
-- use 'neovim/nvim-lspconfig'
-- This line placed last in file because i found 
-- that some plugins in this config obscure 
-- mappings of LSP server if it included before them
require "nvim-lspconfig"
-- EOF nvim.lua

-- nvim-lspconfig.lua

-- NEOVIM LSP DEFAULT CONFIG --
-- Mappings.
-- See `:help vim.diagnostic.*` for documentation on any of the below functions
local opts = { noremap=true, silent=true }
vim.keymap.set('n', '<space>e', vim.diagnostic.open_float, opts)
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts)
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts)
vim.keymap.set('n', '<space>q', vim.diagnostic.setloclist, opts)

-- 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)
  -- Enable completion triggered by <c-x><c-o>
  vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')

  -- Mappings.
  -- See `:help vim.lsp.*` for documentation on any of the below functions
  local bufopts = { noremap=true, silent=true, buffer=bufnr }
  vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, bufopts)
  vim.keymap.set('n', 'gd', vim.lsp.buf.definition, bufopts)
  vim.keymap.set('n', 'K', vim.lsp.buf.hover, bufopts)
  vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, bufopts)
  vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, bufopts)
  vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, bufopts)
  vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, bufopts)
  vim.keymap.set('n', '<space>wl', function()
    print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
  end, bufopts)
  vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, bufopts)
  vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, bufopts)
  vim.keymap.set('n', '<space>ca', vim.lsp.buf.code_action, bufopts)
  vim.keymap.set('n', 'gr', vim.lsp.buf.references, bufopts)
  vim.keymap.set('n', '<space>f', vim.lsp.buf.formatting, bufopts)
end

local lsp_flags = {
  -- This is the default in Nvim 0.7+
  debounce_text_changes = 150,
}

util = require "lspconfig/util"

require'lspconfig'.gopls.setup {
    cmd = {'gopls'},
    filetypes = {"go", "gomod"},
    root_dir = util.root_pattern("go.work", "go.mod", ".git"),
	-- for postfix snippets and analyzers
	capabilities = capabilities,
	    settings = {
	      gopls = {
		      experimentalPostfixCompletions = true,
		      analyses = {
		        unusedparams = true,
		        shadow = true,
		     },
		     staticcheck = true,
		    },
	    },

    on_attach = on_attach,
    flags = lsp_flags,
}

Автодополнение - ms-jpq/coq_nvim и сниппеты - ms-jpq/coq.artifacts необходимые спутники LSP сервера. Их подключение происходит просто в init.lua. У этой пары есть конкуренты в виде группы nvim-cmp, но их подключение и настройка несколько сложнее. Нужно сказать, что перед использованием этих расширений необходимо сделать некоторые телодвижения

sudo apt-get install python3-venv
# далее в консоли neovim
:COQdeps
:COQnow

Подключаются эти расширения через init.lua, отдельный файл, на мой взгляд, им не нужен

...
-- use 'ms-jpq/coq_nvim'
-- Config coq.nvim autostart
vim.cmd([[
let g:coq_settings = {'auto_start': 'shut-up'}
]])
-- Config to use with gopls
local lsp = require "lspconfig"
local coq = require "coq"
...

На этом минимальную настройку поддержки IDE функций golang в neovim можно считать законченной. Сейчас мы уже имеем автодополнение кода по вызову ctrl-c+ctrl-o, автодополнение кода на основе анализа имеющегося текста, сниппеты, go to definition, просмотр godoc по K (shift-k), индикацию ошибок прямо в коде и навигацию по ним. Чего не хватает - возможностей vim-go, а именно вызова команд go по горячим клавишам, например staticchek или goimports, но это решается подключением vim-go по образцу конфигурации vim

Vim

Общение с системным clipboard в vim, скорее всего, будет доступно по умолчанию, но в любом случае всё описано ниже

# проверим есть ли поддежка
vim --version | grep clipboard
# должна присутствовать строчка +clipboard
# если её нет, то необходимо пересобрать vim
# с поддержкой clipboard

git clone https://github.com/vim/vim
cd vim/
./configure\ 
  --enable-cscope\
  --with-features=huge\
  --with-x\
  --with-compiledby="some guy"
make -j6
sudo make install

Настраивать vim будем с использованием плагинов vim-go и coc.nvim (не путать с решением выше, там coq). Подготовим файлы конфигурации и разместим их на своих местах (если это ещё не сделано):

# основной файл конфигурации
touch ~/.vimrc
mkdir ~/.vim

# настройки плагина vim-go
touch ~/.vim/vim-go.vim
# настройки плагина coc.nvim
touch ~/.vim/coc.vim
# файл конфигурации coc.vim
touch ~/.vim/coc-settings.json

Скачиваем и устанавливаем менеджер пакетов vim-plug

curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

Далее показана та часть .vimrc, которая отвечает за подключение собственно vim-plug и плагинов, которые сделают из ванильного vim подобие golang IDE

call plug#begin()
" Golang vim plugin
  Plug 'fatih/vim-go'

  " Autocomplete plugin. Very complex node.js plugin with many features,
  " many languages support and so on. I prefer to use vim-go instead of it by now.
  " May be plugged whihout golang extension just for autocomplete and use
  " vim-go for golang features.
  Plug 'neoclide/coc.nvim', {'branch': 'release'}
call plug#end()
...
source ~/.vim/vim-go.vim
source ~/.vim/coc.vim
...
      

fatih/vim-go - основной плагин, добавляет поддержку golang, работает через общение с gopls и остальными утилитами golang. Позволяет выполнять все необходимые манипуляции с кодом через команды - :GoRun, :GoBuild, :GoLint, :GoFmt и т.д. В моём конфиге настроены несколько шорткатов (помимо ctrl-x+ctrl-o и shift-k для godoc) - <leader>l для вызова линтера (staticcheck), <leader>f для форматирования и импортов (goimports).
neoclide/coc.nvim - на самом деле это очень внушительный комбайн для автодополнения кода, работы с LSP и ещё кучей возможностей. Поддерживает собственные расширения (:CocInstall coc-go например, CocUninstal - удалить, CocList - управление расширениями) через которые можно настроить сам golang и вообще любой другой язык. В моём случае он использует два расширения - coc-go для поддержки golang, coc-snippets очевидно для сниппетов и выполняет функцию нескучного автодопоaлнения произвольного текста. Для работы использует node.js.
Далее приведён vim-go.vim, который отвечает за настройку этого расширения. Файл конфигурации для coc.nvim слишком велик, что бы привести его здесь, его можно найти в репозитории.

" Plug 'fatih/vim-go'
" Golang vim-go plugin hightlights options
let g:go_highlight_types = 1
let g:go_highlight_fields = 1
let g:go_highlight_functions = 1
let g:go_highlight_methods = 1
let g:go_highlight_operators = 1
let g:go_highlight_build_constraints = 1
let g:go_highlight_structs = 1
let g:go_highlight_generate_tags = 1
let g:go_highlight_space_tab_error = 0
let g:go_highlight_array_whitespace_error = 0
let g:go_highlight_trailing_whitespace_error = 0
let g:go_highlight_extra_types = 1
let g:go_highlight_interfaces = 1
let g:go_highlight_operators = 1

" Show identifier info in status bar
let g:go_auto_type_info = 1

let g:go_doc_popup_window = 1
let g:go_metlainter_command = "staticcheck"
" May be set to gofmt instead, beacuse of some people claims
" that goimports may take long time for work.
let g:go_fmt_command = "goimports" 
" Some vim-go remaps for easy invoke `go vet`, `gofmt`,`staticcheck`
" and `go-doc. Remaping works with \v, \f, \l and \d respectively
autocmd FileType go nnoremap <leader>f :GoFmt<CR>
autocmd FileType go nnoremap <leader>l :GoMetaLinter<CR>
autocmd FileType go nnoremap <leader>d :GoDoc<CR>
" go vet can be replaced with statickcheck
" autocmd FileType go nnoremap <leader>v :GoVet!<CR>

" Supress vim-go autocomplete preview window
set completeopt-=preview

" Autoclose vim-go autocomplete preview window when
" close autocomplete popup hover window
augroup completion_preview_close
  autocmd!
  autocmd CompleteDone * if !&previewwindow && &completeopt =~ 'preview' | silent! pclose | augroup END

vim-go по умолчанию при вызове автодополнеaния показывает godoc в окне preview vim. В конфиге прописан способ от этого избавится. Так же, этот плагин отображает результат вызова линтера в quickfix, который, в свою очередь любит появляться не там где надо, поэтому настроено перемещение этого pane в самую нижнюю позицию при его появлении и шорткат для быстрого закрытия quickfix

" Move newborn quickfix window to bottomest place.
" This trigger takes advantage of the fact that the quickfix window 
" can be easily distinguished by its file-type, qf. The wincmd J command 
" is equivalent to the [Ctrl+W, Shift+J] shortcut sequence 
" instructing Vim to move the current window to the very bottom of the screen
:autocmd FileType qf wincmd J

" Map to close quickfix window - <leader>q
nnoremap <leader>q :cclose<CR>

coc-snippets может использовать в работе UltiSnips, для чего нужно установить python расширение что бы была возможность выполнять команду :pyx

pip install pynvim

С другой стороны, если эта фича не завелась, можно отключить её в coc-settings.json

{
  "languageserver": {
    "go": {
      "command": "gopls",
      "rootPatterns": ["go.mod"],
      "trace.server": "verbose",
      "filetypes": ["go"]
    }
  },
  "snippets.ultisnips.enable": false,
}