Редакторы и IDE Продвинутый Vim: регистры, макросы, текстовые объекты, буферы, окна, quickfix
0%

Продвинутый Vim: регистры, макросы, текстовые объекты, буферы, окна, quickfix

Продвинутый 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 — вся строка).

Практическое следствие: любой новый оператор автоматически работает со всеми областями, а любой новый текстовый объект — со всеми операторами. Плагин, добавляющий оператор комментирования (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 буферов обмена, и системный — лишь два из них.

Карта регистров Vim

Синтаксис один: "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 раз; остановится на первой ошибке

Правила, которые превращают макрос из фокуса в инструмент:

  1. Начинайте с 0 или ^. Макрос должен быть независим от того, где стоит курсор.
  2. Заканчивайте переходом к следующей единице работы (j, n, }). Тогда 100@a работает.
  3. Опирайтесь на структуру, а не на количество. f( вместо 10l, ci" вместо 4x.
  4. Пусть макрос падает. Ошибка (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

Инструмент Область Стоимость подготовки Когда выбирать
. (точка) одно изменение рядом 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 это три независимых понятия.

Буферы, окна и вкладки в 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/

Что здесь сделано правильно и стоит копировать:

  1. Декларативный tools.json + скрипт установки. Окружение описано данными, а не последовательностью brew install в истории команд. Разворачивается на новой машине одной командой.
  2. Docker-образ окружения в CI. Конфиг проверяется на сборке, а не «у меня работает». Это редкость среди дотфайлов и это правильно — тема терминального workflow.
  3. Единая точка входа для клавиш. Подключён vim-which-key, а в шапке .vimrc — человекочитаемая таблица всех мэппингов комментарием. Через полгода это и есть документация.
  4. Разделение на функции инициализации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 мс превращает диск в жертву.

Типичные ошибки

  1. Отключить стрелки и Esc, чтобы «учиться правильно». Идея популярная и вредная: боль вы получите сразу, навык — нет. Учиться нужно текстовым объектам и ., а не запрету стрелок.
  2. Ставить 40 плагинов на второй день. Каждый плагин — это чужие мэппинги, чужой <Tab> и +50 мс к старту. Правило: плагин ставится, когда вы можете сформулировать, какую конкретную операцию вы делаете руками уже третий раз.
  3. nmap вместо nnoremap. nmap рекурсивен: если правая часть содержит клавишу, которую кто-то переопределил, вы получите бесконечный цикл или тихо неверное поведение. Всегда nnoremap/inoremap/xnoremap, кроме случая, когда правая часть — <Plug>(...), там nmap обязателен.
  4. set paste в конфиге. См. разбор выше — это глобальный выключатель половины возможностей insert-режима.
  5. Не включить set hidden. Без него не работают ни :bufdo, ни нормальная многобуферная навигация.
  6. Забыть undodir. Persistent undo без существующего каталога молча не пишется.
  7. Пользоваться hjkl вместо поиска. 10j хуже, чем /имяФункции<CR>, и хуже, чем }, и хуже, чем ]m. Vim побеждает не скоростью движения, а точностью прыжка.
  8. Делать :cdo там, где нужен :cfdo. Замена выполнится столько раз, сколько совпадений в файле.
  9. Оставлять :s без флага e внутри bufdo/cfdo. Первый же файл без совпадений прервёт весь проход.
  10. Не делать коммит перед многофайловой заменой. 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 и удаление всего непонятного — регулярная гигиена.

Источники

Что дальше

Ванильный Vim упирается в потолок там, где нужны асинхронность, нормальный API для плагинов и семантика языка. Ровно эти три вещи решает Neovim: Lua вместо vimscript, встроенный LSP-клиент вместо coc.nvim, Treesitter вместо regex-подсветки. В следующей статье собираем современный конфиг с нуля и разбираем реальные dotfiles построчно.

Neovim и конфигурация: Lua, LSP, Treesitter, плагины, разбор реальных dotfiles

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов