Neovim и конфигурация: Lua, LSP, Treesitter, плагины, разбор реальных dotfiles
В прошлой статье мы разобрали Vim как язык редактирования: регистры, макросы, текстовые объекты, quickfix. Всё это работает в Neovim без единой правки — модель редактирования осталась той же. Различие лежит ниже: в том, как редактор устроен внутри и на чём его настраивают.
Эта статья — про инженерную часть. Что именно форкнули в 2014 году и зачем. Почему Lua. Как работает встроенный LSP-клиент и чем он отличается от coc.nvim. Что Treesitter даёт сверх подсветки. Как собрать конфигурацию, которая стартует за 50 мс и не разваливается через год. И в конце — разбор настоящего публичного репозитория dotfiles, где видно и хорошие решения, и типичное наслоение.
Что произошло в 2014 году
Vim к тому моменту был монолитом на C, который писал и поддерживал по сути один человек — Брам Моленар. Архитектурно у него было два больных места:
- Всё синхронное. Vim 7 работал в одном потоке с одним циклом событий. Запуск линтера
или сборки блокировал редактор. Плагины обходили это костылями через
vimproc, скрытые терминалы и питоновские треды — каждый по-своему, все хрупко. - Vim script. Язык, выросший из формата конфигурации: медленный, без нормальной модели модулей. Тяжёлые плагины писали на Python/Ruby через встроенные интерпретаторы, что делало Vim заложником версии Python в системе.
Тьяго де Арруда предложил серию патчей на асинхронность, они не были приняты, и он собрал средства на Bountysource и форкнул проект. Цели форка формулировались инженерно: убрать легаси под мёртвые платформы, вынести UI из ядра, сделать API вместо монолита, поддерживать многих контрибьюторов вместо одного мейнтейнера.
Важно: это не была война редакторов. Vim в ответ ускорился. Vim 8.0 (2016) получил собственные jobs, channels и штатные пакеты — во многом под давлением форка. Vim 9.0 (2022) принёс Vim9script, компилируемый диалект, местами в десятки раз быстрее старого. Оба проекта живы, и оба выиграли от конкуренции.
Архитектура: ядро, RPC, клиенты
Ключевое архитектурное решение — UI не является частью редактора. Ядро запускается как
процесс, говорящий с внешним миром по msgpack-RPC. Терминальный nvim — просто один из
клиентов этого протокола, встроенный для удобства.
Практические следствия, важные даже если вы никогда не откроете :h api:
- Любой процесс может управлять редактором. Neovide рисует ту же сессию с GPU-рендерингом, Firenvim подставляет Neovim в текстовые поля браузера, расширение VS Code Neovim использует настоящее ядро вместо эмуляции модальности.
- Плагины могут быть написаны на чём угодно. Плагин на Rust — просто процесс, подключённый
к RPC. Так устроен fuzzy-матчинг в
blink.cmp. - Ничто не блокирует редактор. Языковой сервер, форматтер и
ripgrepживут в цикле событий libuv. Покаgoplsиндексирует монорепозиторий, вы продолжаете печатать. - Эмуляция vs. встраивание. Плагины «Vim mode» реализуют модальность заново и всегда отстают. Встраивание настоящего ядра даёт полную совместимость, но тащит за собой модель буферов Neovim, которая плохо стыкуется с вкладками хост-редактора. Это разменные варианты, а не «правильный» и «неправильный» — подробнее в статье про VS Code.
Почему Lua
Vim script в Neovim продолжает работать: любой .vimrc подхватится. Но конфигурация на Lua —
другой класс инструмента.
LuaJIT с семантикой Lua 5.1 — один из самых быстрых динамических рантаймов вообще. На горячих участках (подсветка, статуслайн, обработчики событий) разница со старым Vim script измеряется в десятки раз.
Модули. require("plugins.lsp") кеширует результат и даёт структуру из десятка маленьких
файлов вместо тысячестрочного монолита. У Vim script для этого нет ничего, кроме source
и соглашения autoload/.
Настоящие данные — таблицы, замыкания, функции первого класса:
vim.api.nvim_create_autocmd("FileType", {
pattern = { "typescript", "typescriptreact" },
callback = function(args)
vim.bo[args.buf].shiftwidth = 2 -- vim.bo — буферная опция, а не глобальная
end,
})
callback здесь — функция, которую можно протестировать, переиспользовать, обернуть в pcall
и передать в другой модуль. В Vim script на её месте была бы строка, которую интерпретатор
разберёт когда-нибудь потом.
Честная оговорка. Lua 5.1 — не самый приятный язык: индексация с единицы, неочевидное
поведение nil в таблицах, нет continue, нет типов (частично компенсируют аннотации LuaLS).
Vim9script на микробенчмарках местами обгоняет LuaJIT. Но вся экосистема плагинов последних
пяти лет — на Lua, и практически это решает вопрос.
Скелет конфигурации
Neovim читает ~/.config/nvim/init.lua; всё, что лежит в ~/.config/nvim/lua/, доступно
через require.
~/.config/nvim/
├── init.lua -- 15 строк: порядок загрузки и больше ничего
├── lua/core/{options,keymaps,autocmds}.lua
├── lua/plugins/{lsp,completion,treesitter,editor,ui}.lua
└── lazy-lock.json -- зафиксированные версии плагинов, В ГИТ ОБЯЗАТЕЛЬНО
Правило, экономящее больше всего времени: один файл — одна ответственность, длиной не больше двух экранов. Конфиг растёт монотонно; если он растёт в одном файле, через год вы перестанете понимать, какая строка что делает, и начнёте бояться его трогать.
-- init.lua
vim.g.mapleader = " " -- ДО загрузки lazy.nvim, иначе маппинги плагинов уедут
vim.g.maplocalleader = "\\"
require("core.options"); require("core.keymaps"); require("core.autocmds")
-- bootstrap: конфиг должен разворачиваться на чистой машине сам
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable",
"https://github.com/folke/lazy.nvim.git", lazypath })
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup("plugins", {
performance = { rtp = { disabled_plugins = { "gzip", "zipPlugin", "tutor", "netrwPlugin" } } },
})
-- lua/core/options.lua
local o = vim.opt
o.number, o.relativenumber = true, true -- относительная нумерация окупается вместе с 5j / 12k
o.signcolumn = "yes" -- фиксируем колонку знаков, иначе текст прыгает при диагностике
o.termguicolors = true
o.expandtab, o.shiftwidth, o.tabstop = true, 2, 2
o.ignorecase, o.smartcase = true, true -- без учёта регистра, пока в запросе нет заглавных
o.inccommand = "split" -- предпросмотр :s вживую — фича Neovim, которой нет в Vim
o.undofile = true -- undo переживает перезапуск; вместе с undo-tree это суперсила
o.swapfile = false
o.updatetime = 250 -- влияет на CursorHold: подсказки и подсветка ссылок
o.timeoutlen = 400 -- сколько ждать продолжения последовательности клавиш
o.scrolloff = 8
o.splitright, o.splitbelow = true, true
Отдельно про clipboard = "unnamedplus" — соединение безымянного регистра с системным буфером.
Удобно, но цена: любое x, d, c затирает системный буфер. Многие вместо этого оставляют
регистры раздельными и вешают явные маппинги:
-- lua/core/keymaps.lua
local map = vim.keymap.set
map({ "n", "v" }, "<leader>y", '"+y', { desc = "Копировать в системный буфер" })
map("x", "<leader>p", [["_dP]], { desc = "Вставить без затирания регистра" })
map({ "n", "v" }, "<leader>d", [["_d]], { desc = "Удалить в чёрную дыру" })
map("v", "J", ":m '>+1<CR>gv=gv") -- двигать выделение, сохраняя отступы
map("n", "n", "nzzzv") -- центрировать при прыжках: глаза не теряют позицию
map("n", "<Esc>", "<cmd>nohlsearch<CR>", { desc = "Снять подсветку поиска" })
map("t", "<Esc><Esc>", "<C-\\><C-n>", { desc = "Выход из терминального режима" })
desc — не косметика: which-key и :Telescope keymaps показывают именно его. Без описаний
через полгода вы не вспомните, что делает <leader>gh.
-- lua/core/autocmds.lua — clear = true обязателен, иначе при :source обработчики дублируются
local group = vim.api.nvim_create_augroup("UserConfig", { clear = true })
vim.api.nvim_create_autocmd("TextYankPost", { -- дешёвый фидбэк, что yank сработал
group = group,
callback = function() vim.hl.on_yank({ timeout = 150 }) end,
})
vim.api.nvim_create_autocmd("BufReadPost", { -- вернуться на позицию, где закрыли файл
group = group,
callback = function(args)
local mark = vim.api.nvim_buf_get_mark(args.buf, '"')
if mark[1] > 0 and mark[1] <= vim.api.nvim_buf_line_count(args.buf) then
pcall(vim.api.nvim_win_set_cursor, 0, mark)
end
end,
})
Менеджеры плагинов и ленивая загрузка
| Менеджер | Язык | Ленивая загрузка | Лок-файл | Статус |
|---|---|---|---|---|
| lazy.nvim | Lua | богатая: event/ft/cmd/keys | lazy-lock.json |
де-факто стандарт |
| mini.deps | Lua | ручная, через later() |
снапшот | минималистичный |
| paq-nvim | Lua | нет | нет | ~300 строк, читается целиком |
| vim-plug | Vim script | примитивная: for, on |
нет | работает и в Vim |
| packer.nvim | Lua | да | да | не поддерживается, мигрируйте |
встроенный vim.pack |
— | — | — | новый, см. :h vim.pack |
Идея lazy.nvim: плагин не грузится, пока не понадобится. Триггером служит событие, тип файла, команда или нажатие клавиши.
Измерять, а не гадать:
nvim --startuptime /tmp/start.log +q && sort -k2 -rn /tmp/start.log | head -25
nvim +"Lazy profile" # время каждого плагина
nvim +checkhealth # чего не хватает в окружении
Порог, за которым старт начинает раздражать, — около 100 мс; ниже 60 мс редактор ощущается мгновенным. Выше 300 мс появляется соблазн держать один экземпляр открытым постоянно, и вы теряете главное преимущество терминального редактора — дешёвый запуск.
Три главных пожирателя бюджета: жадно загруженные Treesitter-парсеры (ensure_installed = "all"
почти всегда ошибка), движок автодополнения без event = "InsertEnter" и плагины «на всякий
случай». Тест на последнее простой: удалите плагин и поживите неделю.
LSP: как работает встроенный клиент
Language Server Protocol придумали в Microsoft, чтобы разорвать комбинаторику «N редакторов × M языков». Языковой сервер — отдельный процесс, который понимает язык и отвечает на JSON-RPC-запросы: где определение, какие ссылки, что не так с файлом, как его отформатировать. Редактор про язык не знает ничего.
Канонический способ на 0.9–0.11 — через nvim-lspconfig, коллекцию готовых описаний серверов:
-- lua/plugins/lsp.lua
return {
{
"neovim/nvim-lspconfig",
event = { "BufReadPre", "BufNewFile" },
dependencies = { { "mason-org/mason.nvim", config = true }, "mason-org/mason-lspconfig.nvim" },
config = function()
-- маппинги вешаем ТОЛЬКО когда сервер реально прицепился к буферу
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("UserLspAttach", { clear = true }),
callback = function(ev)
local opts, map = { buffer = ev.buf }, vim.keymap.set
map("n", "gd", vim.lsp.buf.definition, opts)
map("n", "gr", vim.lsp.buf.references, opts)
map("n", "K", vim.lsp.buf.hover, opts)
map("n", "<leader>rn", vim.lsp.buf.rename, opts)
map({ "n", "v" }, "<leader>ca", vim.lsp.buf.code_action, opts)
map("n", "]d", function() vim.diagnostic.jump({ count = 1 }) end, opts)
-- фичу включаем, только если сервер объявил её в capabilities
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if client and client:supports_method("textDocument/inlayHint") then
vim.lsp.inlay_hint.enable(true, { bufnr = ev.buf })
end
end,
})
vim.diagnostic.config({
virtual_text = { spacing = 2, prefix = "●" },
severity_sort = true,
float = { border = "rounded", source = true },
})
local lspconfig = require("lspconfig")
local caps = require("blink.cmp").get_lsp_capabilities()
for server, cfg in pairs({
gopls = { settings = { gopls = { staticcheck = true } } },
ts_ls = {}, elixirls = {},
lua_ls = { settings = { Lua = { diagnostics = { globals = { "vim" } } } } },
}) do
cfg.capabilities = caps
lspconfig[server].setup(cfg)
end
end,
},
}
Начиная с 0.11 то же самое делается штатным API — описание сервера кладётся в отдельный файл
~/.config/nvim/lsp/gopls.lua, а в init.lua остаётся vim.lsp.enable({ "gopls", "lua_ls" }):
return {
cmd = { "gopls" },
filetypes = { "go", "gomod", "gowork" },
root_markers = { "go.work", "go.mod", ".git" },
settings = { gopls = { staticcheck = true } },
}
nvim-lspconfig при этом не умирает — он остаётся ценной базой выверенных команд запуска
и root_markers для сотен серверов. Просто теперь это данные, а не движок. Заодно 0.11
принёс дефолтные маппинги grn/gra/grr/gri и K: мнемоника «g + r + буква» непривычна
первые дни, зато не конфликтует с пользовательскими биндами и работает на чужой машине.
coc.nvim: честное сравнение
coc.nvim появился раньше встроенного клиента и решал реальную проблему — приносил в Vim
инфраструктуру расширений VS Code.
Встроенный vim.lsp |
coc.nvim |
|
|---|---|---|
| Зависимости | нет | Node.js обязателен |
| Расширения | плагины Neovim | свои coc-* |
| Конфигурация | Lua, в общем конфиге | JSON в coc-settings.json, отдельный слой |
| Работает в Vim | нет | да |
| Память | процесс на сервер | плюс постоянный Node-процесс |
| Настройка «из коробки» | собирается самому | быстрее стартовать |
| Интеграция с Telescope, trouble, dap | полная | через мосты |
Правило простое: на Neovim берите встроенный клиент, он давно перерос детские болезни.
На Vim coc.nvim — абсолютно рабочее решение, стыдиться нечего. Чего делать точно не стоит —
держать оба одновременно; почему, видно в разборе dotfiles ниже.
Автодополнение
vim.lsp даёт источник кандидатов, но не UI. Варианты: blink.cmp
(современный дефолт, fuzzy-матчинг на Rust), nvim-cmp
(многолетний стандарт, гибче, но собирается из десятка плагинов) и встроенное
vim.lsp.completion.enable() — без плагинов вообще, UI аскетичный.
-- lua/plugins/completion.lua
return {
{
"saghen/blink.cmp",
event = "InsertEnter", -- ключевое: не грузим при старте
version = "*",
dependencies = { "rafamadriz/friendly-snippets" },
opts = {
keymap = { preset = "default" }, -- <C-y> подтвердить, <C-n>/<C-p> навигация
sources = { default = { "lsp", "path", "snippets", "buffer" } },
completion = { accept = { auto_brackets = { enabled = true } } },
signature = { enabled = true },
},
},
}
Одна настройка сильно влияет на ощущения: не подтверждайте дополнение по Enter. Enter должен
вставлять перенос строки. Иначе вы будете регулярно получать случайно вставленный идентификатор
вместо новой строки. Подтверждение — <C-y> или <Tab>.
Treesitter: синтаксис, который редактор понимает
Классическая подсветка в Vim — набор регулярных выражений. Регулярки не умеют считать
вложенность, ломаются на длинных файлах и не отличают переменную от свойства объекта. Отсюда
знакомая боль: подсветка «уезжает» в большом JSX-файле, и приходится жать :syntax sync fromstart.
Tree-sitter — генератор инкрементальных парсеров, рождённый в Atom и ставший стандартом. Он строит настоящее синтаксическое дерево и обновляет его при каждой правке, перестраивая только затронутые поддеревья.
Стоимость правки пропорциональна размеру правки и глубине дерева, а не длине файла — поэтому
подсветка не деградирует на файле в двадцать тысяч строк. Плюс парсер устойчив к ошибкам:
недописанная функция не ломает дерево, поддерево помечается как ERROR, остальное разбирается
дальше. Это критично — код в редакторе синтаксически невалиден большую часть времени.
-- lua/plugins/treesitter.lua
return {
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
event = { "BufReadPost", "BufNewFile" },
dependencies = { "nvim-treesitter/nvim-treesitter-textobjects" },
opts = {
-- только используемые языки: каждый парсер — это компиляция и мегабайты
ensure_installed = { "lua", "vim", "vimdoc", "query", "go", "typescript", "tsx",
"elixir", "heex", "json", "yaml", "markdown", "bash" },
highlight = {
enable = true,
disable = function(_, buf) -- на файлах >1 МБ парсинг дороже пользы
local ok, stats = pcall(vim.uv.fs_stat, vim.api.nvim_buf_get_name(buf))
return ok and stats and stats.size > 1024 * 1024
end,
},
indent = { enable = true },
-- расширять выделение по узлам дерева: <CR> вверх, <BS> обратно
incremental_selection = { enable = true, keymaps = {
init_selection = "<CR>", node_incremental = "<CR>", node_decremental = "<BS>" } },
textobjects = {
select = {
enable = true,
lookahead = true, -- прыгнет к следующему объекту, если курсор вне него
keymaps = {
["af"] = "@function.outer", ["if"] = "@function.inner",
["aa"] = "@parameter.outer", ["ia"] = "@parameter.inner",
},
},
move = { enable = true, set_jumps = true,
goto_next_start = { ["]f"] = "@function.outer" },
goto_previous_start = { ["[f"] = "@function.outer" } },
},
},
config = function(_, opts) require("nvim-treesitter.configs").setup(opts) end,
},
}
Текстовые объекты по дереву — то, ради чего стоит ставить Treesitter, даже если подсветка вас
устраивала. daf удаляет функцию целиком независимо от языка и стиля отступов, cia меняет
аргумент, ]f прыгает к следующей функции. Сравните с
классическими текстовыми объектами Vim: di( работает
на скобках, но «функция» и «аргумент» — понятия языка, а не скобок.
Второй недооценённый механизм — инъекции языков: SQL внутри Go-строки, CSS внутри
шаблонного литерала JS, HEEx-шаблон внутри Elixir-модуля подсвечиваются своими парсерами.
Регулярками это не выражается в принципе. Свои запросы пишутся на языке запросов Tree-sitter
и кладутся в ~/.config/nvim/after/queries/<язык>/highlights.scm; отлаживать удобно через
:InspectTree (дерево буфера) и :Inspect (какая группа подсветки применена под курсором).
Осторожно: в 2025 году nvim-treesitter переехал на ветку main с изменённым API —
гайды из интернета могут не совпадать с тем, что установлено у вас. Сверяйтесь с README
той версии, которую зафиксировал ваш lazy-lock.json.
Отладка внутри редактора
Историческое слабое место терминальных редакторов закрывает nvim-dap: он реализует Debug Adapter Protocol — тот же, что использует VS Code, поэтому адаптеры переиспользуются напрямую.
{
"mfussenegger/nvim-dap",
dependencies = { "rcarriga/nvim-dap-ui", "nvim-neotest/nvim-nio", "leoluz/nvim-dap-go" },
keys = { -- keys = ленивая загрузка: dap не грузится, пока не начали отлаживать
{ "<F5>", function() require("dap").continue() end, desc = "Debug: продолжить" },
{ "<leader>b", function() require("dap").toggle_breakpoint() end, desc = "Точка останова" },
},
config = function()
local dap, dapui = require("dap"), require("dapui")
dapui.setup(); require("dap-go").setup()
-- панель отладки открывается и закрывается вместе с сессией
dap.listeners.before.launch.dapui_config = dapui.open
dap.listeners.before.event_terminated.dapui_config = dapui.close
end,
}
Честная оценка: работает, но настраивается заметно дольше, чем в JetBrains, где отладчик просто есть. Если вы отлаживаетесь степпером каждый день — это аргумент в пользу IDE, и его не надо рационализировать.
Разбор реальных dotfiles
Теория без живого примера плохо усваивается, поэтому разберём публичный репозиторий github.com/the-homeless-god/dotfiles. Он ценен именно тем, что это не вылизанная витрина, а рабочая конфигурация, которую ведут годами.
dotfiles/
├── configs/ -- конфиги, раскладываемые в $HOME
│ ├── .vimrc -- 998 строк Vim script, ~50 плагинов
│ ├── .zshrc, .alacritty.toml, .gitconfig, .editorconfig
│ ├── .config/tmux/.tmux.conf, .config/lf/, .config/vifm/
│ ├── tools.json -- декларативный каталог устанавливаемых инструментов
│ └── fonts/ -- FiraCode и MesloLGS NF, закоммиченные бинарниками
├── scripts/install-tools.sh -- ~38 КБ установщика, есть режим --interactive
├── ci/test-install.sh
├── docs/ru.md, docs/en.md
├── Dockerfile, docker-compose.yml, Makefile
└── .github/workflows/ -- CI и публикация образа в ghcr.io
Первое наблюдение, важное для темы статьи: это Vim-конфигурация, а не Neovim. Vim script,
vim-plug, LSP-слой на coc.nvim. И это живой, нормальный выбор в 2026 году. Смотреть полезно
именно на такой конфиг: большинство мигрирует не с нуля, а вот отсюда.
Что сделано хорошо
Установка автоматизирована и проверяема. Большинство dotfiles-репозиториев — набор файлов и README в стиле «скопируйте себе». Здесь есть установщик, Makefile, Dockerfile и GitHub Actions, публикующий образ в ghcr.io. Практический эффект: конфиг проверяется в CI на чистой системе. Это ровно тот класс проблем, где dotfiles ломаются чаще всего — «у меня работает, потому что три года назад я что-то поставил руками и забыл».
Каталог инструментов вынесен в данные. configs/tools.json описывает устанавливаемое
по категориям (essential, development, file_management, media) с локализованными
названиями, а установщик читает JSON и строит интерактивное меню. Правильное разделение:
список инструментов меняется часто, логика установки — редко.
Конфиг документирован сверху. Первые 55 строк .vimrc — карта горячих клавиш блоками:
навигация, файлы и поиск, редактирование, git, терминал. Плюс подключён vim-which-key.
Через полгода эти строки экономят часы.
Окружение целостное, а не только редактор. tmux с tmux-resurrect и tmux-continuum
(сессии переживают перезагрузку), lf и vifm с превью, ripgrep/fd/fzf, привязанные
к маппингам редактора — то, о чём отдельная
статья про терминальный workflow.
Шрифты закоммичены в репозиторий. Спорно по весу, но решает реальную проблему: patched Nerd Fonts — самая частая причина «у меня вместо иконок квадратики». Сознательный размен.
Что бы я поменял
Здесь честность важнее вежливости — эти паттерны встречаются почти в каждом долгоживущем конфиге, включая мои собственные.
Четыре конкурирующих движка автодополнения и диагностики. В списке плагинов есть
coc.nvim, YouCompleteMe, ALE и asyncomplete.vim. Каждый — самодостаточный слой,
и каждый хочет владеть popup-меню, omnifunc, знаками в signcolumn и обработкой <Tab>.
Симптомы предсказуемы: дублирующиеся диагностики, гонки за popup, «иногда автодополнение
не срабатывает», лишние сотни миллисекунд на старте, постоянный Node-процесс от coc плюс
скомпилированный ycmd от YCM. Это классика накопления: инструмент добавлялся, когда
предыдущий чем-то не устроил, но предыдущий не удалялся — «вдруг сломается».
vim-polyglot вместе с отдельными синтаксическими плагинами (vim-javascript,
typescript-vim, vim-jsx-pretty, vim-graphql). Polyglot по своей природе — сборник тех же
самых плагинов. Двойная загрузка синтаксиса даёт трудноуловимые баги подсветки и отступов,
причём именно те, которые потом «лечат» новыми плагинами.
set paste в конфиге. Один из самых известных футганов Vim: режим paste отключает
автоотступ, аббревиатуры и все маппинги insert-режима — включая маппинги автодополнения,
которые настраиваются ниже по этому же файлу. В современных терминалах с bracketed paste
строка не нужна.
Противоречащие настройки в разных частях файла. set hidden в одном блоке и set nohidden
с bufhidden=wipe через четыреста строк; set number! (переключатель) и set number
(установка) в разных местах. Побеждает последняя выполненная строка, но понять какая можно,
только прочитав файл целиком. Прямое следствие монолита на тысячу строк: дублирование не видно.
Той же природы дубликаты в списке плагинов (vim-floaterm и vim-fugitive объявлены дважды)
и два тайлинг-плагина в tmux (tmux-tilit и tmux-tilish), перехватывающих одни клавиши.
Нет фиксации версий. vim-plug не создаёт лок-файла, значит :PlugUpdate может привезти
ломающее изменение, а откатиться к «как было вчера» нечем. Пожалуй, самая дорогая проблема
из перечисленных: проявляется в худший момент — когда вы срочно настраиваете машину.
Как это выглядело бы после переезда
| Было (Vim script) | Стало (Neovim + Lua) | Комментарий |
|---|---|---|
| coc + YCM + ALE + asyncomplete | vim.lsp + blink.cmp + conform.nvim |
один слой вместо четырёх, без Node |
vim-polyglot + 5 syntax-плагинов |
nvim-treesitter |
настоящий парсер вместо регулярок |
nerdtree + git-плагин к нему |
oil.nvim или neo-tree.nvim |
oil редактирует каталог как буфер |
fzf.vim + vim-ripgrep |
telescope.nvim или fzf-lua |
единый интерфейс поиска |
vim-which-key |
which-key.nvim |
берёт описания из desc маппингов |
vim-fugitive |
vim-fugitive (он же) |
лучший git-плагин, менять не на что |
ultisnips + vim-snippets |
friendly-snippets |
без зависимости от Python |
vim-airline |
lualine.nvim |
заметно дешевле по отрисовке |
| нет лок-файла | lazy-lock.json |
воспроизводимость |
| 998 строк в одном файле | 8–10 модулей по 40–80 строк | правится без страха |
Оценка трудозатрат по опыту: две-три вечерних сессии до рабочего паритета и месяц-полтора, чтобы перестать спотыкаться о мышечную память. Не мигрируйте перед дедлайном — классическая ошибка, после которой откатываются и делают вывод «Neovim не взлетел».
Типичные ошибки
- Копирование чужого конфига целиком. Самый частый способ получить редактор, который вы не понимаете и не можете починить. kickstart.nvim — один документированный файл, который вам предлагают прочитать и разобрать; LazyVim — фреймворк со своими абстракциями, где кастомизация означает изучение ещё одного слоя. Начинать с kickstart честнее.
- Конфиг как хобби вместо работы. Настраивать редактор приятнее, чем писать код. Здоровая практика — копить список изменений и вносить их пачкой раз в месяц. Ориентир: больше дня за квартал — это уже не оптимизация, а прокрастинация.
lazy-lock.jsonне в гите. Без него dotfiles не воспроизводимы — весь смысл лок-файла в том, чтобы через год на другой машине получить те же версии.vim.optтам, где нуженvim.bo/vim.wo. Установка глобальной опции из FileType-автокоманды меняет её для всех буферов.- Автокоманды без
augroup ... clear = true. При каждом:sourceобработчики дублируются, и один и тот же коллбэк начинает срабатывать дважды, трижды, десять раз. - Смешивание менеджеров плагинов. Остатки
packerв~/.local/share/nvim/site/packпосле переезда на lazy продолжают загружаться штатным механизмом пакетов и дают призрачные плагины, которых нет в конфиге. Чистите каталог данных. - Вера, что
:checkhealthпокажет всё. Он найдёт отсутствующие бинарники и кривые парсеры, но не логические конфликты плагинов — их ловят только удалением по одному.
Когда Neovim — правильный выбор, а когда нет
Скорее да, если вы много работаете по SSH и в контейнерах; окружение должно разворачиваться скриптом; ваши языки имеют хорошие LSP-серверы (Go, Rust, TypeScript, Python, Elixir); терминал уже ваш дом, и там живут tmux, git и ripgrep; вы готовы вложить 20–40 часов в первые месяцы.
Скорее нет, если основной стек — крупный Java/Kotlin- или .NET-монолит, где рефакторинги JetBrains экономят часы, а не минуты; вы отлаживаетесь степпером ежедневно; нужны GUI-инструменты профилирования и работы с БД в одном окне.
Промежуточный вариант, который недооценивают: оставить основную IDE и включить в ней качественную vim-эмуляцию (IdeaVim, VSCodeVim или расширение с настоящим ядром Neovim). Это даёт 80% выгоды от модального редактирования при 5% стоимости перехода. Подробное сравнение — в заключительной статье трека.
Мини-итог
- Neovim — не «Vim с плагинами получше», а другая архитектура: ядро с msgpack-RPC API, циклом событий libuv, встроенным LuaJIT и Treesitter; UI отделён от редактора.
- Lua даёт модульность, скорость и настоящие данные вместо строк. Vim script продолжает работать, мигрировать «всё и сразу» не обязательно.
- LSP превратил «поддержку языка» из свойства редактора в свойство экосистемы. С 0.11 клиент
настраивается штатным
vim.lsp.config/vim.lsp.enable, аnvim-lspconfigостаётся базой готовых описаний серверов. - Treesitter — не про красивые цвета, а про структуру: текстовые объекты по функциям и аргументам, инъекции языков, устойчивость к синтаксическим ошибкам.
- Ленивая загрузка и
lazy-lock.jsonпревращают конфиг из хрупкого артефакта в воспроизводимый. Измеряйте старт, а не угадывайте. - Разбор чужих dotfiles полезнее чтения гайдов. Повторяющиеся грабли: несколько движков
автодополнения сразу, дублирование синтаксических плагинов,
set paste, монолитный файл и отсутствие фиксации версий.
Источники
- Документация Neovim, а также
:h lua-guide,:h lsp,:h treesitter— первичный источник, всегда актуальнее статей в интернете - Neovim Charter — цели проекта и мотивация форка
- Language Server Protocol Specification 3.17
- Debug Adapter Protocol
- Tree-sitter: документация и язык запросов
- Max Brunsfeld. Incremental Parsing in Tree-sitter (Strange Loop)
- lazy.nvim — спецификации плагинов и ленивая загрузка
- kickstart.nvim — конфиг-учебник, который надо читать, а не копировать
- the-homeless-god/dotfiles — разобранный выше репозиторий
- Drew Neil. Practical Vim, 2nd ed. — лучшая книга про модель редактирования, целиком применима к Neovim
Что дальше
Мы посмотрели на редактор, который довели до состояния IDE, оставив его текстовым. Следующий шаг — семейство, которое пошло ровно в противоположную сторону: не редактор с расширениями, а среда исполнения Lisp, в которой редактор оказался одним из приложений.