Полное руководство по настройке конфигурации NeoVim для разработчиков - часть 1

Neovim (nvim) - это тот же vim, только улучшенный, переписанный и перетянувший на свою сторону большую часть комьюнити оригинального редактора. Сейчас этот проект сильно обогнал оригиналный vim по статистике на github (34 vs 75k звезд на 2024 год), так что если ты по какой-то причине решил начать программировать в консоли в 2024 - neovim это твой выбор.

Зачем вообще это нужно, когда есть удобные IDE и современные редакторы вроде visual studio code? Обычно приводят два аргумента:

  1. Скорость. В любом vim-подобном редакторе основная фишка - это комбинации. И при должной сноровке ты можешь перемещаться по коду и редактировать его сильно быстрее, чем брать мышку в руки каждые несколько секунд;
  2. Легкость. Nvim - это утилита, которая открывается в консоли и потребляет очень мало ресурсов. IDE - это большая и очень тяжелая программа, которая на маке 22 года с трудом позволяет открыть 3-4 проекта сразу.

С первым аргументом я согласен отчасти: мы больше читаем код и думаем что написать, чем набираем его на клавиатуре. Поэтому, если ты думаешь, что освоение nvim даст тебе фору над коллегой с мышкой - нет, не даст. Но, где ты действительно обыграешь коллегу с мышкой - это в удобстве. Привыкнув к vim-комбинациям ты уже не сможешь печатать как раньше - это будет казаться очень неудобным.

Со вторым аргументом я согласен полностью - nvim легкий и быстрый, и возможность запустить привычный мне редактор в любой консоли, на любой современной и не очень операционной системе, и на любом устройстве - от старого ноутбука до телефона лично мне нравится.

У этой статьи нет задачи продать тебе этот редактор - он определенно не для всех. Сначала тебе нужно учиться с ним работать, а затем нужно время, чтобы его настроить. Если эта идея тебе не нравится - скорее всего, проще купить подписку на JetBrains или скачать visual studio code, открыть и все сразу будет работать. Если же тебе нравится linux, нравится конфигурировать инструменты с которыми ты работаешь, и нравится идея open source - то и nvim с большой долей вероятности тебе понравится тоже.

В рамках этой статьи я не буду рассказывать как работать внутри vim - есть множество уроков и материалов, где это описано. Я же расскажу как nvim сконфигурировать с нуля и получить в результате редактор, очень похожий на IDE как внешне, так и по функционалу.

Вся настройка выполнялась на MacBook под MacOS 14, в качестве терминала использовался WezTerm. WezTerm накладывает свои особенности при работе с хот-кеями, о которых я упомяну ниже, но тот же самый конфиг без проблем можно запустить и на любой unix-подобной системе, что будет даже немного проще. Я буду разворачивать все с нуля на чистой системе, чтобы закрыть все вопросы.

Для начала скачаем и установим WezTerm - лучший на мой взгляд терминал, который тоже можно кастомизировать: https://wezfurlong.org/wezterm/installation.html, скачиваем и устанавливаем пакет под свою ОС.

Из коробки, конечно, восторг не вызывает:

Давай это исправим. Создаем конфиг:

poltora ~: mkdir -p ~/.config/wezterm/
poltora ~: touch ~/.config/wezterm/wezterm.lua
poltora ~: vim ~/.config/wezterm/wezterm.lua

Открывается vim, куда мы вставляем вставляем следующие настройки:

local wezterm = require 'wezterm'

local config = {}

if wezterm.config_builder then
  config = wezterm.config_builder()
end

config.color_scheme = 'Atelierdune (light) (terminal.sexy)'
config.font = wezterm.font('JetBrains Mono', { weight = 'Medium' })
config.font_size = 16
config.hide_tab_bar_if_only_one_tab = true
config.window_background_opacity = 0.95

return config
Для wezterm доступно множество тем, ты можешь найти и прописать в конфиг выше любую на твой вкус - https://wezfurlong.org/wezterm/config/appearance.html. Аналогично и с другими настройками - благодаря подробной документации он очень легко и удобно кастомизируется.

Нажимаем :wqa, перезапускаем терминал и вот что получается:

Теперь установим neovim. Есть множество способов установки, в зависимости от системы и окружения, подробно о каждом можно почитать в официальном мане - https://github.com/neovim/neovim/blob/master/INSTALL.md

Самый простой для MacOS - установить как пакет:

poltora ~: brew install neovim

После установки - запускаем nvim:

poltora ~: nvim .

Открывается дефолтный редактор, который, мягко говоря, IDE особо не напоминает:

Закрываем комбинацией :q и начинаем конфигурировать. Прежде всего создадим корневой файл конфига:

mkdir ~/.config/nvim
touch ~/.config/nvim/init.lua
nvim ~/.config/nvim/init.lua

Конфиг по большей части будет состоять из настроек для различных плагинов. Для того, чтобы установить плагины нам нужен специальный менеджер, который я сейчас и предлагаю добавить.

Менеджер плагинов

Есть множество менеджеров, я пользуюсь одним из самых популярных - VimPlug. Устанавливаем командой:

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

Теперь открываем файл конфига ~/.config/nvim/init.lua, созданный выше, и добавляем в него следующую структуру:

local vim = vim
local Plug = vim.fn['plug#']

vim.call('plug#begin')

vim.call('plug#end')

В последующем все плагины мы будем добавлять между тегами vim.call('plug#begin') и vim.call('plug#end'). Сохраняем, перезапускаем и если в консоли нет ошибок - менеджер успешно установлен.

Чтобы не потеряться в наших конфигах, я буду добавлять все настройки и конфигурацию для каждого плагина в отдельный lua-файл, который будет лежать рядом с init.lua. Давай создадим файл common.lua и пропишем туда ряд базовых настроек.

common.lua:

-- отобразить номера строк
vim.wo.number = true
-- включить управление мышью
vim.g.mouse = 'a'
vim.opt.encoding="utf-8"
-- выключаем своп файла
vim.opt.swapfile = false

-- устанавливаем настройки табуляции и отступов
vim.opt.scrolloff = 7
vim.opt.tabstop = 4
vim.opt.softtabstop = 4
vim.opt.shiftwidth = 4
vim.opt.autoindent = true

vim.opt.fileformat = "unix"

Теперь возвращаемся в init.lua и подключаем этот файл:

local vim = vim
local Plug = vim.fn['plug#']

vim.call('plug#begin')

vim.call('plug#end')

home=os.getenv("HOME")
package.path = home .. "/.config/nvim/?.lua;" .. package.path

require"common"

Перезагружаем nvim, и если видим номера строк - все работает:

Темы

Сначала сделаем так, чтобы nvim визуально походил на IDE. Есть крутой сайт с каталогом тем от сообщества - https://vimcolorschemes.com/, где можно выбрать тему на свой вкус. Я привык работать со светлыми темами, и методом проб и ошибок подобрал для себя лучшую на мой взгляд - kanagawa, которую и предлагаю установить. Создаем новый файл theme.lua, куда добавляем:

vim.cmd.colorscheme("kanagawa")

В init.lua подключаем новый плагин и файл theme.lua:

...
Plug('rebelot/kanagawa.nvim')

vim.call('plug#end')
...
dofile"theme.lua"

Перезагружаем nvim, выполняем :PlugInstall, перезагружаем еще раз и видим, что тема применилась:

Теперь сделаем так, чтобы дерево файлов отображалось так же, как мы привыкли в IDE.

Nvim-Tree - дерево файлов

Для отображения дерева файлов будем использовать плагин nvim-tree. Создаем файл vimtree.lua и добавляем туда следующие настройки:

-- disable netrw at the very start of your init.lua
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1

-- optionally enable 24-bit colour
vim.opt.termguicolors = true

-- empty setup using defaults
require("nvim-tree").setup()

-- OR setup with some options
require("nvim-tree").setup({
  sort = {
    sorter = "case_sensitive",
  },
  view = {
    width = 30,
  },
  renderer = {
    group_empty = true,
  },
  filters = {
    dotfiles = true,
  },
})

Теперь добавляем плагин nvim-tree и плагин для поддержки иконок файлов и активируем файл с настройками nvim-tree в init.lua:

...
Plug('kyazdani42/nvim-tree.lua')
Plug('kyazdani42/nvim-web-devicons')

vim.call('plug#end')

