Предисловие
В процессе знакомства с Neovim было прочитано много статей, конфигураций на Github, было просмотрено огромное количество роликов на Youtube на тему настройки, но в большинстве случаев приходилось донастраивать все под себя. В этой статье я расскажу как я настроил Neovim для разработки на Go, используя только Lua плагины и init.lua
.
Эта статья может быть полезна тебе, если:
пишешь на Go
есть конфиг на Vimscript, но хочется на Lua
хочется пересесть с тяжелых современных IDE или текстовых редакторов, таких как Goland, Vscode и других, на Neovim
Vimscript против Lua
Подробного их сравнения в этой статье не будет, так как это выходит за ее рамки, но основные преимущества Lua перед Vimscript это:
Скорость. Плагины написанные на Lua будут работать быстрее чем их реализации на Vimscript
Модульность. С Lua ты сможешь навести порядок - горячие клавиши в одном файле, настройки в другом, плагины в третьем т.д.
Простота и поддерживаемость. Vimscript очень специфичен и используется только внутри Vim. Lua же в свое время является одним из самых популярных встраиваемых скриптовых языков за счет свой скорости и простоты, например, на нем написан интерфейс World of Warcraft и на нем пишут плагины для Kong API Gateway
Структура конфига
Lua позволяет разбирать настройки по файлам (модулям), сделать это можно сделать следующим образом:
├── init.lua
└── lua
├── autocommands.lua
├── keymaps.lua
├── lsp.lua
├── options.lua
└── plugins
├── init.lua
init.lua
- инициализирует модули из директории lua, файлы init инициализируются по умолчанию, для них не нужно прописыватьrequire('init.lua')
lua/options.lua
- основные настройки Neovimlua/keymaps.lua
- горячие клавишиlua/lsp.lua
- конфигурация Language Server Protocol для автодополнения и поддержки языков, как в твоем любимом IDElua/autocommands.lua
- автокоманды, например, сортировка импортов после сохраненияlua/plugins/init.lua
- инициализация плагинов; конфигурацию для конкретных плагинов я храню вlua/plugins/<plugin_name>.lua
После этого, чтобы подгрузились все модули, необходимо инициализировать их в init.lua
:
-- ~/.config/nvim/init.lua
require('options')
require('keymaps')
require('autocommands')
require('plugins')
require('lsp')
При инициализации модулей порядок важен, поэтому не стоит, например, в модуле keymaps
держать горячие клавиши для плагинов, а лучше переместить их в конкретный модуль в lua/plugins/<plugin_name>.lua
Плагин менеджер
Для установки плагинов для Lua конфигураций можно использовать плагин менеджер Packer.nvim - Lua альтернатива для популярного vim-plug.
Базовая конфигурация Packer:
-- ~/.config/nvim/lua/plugins/init.lua
vim.cmd [[packadd packer.nvim]]
return require('packer').startup(function(use)
use 'wbthomason/packer.nvim'
-- ...
end)
Если нужно, чтобы Packer автоматически устанавливался на любом ПК, можно использовать следующий сниппет:
-- ~/.config/nvim/lua/plugins/init.lua
local fn = vim.fn
local install_path = fn.stdpath('data')..'/site/pack/packer/start/packer.nvim'
if fn.empty(fn.glob(install_path)) > 0 then
packer_bootstrap = fn.system({'git', 'clone', '--depth', '1', 'https://github.com/wbthomason/packer.nvim', install_path})
end
return require('packer').startup(function(use)
use 'wbthomason/packer.nvim'
-- ...
if packer_bootstrap then
require('packer').sync()
end
end)
Установка плагинов
Для установки и обновления плагинов можно использовать команду :PackerSync
.
Обязательные плагины
Для упрощенной настройки LSP, автодополнений и навигации по файлам были собраны наиболее популярные плагины в сообществе и приведены ниже:
-- ~/.config/nvim/lua/plugins/init.lua
-- ...
return require('packer').startup(function(use)
use 'wbthomason/packer.nvim'
-- набор Lua функций, используется как зависимость в большинстве
-- плагинов, где есть работа с асинхронщиной
use 'nvim-lua/plenary.nvim'
-- конфиги для LSP серверов, нужен для простой настройки и
-- возможности добавления новых серверов
use 'neovim/nvim-lspconfig'
-- зависимости для движка автодополнения
use 'hrsh7th/cmp-nvim-lsp'
use 'hrsh7th/cmp-buffer'
use 'hrsh7th/cmp-path'
-- движок автодополнения для LSP
use 'hrsh7th/nvim-cmp'
-- парсер для всех языков программирования, цветной код как в твоем
-- любимом IDE
use {
'nvim-treesitter/nvim-treesitter',
run = ':TSUpdate',
config = function()
-- так подгружается конфигурация конкретного плагина
-- ~/.config/nvim/lua/plugins/treesitter.lua
require('plugins.treesitter')
end
}
-- навигация по файлам, fzf, работа с буферами и многое другое
-- если больше привыкли к файловому дереву, есть альтернатива - nvim-tree
-- https://github.com/kyazdani42/nvim-tree.lua
use {
'nvim-telescope/telescope.nvim',
config = function()
require('plugins.telescope')
end
}
end)
Дополнительные плагины
Есть ряд плагинов, которые ускорят разработку, они в большей степени заменяют функционал, к которому большинство привыкло в современных IDE и редакторах, а некоторых из них "улучшают" внешний вид Neovim, если вам это необходимо:
-- ~/.config/nvim/lua/plugins/init.lua
-- ...
return require('packer').startup(function(use)
-- плагины из предыдущего абзаца
-- Иконки для расширений файлов (для корректной работы нужен
-- установленный один из Nerd шрифтов в терминале) - опционален
-- https://github.com/ryanoasis/nerd-fonts
use {
'kyazdani42/nvim-web-devicons',
config = function()
require('nvim-web-devicons').setup({ default = true; })
end
}
-- ИИ автодополнения
use {
'tzachar/cmp-tabnine',
run='./install.sh'
}
-- иконки в выпадающем списке автодополнений (прямо как в vscode)
use('onsails/lspkind-nvim')
-- статусбар, аналог vim-airline, только написан на lua
use {
'nvim-lualine/lualine.nvim',
config = function()
require('plugins.lualine')
end
}
-- отображение буфферов/табов в верхнем горизонтальном меню
-- p.s. сам не использую, мне хватает telescope
use {
'akinsho/bufferline.nvim',
tag = "v2.*",
config = function()
require('plugins.bufferline')
end
}
-- движок сниппетов
use {
'L3MON4D3/LuaSnip',
after = 'friendly-snippets',
config = function()
require('luasnip/loaders/from_vscode').load({
paths = { '~/.local/share/nvim/site/pack/packer/start/friendly-snippets' }
})
end
}
-- автодополнения для сниппетов
use 'saadparwaiz1/cmp_luasnip'
-- набор готовых сниппетов для всех языков, включая go
use 'rafamadriz/friendly-snippets'
-- плагин для простого комментирования кода
use {
'numToStr/Comment.nvim',
config = function()
require('Comment').setup()
end
}
-- автоматические закрывающиеся скобки
use {
'windwp/nvim-autopairs',
config = function()
require("nvim-autopairs").setup()
end
}
-- набор утилит для Go
use {
'olexsmir/gopher.nvim',
config = function()
require('plugins.gopher')
end
}
-- ...
end)
P.S. Стоит заметить, что если в ~/config/nvim/lua/plugins/init.lua
у плагинов прописано поле config
с подгрузкой модуля (как в конфигурации выше), но этого модуля не существует, или для описанного плагина не был вызван setup()
, плагин не будет подгружен или при входе в Neovim всплывет ошибка.
Плагины для Go
Есть множество вариантов использования Neovim как IDE для Go. Например можно использовать coc.nvim - альтернатива нативному LSP с еще более простой настройкой и поддержкой множества серверов, но меньшей скоростью, так как написан он на nodejs.
Также можно использовать vim-go, который имеет поддержку gopls
LSP и поддержку языка Go, что позволяет не выходят из редактора выполнять такие команды, как GoInstall
, GoBuild
и другие. Но не смотря на наличие таких целостных решений я решил пойти другим путем и использовать максимально модульные и простые решения, остановившись на нативном LSP и плагине gopher.nvim.
нативный LSP - позволяет настроить автодополнения, диагностику, форматирование (настройка LSP будет рассмотрена чуть позже)
gopher.nvim - набор утилит для Go (например позволяет в одну команду добавить/удалить теги к структуре или сгенерировать табличные тесты, полный список команд здесь)
Устанавливается gopher аналогично с другими плагинами:
-- ~/.config/nvim/lua/plugins/init.lua
-- ...
return require('packer').startup(function(use)
-- плагины из предыдущего абзаца
use {
'olexsmir/gopher.nvim',
config = function()
require('plugins.gopher')
end
}
-- ...
end)
Настройка плагина gopher будет приведена в абзаце "Настройка LSP и плагинов".
Настройка LSP и плагинов
Подсветка синтаксиса и навигация
nvim-treesitter - подсветка синтаксиса на основе парсеров tree-sitter
-- ~/.config/nvim/lua/plugins/treesitter.lua
require('nvim-treesitter.configs').setup{
-- список парсеров, список доступных парсеров можно посмотреть в документации
-- либо устаналивать все, чтобы подсветка синтаксиса работала везде корректно
-- https://github.com/nvim-treesitter/nvim-treesitter
ensure_installed = 'all',
-- установка phpdoc падает на m1
ignore_install = { 'phpdoc' },
-- включить подсветку
highlight = { enable = true }
}
telescope - навигация по фалам, буферам, греп и многое другое
-- ~/.config/nvim/lua/plugins/telescope.lua
require('telescope').setup {
pickers = {
buffers = {
-- начинать в normal моде при открытии списка буферов
initial_mode = 'normal'
}
}
}
local map = vim.api.nvim_set_keymap
local opts = {noremap = true, silent = true}
-- горячие клавиши
map('n', '<leader>ff', '<cmd>Telescope find_files<CR>', opts)
map('n', '<leader>fg', '<cmd>Telescope live_grep<CR>', opts)
map('n', '<leader>fb', '<cmd>Telescope buffers<CR>', opts)
LSP
Для корректной работы LSP с Go я использую gopls
сервер, его нужно установить отдельно. Всю конфигурацию предпочитаю хранить в одном модуле, будь то инициализация LSP для разных ЯП или настройка автодополнений и ИИ к ним (tabnine).
Моя конфигурация LSP (за основу взята стандартная конфигурация из документации lspconfig с небольшими изменениями):
-- ~/.config/nvim/lua/lsp.lua
local map = vim.keymap.set
local opts = { noremap=true, silent=true }
-- удалить ошибки диагностики в левом столбце (SignColumn)
vim.diagnostic.config({signs=false})
-- стандартные горячие клавиши для работы с диагностикой
map('n', '<leader>e', vim.diagnostic.open_float, opts)
map('n', '[d', vim.diagnostic.goto_prev, opts)
map('n', ']d', vim.diagnostic.goto_next, opts)
map('n', '<leader>q', vim.diagnostic.setloclist, opts)
local on_attach = function(client, bufnr)
vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')
local bufopts = { noremap=true, silent=true, buffer=bufnr }
-- стандартные горячие клавиши для LSP, больше в документации
-- https://github.com/neovim/nvim-lspconfig
map('n', 'gD', vim.lsp.buf.declaration, bufopts)
map('n', 'gd', vim.lsp.buf.definition, bufopts)
map('n', 'K', vim.lsp.buf.hover, bufopts)
map('n', 'gi', vim.lsp.buf.implementation, bufopts)
map('n', '<C-k>', vim.lsp.buf.signature_help, bufopts)
map('n', '<leader>wa', vim.lsp.buf.add_workspace_folder, bufopts)
map('n', '<leader>wr', vim.lsp.buf.remove_workspace_folder, bufopts)
map('n', '<leader>wl', function()
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, bufopts)
map('n', '<leader>D', vim.lsp.buf.type_definition, bufopts)
map('n', '<leader>rn', vim.lsp.buf.rename, bufopts)
map('n', '<leader>ca', vim.lsp.buf.code_action, bufopts)
map('n', 'gr', vim.lsp.buf.references, bufopts)
map('n', '<leader>f', vim.lsp.buf.formatting, bufopts)
end
-- инициализация LSP для различных ЯП
local lspconfig = require('lspconfig')
local util = require('lspconfig/util')
local function config(_config)
return vim.tbl_deep_extend('force', {
capabilities = require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities()),
}, _config or {})
end
-- иницализация gopls LSP для Go
-- https://github.com/golang/tools/blob/master/gopls/doc/vim.md#neovim-install
lspconfig.gopls.setup(config({
on_attach = on_attach,
cmd = { 'gopls', 'serve' },
filetypes = { 'go', 'go.mod' },
root_dir = util.root_pattern('go.work', 'go.mod', '.git'),
settings = {
gopls = {
analyses = {
unusedparams = true,
shadow = true,
},
staticcheck = true,
}
}
}))
Автодополнения
За автодополнения отвечает плагин cmp и если необходимо можно добавить к нему tabnine. Пример настройки:
-- ~/.config/nvim/lua/lsp.lua
-- ...
map('n', '<leader>f', vim.lsp.buf.formatting, bufopts)
end
local cmp = require('cmp')
local lspkind = require('lspkind')
local source_mapping = {
buffer = '[Buffer]',
nvim_lsp = '[LSP]',
nvim_lua = '[Lua]',
cmp_tabnine = '[TN]',
path = '[Path]',
}
cmp.setup({
mapping = cmp.mapping.preset.insert({
['<C-y>'] = cmp.mapping.confirm({ select = true }),
['<C-d>'] = cmp.mapping.scroll_docs(-4),
['<C-u>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
}),
formatting = {
format = function(entry, vim_item)
vim_item.kind = lspkind.presets.default[vim_item.kind]
local menu = source_mapping[entry.source.name]
if entry.source.name == 'cmp_tabnine' then
if entry.completion_item.data ~= nil and entry.completion_item.data.detail ~= nil then
menu = entry.completion_item.data.detail .. ' ' .. menu
end
vim_item.kind = ''
end
vim_item.menu = menu
return vim_item
end
},
sources = cmp.config.sources({
{ name = 'cmp_tabnine' },
{ name = 'nvim_lsp' },
}, {
{ name = 'buffer' },
})
})
-- конфиг для ИИ автодополнений
local tabnine = require('cmp_tabnine.config')
tabnine:setup({
max_lines = 1000,
max_num_results = 20,
sort = true,
run_on_every_keystroke = true,
snippet_placeholder = '..',
})
-- инициализация LSP для различных ЯП
local lspconfig = require('lspconfig')
-- ...
Сниппеты
Чтобы автодополнения работали и для сниппетов, в sources
объект в setup
для cmp
нужно прокинуть название движка сниппетов - luasnip
, а также описать expand
функцию в snippet
объекте.
-- ~/.config/nvim/lua/lsp.lua
-- ...
cmp.setup({
snippet = {
expand = function(args)
require('luasnip').lsp_expand(args.body)
end,
},
-- ...
sources = cmp.config.sources({
{ name = 'cmp_tabnine' },
{ name = 'nvim_lsp' },
{ name = 'luasnip' }, -- сюда
}, {
{ name = 'buffer' },
})
})
-- ...
Если хочется использовать другой движок для сниппетов, как их подключить есть в документации cmp.
Автокоманды
С помощью автокоманд можно описать команды, которые будут срабатывать на:
чтение и запись в файл
вход или выход из буфера
выход из Neovim
Подробное описание можно посмотреть здесь.
Список автокоманд, которые я использую:
Отключить комментирование новой строки (без этой автокоманды, если переходить на новую строку с помощью
o
илиO
строка будет закомментирована)
-- ~/.config/nvim/lua/autocommands.lua
vim.api.nvim_create_autocmd({'BufEnter'}, {
pattern = '*',
callback = function()
opt.fo:remove('c')
opt.fo:remove('r')
opt.fo:remove('o')
end
})
Сортировка Go импортов на сохранение файла (взято здесь)
-- ~/.config/nvim/lua/autocommands.lua
vim.api.nvim_create_autocmd({'BufWritePre'}, {
pattern = '*.go',
callback = function()
local params = vim.lsp.util.make_range_params(nil, vim.lsp.util._get_offset_encoding())
params.context = { only = {'source.organizeImports'} }
local result = vim.lsp.buf_request_sync(0, 'textDocument/codeAction', params, 3000)
for _, res in pairs(result or {}) do
for _, r in pairs(res.result or {}) do
if r.edit then
vim.lsp.util.apply_workspace_edit(r.edit, vim.lsp.util._get_offset_encoding())
else
vim.lsp.buf.execute_command(r.command)
end
end
end
end,
})
Форматирование Go файлов на запись:
-- ~/.config/nvim/lua/autocommands.lua
vim.api.nvim_create_autocmd({'BufWritePre'}, {
pattern = '*.go',
callback = function()
vim.lsp.buf.formatting_sync(nil, 3000)
end
})
Удаление плавающих пробелов на запись:
-- ~/.config/nvim/lua/autocommands.lua
local TrimWhiteSpaceGrp = vim.api.nvim_create_augroup
('TrimWhiteSpaceGrp', {})
vim.api.nvim_create_autocmd('BufWritePre', {
group = TrimWhiteSpaceGrp,
pattern = '*',
command = '%s/\\s\\+$//e',
})
Подсветка скопированных строк
-- ~/.config/nvim/lua/autocommands.lua
local YankHighlightGrp = vim.api.nvim_create_augroup('YankHighlightGrp', {})
vim.api.nvim_create_autocmd('TextYankPost', {
group = YankHighlightGrp,
pattern = '*',
callback = function()
vim.highlight.on_yank({
higroup = 'IncSearch',
timeout = 40,
})
end,
})
Общие горячие клавиши
Горячие клавиши для плагинов лучше описывать в модулях соответствующих плагинов. В ~/.config/nvim/lua/keymaps.lua
я обычно описываю горячие клавиши, не зависящие от внешних зависимостей.
-- ~/.config/nvim/lua/keymaps.lua
local map = vim.api.nvim_set_keymap
local opts = {noremap = true, silent = true}
-- переход в Ex мод
map('n', '<leader>pv', ':Ex<CR>', opts)
map('n', 'Q', '<nop>', opts) -- анбинд Q
-- упрощенная индентация
map('v', '<', '<gv', opts)
map('v', '>', '>gv', opts)
-- отключение стрелочек (только hjkl)
map('', '<up>', '', opts)
map('', '<down>', '', opts)
map('', '<left>', '', opts)
map('', '<right>', '', opts)
Источники
Мои dotfiles