У вас два языка. Vimscript - родной язык Vim, на нём написаны тысячи плагинов и почти весь legacy-мир. Lua - встроенный язык Neovim, на котором сегодня пишут новые конфиги и плагины. В этой главе мы разберём оба: ровно столько, чтобы вы понимали чужой код и писали свой. Цель - не превратить вас в разработчика плагинов, а дать рабочий минимум: переменные, функции, условия, циклы, autocommands и API-вызовы.
Сразу зафиксируем установку 2026 года: новый код пишем на Lua. Vimscript нужен, чтобы читать старые конфиги, работать в чистом Vim (на сервере, где Neovim не поставить) и понимать, что делает плагин под капотом. Поэтому идём в таком порядке: сначала Vimscript как фундамент и "лингва франка" экосистемы, затем Lua как основной инструмент, и в конце - когда что выбирать.
Зачем вообще скриптить редактор
Большинство пользователей Vim/Neovim никогда не пишут больше десятка строк скрипта - и это нормально. Но даже простой конфиг постоянно сталкивается с задачами, которые без скриптинга не решить:
- Реакция на события. "При сохранении файла - отформатируй", "при входе в файл - восстанови позицию курсора", "при копировании - подсвети скопированное". Это всё autocommands.
- Логика в маппингах. "Если плавающее окно открыто - закрой, иначе открой" - это условие внутри функции, привязанной к клавише.
- Динамические команды. Собрать команду из переменных и выполнить её (:execute в Vimscript, vim.cmd в Lua).
- Чтение чужого кода. Каждый плагин - это скрипт. Понимая язык, вы чините проблемы сами, а не гадаете.
Часть 1. Vimscript
Переменные и области видимости (scopes)
В Vimscript переменная объявляется и присваивается командой :let, а удаляется через :unlet:
Код: Выделить всё
let count = 5
let name = "neovim"
let list = [1, 2, 3]
let dict = {'key': 'value'}
unlet count
Код: Выделить всё
Префикс | Область | Пример
---------+-----------------------------------+------------------
g: | глобальная (доступна везде) | g:mapleader
b: | локальная для буфера | b:current_syntax
w: | локальная для окна | w:my_flag
t: | локальная для вкладки (tab) | t:project
l: | локальная для функции | l:tmp
s: | локальная для скрипта (файла) | s:counter
v: | предопределённые переменные Vim | v:count, v:true
a: | аргумент функции | a:name
& | опция | &number, &l:wrap
@ | регистр | @a, @+, @/
$ | переменная окружения | $HOME
Код: Выделить всё
let g:my_setting = 1 " глобальная настройка плагина
let b:did_ftplugin = 1 " 'этот ftplugin уже отработал в буфере'
let &number = 1 " то же, что :set number
let @a = 'yyp' " положить текст в регистр a (макрос!)
echo $HOME " переменная окружения
Условия и циклы
Управляющие конструкции выглядят многословно, но читаются однозначно. Каждый блок закрывается своим end-словом:
Код: Выделить всё
if &filetype ==# 'python'
echo 'Это Python'
elseif &filetype ==# 'lua'
echo 'Это Lua'
else
echo 'Что-то ещё'
endif
Циклы - for и while:
Код: Выделить всё
for item in [10, 20, 30]
echo item
endfor
let i = 0
while i < 3
echo i
let i += 1
endwhile
Функция объявляется через function ... endfunction. Имя пользовательской функции должно начинаться с большой буквы (либо иметь scope-префикс вроде s:). Аргументы внутри тела доступны с префиксом a:.
Код: Выделить всё
function! Greet(name)
return 'Привет, ' . a:name
endfunction
echo Greet('Vim') " => Привет, Vim
:execute - выполнить собранную строку
:execute берёт строку и выполняет её как Ex-команду. Это нужно, когда команду нельзя записать статически - например, имя файла или число берётся из переменной:
Код: Выделить всё
let n = 10
execute 'normal! ' . n . 'j' " прыгнуть на 10 строк вниз
execute 'edit ' . expand('~/.vimrc')
let pattern = 'TODO'
execute 'vimgrep /' . pattern . '/ %'
Autocommands и augroup
Autocommand - это "выполни команду, когда произойдёт событие". События - это BufWritePre (перед сохранением), BufReadPost (после открытия), FileType (определён тип файла), TextYankPost (после копирования) и десятки других (полный список - :help autocmd-events).
Код: Выделить всё
autocmd BufWritePre *.py call Format()
autocmd FileType markdown setlocal wrap
Код: Выделить всё
augroup MyFormatting
autocmd!
autocmd BufWritePre *.py call Format()
autocmd FileType markdown setlocal wrap
augroup END
Vim9script: что это и когда
Vim 9.0 принёс Vim9script - переработанный диалект, который компилируется в инструкции и работает в 10-100 раз быстрее классического Vimscript. Синтаксис приближен к JS/TS: var вместо let, комментарии через # вместо ", привычные (), классы и объекты (Vim 9.0+), команда :defer для гарантированной очистки ресурсов при выходе из функции.
Код: Выделить всё
vim9script
var name = 'Vim9'
def Greet(who: string): string
return $'Привет, {who}'
enddef
echo Greet(name)
Часть 2. Lua в Neovim
Lua - маленький, быстрый язык со встроенным в Neovim интерпретатором (LuaJIT). Если вы знаете любой С-подобный язык, синтаксис освоится за час. Базовое отличие от Vimscript: одна точка входа init.lua, нормальные таблицы ({}) как единственная структура данных, и набор пространств имён vim.* для управления редактором.
Переменные в Lua
Код: Выделить всё
local count = 5 -- локальная (почти всегда нужна local)
local name = "neovim"
local list = { 1, 2, 3 }
local dict = { key = "value" }
GLOBAL = 1 -- без local - глобальная (избегайте)
Код: Выделить всё
if vim.bo.filetype == "python" then
print("Это Python")
elseif vim.bo.filetype == "lua" then
print("Это Lua")
else
print("Что-то ещё")
end
for _, item in ipairs({ 10, 20, 30 }) do
print(item)
end
local function greet(who)
return "Привет, " .. who
end
Это сердце всего. Вместо scope-префиксов Vimscript у Lua - отдельные таблицы под каждую задачу:
Код: Выделить всё
Пространство | Что это | Аналог в Vimscript
-----------------------------------------+--------------------------------------+--------------------
vim.opt | опции редактора | :set / &
vim.o / vim.bo / vim.wo | опции (глобальные / буфер / окно) | &, &l:
vim.g | глобальные переменные | g:
vim.b / vim.w / vim.t | переменные буфера / окна / вкладки | b: / w: / t:
vim.fn | вызов любой функции Vimscript | call func()
vim.api | низкоуровневый API Neovim (nvim_*) | - (только Neovim)
vim.keymap | маппинги | :map / :noremap
vim.cmd | выполнить Ex-команду / Vimscript | :execute
vim.diagnostic, vim.lsp, vim.treesitter | подсистемы | - (только Neovim)
Код: Выделить всё
vim.opt.number = true -- :set number
vim.opt.relativenumber = true
vim.opt.shiftwidth = 2
vim.opt.clipboard = "unnamedplus"
vim.g.mapleader = " " -- глобальная переменная (leader)
vim.bo.filetype -- filetype текущего буфера (read)
vim.wo.wrap = false -- wrap для текущего окна
Код: Выделить всё
vim.opt.shortmess:append("c")
vim.opt.wildignore:append({ "*.o", "*.pyc" })
vim.fn - мост в Vimscript
Не всё в Neovim переписано на Lua-API; огромный пласт встроенных функций живёт в Vimscript. vim.fn вызывает любую из них:
Код: Выделить всё
local home = vim.fn.expand("~")
local line = vim.fn.line(".") -- номер текущей строки
local files = vim.fn.glob("*.lua", false, true) -- список файлов
local choice = vim.fn.input("Имя: ")
vim.api - низкоуровневый API
vim.api - это семейство функций nvim_*: прямой доступ к буферам, окнам, автокомандам, подсветке. Оно мощнее и быстрее vim.fn, но многословнее. Примеры:
Код: Выделить всё
local bufnr = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) -- все строки буфера
vim.api.nvim_buf_set_lines(0, 0, 0, false, { "новая первая строка" })
local win = vim.api.nvim_get_current_win()
vim.keymap.set
Маппинги подробно разобраны в главе 21, поэтому здесь только напомним форму - она пригодится в наших примерах:
Код: Выделить всё
vim.keymap.set("n", "<leader>w", function()
vim.cmd.write()
end, { desc = "Сохранить файл" })
vim.cmd - выполнить Ex-команду из Lua
Когда нужного Lua-API нет, либо проще написать классическую команду, используйте vim.cmd:
Код: Выделить всё
vim.cmd("colorscheme habamax")
vim.cmd.colorscheme("habamax") -- то же через таблично-функциональную форму
vim.cmd("normal! gg")
vim.cmd([[
highlight Comment gui=italic
syntax on
]])
Тонкость: vim.cmd - это одновременно и функция, и таблица. Можно вызвать строкой - vim.cmd("colorscheme habamax"), а можно через табличо-функциональный сахар - vim.cmd.colorscheme("habamax"), vim.cmd.edit(f), vim.cmd.write(). Второй вид удобнее и безопаснее (аргументы не нужно склеивать вручную в строку), и дальше в примерах он встречается именно в такой форме.
Autocommands на Lua
Нативный способ Neovim - vim.api.nvim_create_autocmd + vim.api.nvim_create_augroup. Это прямой аналог augroup/autocmd из Vimscript, но колбэком может быть Lua-функция:
Код: Выделить всё
local grp = vim.api.nvim_create_augroup("MyConfig", { clear = true })
-- Подсветить скопированный текст
vim.api.nvim_create_autocmd("TextYankPost", {
group = grp,
callback = function()
vim.hl.on_yank({ timeout = 200 }) -- начиная с Neovim 0.11; vim.highlight - устаревший алиас
end,
})
-- Включить wrap только для markdown
vim.api.nvim_create_autocmd("FileType", {
group = grp,
pattern = "markdown",
callback = function()
vim.wo.wrap = true
end,
})
-- Удалить хвостовые пробелы перед сохранением
vim.api.nvim_create_autocmd("BufWritePre", {
group = grp,
pattern = "*",
callback = function()
local view = vim.fn.winsaveview()
vim.cmd([[silent! %s/\s\+$//e]])
vim.fn.winrestview(view)
end,
})
Сравнение "одно и то же на двух языках"
Код: Выделить всё
Задача | Vimscript | Lua (Neovim)
-----------------------+-----------------------------+-----------------------------------------------------------------------------------------
Опция | set number | vim.opt.number = true
Глобальная переменная | let g:x = 1 | vim.g.x = 1
Вызвать функцию | echo expand('~') | vim.fn.expand("~")
Маппинг | nnoremap <leader>w :w<CR> | vim.keymap.set("n", "<leader>w", ":w<CR>") (для бесшумности добавьте { silent = true })
Выполнить команду | execute 'edit ' . f | vim.cmd.edit(f)
Группа автокоманд | augroup X / autocmd! | nvim_create_augroup("X", {clear=true})
Автокоманда | autocmd BufWritePre * ... | nvim_create_autocmd("BufWritePre", {...})
Пример 1. Тоггл подсветки результатов поиска. Хотим по <leader>h переключать hlsearch. Голым маппингом это не сделать - нужна логика.
Код: Выделить всё
-- было: hlsearch включён, мешает после поиска
vim.keymap.set("n", "<leader>h", function()
vim.opt.hlsearch = not vim.opt.hlsearch:get()
end, { desc = "Тоггл подсветки поиска" })
-- стало: одна клавиша включает/выключает подсветку
Пример 2. Восстановление позиции курсора при открытии файла. Классический рецепт: при BufReadPost прыгнуть туда, где вы были в прошлый раз (метка ").
Код: Выделить всё
vim.api.nvim_create_autocmd("BufReadPost", {
group = vim.api.nvim_create_augroup("RestoreCursor", { clear = true }),
callback = function(args)
local mark = vim.api.nvim_buf_get_mark(args.buf, '"')
local lcount = vim.api.nvim_buf_line_count(args.buf)
if mark[1] > 0 and mark[1] <= lcount then
pcall(vim.api.nvim_win_set_cursor, 0, mark)
end
end,
})
-- стало: открыли файл - курсор сразу на месте последней правки
Код: Выделить всё
" Vimscript
command! Config execute 'edit ' . stdpath('config') . '/init.vim'
Код: Выделить всё
-- Lua-эквивалент
vim.api.nvim_create_user_command("Config", function()
vim.cmd.edit(vim.fn.stdpath("config") .. "/init.lua")
end, { desc = "Открыть конфиг" })
-- стало: :Config открывает init.lua откуда угодно
Код: Выделить всё
vim.keymap.set("n", "<leader>j", function()
vim.cmd("normal! " .. vim.v.count1 .. "j")
end, { desc = "Прыжок на N строк" })
-- 3<leader>j -> вниз на 3, просто <leader>j -> вниз на 1 (а не на 0!)
- Пишите новые конфиги и плагины на Lua. Это стандарт экосистемы: быстрее, чище масштабируется, имеет полноценный доступ к нативным подсистемам Neovim (LSP, Treesitter, diagnostics), которых в Vimscript-мире просто нет. Vimscript держите для чтения legacy и работы в чистом Vim.
- Любую автокоманду - в именованный augroup с clear = true. Это снимает целый класс багов с дублированием (format-on-save N раз). Правило одинаково обязательно и в Vimscript (autocmd!), и в Lua.
- Минимизируйте vim.cmd и vim.fn, предпочитая нативный API. vim.cmd([[...]]) - отличный мост для миграции, но не цель. Где есть vim.opt, vim.keymap, vim.api.nvim_* - берите их: они типобезопаснее и не зависят от парсинга строк.
- В чистом Vim новые скрипты - на Vim9script. var, #-комментарии, :defer, классы дают прирост скорости в 10-100 раз и читаемость уровня TS. Но помните: Neovim его не поддерживает - это разные миры.
- local по умолчанию в Lua, scope-префикс по умолчанию в Vimscript. Глобальные переменные без необходимости - источник конфликтов в обоих языках.
- Оборачивайте рискованные вызовы в pcall. В Lua pcall(fn, args) ловит ошибку и не роняет конфиг целиком - особенно важно в автокомандах и при работе с буферами, которых может уже не быть.
- Используйте ==#/==? в Vimscript всегда. Не полагайтесь на ignorecase пользователя при сравнении строк в скриптах.
- Голый autocmd без группы. При повторной загрузке конфига автокоманды накапливаются. Лечится augroup+autocmd! (Vimscript) или augroup с clear = true (Lua).
- v:count вместо v:count1 в маппингах. Без счётчика получаете 0 - действие схлопывается в ничто. Для "по умолчанию один раз" нужен v:count1 / vim.v.count1.
- Сравнение строк через == в Vimscript. Результат зависит от ignorecase и "плавает" у разных пользователей. Пишите ==#.
- Забытый local в Lua. Переменная утекает в глобальный рантайм и конфликтует с плагинами. Почти всегда нужен local.
- Путаница vim.opt и vim.o. Для опций-списков (wildignore, shortmess) нужен vim.opt с :append()/:remove(); голый vim.o со списком не дружит.
- Маппинг через :normal вместо :normal!. В скрипте пользовательские маппинги могут переопределить нужные клавиши; версия с ! их игнорирует.
- Ожидание, что Vim9script заработает в Neovim. Не заработает - Neovim его не реализует. И наоборот: Lua-API vim.* не существует в чистом Vim.
- function без ! при перезагрузке. Повторное определение упадёт с E122; пишите function!.
- Tоггл-маппинг. Напишите на Lua маппинг <leader>n, который переключает relativenumber. Используйте vim.opt.relativenumber:get() и присваивание not ....
- Format-on-save. Создайте augroup с clear = true и автокоманду BufWritePre, которая удаляет хвостовые пробелы только в файлах *.lua и *.py (используйте pattern). Перезагрузите конфиг дважды и убедитесь, что чистка происходит один раз, а не дважды.
- Перевод с Vimscript на Lua. Дано: autocmd FileType gitcommit setlocal spell textwidth=72. Перепишите это автокомандой на Lua через nvim_create_autocmd с колбэком, выставляющим vim.wo.spell и vim.bo.textwidth.
- Пользовательская команда. Сделайте команду :Reload, которая перечитывает ваш init.lua (подсказка: vim.cmd.source(vim.fn.stdpath("config") .. "/init.lua")). Подумайте, какие части конфига от такого перечитывания могут задублироваться и почему clear = true это спасает.
- Функция с условием. Напишите Lua-функцию smart_home(), которая по нажатию 0 прыгает на первый непробельный символ строки, а при повторном нажатии - в самое начало (колонка 0). Привяжите к 0 в normal-режиме.
- Vimscript-разминка. В чистом Vim напишите функцию Wc(), которая через :execute и wordcount() выводит число слов в текущем буфере, и привяжите к <leader>wc. Сравните с тем, как то же делается на Lua через vim.fn.wordcount().
- Скриптинг нужен там, где декларативных настроек мало: события (autocommands), логика в маппингах, динамические команды.
- Vimscript - родной язык Vim: переменные с scope-префиксами (g:/b:/w:/v:/&/@), функции (function!), условия/циклы, :execute для динамики, augroup+autocmd! против дублей. Для нового кода в чистом Vim 2026 - Vim9script (быстрее в 10-100 раз, синтаксис как TS).
- Lua - основной язык Neovim: local-переменные, таблицы, и пространства vim.* - vim.opt (опции), vim.g (глобалы), vim.fn (мост в Vimscript), vim.api (нативный API), vim.keymap (клавиши), vim.cmd (Ex-команды). Автокоманды - через nvim_create_autocmd + nvim_create_augroup({clear=true}).
- Установка 2026: Lua-first для Neovim, Vimscript/Vim9script - для чистого Vim и чтения legacy. Эти миры не пересекаются: Vim9script не работает в Neovim, vim.* не существует в Vim.
- Везде: автокоманды в группах с очисткой, local/scope-префиксы по умолчанию, v:count1 в маппингах, pcall вокруг рискованного.