Предисловие

В процессе знакомства с 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 - основные настройки Neovim

  • lua/keymaps.lua - горячие клавиши

  • lua/lsp.lua - конфигурация Language Server Protocol для автодополнения и поддержки языков, как в твоем любимом IDE

  • lua/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 и плагинов

Подсветка синтаксиса и навигация

  1. 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 }
}
  1. 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

Дотфайлы

Статьи

Комментарии (8)


  1. ysomad Автор
    22.07.2022 12:13
    +1

    Пока статья была на модерации я успел пересмотреть момент по тому, как хранить конфигурацию LSP и раскидал настройки lspconfig, cmp в свои модули в папке plugins, оставив только on_attach функцию в lsp.lua. Посмотреть можно в моих дотфайлах, ссылка есть в конце статьи.


  1. vtb_k
    22.07.2022 12:21
    +1

    Если вдруг кому будет интересно, могу поделиться своим конфигом Емакс для Go.


  1. bobalus
    22.07.2022 12:41
    +1

    Настраивал для linux-окружения на слабом хромбуке (MT8183). Сначала использовал связку vim(не нео) + go-vim + coc.nvim. Работало, скорость не впечатляла(подозреваю в этом node.js) и периодически намертво зависало.

    Перебрался на vim + https://github.com/govim/govim. Конфигурация проста: единственное, что я сделал - переопределил leader key. Скорость и удобство устраивает.

    И по теме статьи - govim как раз тоже не VimScript, а написан на Go и предоставляет API для разработки плагинов.


    1. ysomad Автор
      22.07.2022 12:54
      +1

      интересно, но к сожалению этот плагин не поддерживает neovim.


  1. includedlibrary
    22.07.2022 18:09
    +1

    У вас при использовании встроенного lsp клиента проблем не возникало? Я помню его пытался для хаскеля использовать, в результате перешёл на coc, потому что при длительном использовании пропадала подсветка и переставал работать переход к определению


    1. ysomad Автор
      22.07.2022 18:19
      +1

      нет, никаких проблем не замечал.


    1. ysomad Автор
      22.07.2022 18:23
      +1

      и подсветка не от LSP идет, а от tree sitter'а


      1. includedlibrary
        22.07.2022 18:25
        +1

        У меня был стандартный .vim файл для подсветки, но она всё равно исчезала. Надо будет ещё раз попробовать встроенный клиент, видимо баги в нём исправили