...
require"vimtree"

Перезагружаем, устанавливаем плагин командой :PlugInstall, перезагружаем еще раз и у нас открывается дерево файлов, похожее на то, к чему мы привыкли:

BarBar - вкладки файлов

Теперь добавим в редактор привычные вкладки файлов. Для этого я буду использовать плагин barbar. Создаем файл barbar.lua и добавляем туда настройки:

require'barbar'.setup {
	animation = true,
	clickable = true,
	exclude_ft = {'javascript'},
	exclude_name = {'package.json'},
	focus_on_close = 'left',
	highlight_inactive_file_icons = false,
	highlight_visible = true,
	icons = {
		buffer_index = false,
		buffer_number = false,
		button = '',
		diagnostics = {
		  [vim.diagnostic.severity.ERROR] = {enabled = true, icon = 'ff'},
		  [vim.diagnostic.severity.WARN] = {enabled = false},
		  [vim.diagnostic.severity.INFO] = {enabled = false},
		  [vim.diagnostic.severity.HINT] = {enabled = true},
		},
		gitsigns = {
		  added = {enabled = true, icon = '+'},
		  changed = {enabled = true, icon = '~'},
		  deleted = {enabled = true, icon = '-'},
		},
		filetype = {
		  custom_colors = false,
		  enabled = true,
		},
		separator = {left = '▎', right = ''},
		separator_at_end = true,
		modified = {button = '●'},
		pinned = {button = '', filename = true},
		preset = 'default',
		alternate = {filetype = {enabled = false}},
		current = {buffer_index = true},
		inactive = {button = '×'},
		visible = {modified = {buffer_number = false}},
	},
	insert_at_end = true,
	maximum_padding = 1,
	minimum_padding = 1,
	maximum_length = 30,
	minimum_length = 0,
	semantic_letters = true,
	sidebar_filetypes = {
		NvimTree = true,
		undotree = {text = 'undotree'},
		['neo-tree'] = {event = 'BufWipeout'},
		Outline = {event = 'BufWinLeave', text = 'symbols-outline'},
	},
	letters = 'asdfjkl;ghnmxcvbziowerutyqpASDFJKLGHNMXCVBZIOWERUTYQP',
	no_name_title = nil,
}

Теперь в init.lua добавляем плагин, активируем файл с настройками:

...
Plug('romgrk/barbar.nvim')

vim.call('plug#end')

...
require"barbar"

И после перезагрузки запускаем :PlugInstall, после чего перезагружаем еще раз и видим, что вкладки успешно доехали:

Lualine - строка статуса

Далее, с помощью плагина lualine добавим снизу строку статуса, в которой будет отображаться vim-mode, текущий файл и язык разработки. Создаем файл lua_line.lua (важно, чтобы файл был через нижнее подчеркивание, иначе из-за конфликтов имен nvim его не увидит) и добавляем в него следующие настройки:

require('lualine').setup ({
  options = {
    icons_enabled = true,
    theme = 'auto',
    component_separators = { left = '', right = ''},
    section_separators = { left = '', right = ''},
    disabled_filetypes = {
      statusline = {},
      winbar = {},
    },
    ignore_focus = {},
    always_divide_middle = true,
    globalstatus = false,
    refresh = {
      statusline = 1000,
      tabline = 1000,
      winbar = 1000,
    }
  },
  sections = {
    lualine_a = {'mode'},
    lualine_b = {'branch', 'diff', 'diagnostics'},
    lualine_c = {'filename'},
    lualine_x = {'encoding', 'fileformat', 'filetype'},
    lualine_y = {'progress'},
    lualine_z = {'location'}
  },
  inactive_sections = {
    lualine_a = {},
    lualine_b = {},
    lualine_c = {'filename'},
    lualine_x = {'location'},
    lualine_y = {},
    lualine_z = {}
  },
  tabline = {},
  winbar = {},
  inactive_winbar = {},
  extensions = {}
})

init.lua:

...
Plug('nvim-lualine/lualine.nvim') 
...
vim.call('plug#end')
...
require"lua_line"

И вариант с темной темой:

Простор для кастомизации внешнего вида редактора - бескрайний. Можно прикрутить буквально все, от анимации до плагинов, позволяющих генерировать темы с помощью AI.

