Настройка Neovim/Nvim
Введение
Neovim или просто Nvim — это современный редактор, возникший как замена Vim.
Vim — это текстовый редактор для терминала, уникальный прежде всего наличием различных режимов работы (нормальный, редактирования, выделения, замены, командный), которые позволяют выполнять абсолютно все действия с клавиатуры без использования мыши, что заметно увеличивает скорость работы с текстом. Так же он является программируемым, что, наверное, и является его самым большим плюсом.
Да, вы можете взять VS Code, поставить кучу плагинов чтобы получить необходимый функционал, но сам редактор, плагины настраиваются через JSON, что не позволяет добавить какую-нибудь пользовательскую функцию для сортировки файлов в проводнике или навесить сочетание на вызов функции… Там такое можно сделать через написание расширений, что намного сложнее и трудозатратнее чем в том же виме + в последнем меньше ограничений со стороны API (в VS Code нельзя радикально переделать интерфейс, заменить проводник на другой и т.п.).
Vim возник более 30 с лишним лет назад. Поэтому, в нём нет многих привычных вещей, например, нативной поддержки LSP, которые просто не существовали в момент его создания. Вместо этого vim имеет встроенную поддержку утилиты ctags
, которая была чем-то прорывным в 1991-м, но сейчас ею почти никто не пользуется. Vim нельзя назвать мёртвым: он активно развивается, обрастая функционалом. Но то, что в нем для настраивания используется свой собственный язык Vimscript, тормозит его внедрение.
LSP — это протокол, используемый текстовыми редакторами для автодополнения, получения справки и т. д., ставший стандартом в отрасли. Он был придуман Microsoft и так понравился всем, что сейчас почти нет современных редакторов, которые бы его не поддерживали. LSP представляет собой сетевой клиент-серверный протокол, работающий через JSON-RPC. Это значит, что данные могут запрашиваться и получаться по сети (в основном по локальной, но никто не запрещает через Интернет). Нет принципиальной невозможности реализовать клиент на Vimscript, но скорость работы последнего будет оставлять желать лучшего, так как Vimscript не является сетевым языком как и языком общего назначения, поэтому в нём нет поддержки тредов (он однопоточный) и асинхронности, которые бы ускоряли автодополнение. Не говоря уже о том, что Vimscript просто тормоз.
Замена Vimscript на Lua в Neovim решала эти проблемы. Lua является стандартным встраиваемым языком для скриптинга в играх или для настроек в программах. Сам язык напоминает что-то среднее между JS без классов и чем-то похожим на Pascal наличием оператора end
. Lua не просто быстрый, он очень быстрый, так как поддерживает JIT-компиляцию.
Nvim имеет обратную совместимость с Vim, он поддерживает как задание настроек через vim-файлы, так и почти все его настройки за небольшим исключением. Это означает, что большинство плагинов для Vim будут работать в Nvim.
Установка Neovim в моём дистрибутиве выглядит так:
sudo pacman -S nvim
Во всех популярных дистрибутивах Nvim можно поставить стандартным пакетным менеджером.
Настройки
Главный файл настроек в Nvim располагается в ~/.config/nvim
. Он должен называться init.vim
или init.lua
.
Но для задания конфигов мы будем использовать именно Lua, так как последний поддерживает полноценные модули.
У нас будет такая структура:
~/.config/nvim
├── init.lua
└── lua
├── configs
│ ├── autocmds.lua
│ ├── keymaps.lua
│ ├── options.lua
│ └── plugins.lua
└── utils.lua
В директории с конфигом находится директория lua
. Такое название является обязательным. Файлы в ней можно подключать через require
. В lua
для подключаемых конфигов я создал configs
.
В качестве пакетного менеджера в Nvim часто используют старый, добрый vim-plug, так как он позволяет использовать старый vimrc
вместо написания нового конфига. Для наших же целей он не подходит. Мы будем использовать lazy.nvim, который на данный момент является лучшим пакетным менеджером.
lazy.nvim
по умолчанию грузит модули асинхронно. Вот небольшая справка по его использованию:
-- Основной вызов Lazy.nvim
require("lazy").setup(opts)
-- Где opts — это таблица (массив) с описаниями плагинов.
-- Каждый элемент таблицы описывает один плагин.
{
-- Указывается репозиторий плагина на GitHub
"github_user/repo",
-- Необязательное поле: зависимости (будут установлены автоматически)
dependencies = {
"github_user/dependency1",
"github_user/dependency2",
-- и т.д.
},
-- Настройка плагина: вызывается при его загрузке
-- Можно использовать для ручной инициализации, настройки событий и т.д.
config = function()
local plugin = require("plugin_name")
plugin.setup({
option1 = true,
option2 = "value",
})
end,
-- Альтернатива config: если плагин поддерживает setup(opts),
-- можно указать опции напрямую, и lazy.nvim сам вызовет setup(opts)
opts = {
option1 = true,
option2 = "value",
},
-- Если устраивают настройки по умолчанию
-- (и setup вызывается без параметров), можно передать пустую таблицу
opts = {},
-- Или можно просто указать config = true, если плагин сам настроится при require
config = true,
-- Указывает, загружать ли плагин по требованию (true по умолчанию).
-- Устанавливаем lazy = false, если плагин должен быть загружен сразу (например, тема или ключи)
lazy = false,
-- Определение горячих клавиш
keys = {
{ "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Поиск файлов" },
{ "<C-s>", ":w<CR>", desc = "Сохранить файл", mode = "n" },
-- mode по умолчанию = "n", можно задать "i", "v", "n", "t" и т.п.
},
-- Можно указать команду или событие, при котором плагин будет загружен:
cmd = { "SomeCommand" },
event = { "BufReadPost", "BufNewFile" },
}
-- Альтернатива: можно передать путь к модулю, экспортирующему список плагинов
require("lazy").setup("path.to.plugins")
-- Где path/to/plugins.lua может выглядеть как:
return {
{
"github_user/foo",
opts = {},
},
{
"github_user/bar",
config = function()
require("bar").setup()
end,
},
}
-- Или если в каталоге `lua/path/to/plugins/` несколько файлов:
-- каждый файл экспортирует таблицу(ы) с описанием плагинов:
-- plugins/foo.lua:
return {
"github_user/foo",
opts = {},
}
-- plugins/bar.lua:
return {
"github_user/bar",
config = true,
}
-- Ошибка: модуль не найден
local foo = require("foo")
require("lazy").setup({ ... })
-- Так же приведет к ошибке, так как модуль ленивый
require("lazy").setup({
{
"github_user/foo",
lazy = true, -- это значение по умолчанию
},
})
local foo = require("foo")
-- Таким образом, самый надежный вариант
{
"github_user/foo",
dependencies = { "github_user/bar" }, -- зависимости тоже гарантированно доступны для загрузки
config = function()
local foo = require("foo")
foo.setup {}
local bar = require("bar")
bar.setup()
end,
}
В init.lua
у нас содержится код для автоматической установки lazy.nvim, а также подключение различных модулей с настройками:
-- disable netrw at the very start of your init.lua
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
-- Установка lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
lazypath
})
end
vim.opt.rtp:prepend(lazypath)
-- Клавиша leader должна быть задана до загрузки плагинов
vim.g.mapleader = ' '
-- Настройка плагинов
require("lazy").setup("configs.plugins")
-- Настройки задаем после загрузки плагинов
require("configs.options")
require("configs.keymaps")
require("configs.autocmds")
options.lua
— это общие настройки, которые я позаимствовал из своего старого конфига Vim за небольшими отличиями. Например, в Nvim set hidden
— это поведение по умолчанию, а paste
вообще выбросили.
lua/configs/options.lua
local config_dir = vim.fn.stdpath('config')
local opt = vim.opt
-- Настройки интерфейса
opt.termguicolors = true
opt.lazyredraw = true
opt.background = "dark"
opt.guifont = "JetBrainsMono Nerd Font:h12"
opt.guicursor = "n-v-c:block,i-ci-ve:ver25,r-cr:hor20,o:hor20"
-- Нумерация строк
opt.number = true
--opt.relativenumber = true
-- Подсветка текущей строки
opt.cursorline = true
-- Прокрутка
opt.scrolloff = 5 -- по вертикали
opt.sidescrolloff = 5 -- по горизонтали
-- Отображение различных элементов
opt.signcolumn = "yes"
opt.showcmd = false
opt.laststatus = 3
opt.showmode = false -- не показывать режим (--INSERT и тп) в самом низу
-- Перенос строк
opt.wrap = true
opt.linebreak = true
-- Отображение различных символов
opt.list = true
opt.listchars = {
tab = '→ ',
trail = '·',
nbsp = '␣',
extends = '❯',
precedes = '❮'
}
opt.fillchars = { eob = " " } -- вместо ~ отображаем просто пустые строки
-- Отступы и табуляция
opt.expandtab = true -- заменять символы табуляции на пробелы при отрисовке
opt.tabstop = 4 -- на сколько пробелов заменяется символ табуляции при отображении
opt.shiftwidth = 2 -- количество пробелов, вставляемых при шифтинге
opt.softtabstop = 2 -- ширина отступа
-- автоматически определять количество пробелов для отступа
opt.autoindent = true
opt.smartindent = true
opt.breakindent = true -- при переносе строки добавлять отступы
-- Поиск
opt.ignorecase = true
opt.smartcase = true
opt.incsearch = true
opt.hlsearch = true
opt.wrapscan = true
opt.inccommand = "split"
-- Файлы и буферы
-- В последнем nvim вроде всегда такое поведение
-- opt.hidden = true
-- Используем системный буфер для копирования текста
-- Требует наличия xclip или wl-copy!
opt.clipboard = "unnamedplus"
-- Бекапы, файлы подкачки, undo- и shada-файлы хранятся в ~/.local/state/nvim
-- Отключаем создание бекапов и файлов подкачки
opt.swapfile = false
opt.backup = false
opt.writebackup = false
opt.autoread = true -- Обновлять буфер при изменении файла извне
-- Делаем возможной отмену изменений уже после закрытия файла
opt.undofile = true
opt.undolevels = 1000
opt.shada = "!,'1000,<50,s10,h" -- ограничения для shada-файлов (от share data)
opt.confirm = true
-- Разделение окон
opt.splitbelow = true
opt.splitright = true
-- Мышь и работа с текстом
opt.mouse = "a"
opt.mousemoveevent = true
-- Выделение текста стрелками с зажатым Shift
opt.keymodel = "startsel,stopsel"
-- При переключении системной раскладки перестают работать привязки клавиш.
-- В vim можно включить встроенную русскую раскладку с переключением по Ctrl-6
opt.keymap = "russian-jcukenwin"
-- Делаем раскладкой по умолчанию английскую
opt.iminsert = 0
opt.imsearch = 0
-- Привычное перемещение курсора
opt.whichwrap:append("hl<>[]")
-- Автодополнение
opt.wildmenu = true
opt.wildmode = "longest:full,full"
-- Можно исключить из дополнения определенные типы файлов, имеющие бинарный формат
opt.wildignore:append { '*.o', '*.obj', '*.py[co]', '__pycache__/*', '*.so', '*.zip', '*.rar', '*.tar.*', '*.gz', '*.docx', '*.xlsx', '*.pdf', '*.jpg', '*.jpeg', '*.gif', '*.png' }
opt.completeopt = "menuone,noselect,noinsert"
opt.pumheight = 10 -- высота всплывающего меню с вариантами для дополнения
-- Таймауйты
opt.timeoutlen = 500
opt.updatetime = 250
-- spell
opt.spell = false
opt.spelllang = { "ru", "en" }
local spell_dir = config_dir .. '/spell'
if vim.fn.isdirectory(spell_dir) == 0 then
vim.fn.mkdir(spell_dir, 'p')
end
Тут хранятся пользовательские сочетания клавиш, некоторые из них задаются странным образом — внутри функции:
lua/configs/keymaps.lua
local map = require("utils").map
map('n', '<C-a>', 'ggVG', "Select all text")
map('i', '<C-a>', '<Esc>ggVG', "Select all text")
map('n', '<C-q>', '<cmd>q<CR>', "Close current window")
map('n', '<C-x>', '<cmd>bd<CR>', "Delete current buffer")
-- В Neovim это сочетание уже используется
-- map('', '<C-s>', '<cmd>w<CR>', "Save file")
map('n', '<leader>w', '<cmd>w<CR>', "Save file")
-- Отступы привычнее добавлять с помощью Tab
map('n', '<Tab>', '>>_', "Increase indent")
map('n', '<S-Tab>', '<<_', "Decrease indent")
map('i', '<S-Tab>', '<C-D>', "Decrease indent")
map('v', '<Tab>', '>gv', "Increase indent")
map('v', '<S-Tab>', '<gv', "Decrease indent")
map('n', '<Esc>', '<cmd>nohlsearch<CR><Esc>', "Clear search highlight")
map('n', '<C-Up>', '<cmd>bp<CR>', "Previous buffer")
map('n', '<C-Down>', '<cmd>bn<CR>', "Next buffer")
map('n', '<C-h>', '<C-w>h', "Focus left window")
map('n', '<C-j>', '<C-w>j', "Focus lower window")
map('n', '<C-k>', '<C-w>k', "Focus upper window")
map('n', '<C-l>', '<C-w>l', "Focus right window")
map('n', '<A-h>', '<C-w>H', "Move window left")
map('n', '<A-j>', '<C-w>J', "Move window down")
map('n', '<A-k>', '<C-w>K', "Move window up")
map('n', '<A-l>', '<C-w>L', "Move window right")
map('n', '<A-Left>', '<cmd>vertical resize -2<CR>', "Decrease width")
map('n', '<A-Right>', '<cmd>vertical resize +2<CR>', "Increase width")
map('n', '<A-Down>', '<cmd>resize -2<CR>', "Decrease height")
map('n', '<A-Up>', '<cmd>resize +2<CR>', "Increase height")
map('v', "J", ":m '>+1<CR>gv=gv", "Shift visual selected line down")
map('v', "K", ":m '<-2<CR>gv=gv", "Shift visual selected line up")
map('n', '<leader>h', '<cmd>split<CR>', "Horizontal split")
map('n', '<leader>v', '<cmd>vsplit<CR>', "Vertical split")
-- nvim автоматически добавляет переменную $MYVIMRC, ее не нужно добавлять
map('n', '<leader>ev', '<cmd>edit $MYVIMRC<CR>', "Edit vim config")
map('n', '<leader>sv', '<cmd>so $MYVIMRC<CR>', "Reload vim config")
-- F3-F11 лучше оставить для дебаггера
map('', '<F2>', ":setlocal spell!<cr>", "Toggle spell check")
map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", "find file")
map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>", "Find [using] grep")
map("n", "<leader>fb", "<cmd>Telescope buffers<cr>", "find buffer")
-- Можно использовать любое из сочетаний с Ctrl+T/N/P, так как на них навешен бесполезный функционал
map("n", "<C-t>", "<cmd>NvimTreeToggle<CR>", "Toggle nvim tree")
-- Можно через telescope тоже самое делать
local setup_lsp_keymaps = function(_, bufnr)
map("n", "gd", vim.lsp.buf.definition, "Go to definition", bufnr)
map("n", "gD", vim.lsp.buf.declaration, "Go to declaration", bufnr)
map("n", "gi", vim.lsp.buf.implementation, "Go to implementation", bufnr)
map("n", "gr", vim.lsp.buf.references, "List references", bufnr)
map("n", "K", vim.lsp.buf.hover, "Hover documentation", bufnr)
-- На эту клавишу в режиме редактирования по умолчанию уже задано это действие
map("n", "<C-s>", vim.lsp.buf.signature_help, "Signature help", bufnr)
map("n", "<leader>rn", vim.lsp.buf.rename, "Rename symbol", bufnr)
map("n", "<leader>ca", vim.lsp.buf.code_action, "Code action", bufnr)
map("n", "]d", vim.diagnostic.goto_next, "Next diagnostic", bufnr)
map("n", "[d", vim.diagnostic.goto_prev, "Previous diagnostic", bufnr)
map("n", "<leader>e", vim.diagnostic.open_float, "Show diagnostics", bufnr)
end
return {
setup_lsp_keymaps = setup_lsp_keymaps
}
В lua/utils.lua
я вынес вспомогательную функцию для задания сочетаний, она выше импортируется:
local M = {}
-- Универсальная функция для биндингов
function M.map(mode, keys, command, desc, bufnr)
local opts = { desc = desc, silent = true }
if bufnr then
opts.buffer = bufnr
end
vim.keymap.set(mode, keys, command, opts)
end
return M
В Nvim LSP встроенный. Например, Ctrl-S
по умолчанию в режиме редактирования вызывает справку. Так же нам не нужны никакие плагины для комментариев — для этого есть сочетание gc
.
lua/configs/plugins.lua
local setup_lsp_keymaps = require("configs.keymaps").setup_lsp_keymaps
return {
-- Файловый менеджер
{
"nvim-tree/nvim-tree.lua",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("nvim-tree").setup({
view = { width = 30 },
filters = { dotfiles = false },
git = { enable = true },
actions = {
open_file = {
quit_on_open = true, -- закроет дерево после открытия файла
},
},
-- авто-закрытие при последнем буфере
-- только если включена эта настройка:
hijack_netrw = true,
})
end,
},
-- Поиск по файлам
{
"nvim-telescope/telescope.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
config = function()
require('telescope').setup({
-- Тут какие-то настройки
})
-- require('telescope').load_extension('fzf')
end
},
-- tree-sitter используется для парсинга сходников
-- :checkhealth nvim-treesitter
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
config = function()
local configs = require("nvim-treesitter.configs")
configs.setup({
-- Парсеры для каждого языка нужно ставить отдельно
ensure_installed = { "c", "lua", "vim", "vimdoc", "python", "go", "rust", "java", "javascript", "php", "vue", "html", "json", "toml", "yaml" },
sync_install = false,
auto_install = true,
highlight = { enable = true },
indent = { enable = true },
})
end
},
-- LSP и Автодополнение
-- https://medium.com/@rishavinmngo/how-to-setup-lsp-in-neovim-1c3e5073bbd1
{
"hrsh7th/nvim-cmp",
dependencies = {
'neovim/nvim-lspconfig',
'hrsh7th/cmp-nvim-lsp',
'hrsh7th/cmp-buffer',
'hrsh7th/cmp-path',
'hrsh7th/cmp-cmdline'
},
config = function()
local cmp = require('cmp')
cmp.setup({
sources = {
{ name = 'nvim_lsp' },
{ name = 'buffer' },
},
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 }),
})
})
local lspconfig = require('lspconfig')
local capabilities = require("cmp_nvim_lsp").default_capabilities()
-- npm install -g pyright
-- sudo pacman -S gopls lua-language-server
-- Mason может автоматически ставить зависимости
-- Тут нужно вписать названия серверов, поддерживаемых nvim-lspconfig
local servers = {
'pyright',
'gopls',
'lua_ls',
}
for _, lsp in ipairs(servers) do
lspconfig[lsp].setup {
on_attach = setup_lsp_keymaps,
capabilities = capabilities,
}
end
end
},
-- Выделение отступов
{
"lukas-reineke/indent-blankline.nvim",
config = function()
require("ibl").setup()
end
},
-- Нижняя строка статуса
{
"nvim-lualine/lualine.nvim",
dependencies = 'nvim-tree/nvim-web-devicons',
config = function()
require('lualine').setup({
sections = {
lualine_x = {
-- Добавим отображение раскладки
{
function()
if vim.opt.iminsert:get() > 0 and vim.b.keymap_name then
return '⌨ ' .. vim.b.keymap_name
end
return ''
end,
cond = function() -- Показывать только если раскладка активна
return vim.opt.iminsert:get() > 0 and vim.b.keymap_name ~= nil
end,
},
'encoding',
'fileformat',
'filetype',
}
}
})
end
},
-- Верхняя строка статуса (заменяет табы)
{
'akinsho/bufferline.nvim',
version = "*",
dependencies = 'nvim-tree/nvim-web-devicons',
config = function()
require("bufferline").setup {
options = {
mode = "buffers",
separator_style = "slant"
}
}
end,
},
-- Тема
{
"folke/tokyonight.nvim",
lazy = false,
priority = 1000,
opts = {},
config = function()
require("tokyonight").setup({
-- Какие-то настройки
})
vim.cmd [[colorscheme tokyonight-storm]]
end
},
-- Фиксим прозрачность
{
"xiyaowong/transparent.nvim",
config = function()
require('transparent').setup({
extra_groups = {
'NormalFloat', -- plugins which have float panel such as Lazy, Mason, LspInfo
},
exclude_groups = { 'CursorLine' },
})
end
}
}
В plugins.lua
перечислены плагины. Обратите внимание, что внутри почти каждого блока объявлена функция config
. Она вызывается после установки и/или после готовности плагина к использованию, после чего в большинстве случаев нужно импортировать плагин и вызвать функцию setup
, объявленную внутри, в которую передаётся ассоциативный массив с настройками.
В Neovim парсинг файлов с исходниками происходит через плагины для Tree-sitter (является зависимостью редактора). С помощью него строится синтаксическое дерево, которое используется для автодополнения, анализа кода, а также опционально для подсветки синтаксиса. Пакет с одноимённым названием, который мы устанавливаем, лишь содержит список парсеров, доступных для скачивания и установки, а также может их скачивать.
nvim-lspconfig
содержит названия LSP-серверов и их стандартные настройки, которые можно переопределить. Языковые сервера можно устанавливать автоматически с помощью Mason. Их установка с помощью последнего позволяет изолировать окружение для разработки. Однако, если у вас все эти средства уже установлены, например, так как вами используется VSCode, то Mason будет лишним.
Помимо плагина для комментариев, не нужен и плагин для editorconfig
, так как последний поддерживается из коробки.
В lua/configs/autocmds.lua
находятся команды, которые активируются при определённых событиях:
-- Автоперечтение файла при изменении
vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold" }, {
command = "checktime",
})
-- Форматирование при сохранении
vim.api.nvim_create_autocmd("BufWritePre", {
callback = function()
vim.lsp.buf.format {
async = false
}
end,
})
-- Показывать всплывающие окна с ошибками при наведении курсора
vim.api.nvim_create_autocmd("CursorHold", {
callback = function()
vim.diagnostic.open_float(nil, { focusable = false })
end,
})
Заключение
Получившийся конфиг полностью готов к использованию. Вы можете ознакомиться с ним, посетив мой репозиторий на Github.
А так же себе его развернуть:
git clone https://github.com/s3rgeym/nvim-config ~/.config/nvim
Nvim, хотя и является редактором для терминала, но так же для него существуют т. н. фронтенды. Из популярных — Neovim-qt и Neovide. Последний имеет поддержку лигатур, GPU-ускорение и анимации для прокрутки текста и курсора.
Альтернативой Nvim являются Kakoune и Helix. Если первый представляет собой скорее Vim здорового человека (или криворука, который не в состоянии настроить первый — решайте сами), то Helix можно рассматривать как преднастроенный Nvim, где вместо ковыряния Lua-лапши можно редактировать TOML. Для кого-то это будет огромным плюсом, но, как по мне, это накладывает гигантские ограничения и противоречит самой концепции программируемого редактора. Хотя я их и не советую использовать, но об их существовании стоит упомянуть, так как они скорее служат для того, чтобы понять, подходит ли для тебя Vim/Nvim.