Продвинутый Vim: регистры, макросы, текстовые объекты, буферы, окна, quickfix
В первой статье трека мы разобрали модальность и первые команды. На этом уровне Vim выглядит как редактор со странным способом ввода: вместо Ctrl+клавиша — буквы, вместо мышки — hjkl. Скорость почти не выросла, зато выросло раздражение. Это нормально: базовый Vim действительно не даёт выигрыша.
Выигрыш начинается там, где вы перестаёте помнить команды и начинаете составлять их. Vim — не набор из трёхсот шорткатов, а маленький язык с грамматикой, регистровой памятью, средством записи программ (макросы) и структурой рабочего пространства (буферы, окна, списки). Триста шорткатов невозможно выучить; грамматику из пятнадцати правил — можно за неделю, а комбинаций она порождает тысячи.
Эта статья — про переход от «знаю команды» к «умею собирать операции». Всё, что здесь есть, работает в ванильном Vim 8+ без единого плагина; в Neovim — тоже, плюс к этому добавляются Lua и LSP.
Грамматика: оператор + область
Ключевая идея, которую vi изобрёл в 1976-м и которую с тех пор копируют все модальные редакторы: команда редактирования состоит из оператора (что делать) и области (над чем).
[счётчик] оператор [счётчик] область
Операторы: d (delete), y (yank), c (change), > / < (сдвиг отступа), gU / gu (регистр букв), g~ (инвертировать), = (переформатировать отступы), gq (переформатировать по ширине), ! (пропустить через внешнюю команду).
Область задаётся тремя способами: движением (w, }, $, f;, /foo<CR>), текстовым объектом (iw, ap, i{) или удвоением самого оператора (dd, yy, gUU — вся строка).
3"] --> B["оператор
d y c gU gq >"] B --> C{"чем задаём область?"} C -->|движением| D["w } $ f; /re CR"] C -->|текстовым объектом| E["iw ap i{ it a'] C -->|удвоением| F["dd yy cc — строка целиком"] D --> G["диапазон текста
+ характер: посимвольный,
построчный, блочный"] E --> G F --> G G --> H["операция выполнена"] H --> I["старое содержимое → регистр"] H --> J["изменение → повторяется точкой ."]
Практическое следствие: любой новый оператор автоматически работает со всеми областями, а любой новый текстовый объект — со всеми операторами. Плагин, добавляющий оператор комментирования (gc), сразу даёт вам gcap (закомментировать абзац), gcip, 3gcj, gc/TODO<CR>. Это и есть настоящая причина, по которой Vim остаётся конкурентоспособным: композиция вместо перечисления.
Текстовые объекты — то, ради чего стоит остаться
Движения работают «от курсора». Текстовые объекты работают «вокруг курсора» — и это принципиально удобнее, потому что избавляют от позиционирования. Не нужно доезжать до начала слова, чтобы его заменить: ciw работает из любой точки слова.
Префикс i — inner (внутренность без ограничителей), a — a/around (вместе с ограничителями и, для слов, с примыкающим пробелом).
| Объект | Что захватывает | Типичное применение |
|---|---|---|
iw / aw |
слово / слово с пробелом | ciw — переименовать идентификатор под курсором |
iW / aW |
«слово» из непробельных символов | ciW для some-thing.with.dots целиком |
is / as |
предложение | правка текста, коммит-сообщений |
ip / ap |
абзац (до пустой строки) | dap, gqip, >ip — блок кода между пустыми строками |
i" i' i` |
внутри кавычек | ci" — заменить строковый литерал |
i( i{ i[ (и ib, iB) |
внутри скобок | di{ — вычистить тело функции |
it / at |
содержимое XML/HTML-тега | cit — заменить текст внутри <td> |
i< / a< |
внутри угловых скобок | дженерики, атрибуты |
Пара, которую стоит вбить в мышечную память первой:
" курсор где угодно внутри строки-литерала
ci" " → стереть содержимое кавычек и войти в insert
" курсор где угодно внутри тела функции
di{ " → тело пусто, скобки целы
" курсор на аргументе
caw " → заменить аргумент вместе с пробелом-разделителем
" курсор внутри вызова
yi( " → скопировать список аргументов
Важная деталь, о которой редко пишут: для объектов-кавычек и скобок Vim сам ищет ближайший подходящий ограничитель на строке, даже если курсор перед ним. Поэтому ci" в начале строки const x = "hello"; всё равно попадёт внутрь кавычек. Для скобок так не работает: ci( требует, чтобы курсор был внутри пары или на скобке.
Расширить набор объектов стоит двумя вещами (обе — де-факто стандарт, обе крошечные):
vim-textobj-user— фреймворк для своих объектов;targets.vim— добавляетin(,il",i,(аргумент функции), поиск объекта вперёд по строке, работу с разделителями.
i, из targets.vim окупается мгновенно: ci, меняет один аргумент функции, da, удаляет его вместе с запятой. В ванильном Vim это делается вручную и каждый раз по-разному.
Оператор ! — забытая суперсила
!{область}{команда} пропускает диапазон через внешнюю программу и заменяет текст выводом. Это превращает любой юникс-инструмент в оператор Vim.
!ip sort -u " отсортировать абзац, убрав дубли
!G column -t " выровнять колонками от курсора до конца файла
:%!jq . " форматировать весь буфер как JSON
:'<,'>!python3 -c "import sys,base64;print(base64.b64decode(sys.stdin.read()).decode())"
Стоимость — ноль строк конфига. Замена — плагин на 500 строк, который делает то же самое хуже.
Регистры: где Vim хранит текст
Голая формулировка «Vim не использует системный буфер обмена» неверна и вредна. Правильная: у Vim свой набор из ~50 буферов обмена, и системный — лишь два из них.
Синтаксис один: "x перед оператором задаёт регистр. "ayiw — скопировать слово в регистр a, "ap — вставить оттуда. В insert-режиме и в командной строке — Ctrl-R x.
Главная боль и её лечение
Классический сценарий: скопировали строку (yy), нашли место, удалили строку-жертву (dd), вставили (p) — и вставилась удалённая строка. Потому что dd затёр безымянный регистр "", из которого читает p.
Три рабочих лечения, от простого к правильному:
" 1. Читать из yank-регистра, который delete не трогает
"0p
" 2. Удалять в чёрную дыру, когда содержимое не нужно
"_dd
" 3. Визуальная замена без потери регистра: P вместо p
" (в Vim 8.2+ визуальный P не забирает выделенное в "")
viwP
Третий вариант — самый частый в реальной работе: скопировали шаблон, выделяете куски и раскладываете P, регистр не портится. В более старых Vim выручает мэппинг:
" вставка в визуальном режиме, не затирающая регистр
xnoremap <expr> p 'pgv"' . v:register . 'y`>'
Кольцо удалений "1–"9
Каждое удаление размером от строки и больше (dd, dap, d/foo<CR> через строки) сдвигает кольцо: бывшее "1 становится "2 и так далее. Мелкие удаления в пределах строки идут в "-.
Отсюда идиома «перебор истории удалений»: "1p, затем u. — точка при повторе "1p автоматически инкрементирует номер регистра, поэтому u. u. u. листает "2, "3, "4. Это встроенное поведение, а не фокус.
Регистры для скриптов и командной строки
" вставить в командную строку слово под курсором — Ctrl-R Ctrl-W
:%s/<C-r><C-w>/newName/g
" вставить последний поиск в замену — Ctrl-R /
:%s/<C-r>//replacement/g
" вычислить выражение прямо в insert-режиме — Ctrl-R =
" набрали: 17*3<CR> → в текст попадёт 51
" @: повторяет последнюю ex-команду; @@ — ещё раз
Ctrl-R Ctrl-W экономит больше времени, чем половина плагинов: вы никогда не набираете длинное имя переменной руками.
Макросы: программа из ваших же нажатий
Макрос — это буквально текст в регистре, который Vim подаёт себе на вход как нажатия клавиш. Из этого следуют все его свойства.
qa " начать запись в регистр a
... " делаете правку одной строки, заканчивая переходом к следующей
q " остановить запись
@a " проиграть
@@ " проиграть последний макрос ещё раз
100@a " проиграть 100 раз; остановится на первой ошибке
Правила, которые превращают макрос из фокуса в инструмент:
- Начинайте с
0или^. Макрос должен быть независим от того, где стоит курсор. - Заканчивайте переходом к следующей единице работы (
j,n,}). Тогда100@aработает. - Опирайтесь на структуру, а не на количество.
f(вместо10l,ci"вместо4x. - Пусть макрос падает. Ошибка (
f(не нашёл скобку) прерывает выполнение — это защита, а не проблема. Ставьте заведомо большой счётчик и позволяйте макросу остановиться там, где данные кончились.
Макрос — это текст, значит его можно чинить
Записали макрос, он почти правильный. Вместо перезаписи с нуля:
:new " временный буфер
"ap " выгрузили макрос как текст
" правим руками; управляющие символы вставляются через Ctrl-V Esc и т.п.
0"ay$ " положили обратно в регистр a
Или из скрипта, без выгрузки:
" \e — это Esc в двойных кавычках Vim-скрипта
:let @a = "0f(ci(nil\<Esc>j"
Это же — способ положить полезные макросы в .vimrc навсегда.
Рекурсивный макрос
Если очистить регистр и в конце макроса вызвать его же, макрос будет повторяться до первой ошибки — не нужно угадывать счётчик.
qaq " очистить регистр a (запись пустого макроса)
qa " начать запись
0f,r;j " полезная работа + переход на строку ниже
@a " вызов самого себя — на момент записи регистр пуст, ошибки нет
q
@a " запуск: пройдёт по всем строкам и остановится, когда f, не найдёт запятую
Приём красивый, но в проде я его почти не использую: :g/шаблон/norm (ниже) делает то же самое понятнее и быстрее.
Макрос по нескольким файлам
:argdo normal @a " по всем файлам из списка аргументов
:bufdo normal! @a " по всем буферам; ! — игнорируя пользовательские мэппинги
:argdo normal @a | update " ...и сохранить изменённые
normal! с восклицательным знаком обязателен, если макрос должен вести себя предсказуемо: без него ваши же мэппинги переопределят команды внутри макроса.
Чем повторять: точка, макрос, :global, :cdo
одно и то же руками)) Точка повторяет последнее изменение ноль подготовки только одно изменение, не последовательность ломается о плагины без repeat.vim Макрос запись реальных нажатий живёт в регистре и правится как текст хорош для нерегулярных правок падает на первой же неоднородности ex-команды s с флагами g и c g по шаблону плюс norm весь буфер за один заход детерминированно и логируемо Списковые do bufdo windo tabdo argdo cdo cfdo ldo многофайловый рефакторинг обязательно с update и резервной веткой
| Инструмент | Область | Стоимость подготовки | Когда выбирать |
|---|---|---|---|
. (точка) |
одно изменение рядом | 0 | правка 2–5 мест, которые видно глазами |
Макрос @a |
буфер или список файлов | 5–20 секунд записи | 10–200 однотипных, но структурно капризных правок |
:s |
буфер / диапазон | секунда | простая замена по regex |
:g/pat/cmd |
буфер | секунда | «сделать X со всеми строками, где Y» |
:cfdo + quickfix |
весь проект | 10–30 секунд | переименование по репозиторию без LSP |
| LSP rename | весь проект, семантически | настройка окружения | когда язык и сервер это умеют — всегда предпочтительнее |
Отдельно про :global — команда, которую недооценивают чаще всех остальных:
:g/TODO/d " удалить все строки с TODO
:g!/^#/d " удалить всё, КРОМЕ комментариев (то же: :v/^#/d)
:g/^\s*$/d " схлопнуть пустые строки
:g/console\.log/norm! dd " удалить отладочные логи
:g/^func /norm! O// TODO: doc " вставить комментарий перед каждой функцией
:g/pattern/t$ " скопировать все совпавшие строки в конец файла
:g/pattern/m0 " перенести их в начало, в обратном порядке
:g/^/norm! A; " дописать ; в конец каждой строки
Схема :g/шаблон/norm! команды покрывает, по моему опыту, около 70 % задач, ради которых люди записывают макросы, и делает это устойчивее: она не зависит от позиции курсора и не падает посередине.
Буферы, окна, вкладки: правильная ментальная модель
Здесь ошибается почти каждый, кто пришёл из VS Code. Во вкладочных редакторах «вкладка = файл». В Vim это три независимых понятия.
- Буфер — файл, загруженный в память, вместе с undo-историей, метками и незаписанными изменениями. Буферов может быть 200.
- Окно — прямоугольник на экране, показывающий буфер. Два окна могут показывать один буфер (удобно: верх и низ длинного файла).
- Вкладка (tab page) — именованная раскладка окон, а не файл. Правильное применение: «вкладка для фичи», «вкладка для тестов» — а не «вкладка на каждый .go».
Жизненный цикл буфера и почему hidden обязателен
Без set hidden Vim отказывается уходить из буфера с несохранёнными правками — и вы либо сохраняете полуготовое, либо теряете. С set hidden буфер просто уходит в фон целиком. Это одна строка, которая меняет ощущение от редактора сильнее, чем десять плагинов.
Минимальный рабочий набор для навигации:
set hidden
" переключение
nnoremap <leader>b :ls<CR>:b<Space>
nnoremap <C-^> <C-^> " Ctrl-^ — прыжок в альтернативный буфер (последний)
nnoremap ]b :bnext<CR>
nnoremap [b :bprevious<CR>
" закрыть буфер, не разрушая раскладку окон:
" :bd закрывает окно вместе с буфером, что почти всегда не то, чего вы хотели
command! Bd bprevious | bdelete #
Ctrl-^ (он же Ctrl-6) — самая недооценённая клавиша Vim. Переключение «код ↔ тест» в одно нажатие, без списков и fuzzy-поиска.
Окна
:split / :vsplit " разделить; <C-w>s / <C-w>v
<C-w>hjkl " перейти в соседнее окно
<C-w>HJKL " переместить окно к краю (сменить раскладку)
<C-w>= " выровнять размеры
<C-w>_ / <C-w>| " развернуть по высоте / ширине
<C-w>o " оставить только текущее окно
<C-w>T " вынести текущее окно в новую вкладку
:vertical resize 100 " точный размер
Полезные настройки:
set splitbelow splitright " новые окна появляются там, где их ждёт глаз
set winminheight=0 " сжатые окна схлопываются в статусную строку
set equalalways " автовыравнивание при открытии/закрытии
:bufdo / :windo / :tabdo / :argdo
Все четыре выполняют команду в каждом элементе списка. Единственная тонкость, из-за которой они «не работают»: без set hidden :bufdo падает на первом же изменённом буфере, а без update изменения не сохраняются.
:bufdo %s/oldApi/newApi/ge | update
" ^^ флаг e — не ругаться, если совпадений нет; без него bufdo прервётся
argdo работает по списку аргументов (:args src/**/*.go), и это часто лучше, чем bufdo: список аргументов вы задаёте явно, а буферов могло случайно накопиться.
Quickfix: конвейер для многофайловых правок
Quickfix — общий механизм «списка позиций в файлах»: результаты компиляции, grep, линтера, LSP-диагностики. Всё, что умеет выдавать файл:строка:колонка: сообщение, встраивается в него через errorformat.
Настройка, после которой :grep перестаёт быть медленным анахронизмом:
" использовать ripgrep вместо системного grep
if executable('rg')
set grepprg=rg\ --vimgrep\ --smart-case\ --hidden\ --glob\ '!.git'
set grepformat=%f:%l:%c:%m
endif
" не открывать первый результат автоматически (Vim-only quirk)
cnoreabbrev <expr> grep (getcmdtype() ==# ':' && getcmdline() ==# 'grep') ? 'silent grep' : 'grep'
" quickfix открывается сам после любой команды, которая его наполняет
augroup qf_auto
autocmd!
autocmd QuickFixCmdPost [^l]* cwindow
autocmd QuickFixCmdPost l* lwindow
augroup END
Навигация:
:copen / :cclose " окно списка
:cnext / :cprev " следующая/предыдущая позиция (мэппинги ]q / [q)
:cfirst / :clast
:cnewer / :colder " Vim хранит 10 последних quickfix-списков — можно вернуться
:cdo {cmd} " команда на каждой ЗАПИСИ списка
:cfdo {cmd} " команда на каждом ФАЙЛЕ списка (обычно нужен именно он)
Разница :cdo и :cfdo — источник тихих багов. Если в файле пять совпадений, :cdo %s/a/b/g выполнит замену по всему файлу пять раз (второй–пятый раз — впустую или, при неудачном regex, разрушительно). Для %s всегда берите :cfdo.
Location list (:lgrep, :lopen, :lnext) — то же самое, но привязанное к конкретному окну. Практическое правило: quickfix — глобальные вещи (сборка, поиск по проекту), location list — локальные (диагностика текущего файла). Тогда линтер не затирает результаты grep.
Мэппинги, которые стоит завести сразу (в духе vim-unimpaired):
nnoremap ]q :cnext<CR>zz
nnoremap [q :cprevious<CR>zz
nnoremap ]l :lnext<CR>zz
nnoremap [l :lprevious<CR>zz
nnoremap <leader>q :call ToggleQF()<CR>
function! ToggleQF() abort
" если окно quickfix уже открыто — закрыть, иначе открыть
if empty(filter(getwininfo(), 'v:val.quickfix && !v:val.loclist'))
copen
else
cclose
endif
endfunction
Реальный сценарий рефакторинга без LSP
:grep -w 'GetUserById' --type go
:copen " глазами проверяем список — это важно
:cfdo %s/\<GetUserById\>/GetUserByID/ge
:cfdo update
Полминуты на переименование по всему репозиторию. Ограничение честное: это текстовая замена, она не различает одноимённые методы разных типов и не тронет вызовы через рефлексию. Если для языка есть LSP-сервер с textDocument/rename — берите его; текстовый способ хорош там, где сервера нет, файлы не парсятся или правка не семантическая (миграция строк логов, конфигов, шаблонов).
Метки, прыжки и undo-дерево
ma " поставить метку a в текущем буфере (a–z локальны, A–Z глобальны)
`a " прыжок в точную позицию метки
'a " прыжок на строку метки
`` " туда, откуда прыгнули последним прыжком
`. " место последнего изменения
`^ " место последнего выхода из insert
'" " позиция при прошлом закрытии файла
:marks " список
Списки прыжков и изменений — разные вещи, и обе полезны:
Ctrl-O/Ctrl-I— назад/вперёд по jumplist (прыжки: поиск,G,gg,{, теги);g;/g,— назад/вперёд по changelist (позиции ваших изменений).
g; возвращает туда, где вы только что правили код, после того как сходили посмотреть определение. Проще любого плагина «недавние позиции».
Undo в Vim — не стек, а дерево. Если вы отменили правку и написали что-то новое, старая ветка не потеряна:
g- / g+ " перемещение по времени, а не по ветке
:earlier 10m " состояние буфера 10 минут назад
:later 1f " на одно сохранение вперёд
:undolist " ветки
И обязательная настройка — постоянный undo между сессиями:
if has('persistent_undo')
set undofile
set undodir=~/.vim/undo// " два слэша — хранить полный путь в имени файла
silent! call mkdir(expand('~/.vim/undo'), 'p', 0700)
endif
Двойной слэш обязателен: без него main.go из двух проектов делят один undo-файл. mkdir тоже обязателен — если каталога нет, Vim молча не пишет undo, и вы обнаружите это в худший момент. Для просмотра дерева удобен undotree.
Сессии и складки
:mksession! ~/.vim/sessions/project.vim " раскладка окон, буферы, cwd
vim -S ~/.vim/sessions/project.vim
set sessionoptions-=options " не тащить настройки — иначе сессия «замораживает» конфиг
Автоматизация сессий — vim-obsession: :Obsess один раз, дальше сессия пишется сама при каждом изменении раскладки.
Складки. Ванильный foldmethod=syntax тормозит на больших файлах, indent — грубоват, но быстр и предсказуем:
set foldmethod=indent
set foldlevelstart=99 " файлы открываются РАЗВЁРНУТЫМИ
set nofoldenable " альтернатива: складки выключены до za
foldlevelstart=99 — важная строка. Ничто так не отпугивает от Vim, как файл, открывшийся одной свёрнутой строкой. В Neovim складки лучше строить на Treesitter — об этом в следующей статье.
Разбор реальной конфигурации: the-homeless-god/Dotfiles
Полезнее любого списка «топ-10 плагинов» — прочитать чужой рабочий конфиг и понять, какие решения там осознанные, а какие накопились. Возьмём публичный репозиторий github.com/the-homeless-god/Dotfiles. Структура верхнего уровня:
Dotfiles/
├── configs/ # сами дотфайлы
│ ├── .vimrc # ~700 строк vimscript
│ ├── .zshrc .gitconfig .editorconfig .alacritty.toml .lfrc
│ ├── .config/ # конфиги XDG-приложений
│ ├── tmux.sh # настройка tmux скриптом
│ └── tools.json # декларативный список инструментов для установки
├── scripts/ # install-tools.sh и утилиты
├── ci/ .github/workflows/ # сборка и публикация Docker-образа
├── Dockerfile docker-compose.yml Makefile
└── docs/ demo/
Что здесь сделано правильно и стоит копировать:
- Декларативный
tools.json+ скрипт установки. Окружение описано данными, а не последовательностьюbrew installв истории команд. Разворачивается на новой машине одной командой. - Docker-образ окружения в CI. Конфиг проверяется на сборке, а не «у меня работает». Это редкость среди дотфайлов и это правильно — тема терминального workflow.
- Единая точка входа для клавиш. Подключён
vim-which-key, а в шапке.vimrc— человекочитаемая таблица всех мэппингов комментарием. Через полгода это и есть документация. - Разделение на функции инициализации —
s:initVimVariables(),s:initVimStartup(),s:initVimHotkeys(). Vimscript этого не требует, но структура спасает файл на 700 строк от превращения в свалку.
Что там стоит разобрать критически — не чтобы поругать автора (конфиг живой и рабочий), а потому что это ровно те грабли, на которые наступают все:
| Наблюдение в конфиге | Почему это проблема | Как чинить |
|---|---|---|
set paste в общих настройках |
Глобально отключает все inoremap, аббревиатуры, автоотступ и coc#pum#confirm(). В файле десятки insert-мэппингов, которые из-за этого не работают |
Убрать. Для вставки использовать "+p или set pastetoggle=<F3>; в Vim 8.2+/Neovim с bracketed paste это вообще не нужно |
set hidden в начале и set nohidden + set bufhidden=wipe в конце |
Последнее выигрывает: буферы уничтожаются при уходе, gb/:bnext теряют undo-историю, :bufdo ломается |
Оставить только set hidden |
Четыре системы автодополнения сразу: YouCompleteMe, coc.nvim, ALE (ale_completion_enabled = 1), asyncomplete |
Конкурируют за <Tab>, completeopt и omnifunc; YCM ещё и требует компиляции. Старт замедляется на секунды |
Оставить одну (обычно coc.nvim в Vim, встроенный LSP в Neovim), остальные удалить |
<C-h> мапится дважды: сначала <C-w>h, позже :%s///g<Left><Left><Left> |
Побеждает последний. Навигация по окнам молча ломается | Диагностика: :verbose nmap <C-h> покажет файл и строку конфликта |
<C-f> тоже дважды (:Rg и /) |
То же самое | Один раз пройтись :verbose map по всем горячим клавишам |
set termwinsize = "10*0" |
Пробелы вокруг = в :set — синтаксическая ошибка; правильно set termwinsize=10x0 |
Проверять конфиг через vim -u ~/.vimrc -c 'messages' -c q |
Три подряд set cursorlineopt=... (number, затем screenline, затем both) |
Это не флаги, накапливающиеся по строкам, — каждая присваивает заново. Работает только последняя | Одна строка со всеми значениями через запятую |
set clipboard=unnamed, ниже set clipboard+=unnamedplus, ниже снова оба |
Дубли безвредны, но показывают, что файл рос слоями | Один блок настроек, один раз |
inoremap ( ()<Left> и т. п. вручную при подключённом coc-pairs |
Двойная вставка скобок и сломанный Ctrl-V ( |
Выбрать одно: плагин или ручные мэппинги |
Plug 'tpope/vim-fugitive' и Plug 'voldikss/vim-floaterm' объявлены по два раза |
vim-plug стерпит, но это симптом отсутствия ревизии | Раз в полгода — аудит списка плагинов |
Главный вывод из этого разбора не «конфиг плохой», а: конфиг — это код, и он гниёт так же. Полезная гигиена:
# сколько времени тратится на старт и на что именно
vim --startuptime /tmp/vim.log +q && sort -k2 -nr /tmp/vim.log | head -20
# найти конфликтующий мэппинг
vim -c 'verbose nmap <C-h>'
# проверить конфиг на ошибки, не открывая файлов
vim -u ~/.vimrc -c 'messages' -c 'qa!'
# запустить без конфига — быстрый ответ на «это плагин или сам Vim?»
vim -u NONE -N
Правило, которое экономит часы: любую строку в конфиге, о которой вы не можете сказать, зачем она — удалите. Если что-то сломается, вы за минуту поймёте, что именно; если нет — конфиг стал короче.
Производительность и большие файлы
Vim быстрый, пока не встречает три вещи: длинные строки, foldmethod=syntax и regex-подсветку на мегабайтных файлах.
" аварийный режим для больших файлов
augroup bigfile
autocmd!
autocmd BufReadPre * if getfsize(expand('<afile>')) > 10*1024*1024
\ | setlocal noswapfile noundofile nowrap syntax=off foldmethod=manual
\ | endif
augroup END
set lazyredraw " не перерисовывать во время макросов — заметно ускоряет 1000@a
set synmaxcol=300 " не подсвечивать сверхдлинные строки (минифицированный JS)
set regexpengine=0 " автовыбор движка; 1 = старый backtracking, 2 = NFA
set ttyfast " в Vim 8 включено по умолчанию, в старых — нет
set lazyredraw даёт реальный выигрыш именно на макросах: без него Vim перерисовывает экран на каждой итерации. На 5000 повторений это разница в порядок.
Отдельно про updatetime: значение по умолчанию 4000 мс, конфиги с LSP ставят 300. Это влияет не только на отзывчивость подсказок, но и на частоту записи swap-файла. 300 мс — разумный компромисс; 50 мс превращает диск в жертву.
Типичные ошибки
- Отключить стрелки и
Esc, чтобы «учиться правильно». Идея популярная и вредная: боль вы получите сразу, навык — нет. Учиться нужно текстовым объектам и., а не запрету стрелок. - Ставить 40 плагинов на второй день. Каждый плагин — это чужие мэппинги, чужой
<Tab>и +50 мс к старту. Правило: плагин ставится, когда вы можете сформулировать, какую конкретную операцию вы делаете руками уже третий раз. nmapвместоnnoremap.nmapрекурсивен: если правая часть содержит клавишу, которую кто-то переопределил, вы получите бесконечный цикл или тихо неверное поведение. Всегдаnnoremap/inoremap/xnoremap, кроме случая, когда правая часть —<Plug>(...), тамnmapобязателен.set pasteв конфиге. См. разбор выше — это глобальный выключатель половины возможностей insert-режима.- Не включить
set hidden. Без него не работают ни:bufdo, ни нормальная многобуферная навигация. - Забыть
undodir. Persistent undo без существующего каталога молча не пишется. - Пользоваться
hjklвместо поиска.10jхуже, чем/имяФункции<CR>, и хуже, чем}, и хуже, чем]m. Vim побеждает не скоростью движения, а точностью прыжка. - Делать
:cdoтам, где нужен:cfdo. Замена выполнится столько раз, сколько совпадений в файле. - Оставлять
:sбез флагаeвнутриbufdo/cfdo. Первый же файл без совпадений прервёт весь проход. - Не делать коммит перед многофайловой заменой.
git diff— единственный нормальный способ проверить результат:cfdo. Незакоммиченное рабочее дерево делает откат невозможным.
Мини-конфиг: 40 строк, которые дают 80 % пользы
" ---------- поведение ----------
set nocompatible hidden confirm
set undofile undodir=~/.vim/undo//
silent! call mkdir(expand('~/.vim/undo'), 'p', 0700)
set noswapfile nobackup writebackup
set updatetime=300 timeoutlen=500 ttimeoutlen=10
" ---------- поиск ----------
set ignorecase smartcase incsearch hlsearch
nnoremap <Esc><Esc> :nohlsearch<CR>
if executable('rg')
set grepprg=rg\ --vimgrep\ --smart-case
set grepformat=%f:%l:%c:%m
endif
" ---------- вид ----------
set number relativenumber signcolumn=yes scrolloff=5 sidescrolloff=8
set splitbelow splitright
set foldmethod=indent foldlevelstart=99
set list listchars=tab:»\ ,trail:·,nbsp:␣
" ---------- отступы ----------
set expandtab tabstop=2 shiftwidth=2 softtabstop=2 shiftround smarttab autoindent
" ---------- клавиши ----------
let mapleader = ' '
nnoremap <leader>w :w<CR>
nnoremap <leader>b :ls<CR>:b<Space>
nnoremap ]q :cnext<CR>zz
nnoremap [q :cprevious<CR>zz
nnoremap ]b :bnext<CR>
nnoremap [b :bprevious<CR>
" центрировать после прыжков поиска — глазам сильно легче
nnoremap n nzzzv
nnoremap N Nzzzv
" перемещать выделенные строки
xnoremap J :m '>+1<CR>gv=gv
xnoremap K :m '<-2<CR>gv=gv
" не терять выделение при сдвиге
xnoremap < <gv
xnoremap > >gv
" вставка, не затирающая регистр
xnoremap <leader>p "_dP
" системный буфер — явно, а не через set clipboard
nnoremap <leader>y "+y
xnoremap <leader>y "+y
nnoremap <leader>Y "+Y
" ---------- подсветка скопированного ----------
augroup yank_hl
autocmd!
autocmd TextYankPost * silent! lua vim.highlight.on_yank({timeout=200})
augroup END
Последний блок работает только в Neovim; в Vim 8 его аналог — плагин vim-highlightedyank. Разница между set clipboard=unnamedplus и явным <leader>y: с глобальной настройкой каждое x и dd засоряет системный буфер обмена, и вы теряете скопированное из браузера. Явный префикс на порядок предсказуемее.
Честно: где Vim проигрывает
Без фанатизма. Продвинутый Vim хорош в:
- скорости и точности текстовых операций (объекты, макросы,
:g, quickfix); - работе по SSH и в контейнерах — везде, где нет GUI;
- воспроизводимости окружения: конфиг — текст под git;
- нулевом потреблении памяти на 20 открытых проектах.
И объективно проигрывает:
- семантическим рефакторингам — «извлечь метод», «изменить сигнатуру с обновлением всех вызовов», «переместить класс в другой пакет». JetBrains-инструменты делают это на полноценной модели проекта, LSP — лишь частично (подробнее в сравнении);
- отладке — DAP в Neovim работает, но по удобству это уровень 2015 года относительно современных IDE;
- первым двум неделям — за это время в VS Code вы уже продуктивны, а в Vim ещё нет. Это реальная стоимость, и её нужно закладывать честно;
- работе в команде на общей машине или в парном программировании — ваш конфиг никто, кроме вас, не понимает.
Разумная стратегия для многих: Vim-режим внутри другой IDE (IdeaVim, VSCodeVim) даёт 80 % грамматики без 100 % стоимости — но без quickfix, :g и настоящих буферов, то есть ровно без того, чему посвящена эта статья.
Мини-итог
- Vim — язык: оператор + область. Учите не команды, а способы задавать область.
- Текстовые объекты (
ciw,di{,ci",cit) убирают позиционирование курсора как задачу. - Регистров ~50;
""затирается всем подряд,"0— только yank’ом,"_— чёрная дыра,"+— система. - Макрос — текст в регистре: его можно выгрузить, отредактировать и положить в конфиг.
:g/шаблон/norm!покрывает большинство задач, ради которых записывают макросы, и делает это надёжнее.- Буфер, окно и вкладка — разные сущности.
set hiddenобязателен. - Quickfix +
:cfdo— многофайловый рефакторинг без LSP; всегда после коммита и с флагомeв:s. - Конфиг гниёт:
:verbose map,--startuptimeи удаление всего непонятного — регулярная гигиена.
Источники
- vimhelp.org — вся официальная документация в вебе. Ключевые разделы:
change.txt— регистры,motion.txt— текстовые объекты,quickfix.txt,repeat.txt— макросы и:normal,windows.txt— буферы и окна. - Drew Neil, «Practical Vim», 2nd ed. — лучшая книга именно про этот уровень: точка, регистры, макросы, quickfix.
- Drew Neil, Vimcasts — короткие разборы приёмов, актуальны до сих пор.
- Bram Moolenaar, «Seven habits of effective text editing» — почему быстрое редактирование строится на замечании повторов.
- vim-galore — самый полный открытый справочник по практике Vim.
- Steve Losh, «Learn Vimscript the Hard Way» — если хочется писать функции, а не только
set. - Vim Tips Wiki: Power of g — исчерпывающий каталог применений
:global. - github.com/the-homeless-god/Dotfiles — публичная конфигурация, разобранная выше.
Что дальше
Ванильный Vim упирается в потолок там, где нужны асинхронность, нормальный API для плагинов и семантика языка. Ровно эти три вещи решает Neovim: Lua вместо vimscript, встроенный LSP-клиент вместо coc.nvim, Treesitter вместо regex-подсветки. В следующей статье собираем современный конфиг с нуля и разбираем реальные dotfiles построчно.
Neovim и конфигурация: Lua, LSP, Treesitter, плагины, разбор реальных dotfiles