Теперь, когда базовый внешний вид стал похож на IDE пора научить nvim понимать наш код, выдавать подсказки и подсвечивать синтаксис.

TreeSitter - дерево кода

Следующее, что хочется сделать - добавить подсветку кода. TreeSitter - это утилита, которая умеет разбирать открытый в редакторе код и строить из этого него синтаксическое дерево. Синтаксическое дерево - это структура данных, в которой хранится разобранный на куски код. Редакторы, которые работают с таким деревом, понимают, как подсвечивать ту или иную часть кода, тем самым мы получаем полноценную подсветку синтаксиса.

На момент написания этой статьи TreeSitter - самый популярный способ построить синтаксическое дерево. Чтобы подружить nvim и TreeSitter нам нужен драйвер, которым выступит плагин https://github.com/nvim-treesitter/nvim-treesitter. Подключаем плагин в конфиг:

local vim = vim
local Plug = vim.fn['plug#']

vim.call('plug#begin')

Plug('nvim-treesitter/nvim-treesitter', {['do'] = ':TSUpdate'})

vim.call('plug#end')

Создаем файл treesitter.lua и добавляем в него конфигурацию из репозитория плагина:

require'nvim-treesitter.configs'.setup {
    ensure_installed = "all",
	highlight = { enable = true },
	incremental_selection = { enable = true },
	textobjects = { enable = true },
}

Сохраняем, перезапускаем nvim и выполняем команду :PlugInstall, после выполнения которой отобразится интерфейс VimPlug с сообщением, что плагин для TreeSitter успешно установлен. Теперь нужно еще раз перезапустить nvim чтобы TreeSitter установил языковые парсеры. Теперь посмотрим, как стал выглядеть синтаксис go-проекта:

Можно сравнить со скрином выше и оценить разницу. Стало красиво, но писать код все еще неудобно - нет привычного автокомплита, подсказок, подсветки ошибок и описания типов. Давай это исправим.

LSP-сервер

TreeSitter научил nvim оформлять код так, как мы привыкли. Теперь нужно научить nvim выдавать подсказки, подсвечивать ошибки и обрабатывать код привчным нам способом. Для этого мы будем использовать LSP.

LSP (language server protocol) - протокол, по которому клиент (редактор или IDE) общается с сервером, который дает клиенту инструкции. Например, клиент может передать серверу переменную, после которой мы поставили точку, а сервер в ответ отдаст клиенту все варианты автокомплита, которые возможны для этой переменной. Таким образом, чтобы научить редактор понимать код, нам нужно поднять сервер, знающий все про этот код и про язык, на котором он написан, и научить редактор общаться с этим сервером.

Для каждого языка есть свой LSP-сервер, который сначала нужно установить в систему, а затем через какой-либо драйвер подключить к редактору. В качестве драйвера у нас будет выступать плагин https://github.com/neovim/nvim-lspconfig. У плагина отличная документация, где можно найти как настроить lsp для множества языков.

В рамках этой статьи я настрою LSP для golang. Для дальнейших манипуляций я подразумеваю, что у тебя уже установлен go.

Для golang есть несколько вариантов LSP-серверов, я буду использовать самый популярный - gopls:

brew install gopls

Теперь создаем файл lsp.lua и добавляем туда следующие настройки:

local nvim_lsp = require'lspconfig'

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

-- golang
nvim_lsp.gopls.setup ({
  on_attach = on_attach,
  flags = {
	-- This will be the default in neovim 0.7+
	debounce_text_changes = 150,
  },
  capabilities = {
	  workspace = {
		  didChangeWatchedFiles = {
			  dynamicRegistration = true,
		  },
	  },
  }
})

vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
	vim.lsp.diagnostic.on_publish_diagnostics, {
		virtual_text = false
	}
)

Секция с маппингом и инициализация сервера - стандартные, из примеров конфигов репозитория nvim-lspconfig.

init.lua:

Plug('neovim/nvim-lspconfig') 
...
vim.call('plug#end')
...
require"lsp"

