Хорошая новость – Neovim не стоит на месте и развивается. Многие вещи, которые раньше реализовывались громоздкими и тормозными плагинами, становятся встроенными.
Что появилось нового в 2025?
:restart
— данная команда перезапускает редактор, применяя новые настройки. Прошло более 10 лет прежде чем по просьбам трудящихся та была добавлена. Раньше приходилось для этого выходить из редактора и запускать его по новой.- Встроенный менеджер плагинов.
- Поддержка автодополнения.
Для демонстрации новых возможностей я набросал небольшой конфиг. Структура файлов:
.
├── init.lua
└── lua
└── user
├── autocmds.lua
├── autopairs.lua
├── gitsigns.lua
├── keymaps.lua
├── lsp.lua
├── options.lua
├── plugins.lua
└── treesitter.lua
Как опробовать?
В репозиториях популярных дистрибутивов Neovim все еще старый (0.11.4 на момент написания), а поэтому нужно пользоваться неофициальными/сторонними.
В Arch Linux вы можете поставить пакет neovim-git
из AUR либо из репозиториев archlinuxcn
или chaotic-aur
(последние предпочтительнее):
sudo pacman -S neovim-git
Менеджер плагинов
В Neovim 0.12 появился менеджер плагинов, который в отличие от встроенного вимовского packadd
умеет их скачивать с помощью git.
./lua/user/plugins.lua
vim.pack.add({
"https://github.com/neovim/nvim-lspconfig",
"https://github.com/williamboman/mason.nvim",
"https://github.com/williamboman/mason-lspconfig.nvim",
"https://github.com/nvim-treesitter/nvim-treesitter",
"https://github.com/nvim-mini/mini.pairs",
"https://github.com/lewis6991/gitsigns.nvim",
"https://github.com/folke/which-key.nvim",
{ src = "https://github.com/catppuccin/nvim", name = "catppuccin" },
})
Для обновления же можно использовать vim.pack.update()
.
Плюсы:
- встроенный;
- простой;
- поддерживает git.
Минусы:
- более медленный чем lazy.nvim, так как не умеет динамически подгружать модули-плагины по мере необходимости.
LSP и автодополнение
В данном примере я использовал классику в виде связки nvim-lspconfig
(содержит настройки) и Mason (позволяет ставить не только сервера, но и линтеры, дебагеры и тп). Останавливаться на их описании я не буду. Они всем хорошо должны быть известны.
Конфиг может выглядеть примерно так:
./lua/user/lsp.lua
-- :h lsp
-- Mason + LSP
-- Языковые сервера можно ставить через :Mason
require("mason").setup()
require("mason-lspconfig").setup({
ensure_installed = {
"vimls",
"lua_ls",
"stylua",
"bashls",
"pyright",
"ruff",
},
})
local capabilities = vim.lsp.protocol.make_client_capabilities()
vim.lsp.config("*", {
capabilities = capabilities,
root_markers = { ".git", "." },
})
-- Диагностика
vim.diagnostic.config({
virtual_lines = { current_line = true },
severity_sort = true,
})
-- Рекомендованные настройки для автодополнения
vim.opt.completeopt:append({ "menuone", "noselect", "popup", "fuzzy" })
vim.opt.shortmess:append("c")
-- Количество элементов в меню автодополнения
vim.opt.pumheight = 10
local lsp_group = vim.api.nvim_create_augroup("lsp", { clear = true })
vim.api.nvim_create_autocmd("LspAttach", {
group = lsp_group,
callback = function(args)
-- Disable defaults
-- Unset 'formatexpr'
-- vim.bo[args.buf].formatexpr = nil
-- -- Unset 'omnifunc'
-- vim.bo[args.buf].omnifunc = nil
-- -- Unmap K
-- vim.keymap.del("n", "K", { buffer = args.buf })
-- -- Disable document colors
-- vim.lsp.document_color.enable(false, args.buf)
local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
local methods = vim.lsp.protocol.Methods
local map = function(mode, lhs, rhs, opts)
opts = type(opts) == "string" and { desc = opts } or opts or {}
opts = vim.tbl_extend(
"force",
{ silent = true, noremap = true, buffer = args.buf },
opts
)
vim.keymap.set(mode, lhs, rhs, opts)
end
-- Автодополнение
if client:supports_method(methods.textDocument_completion) then
-- По умолчанию варианты дополнения рлказываются при вводе точки
local chars = {}
for i = 32, 126 do
table.insert(chars, string.char(i))
end
client.server_capabilities.completionProvider.triggerCharacters = chars
vim.lsp.completion.enable(
true,
client.id,
args.buf,
{ autotrigger = true }
)
map("i", "<C-Space>", vim.lsp.completion.get, "Show completion menu")
local pum_keymaps = {
["<cr>"] = "<c-y>",
["<tab>"] = "<c-n>",
["<s-tab>"] = "<c-p>",
["<down>"] = "<c-n>",
["<up>"] = "<c-p>",
["<esc>"] = "<c-e>",
}
for k, v in pairs(pum_keymaps) do
map("i", k, function()
return vim.fn.pumvisible() == 1 and v or k
end, { expr = true })
end
end
-- Дополнение для LLM
if client:supports_method(methods.textDocument_inlineCompletion) then
vim.lsp.inline_completion.enable(true, { buffer = args.buf })
map("i", "<c-cr>", function()
if vim.lsp.inline_completion.get() then
return "<c-cr>"
end
end, { expr = true, replace_keycodes = true })
end
-- Inlay Hints toggle
if client:supports_method("textDocument/inlayHint") then
map("n", "<leader>ih", function()
vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
end, "Toggle inlay hints")
end
-- Форматирование перед сохранением
if client:supports_method("textDocument/formatting") then
vim.api.nvim_create_autocmd("BufWritePre", {
group = lsp_group,
buffer = args.buf,
callback = function()
vim.lsp.buf.format({
bufnr = args.buf,
id = client.id,
timeout_ms = 1000,
})
end,
})
end
-- Документация при выборе варианта дополнения
vim.api.nvim_create_autocmd("CompleteChanged", {
group = lsp_group,
buffer = args.buf,
callback = function()
local info = vim.fn.complete_info()
local item = vim.tbl_get(
vim.v.completed_item,
"user_data",
"nvim",
"lsp",
"completion_item"
)
if item == nil then
return
end
vim.lsp.buf_request_all(
args.buf,
methods.completionItem_resolve,
item,
function(response)
if #response ~= 1 then
return
end
local client_id = vim.tbl_get(response[1], "context", "client_id")
if client_id ~= client.id then
return
end
local doc =
vim.tbl_get(response[1], "result", "documentation", "value")
if doc == nil then
return
end
local win =
vim.api.nvim__complete_set(info["selected"], { info = doc })
if win.winid == nil or not vim.api.nvim_win_is_valid(win.winid) then
return
end
vim.api.nvim_win_set_config(win.winid, { border = "rounded" })
vim.treesitter.start(win.bufnr, "markdown")
vim.wo[win.winid].conceallevel = 3
end
)
end,
})
-- Показывать сигнатуры при наведении курсора
-- Для получения фокуса нужно нажать Ctrl-S. По умолчанию сигнатура
-- вызывается этим сочетанием, и обычно повторное сочетание устанавливает
-- фокус на окно (работает с K в нормальном режиме). q — для выхода.
vim.api.nvim_create_autocmd("CursorHoldI", {
group = lsp_group,
buffer = args.buf,
callback = function()
vim.lsp.buf.signature_help({
silent = true,
focus = false,
border = "rounded",
--max_height = 15,
})
end,
})
end,
})
Собственно автодополнение включается с помощью вызова vim.lsp.completion.enable(true)
. Функция vim.lsp.completion.get()
триггерит автодополнение. Рекомендованными настройками для отображения дополнений в всплывающем окне является такое значение:
vim.opt.completeopt = { "menuone", "noselect", "popup" }
Я его слегка мофицировал. Вместо noselect
можно использовать noinsert
, тогда при нажатии Enter
или Ctrl-Y
будет вставлен первый вариант. Это не всегда удобно. Ctrl-N
/Ctrl-P
/Стрелки вверх и вниз служат для выбора вариантов. Автодоплнение можно вызвать принудительно с помощью Ctrl-X
, Ctrl-O
, но мне больше нравится Ctrl-Space
как в большинсиве редакторов.
Остальные файлы
Для демонстрации новых возможностей так же понадобятся дополнительные файлы.
Главный конфиг:
./init.lua
local g = vim.g
-- Клавишу leader надо объявить до загрузки плагинов
g.mapleader = ' '
g.maplocalleader = g.mapleader
if vim.fn.has('termguicolors') == 1 then
-- Некоторые плагины могут неправильно работать, если не установить предварительно это значение
vim.opt.termguicolors = true
end
-- Плагины и их настройка
require("user.plugins")
require("user.lsp")
require("user.treesitter")
require("user.autopairs")
require("user.gitsigns")
-- Настройки должны идти после плагинов, так как те могут их переопределить
require("user.options")
require("user.keymaps")
require("user.autocmds")
Базовые настройки редактора:
./lua/user/options.lua
local opt = vim.opt
-- Нумерация строк
opt.number = true
-- Мне не нравится относительная нумерация строк
-- opt.relativenumber = true
-- Подсветка текущей строки
opt.cursorline = true
-- Прокрутка
opt.scrolloff = 8
opt.sidescrolloff = 8
-- Отображение элементов интерфейса
opt.signcolumn = "yes"
opt.laststatus = 3
-- Только при использовании statusline
-- opt.showmode = false
opt.shortmess:append("I")
-- Перенос строк
opt.linebreak = true
-- Отображение различных символов
opt.list = true
opt.listchars = {
tab = '→ ',
lead = '·',
trail = '·',
nbsp = '␣',
extends = '⟩',
precedes = '⟨',
}
-- Отступы и форматирование
opt.expandtab = true
opt.tabstop = 4
opt.shiftwidth = 2
opt.softtabstop = 2
opt.autoindent = true
opt.smartindent = true
opt.breakindent = true
opt.textwidth = 80
opt.colorcolumn = "+1"
opt.formatoptions = {
c = true,
q = true,
j = true,
r = true,
n = true,
l = true,
}
-- Максимальный размер строки для подсветки
opt.synmaxcol = 1024
-- Поиск
opt.ignorecase = true
opt.smartcase = true
opt.inccommand = "split"
-- Файлы и буферы
opt.clipboard = "unnamedplus"
opt.swapfile = false
opt.backup = false
opt.writebackup = false
opt.autoread = true
opt.undofile = true
opt.undolevels = 1000
-- opt.shada = "!,'1000,<50,s10,h"
opt.confirm = true
-- Разделение окон
opt.splitbelow = true
opt.splitright = true
-- Фолдинг
opt.foldmethod = "expr"
opt.foldlevelstart = 99
opt.foldenable = true
-- Мышь, перемещение курсора и выделение текста
opt.mouse = "a"
opt.mousemoveevent = true
opt.whichwrap = 'h,l,<,>,[,]'
-- opt.keymodel = "startsel,stopsel"
-- В vim есть встроенная поддержка русской раскладки. Мы можем переключаться на нее и обратно с помощью Ctrl-6, при этом при включенной русской, связки клавиш продолжат работать.
opt.keymap = "russian-jcukenwin"
opt.iminsert = 0
opt.imsearch = 0
-- Автодополнение
opt.wildmenu = true
opt.wildmode = "longest:full,full"
opt.wildignore:append {
'*.o',
'*.obj',
'*.pyc',
'*.so',
'.git',
'.svn',
'__pycache__',
}
-- Сессии
opt.sessionoptions:append("localoptions")
-- Таймауты
opt.timeoutlen = 500
opt.updatetime = 200
-- Тема
vim.cmd.colorscheme [[catppuccin-macchiato]]
Автоматические команды, которым не нашлось места в других местах:
./lua/user/autocmds.lua
-- Восстановление позиции курсора при открытии файла
vim.api.nvim_create_autocmd("BufReadPost", {
callback = function(ev)
local row, col = unpack(vim.api.nvim_buf_get_mark(ev.buf, '"'))
if row > 0 and row <= vim.api.nvim_buf_line_count(ev.buf) then
pcall(vim.api.nvim_win_set_cursor, 0, { row, col })
end
end,
})
Основные сочетания клавиш:
./lua/user/keymaps.lua
local function cmd(command)
return "<Cmd>" .. command .. "<CR>"
end
local wk = require("which-key")
wk.setup()
wk.add({
-- Files
{ "<leader>e", vim.cmd.Ex, desc = "Explorer (:Ex)" },
-- Buffers
{ "<leader>b", ":ls<CR>:b<Space>", desc = "Buffers" },
{ "<tab>", vim.cmd.bnext, desc = "Next buffer" },
{ "<s-tab>", vim.cmd.bprev, desc = "Prev buffer" },
-- Navigation for wrapped lines
{ "k", "gk", silent = true },
{ "j", "gj", silent = true },
{ "<Up>", "gk", silent = true },
{ "<Down>", "gj", silent = true },
-- Windows
{ "<c-h>", "<c-w>h", desc = "Left window" },
{ "<c-j>", "<c-w>j", desc = "Down window" },
{ "<c-k>", "<c-w>k", desc = "Up window" },
{ "<c-l>", "<c-w>l", desc = "Right window" },
-- ... еще сочетания
{
"<leader>?",
function()
wk.show({ global = false })
end,
desc = "Buffer Local Keymaps (which-key)",
},
{
"<leader>k",
wk.show,
desc = "Show all keyamps"
},
})
В конфиге выше используется which-key
. Этот плагин следовало бы сделать встроенным как в helix
, так как он служит для напоминания связок, которые постоянно забываешь.
Treesitter — это библиотека для парсинга исходников и построения синтаксического дерева с которым в дальнейшем работает LSP. Является зависимостью редактора, а вот одноименный плагин лишь содержит ссылки для скачивания и установки парсеров различных языков:
./lua/user/treesitter.lua
require("nvim-treesitter.configs").setup({
-- https://github.com/nvim-treesitter/nvim-treesitter#supported-languages
ensure_installed = { "vim", "lua", "bash", "python" },
sync_install = false,
auto_install = true,
highlight = { enable = true },
indent = { enable = true },
})
Закрытие парных скобок и кавычек:
./lua/user/autopairs.lua
require('mini.pairs').setup()
Отображает слева перед номерами строк, имеющиеся изменения в git:
./lua/user/gitsigns.lua
require('gitsigns').setup()
Заключение
Встроенное автодополнение реализовано довольно просто. Нам не нужно писать для этого много строчек конфигов. С менеджерами плагинов же ситуация до этого была плачевной: был «родной» lazy.nvim
, который как бы является частью другого проекта (одноименная сборка), и vim-plug
, созданный для старшего брата (vim) и требующий использования Vim Script. Первый к тому же довольно громоздкий, что заставляло пользоваться устаревшими альтернативами, проектами заброшенными годы назад. Пришло время переписывать конфиги.