Эта глава - про то, как устроено современное автодополнение, чем два движка отличаются, какие источники и сниппеты к ним подключают и как настроить клавиши так, чтобы дополнение помогало, а не мешало. Мы опираемся на нативный LSP из главы 25 - именно язык-сервер поставляет самых "умных" кандидатов.
Зачем плагин поверх встроенного
Встроенный Ctrl-X Ctrl-O (omni) и Ctrl-N (keyword) решают базовую задачу, но требуют от вас явного вызова и ручного перебора. Современный движок дополнения добавляет поверх этого:
- Автоматическое всплывающее меню по мере набора - не нужно нажимать Ctrl-N.
- Объединение нескольких источников в один отсортированный список: LSP, слова из буфера, пути файлов, сниппеты.
- Нечёткий матчинг с ранжированием - кандидаты сортируются по релевантности, частоте использования (frecency) и близости в коде (proximity), а не по алфавиту.
- Превью документации и сигнатур прямо в меню.
- Развёртывание сниппетов с табуляцией по плейсхолдерам.
Два движка: nvim-cmp и blink.cmp
nvim-cmp - зрелый стандарт ушедшей эпохи
nvim-cmp (hrsh7th/nvim-cmp) много лет был де-факто стандартом. Его сила - модульность и экосистема: сам по себе он почти ничего не умеет, но к нему подключают десятки плагинов-источников (cmp-nvim-lsp, cmp-buffer, cmp-path, cmp_luasnip, cmp-cmdline и так далее). Если вам нужна тонкая, нестандартная кастомизация или у вас уже сложился рабочий конфиг - nvim-cmp остаётся полностью валидным выбором.
Минусы - оборотная сторона модульности. Чтобы получить базовый набор (LSP + буфер + путь + сниппеты), нужно прописать "зоопарк" из шести-восьми отдельных плагинов. И производительность: по умолчанию nvim-cmp использует дебаунс около 60 мс и при больших списках кандидатов может давать заметные подтормаживания (hitches) в диапазоне 2-50 мс.
blink.cmp - быстрый "всё в одном"
blink.cmp (saghen/blink.cmp) - современный движок, который в 2026 стал движком автодополнения по умолчанию в LazyVim и в kickstart.nvim. Ключевые отличия:
- Batteries included. Источники LSP, buffer, path, snippets, а также автодополнение командной строки (cmdline) и подсказки сигнатур (signature help) встроены. Никакого зоопарка плагинов-источников.
- Скорость. blink.cmp пересчитывает кандидатов на каждое нажатие с оверхедом всего 0.5-4 мс на одном ядре (асинхронно), тогда как nvim-cmp опирается на дебаунс ~60 мс.
- Нечёткий матчер на Rust (проект Frizbee, с SIMD): устойчив к опечаткам, даёт бонусы по frecency и proximity. Есть и чистый Lua-фоллбэк, если вы не хотите собирать Rust-модуль.
Практический ориентир:
- Новый конфиг, хочется "просто работало" и быстро - берите blink.cmp. Это направление развития экосистемы и дефолт в популярных дистрибутивах.
- Уже есть рабочий nvim-cmp или нужна экзотическая кастомизация через редкий cmp-*-source - оставайтесь на nvim-cmp, мигрировать без причины не нужно.
- Особый случай. Известна деградация производительности blink.cmp на очень больших файлах некоторых типов (например, массивный TeX). LazyVim позволяет вернуться на nvim-cmp через :LazyExtras -> включить Extra coding.nvim-cmp. Одновременно работает только один движок.
Код: Выделить всё
Критерий | nvim-cmp | blink.cmp
---------------------------------+---------------------------------+--------------------------------------
Источники из коробки | нет, отдельные плагины | LSP, buffer, path, snippets, cmdline
Скорость | дебаунс ~60 мс, хитчи 2-50 мс | 0.5-4 мс на нажатие, async
Fuzzy-матчер | Lua | Rust/SIMD (Frizbee) + Lua-фоллбэк
Signature help | через cmp отдельно | встроен
Дефолт в LazyVim/kickstart 2026 | нет (опц. Extra) | да
Зрелость экосистемы источников | максимальная | растущая (+ blink.compat)
Источники дополненияВажная деталь установки: в blink.cmp идёт активная разработка v2 с ломающими изменениями (и доп. зависимостью blink.lib). Пока v2 не стабилизировалась, фиксируйте стабильную ветку: version = '1.*' или branch = 'v1' в lazy.nvim. Установка без пина версии затянет вас в нестабильную v2.
Источник (source) - это поставщик кандидатов. Движок собирает кандидатов из всех активных источников, сливает, ранжирует и показывает. Четыре базовых источника:
Код: Выделить всё
Источник | Что даёт | Когда полезен
----------+----------------------------------------------------------------+-------------------------------------------------------
LSP | Символы из языкового сервера: функции, методы, поля, импорты | Основной "умный" источник; знает типы и контекст
buffer | Слова из открытых буферов | Любой текст, имена переменных без LSP, Markdown, логи
path | Пути файлов и каталогов | Импорты по пути, ссылки, конфиги
snippets | Готовые шаблоны кода (см. ниже) | Boilerplate: циклы, функции, конструкции языка
Код: Выделить всё
-- nvim-cmp: источники задаются вручную
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'luasnip' },
{ name = 'path' },
}, {
{ name = 'buffer' }, -- второй уровень: показывается, если первый пуст
})
Код: Выделить всё
-- blink.cmp: батарейки внутри
opts = {
sources = {
default = { 'lsp', 'path', 'snippets', 'buffer' },
},
}
Сниппеты: LuaSnip, friendly-snippets и нативный vim.snippet
Сниппеты - это раскрываемые шаблоны: набираете fn, нажимаете развернуть - получаете заготовку функции с плейсхолдерами, по которым прыгаете табуляцией. В экосистеме 2026 есть три уровня.
Нативный vim.snippet
С Neovim 0.10+ в ядре есть собственный движок сниппетов vim.snippet (LSP-совместимый синтаксис: ${1:name}, $0 и т.д.). Прыжки по плейсхолдерам - vim.snippet.jump(1) / vim.snippet.jump(-1). Для большинства пользователей этого достаточно, и он не требует внешних зависимостей. Главное - коллекция готовых сниппетов работает с ним напрямую (см. ниже).
friendly-snippets
rafamadriz/friendly-snippets - большая community-коллекция готовых сниппетов для десятков языков в формате VS Code. Ключевой факт 2026: она работает с нативным vim.snippet без LuaSnip. То есть "сниппеты из коробки" теперь можно получить, не таща тяжёлый движок.
LuaSnip
L3MON4D3/LuaSnip - самый мощный движок: динамические и функциональные сниппеты, узлы-выборы, повтор плейсхолдеров, скриптование на Lua. Он жив и поддерживается. Но он перестал быть обязательной зависимостью: если вам не нужны динамические сниппеты, нативного vim.snippet + friendly-snippets хватает.
Как подключить движок сниппетов
В blink.cmp движок выбирается одной опцией snippets.preset:
Код: Выделить всё
-- blink.cmp: выбор движка сниппетов
opts = {
snippets = { preset = 'default' }, -- 'default' = нативный vim.snippet
-- варианты: 'default' | 'luasnip' | 'mini_snippets'
}
В nvim-cmp сниппеты подключаются через плагин-мост (cmp_luasnip) и в expand-колбэке:Подводный камень при апгрейде: начиная с blink.cmp v0.10 опции LuaSnip переехали в секцию snippets провайдера. Старые конфиги после обновления молча перестают работать - проверьте, что настройки сниппетов лежат в правильной секции.
Код: Выделить всё
-- nvim-cmp + LuaSnip
snippet = {
expand = function(args) require('luasnip').lsp_expand(args.body) end,
},
Настройка клавиш дополнения
Клавиши - самая личная и спорная часть конфига. blink.cmp упрощает выбор готовыми пресетами через keymap.preset:
Код: Выделить всё
Пресет | Tab | Enter | Идея
-----------+----------------------+-----------+-----------------------------------------------------------------------------------------------------------
default | прыжок по сниппету | - | Меню управляется Ctrl-Y/Ctrl-N/Ctrl-P, в духе встроенного Vim
super-tab | принять / прыжок | - | Tab делает всё - привычно выходцам из IDE; приём - только по Tab, Enter остаётся обычным переводом строки
enter | прыжок по сниппету | принять | Подтверждение строго по Enter
Код: Выделить всё
-- blink.cmp: пресет клавиш
opts = {
keymap = { preset = 'super-tab' },
snippets = { preset = 'default' },
}
Код: Выделить всё
Клавиша | Действие
-----------------+---------------------------------------------------------------------------
Ctrl-Space | показать/спрятать меню, вызвать документацию
Ctrl-N / Ctrl-P | следующий / предыдущий кандидат
Ctrl-Y | принять выделенного кандидата
Ctrl-E | закрыть меню (отмена)
Tab / Shift-Tab | прыжок вперёд/назад по плейсхолдерам сниппета (в super-tab - ещё и выбор)
Ctrl-K | показать сигнатуру функции
Код: Выделить всё
-- nvim-cmp: ручной маппинг клавиш
mapping = cmp.mapping.preset.insert({
['<C-n>'] = cmp.mapping.select_next_item(),
['<C-p>'] = cmp.mapping.select_prev_item(),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = cmp.mapping.confirm({ select = true }),
}),
Практические примерыНюанс приёма кандидата в blink.cmp: команда select_and_accept (на которую завязаны Ctrl-Y, Tab в super-tab и Enter в enter) принимает именно выделенный кандидат. По умолчанию меню открывается без преселекта (completion.list.selection.preselect = true лишь подсвечивает первый пункт, а auto_insert управляет тем, подставляется ли текст по мере навигации). Если выделенного элемента нет - например, при preselect = false вы ещё не нажали Ctrl-N/Tab - то select_and_accept принимать нечего, и нажатие "как будто ничего не делает" (падает в fallback). Поэтому привычное "набрал и сразу Ctrl-Y" срабатывает не всегда: сперва убедитесь, что кандидат подсвечен, либо настройте preselect/auto_insert под себя.
Совет по эргономике: не вешайте подтверждение на Tab вслепую, если активно пользуетесь сниппетами и отступами - Tab перегружается тремя смыслами (отступ, выбор кандидата, прыжок по плейсхолдеру). Пресет enter (подтверждение по Enter, прыжки по Tab) для многих менее конфликтен.
Дальше - формат "было -> нажимаем -> стало". Курсор обозначен ▮.
1. LSP-дополнение метода. Язык-сервер знает тип объекта и предлагает методы.
Код: Выделить всё
было: user.nam▮
нажимаем: (меню всплывает само) -> Ctrl-Y
стало: user.name▮
Код: Выделить всё
было: require("./comp▮
нажимаем: (меню: components/, config.lua ...) -> Ctrl-N -> Ctrl-Y
стало: require("./components/▮
Код: Выделить всё
было: fn▮
нажимаем: Ctrl-Y (развернуть сниппет функции)
стало: function name(args)
▮
end
нажимаем: печатаем "greet", затем Tab
стало: function greet(args▮)
нажимаем: печатаем "who"
стало: function greet(who▮)
Код: Выделить всё
было: Подключаем плагин blink▮
нажимаем: (меню из buffer: blink.cmp) -> Ctrl-Y
стало: Подключаем плагин blink.cmp▮
Код: Выделить всё
было: :colorsch▮
нажимаем: Tab (cmdline-меню) -> выбрать
стало: :colorscheme habamax▮
Связный пример рабочего спека. Предполагается, что LSP уже настроен по главе 25.
Код: Выделить всё
-- ~/.config/nvim/lua/plugins/completion.lua
return {
'saghen/blink.cmp',
version = '1.*', -- пин на стабильную ветку v1
dependencies = { 'rafamadriz/friendly-snippets' },
event = 'InsertEnter', -- ленивая загрузка, см. главу 23
opts = {
keymap = { preset = 'super-tab' },
snippets = { preset = 'default' }, -- нативный vim.snippet
sources = {
default = { 'lsp', 'path', 'snippets', 'buffer' },
},
completion = {
documentation = { auto_show = true }, -- превью доков в меню
},
signature = { enabled = true }, -- подсказки сигнатур
fuzzy = { implementation = 'prefer_rust_with_warning' },
},
}
Лучшие практики 2026
- Для нового конфига - blink.cmp. Это дефолт LazyVim и kickstart.nvim, активное направление развития, источники и сниппеты - из коробки.
- Пиньте стабильную ветку. version = '1.*' или branch = 'v1', пока v2 (с blink.lib и breaking changes) не стабилизируется.
- Начинайте со сниппетов на нативном vim.snippet + friendly-snippets. LuaSnip подключайте только когда нужны динамические сниппеты - это убирает тяжёлую зависимость.
- Не дублируйте сложность nvim-cmp по инерции. Слепое копирование старых гайдов с десятком cmp-*-source в 2026 - лишняя работа; blink.cmp даёт те же источники одной строкой.
- Если мигрируете - используйте blink.compat для существующих источников nvim-cmp, чтобы не переписывать их.
- Держите путь отката. Если blink.cmp тормозит на специфичном файле (большой TeX), в LazyVim есть :LazyExtras -> coding.nvim-cmp. Одновременно живёт только один движок.
- Лениво грузите движок по event = 'InsertEnter' - дополнение не нужно до первого входа в insert-режим (см. главу 23 про lazy-loading).
- Помните про нативный минимум. Для совсем лёгких конфигов и серверов вместо плагина годится vim.lsp.completion.enable() (0.11+) или новая опция 'autocomplete' (0.12, в связке с completeopt).
- Установка blink.cmp без пина версии. Затягивает в нестабильную v2, требует blink.lib, ломает конфиг. Всегда version = '1.*'.
- После апгрейда blink.cmp (с v0.10) перестали работать LuaSnip-сниппеты. Опции переехали в секцию snippets провайдера - перенесите их.
- Включены оба движка сразу. nvim-cmp и blink.cmp конфликтуют; работает только один. В LazyVim переключайтесь через Extra, не держите оба активными.
- Ожидание сниппетов "из коробки" без коллекции. Движок vim.snippet сам по себе пуст - нужны friendly-snippets или собственные сниппеты.
- Tab перегружен тремя смыслами. Отступ, выбор кандидата и прыжок по плейсхолдеру конфликтуют; рассмотрите пресет enter.
- Тащить весь старый "зоопарк" cmp-* в новый конфиг 2026. Избыточная сложность; либо переходите на blink.cmp, либо осознанно остаётесь на nvim-cmp.
- Забыть, что "умные" кандидаты приходят от LSP. Если меню показывает только слова из буфера - скорее всего, не запущен язык-сервер (проверьте по главе 25: статус серверов в 0.12 смотрят через :checkhealth vim.lsp, бывший :LspInfo; сам :lsp - это диспетчер с подкомандами :lsp enable/disable/restart, статуса он не печатает, а :LspInfo/:LspRestart/:LspLog в 0.12 удалены).
- Ставить нативный LSP-комплит и плагин одновременно без понимания. vim.lsp.completion.enable() и blink.cmp оба будут пытаться вести меню - выберите что-то одно.
- Поставьте blink.cmp с пином v1. Создайте lua/plugins/completion.lua по примеру выше, перезапустите Neovim, откройте файл на знакомом языке (с настроенным LSP из главы 25) и убедитесь, что меню всплывает само и предлагает методы объекта.
- Сравните источники. Откройте Markdown-файл (без LSP) и проверьте, что работает источник buffer - дополнение по уже введённым словам. Затем откройте код и убедитесь, что приоритет у LSP-кандидатов.
- Сниппеты на нативном движке. Добавьте friendly-snippets в зависимости, разверните сниппет функции/цикла, пройдитесь по плейсхолдерам через Tab/Shift-Tab. Затем переключите snippets.preset на luasnip (поставив LuaSnip) и сравните поведение.
- Переберите пресеты клавиш. Попробуйте по очереди default, super-tab и enter. Опишите для себя, какой меньше конфликтует с вашей привычкой к Tab-отступам.
- Cmdline-дополнение. Не настраивая ничего дополнительно, проверьте, что blink.cmp дополняет командную строку: наберите :colorscheme и нажмите Tab.
- Откат на nvim-cmp (для пользователей LazyVim). Через :LazyExtras включите coding.nvim-cmp, перезапустите Neovim и убедитесь, что движок сменился. Затем верните blink.cmp. Это тренирует понимание, что движок - взаимозаменяемый слой.
- Встроенного дополнения (Ctrl-N, omni, нативный vim.lsp.completion) хватает для базовых сценариев, но богатый UI, мультиисточники, fuzzy-сортировка и сниппеты - задача отдельного движка.
- Два кандидата 2026: nvim-cmp (зрелый, модульный, экосистема источников) и blink.cmp (быстрый, "всё в одном", дефолт LazyVim/kickstart). Для нового конфига - blink.cmp; для устоявшегося nvim-cmp миграция без причины не нужна.
- Источники: LSP (умные кандидаты), buffer, path, snippets - в blink.cmp все включены из коробки.
- Сниппеты: начинайте с нативного vim.snippet + friendly-snippets; LuaSnip - для динамики. После апгрейда blink.cmp проверяйте секцию snippets.
- Пиньте version = '1.*', лениво грузите по InsertEnter, держите путь отката через LazyVim Extra. Умные кандидаты приходят от LSP - без язык-сервера дополнение "глупеет".