Перезапускаем, устанавливаем плагин через :PlugInstall, перезапускаем еще и раз и открываем пример go-кода. Тут важно, что gopls по-умолчанию сконфигурирован так, что распознает код только если в корне проекта лежит go.mod. Еще один важный момент - проверь, что go добавлен в PATH для используемой оболочки. У меня это zsh, и в .zshrc у меня прописано:

export GOPATH=$HOME/golang
export GOROOT=/usr/local/opt/go/libexec
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOPATH
export PATH=$PATH:$GOROOT/bin

Теперь, открыв go код и удостоверившись, что ошибок нет, выполняем :LspInfo:

Видим, что у нас есть активный lsp-клиент - значит, nvim успешно подключился к lsp-серверу. Хотя nvim уже умеет работать с автокомплитом, нам нужно установить ряд плагинов, чтобы мы эту работу смогли увидеть. Для визуализации этой работы я буду использовать пачку плагинов от https://github.com/hrsh7th. Создаем файл cmp.lua и вставляем туда следующий конфиг:

vim.o.completeopt="menu,menuone,noselect"

-- Setup nvim-cmp.
local cmp = require'cmp'

cmp.setup({
snippet = {
  -- REQUIRED - you must specify a snippet engine
  expand = function(args)
	vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
	-- require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
	-- require('snippy').expand_snippet(args.body) -- For `snippy` users.
	-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
  end,
},
window = {
  -- completion = cmp.config.window.bordered(),
  -- documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
  ['<C-b>'] = cmp.mapping.scroll_docs(-4),
  ['<C-f>'] = cmp.mapping.scroll_docs(4),
  ['<C-Space>'] = cmp.mapping.complete(),
  ['<C-e>'] = cmp.mapping.abort(),
  ['<CR>'] = cmp.mapping.confirm({ select = true }),
  ['<Tab>'] = cmp.mapping(function(fallback)
	  local col = vim.fn.col('.') - 1
	  if cmp.visible() then
		cmp.select_next_item(select_opts)
	  elseif col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') then
		fallback()
	  else
		cmp.complete()
	  end
  end, {'i', 's'}),
  ['<S-Tab>'] = cmp.mapping(function(fallback)
	  if cmp.visible() then
		cmp.select_prev_item(select_opts)
	  else
		fallback()
	  end
  end, {'i', 's'}),
}),
sources = cmp.config.sources({
  { name = 'nvim_lsp' },
  { name = 'vsnip' }, -- For vsnip users.
  { name = 'nvim_lsp_signature_help' },
}, {
  { name = 'buffer' },
})
})

-- Set configuration for specific filetype.
cmp.setup.filetype('gitcommit', {
sources = cmp.config.sources({
  { name = 'cmp_git' }, -- You can specify the `cmp_git` source if you were installed it.
}, {
  { name = 'buffer' },
})
})

-- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline('/', {
mapping = cmp.mapping.preset.cmdline(),
sources = {
  { name = 'buffer' }
}
})

-- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline(':', {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({
  { name = 'path' }
}, {
  { name = 'cmdline' }
})
})

Теперь подключаем плагины и конфиг в init.lua:

Plug('hrsh7th/cmp-buffer') 
Plug('hrsh7th/cmp-path')
Plug('hrsh7th/cmp-cmdline') 
Plug('hrsh7th/nvim-cmp')
Plug('hrsh7th/cmp-nvim-lsp' ) 
Plug('hrsh7th/cmp-nvim-lsp-signature-help') 

vim.call('plug#end')
...
require"cmp_config"

Устанавливаем через :PlugInstall, и проверяем:

0:00
/0:27

На этом этапе у нас получился редактор, внешне напоминающий привычный IDE-стиль и умеющий работать с кодом в привычном нам формате. Думаю, что можно логически завершить первую статью на этом моменте, чтобы она не растянулась до совсем неприличных размеров. В следующих сериях настроим привычный поиск по файлам и по вхождениям, настроим удобный маппинг под большинство действий и я накину еще 5-10 плагинов, которые очень упрощают повседневную разработку.

Полный код конфига можно найти тут: https://github.com/itxor/go-rust-nvim-config/tree/master

Старый вариант конфига в nvim-формате (конфиг рабочий, но устаревший и имеет много различий с текущей lua-версией): https://github.com/itxor/go-rust-nvim-config/tree/old_master

Subscribe to vpoltora

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe