Раньше это означало переход в "настоящую IDE". Сегодня Neovim даёт ровно те же умные возможности через LSP - и, что важно для 2026 года, делает это во многом из коробки, без зоопарка плагинов. Эта глава - про то, как превратить Neovim в редактор, который понимает язык.
Что такое LSP и почему это всё изменило
LSP (Language Server Protocol) - это протокол, придуманный Microsoft для VS Code и затем ставший индустриальным стандартом. Идея проста и гениальна: вместо того чтобы каждый редактор реализовывал поддержку каждого языка заново, мы разделяем ответственность.
- Языковой сервер (language server) - отдельная программа, которая умеет анализировать конкретный язык. Например, gopls для Go, rust-analyzer для Rust, lua_ls для Lua, pyright/basedpyright для Python, ts_ls для TypeScript. Сервер парсит проект, строит модель кода и знает про него всё.
- Редактор (LSP-клиент) - в нашем случае Neovim. Он не разбирается в семантике Go или Rust; он просто общается с сервером по стандартному протоколу: "где определение символа под курсором?", "какие тут ошибки?", "переименуй вот это".
Почему это важно понимать, а не просто настроить. Многие проблемы с LSP ("не работает автодополнение", "нет подсветки ошибок", "go-to-definition прыгает не туда") - это либо не установленный/не запущенный сервер, либо неверно определённый корень проекта, либо конфликт двух источников. Когда вы держите в голове схему "клиент Neovim <-> сервер <-> ваш проект", диагностика становится осмысленной.
Разделение ответственности в стеке 2026 года
В Neovim 0.11+ (и тем более 0.12) LSP-клиент стал по-настоящему нативным. Это перекроило всю экосистему. Современный стек состоит из чётко разделённых ролей:
Код: Выделить всё
Компонент | Роль | Обязателен?
-----------------------+------------------------------------------------------------------------------+----------------------
Ядро Neovim (vim.lsp) | Рантайм LSP-клиента, активация серверов, дефолтные маппинги, диагностика | Да, встроено
nvim-lspconfig | Каталог готовых дефолтных конфигов серверов (cmd, filetypes, root_markers) | Нет, но удобно
mason.nvim | Установщик серверов/линтеров/форматтеров одной командой | Нет, но очень удобно
mason-lspconfig.nvim | Мост между именами пакетов Mason и именами конфигов + авто-включение | Нет
conform.nvim | Форматирование (отдельно от LSP, чтобы избежать конфликтов) | Нет, рекомендуется
Ключевой сдвиг: nvim-lspconfig больше не фреймворк, а справочник. Старый паттерн require('lspconfig').gopls.setup{} официально устарел (deprecated) и будет удалён в nvim-lspconfig v3.0.0. Сам плагин жив - но теперь он просто поставляет готовые дефолты поверх нативного API. Для базовой работы LSP он вообще не нужен.Vim vs Neovim: важная оговорка. Всё, о чём эта глава, - про Neovim. В классическом Vim нативного vim.lsp не существует вовсе: ядро Vim не содержит LSP-клиента. Чтобы получить LSP в Vim, нужны сторонние плагины - vim-lsp, coc.nvim (тащит за собой Node.js и собственный движок а-ля VS Code) или ALE. То есть нативный из коробки LSP - это эксклюзив Neovim 0.11+; в Vim тот же результат достижим только через внешний слой. Дальше под "редактором-клиентом" мы всегда подразумеваем Neovim.
Нативный API: vim.lsp.config() и vim.lsp.enable()
Это центральная тема главы. В Neovim 0.11 появилось разделение конфигурации и активации сервера.
- vim.lsp.config('имя', { ... }) - задаёт или переопределяет настройки сервера. Ничего не запускает.
- vim.lsp.enable('имя') - включает сервер: Neovim будет автоматически запускать его для подходящих filetypes.
Код: Выделить всё
-- Настраиваем сервер Lua
vim.lsp.config('lua_ls', {
settings = {
Lua = {
runtime = { version = 'LuaJIT' },
diagnostics = { globals = { 'vim' } }, -- чтобы lua_ls знал про глобал vim
},
},
})
-- Включаем нужные серверы
vim.lsp.enable({ 'lua_ls', 'gopls', 'rust_analyzer', 'pyright' })
Самая чистая структура 2026 года: один файл на сервер в каталоге ~/.config/nvim/lsp/<server>.lua. Neovim 0.11+ автоматически подхватывает эти файлы из runtimepath - так же, как ftplugin/. Никаких вызовов vim.lsp.config() руками: файл и есть конфиг.
Код: Выделить всё
-- ~/.config/nvim/lsp/lua_ls.lua
return {
settings = {
Lua = {
runtime = { version = 'LuaJIT' },
diagnostics = { globals = { 'vim' } },
workspace = { checkThirdParty = false },
},
},
}
Код: Выделить всё
-- ~/.config/nvim/lsp/gopls.lua
return {
settings = {
gopls = {
hints = { -- inlay hints для Go
parameterNames = true,
assignVariableTypes = true,
},
},
},
}
Код: Выделить всё
vim.lsp.enable({ 'lua_ls', 'gopls', 'rust_analyzer', 'pyright' })
Общие дефолты через vim.lsp.config('*', ...)
Звёздочка * применяет настройки ко всем серверам - удобно для общих capabilities (например, для интеграции с blink.cmp, см. главу 26) и корневых маркеров:
Код: Выделить всё
vim.lsp.config('*', {
root_markers = { '.git' },
capabilities = require('blink.cmp').get_lsp_capabilities(),
})
Установка серверов: mason.nvim + mason-lspconfigВажно про root_markers. В нативном API корень проекта надо задавать явно. Старый lspconfig определял его автоматически по хитрым правилам; теперь, если не указать root_markers, сервер может неверно определить корень и, например, не увидеть go.mod или Cargo.toml. Готовые дефолты из nvim-lspconfig обычно уже содержат правильные маркеры для каждого сервера.
Языковой сервер - это внешняя программа, которую надо где-то взять. Можно ставить руками через системный пакетный менеджер (brew install gopls), но удобнее - через Mason, который качает серверы в каталог Neovim и не засоряет систему.
Код: Выделить всё
-- через lazy.nvim (см. главу 23)
{
'mason-org/mason.nvim',
opts = {},
},
{
'mason-org/mason-lspconfig.nvim',
dependencies = { 'mason-org/mason.nvim', 'neovim/nvim-lspconfig' },
opts = {
ensure_installed = { 'lua_ls', 'gopls', 'rust_analyzer', 'pyright' },
},
},
Большое изменение: Mason 2.0 и automatic_enable
В 2025 году вышли Mason 2.0 и mason-lspconfig 2.0 с ломающими изменениями, о которых надо знать:
- Удалены setup_handlers() и опция handlers. Если вы копируете старый гайд с handlers = { ... } - он не заработает.
- Появилась опция automatic_enable (включена по умолчанию): mason-lspconfig сам вызывает vim.lsp.enable() для всех установленных серверов.
Что вы получаете: возможности LSP
Теперь - ради чего всё затевалось. Перечислим ключевые умные функции и то, как они вызываются.
Дефолтные маппинги Neovim 0.11+ (без единого плагина!)
Большое нововведение 0.11 - встроенные LSP-маппинги. Они создаются глобально, как только активен хоть один сервер. Раньше каждый гайд вручную биндил gd/gr/<leader>ca - теперь базовый набор уже есть:
Код: Выделить всё
Клавиша | Действие | LSP-метод
----------------+---------------------------------------------------+---------------------------------------------------
K | Hover: документация и сигнатура под курсором | textDocument/hover
grn | Rename: переименовать символ во всём проекте | textDocument/rename
gra | Code action: быстрые исправления и рефакторинги | textDocument/codeAction
grr | References: все использования символа | textDocument/references
gri | Implementation: к реализации интерфейса | textDocument/implementation
grt | Type definition: к определению типа | textDocument/typeDefinition
grx | Запустить code lens | vim.lsp.codelens.run() / workspace/executeCommand
gO | Document symbol: список символов файла | textDocument/documentSymbol
<C-s> (insert) | Signature help: подсказка по параметрам | textDocument/signatureHelp
[d / ]d | Прыжок к предыдущей/следующей диагностике | vim.diagnostic
Go-to-definition, hover, references на практике
Код: Выделить всё
// Было: курсор на вызове функции processOrder(o)
// ^
// Нажимаем: gd (если забиндили; или :lua vim.lsp.buf.definition())
// Стало: прыгнули к определению func processOrder(o Order) error { ... }
// вернуться назад - Ctrl-o (jumplist, см. главу 9)
Код: Выделить всё
// Было: курсор на time.Now
// ^
// Нажимаем: K
// Стало: всплыло окно с сигнатурой func Now() Time и документацией.
// Ещё раз K - фокус переходит в окно, можно скроллить и закрыть q.
Код: Выделить всё
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, { desc = 'Go to definition' })
Код: Выделить всё
// Было: переменная userCount используется в 12 местах файла и в 3 других файлах
// Нажимаем: grn, вводим totalUsers, Enter
// Стало: все 15 вхождений переименованы - но только настоящие ссылки,
// не строка "userCount" в комментарии или в другой области видимости
Code actions: рефакторинги и быстрые фиксы
gra показывает контекстное меню действий: импортировать недостающий пакет, убрать неиспользуемую переменную, обернуть в if err != nil, извлечь функцию. Список зависит от сервера и позиции курсора.
Код: Выделить всё
// Было: используется fmt.Println, но import "fmt" отсутствует - ошибка
// Нажимаем: gra -> выбираем "Add import: fmt"
// Стало: в блок import добавлена строка "fmt", ошибка исчезла
Диагностика - это ошибки, предупреждения и подсказки, которые сервер присылает по мере анализа кода. Управляет ими отдельный встроенный модуль vim.diagnostic (он работает не только с LSP, но и с линтерами).
Рекомендуемый осознанный конфиг 2026:Подводный камень 0.11. После апгрейда virtual_text (текст ошибки прямо в строке) стал opt-in - то есть выключен по умолчанию. Это частая жалоба "диагностика сломалась после обновления". На самом деле её просто надо включить.
Код: Выделить всё
vim.diagnostic.config({
virtual_text = true, -- текст ошибки в конце строки
-- альтернатива: virtual_lines = { current_line = true }
-- (разворачивает многострочную диагностику под текущей строкой)
severity_sort = true, -- сортировать по важности
underline = true,
update_in_insert = false, -- не дёргать диагностику при наборе
float = { border = 'rounded' },
signs = {
text = {
[vim.diagnostic.severity.ERROR] = 'E',
[vim.diagnostic.severity.WARN] = 'W',
[vim.diagnostic.severity.HINT] = 'H',
[vim.diagnostic.severity.INFO] = 'I',
},
},
})
Код: Выделить всё
Команда / клавиша | Действие
-----------------------------+----------------------------------------------------
[d / ]d | Предыдущая / следующая диагностика
vim.diagnostic.open_float() | Показать полный текст ошибки под курсором в окне
vim.diagnostic.setqflist() | Сбросить все диагностики в quickfix (см. главу 14)
vim.diagnostic.setloclist() | То же в location list текущего окна
Форматирование: conform.nvim, а не LSP напрямую
Многие серверы умеют форматировать код (textDocument/formatting). Базовая ручная команда форматирования через LSP - vim.lsp.buf.format() (например, :lua vim.lsp.buf.format() или маппинг на неё); она просит активный сервер отформатировать текущий буфер. Но полагаться на LSP-форматирование напрямую - источник классической боли: двойное форматирование, когда и LSP-сервер, и отдельный форматтер дерутся за файл, переставляя кавычки туда-сюда.
Практика 2026: вынести форматирование в conform.nvim - тонкий слой, на котором сошлась почти вся экосистема (он заменил устаревший null-ls/none-ls). Вы явно назначаете один форматтер на язык, и conform при необходимости глушит LSP-форматтер.
Код: Выделить всё
{
'stevearc/conform.nvim',
event = 'BufWritePre',
opts = {
formatters_by_ft = {
lua = { 'stylua' },
python = { 'ruff_format' },
javascript = { 'prettierd', 'prettier', stop_after_first = true },
go = { 'goimports', 'gofmt' },
},
format_on_save = {
-- современный ключ: lsp_format = 'fallback'
-- (старый lsp_fallback = true ещё работает как deprecated-алиас)
lsp_format = 'fallback', -- если форматтера нет - упасть на LSP
timeout_ms = 1000,
},
},
}
Inlay hints: подсказки прямо в коде
Inlay hints - это виртуальные аннотации, которые сервер вставляет в текст: имена параметров в вызовах, выведенные типы переменных. Доступны с Neovim 0.10+.
Код: Выделить всё
// Без hints: // С hints (виртуальный текст, серым):
result := compute(10, true) result := compute(width: 10, verbose: true)
nums := []int{1, 2, 3} nums : []int := []int{1, 2, 3}
Код: Выделить всё
vim.lsp.inlay_hint.enable(true) -- глобально включить
Код: Выделить всё
vim.keymap.set('n', '<leader>uh', function()
vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
end, { desc = 'Toggle inlay hints' })
Где вешать собственные LSP-маппинги и включать фичи вроде inlay hints? Не в on_attach каждого сервера (это легаси-стиль), а в одном autocommand-событии LspAttach, которое срабатывает буфер-локально при подключении любого сервера.
Код: Выделить всё
vim.api.nvim_create_autocmd('LspAttach', {
group = vim.api.nvim_create_augroup('my.lsp', { clear = true }),
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
local buf = args.buf
local map = function(keys, fn, desc)
vim.keymap.set('n', keys, fn, { buffer = buf, desc = desc })
end
-- свои привычные раскладки поверх дефолтов 0.11
map('gd', vim.lsp.buf.definition, 'Go to definition')
map('gD', vim.lsp.buf.declaration, 'Go to declaration')
map('<leader>ca', vim.lsp.buf.code_action, 'Code action')
-- включаем фичи только если сервер их поддерживает
if client and client:supports_method('textDocument/inlayHint') then
vim.lsp.inlay_hint.enable(true, { bufnr = buf })
end
-- single owner форматирования: глушим LSP-форматтер,
-- если форматированием владеет conform
if client then
client.server_capabilities.documentFormattingProvider = false
end
end,
})
- Маппинги буфер-локальны (buffer = buf) - они появляются только там, где есть сервер.
- Перед включением фичи всегда проверяем client:supports_method(...) - иначе на серверах без поддержки (например, inlay hints) получите ошибки или мусор.
- augroup с clear = true - чтобы при перечитывании конфига autocommand не дублировался (см. главу 20).
Версия 0.12 (29 марта 2026) ещё расширила нативный LSP:
- Новая интерактивная команда :lsp - нативная команда управления LSP-клиентами с подкомандами (старт, стоп, рестарт клиентов). Важно не путать: одноимённые :LspInfo/:LspRestart/:LspLog/:LspStart/:LspStop встроенными в ядро никогда не были - их предоставлял плагин nvim-lspconfig, а теперь они оттуда удалены. Их функции покрываются так: управление клиентами (старт/стоп/рестарт) - подкомандами нативной :lsp, а :LspInfo сводится к :checkhealth vim.lsp.
- Событие LspProgress - прогресс индексации сервера можно показать в статусной строке.
- Встроенная поддержка расширения выделения через selectionRange (сам LSP-метод textDocument/selectionRange не нов, новизна 0.12 - нативная встроенная поддержка умного расширения выделения, в т.ч. через LSP/treesitter). Это альтернатива incremental selection из nvim-treesitter (см. главу 27): она может её заменить, но не автоматически - результат зависит от сервера/конфигурации. Плюс поддержка методов inlineCompletion, linkedEditingRange, documentLink, semantic tokens только для видимой области (быстрее на больших файлах).
- Нативное автодополнение в insert-режиме (опция 'autocomplete', пока экспериментальная) и встроенный LSP-омникомплит через vim.lsp.completion.enable() - про автодополнение подробно в главе 26.
- Используйте нативный vim.lsp.config / vim.lsp.enable, а не require('lspconfig').xxx.setup{}. Старый путь выдаёт deprecation-warning и будет удалён в nvim-lspconfig v3.0.0. Не смешивайте подходы - это приводит к дублированию и конфликтам конфигов.
- Храните переопределения серверов в ~/.config/nvim/lsp/<server>.lua - один файл на сервер. Чисто, модульно, автоматически подхватывается из runtimepath.
- Общие настройки - через vim.lsp.config('*', {...}), а маппинги и фичи - через autocommand LspAttach с проверкой client:supports_method(). Не пишите on_attach в каждом сервере.
- Не дублируйте дефолтные маппинги 0.11 (grn, gra, grr, gri, grt, gO, K, <C-s>). Они уже встроены - добавляйте только то, чего не хватает (gd).
- Включите virtual_text осознанно - после 0.11 он opt-in. Рассмотрите virtual_lines с current_line = true для разворота многострочных диагностик.
- Один владелец форматирования на язык. Вынесите форматирование в conform.nvim и глушите documentFormattingProvider у LSP, чтобы не было двойного форматирования.
- С mason-lspconfig 2.0 доверьтесь automatic_enable - не вызывайте vim.lsp.enable() повторно вручную для тех же серверов.
- Inlay hints - под тоггл. Они полезны, но шумны; держите клавишу для быстрого включения/выключения.
- Задавайте root_markers явно (хотя бы { '.git' }), особенно если не используете дефолты nvim-lspconfig.
Первый шаг диагностики: проверить, что вообще прикреплено. Прежде чем чинить "не работает LSP", узнайте, какой сервер привязан к буферу и привязан ли вообще. Способы:
- :checkhealth vim.lsp - главный отчёт о состоянии LSP (на 0.12 сюда же сводится :LspInfo): какие клиенты активны, их cmd, корень проекта, поддерживаемые методы, найден ли исполняемый файл сервера в PATH.
- :lua =vim.lsp.get_clients({ bufnr = 0 }) - показать клиентов, прикреплённых к текущему буферу (пустой список = сервер не стартовал; типичные причины: не установлен сервер, не тот filetype, не совпал root_markers).
- :lsp (0.12) - нативная команда: посмотреть и перезапустить/остановить/запустить клиентов.
- :lua vim.lsp.set_log_level('debug') + :lua vim.cmd('e ' .. vim.lsp.get_log_path()) - заглянуть в лог, если сервер падает на старте.
Этот навык важнее любого конкретного фикса: 90% проблем ("нет автодополнения", "go-to-definition молчит") сводятся к "сервер не прикреплён", и :checkhealth vim.lsp сразу показывает почему.
- require('lspconfig').gopls.setup{} на 0.11+. Легаси-путь, выдаёт предупреждение и сломается в v3.0.0. Мигрируйте на vim.lsp.config + vim.lsp.enable.
- Двойной запуск сервера. mason-lspconfig 2.0 сам делает vim.lsp.enable(). Если вы ещё и вручную включаете тот же сервер - он стартует дважды, конфиги конфликтуют.
- Копирование старого Mason-гайда с setup_handlers()/handlers. В Mason 2.0 эти опции удалены - конфиг тихо не работает.
- "Пропала подсветка ошибок". На 0.11 virtual_text стал opt-in. Это не баг - добавьте vim.diagnostic.config({ virtual_text = true }).
- Двойное форматирование. И LSP-сервер, и conform форматируют файл, переставляя код туда-сюда. Назначьте единственного владельца на язык.
- Inlay hints без проверки поддержки. Вызов vim.lsp.inlay_hint.enable(true) на сервере без textDocument/inlayHint даёт ошибки. Проверяйте client:supports_method(...).
- Неверный корень проекта. Без root_markers сервер может не увидеть go.mod/Cargo.toml/package.json и работать "наполовину". Задайте маркеры явно.
- gd ведёт себя странно. Без забинденного vim.lsp.buf.definition клавиша gd делает встроенное Vim-действие (переход к локальному определению по тексту), а не LSP-переход. Это разные вещи.
- Минимальный LSP без плагинов. Установите системно один сервер (например, lua-language-server или gopls). В чистом init.lua пропишите только vim.lsp.enable({'lua_ls'}) (или gopls) и убедитесь, что K, grn, grr работают - без nvim-lspconfig и Mason.
- Каталог lsp/. Создайте ~/.config/nvim/lsp/lua_ls.lua, возвращающий таблицу с settings.Lua.diagnostics.globals = {'vim'}. Проверьте, что предупреждение "undefined global vim" в вашем конфиге исчезло, а в init.lua остался только vim.lsp.enable.
- Свой LspAttach. Напишите autocommand на LspAttach, который вешает буфер-локальные gd (definition) и <leader>ca (code action), и включает inlay hints только при наличии поддержки. Откройте файл с сервером и без - убедитесь, что маппинги появляются лишь там, где есть LSP.
- Диагностика в quickfix. В проекте с несколькими ошибками выполните :lua vim.diagnostic.setqflist() и пройдитесь по ошибкам через :copen, :cnext, :cprev. Сравните с прыжками ]d/[d - когда удобнее каждый способ?
- Тоггл virtual_lines. Настройте vim.diagnostic.config и сделайте клавишу, которая переключает диагностику между virtual_text = true и virtual_lines = { current_line = true }. Прочувствуйте разницу на длинном сообщении об ошибке.
- Single owner форматирования. Подключите conform.nvim с форматтером для одного языка и в LspAttach отключите documentFormattingProvider у LSP. Сохраните файл с намеренно "кривым" отступом и убедитесь, что форматирует именно conform, а не сервер.
- LSP разделяет редактор и понимание языка: языковой сервер анализирует код, Neovim как клиент общается с ним по стандартному протоколу.
- В Neovim 0.11+ LSP-клиент нативный. Канонический способ - vim.lsp.config('сервер', {...}) для настройки и vim.lsp.enable({...}) для активации; конфиги удобно держать пофайлово в ~/.config/nvim/lsp/.
- nvim-lspconfig теперь каталог дефолтов, а не фреймворк; старый setup{} устарел. mason.nvim ставит серверы, mason-lspconfig 2.0 авто-включает их (automatic_enable).
- Базовые маппинги (K, grn, gra, grr, gri, grt, gO, <C-s>, [d/]d) встроены - не переизобретайте их, добавляйте только своё через LspAttach.
- Диагностика живёт в vim.diagnostic; virtual_text стал opt-in - включайте осознанно. Форматирование выносите в conform.nvim (single owner). Inlay hints включайте под проверкой поддержки и под тоггл.
- Дальше: автодополнение поверх LSP - глава 26; точная подсветка и структурное редактирование через Treesitter - глава 27.