Эта глава - смысловой центр всего курса. Здесь вы перестанете воспринимать Vim как набор горячих клавиш и начнёте видеть в нём язык редактирования с собственной грамматикой. Поняв эту грамматику один раз, вы будете выводить нужные команды на лету - в том числе те, которые никогда специально не учили. Это тот самый момент, после которого Vim "щёлкает" и становится по-настоящему быстрым.
Главная идея: команда как предложение
В естественном языке мы строим осмысленные фразы из глаголов и существительных: "удали слово", "измени абзац", "скопируй до конца строки". Vim устроен ровно так же. Команда редактирования - это маленькое предложение:
Код: Выделить всё
команда = [count] оператор [count] движение
- оператор (operator) - это глагол: что сделать. Удалить, изменить, скопировать, сдвинуть отступ.
- движение (motion) - это объект действия: над чем сделать. До конца слова, до конца строки, до следующего абзаца.
- count - необязательный числовой множитель: сколько раз.
Почему это так важно? Потому что знание перестаёт складываться и начинает умножаться. Если вы знаете N движений и M операторов, вам доступно не N + M, а N x M команд. Выучили новое движение } (на абзац вперёд) - и вы автоматически умеете d} (удалить до конца абзаца), c} (изменить его), y} (скопировать), >} (увеличить отступ). Не нужно учить четыре новые команды: вы уже знаете операторы, осталось скомбинировать.
Именно поэтому опытные пользователи Vim не "вспоминают сочетания клавиш". Они конструируют команду из глагола и объекта прямо в момент редактирования - так же, как вы не вспоминаете заранее заученные предложения, а собираете их из слов на ходу.
Операторы: словарь глаголов
Оператор сам по себе ничего не делает - он "ждёт" движение, чтобы понять, к какому участку текста применяться. Нажав оператор, вы переводите Vim в особое состояние operator-pending (ожидание движения), где он буквально ждёт, что вы укажете объект.
Вот основные операторы Vim:
Код: Выделить всё
Оператор | Действие (глагол) | Мнемоника
----------+--------------------------------------+-----------------------------------------
d | удалить (delete) | вырезать текст в регистр
c | изменить (change) | удалить и сразу перейти в режим вставки
y | скопировать (yank) | положить в регистр, не удаляя
> | увеличить отступ | сдвиг строк вправо
< | уменьшить отступ | сдвиг строк влево
= | автоформатирование отступов | по правилам equalprg/indent
gu | в нижний регистр | gu = go lower
gU | в верхний регистр | gU = go Upper
g~ | переключить регистр | инвертировать case
gq | переформатировать текст | перенос по textwidth
gw | переформатировать, сохранив курсор | как gq, но курсор на месте
! | прогнать через внешнюю команду | фильтрация строк через shell
c - это d плюс вставка. Оператор change удаляет указанный участок и сразу оставляет вас в режиме вставки. Это позволяет менять текст в одно движение мысли: не "удали, потом печатай", а "измени вот это".
Операторы с префиксом g (gu, gU, g~, gq, gw) - это полноценные операторы, просто их клавиша состоит из двух символов. g здесь - не модификатор "на лету", а часть имени оператора. После gU Vim точно так же переходит в operator-pending и ждёт движение: gUw сделает заглавным слово вперёд.
gq и gw переформатируют текст по ширине textwidth - незаменимы для аккуратных абзацев в комментариях, Markdown, коммит-сообщениях. Разница в том, что gw возвращает курсор на исходную позицию, а gq оставляет его в конце переформатированного участка.
! - мост к Unix-философии: выделенные строки уходят на стандартный вход внешней программы, а её вывод заменяет их. Например, !}sort отсортирует абзац. А чтобы прогнать остаток файла через awk, нажмите движение !G (оператор ! + движение G до конца файла), а затем введите команду: awk '{print $2}'<CR>. Подробнее об интеграции с shell - в главе 31.
Движения: символьные и линейные, inclusive и exclusive
Движение указывает оператору границу. Но у движений есть два свойства, которые радикально влияют на результат, - и без их понимания вы будете регулярно удалять "на один символ больше или меньше, чем хотел".
Символьные (charwise) и линейные (linewise) движения
- Символьные движения работают с диапазоном символов. w, e, f, t, 0, $, %, }, { - символьные. dw удалит ровно от курсора до начала следующего слова, не трогая остальную строку.
- Линейные движения захватывают целые строки. j, k, G - настоящие linewise по своей природе. dj удалит две строки целиком (текущую и следующую), а не "вниз на символ".
Отдельный нюанс - } и { (на абзац вперёд/назад). Формально это exclusive charwise движения, а не linewise. Но на практике с оператором они почти всегда ведут себя построчно: срабатывает специальное правило exclusive-linewise - когда конец движения попадает в первый столбец, а старт был на/до первого непробельного символа, exclusive-движение повышается до linewise. Поэтому d} обычно удаляет целые строки абзаца, хотя } по своей сути символьное (подробности - :help exclusive-linewise).
Inclusive и exclusive движения
Внутри символьных движений есть второе деление - включает ли движение конечный символ:
- exclusive (исключающее): символ под конечной позицией не входит в диапазон. Таковы, например, w, 0. dw, стоя на начале слова hello world, удалит hello и оставит world - пробел вошёл, а w из world нет.
- inclusive (включающее): конечный символ входит в диапазон. Таковы e, f{char}, t после корректировки, %, а также $. de удалит до конца слова вместе с последней буквой. Именно потому, что $ inclusive, d$ удаляет до конца строки включая последний символ (см. :help $).
Код: Выделить всё
Было (курсор на 'h'): hello world
Нажимаем: dw -> world (удалено "hello " вместе с пробелом)
Было (курсор на 'h'): hello world
Нажимаем: de -> " world" (удалено "hello", пробел остался)
Особый случай: cw ведёт себя как ceНа практике гораздо удобнее и предсказуемее оперировать текстовыми объектами (diw, ci", dap), а не движениями: они не зависят от позиции курсора внутри объекта. Это тема следующей главы 06 - а пока важно понять сам механизм оператор+движение, на котором текстовые объекты потом "вырастут".
Есть одна классическая ловушка грамматики, о которой стоит знать заранее. Логично ожидать, что cw относится к dw так же, как c к d, - но это не совсем так. Если курсор стоит внутри слова (не на пробеле), cw ведёт себя как ce: изменяет до конца текущего слова, не захватывая идущий следом пробел. Аналогично cW работает как cE.
Сделано это намеренно: при изменении слова пробел после него почти никогда не нужно затирать, иначе слова склеились бы. Так что cw - единственный оператор, для которого w тихо превращается в e. У dw, yw, >w такого исключения нет - они захватывают пробел как обычное exclusive-движение.
Код: Выделить всё
Было (курсор на 'e' в hello): hello world
Нажимаем: dw -> lo world (dw от 'e': удалено "hel", пробел НЕ трогаем, потому что курсор в середине)
Нажимаем: cw (вместо 'hel') -> ведёт себя как ce: меняет до конца слова, пробел остаётся
Форсирование типа движения: v, V, <C-v> после оператора
Тип движения (charwise/linewise) можно переопределить на лету прямо в operator-pending, вставив между оператором и движением одну из клавиш v, V или <C-v> (см. :help o_v):
- v - форсировать charwise (и заодно инвертировать inclusive/exclusive у движения);
- V - форсировать linewise;
- <C-v> - форсировать blockwise (прямоугольный блок).
- dvj - удалить вертикально вниз, но charwise: снимет от курсора до того же столбца на следующей строке, а не две строки целиком (как сделал бы обычный dj).
- dV} - удалить абзац строго построчно, даже если правило exclusive-linewise по какой-то причине не сработало бы.
- yvj, cve и т. п. - та же логика для копирования и изменения.
Удвоение оператора: операция над строкой
Что произойдёт, если за оператором ввести... сам этот оператор? Vim трактует удвоенный оператор как команду над текущей строкой целиком (linewise):
Код: Выделить всё
Команда | Значение
-----------+------------------------------------------------------------------------------------------
dd | удалить текущую строку
cc | изменить текущую строку (очистить и войти в insert, сохранив отступ)
yy | скопировать текущую строку (в Vim синоним Y; в Neovim по умолчанию Y переназначен на y$)
>> | увеличить отступ текущей строки
<< | уменьшить отступ
== | выровнять отступ текущей строки
guu / gUU | строку в нижний / верхний регистр
g~~ | переключить регистр строки
gqq | переформатировать строку
Vim vs Neovim: ловушка с Y. В классическом Vim 9.1 Y - синоним yy (скопировать строку целиком). Но в Neovim начиная с 0.6 (2021) дефолтный маппинг изменён: Y переназначен на y$ - скопировать от курсора до конца строки. Это сделано для согласованности с C (c$) и D (d$). Если вы переносите мускульную память между редакторами, держите это в голове: yy копирует строку в обоих, а вот Y ведёт себя по-разному. Чтобы вернуть в Neovim старое поведение, добавьте vim.keymap.set('n', 'Y', 'yy').
Код: Выделить всё
Было (курсор где угодно на 2-й строке):
alpha
beta <- курсор здесь
gamma
delta
Нажимаем: 2dd ->
alpha
delta
count - числовой множитель, который вы вводите перед оператором, перед движением, или и там, и там. Его можно ставить в нескольких местах, и поведение строго определено.
Count перед движением просто повторяет движение: 3w - на три слова вперёд, 5j - на пять строк вниз.
Count перед оператором повторяет всю операцию: 3dd - удалить три строки.
Два count перемножаются. Если вы поставили count и перед оператором, и перед движением, Vim умножает их:
Код: Выделить всё
2d3w == d6w -> удалить 6 слов
3d2j == d6j -> удалить от текущей строки + 6 строк вниз (linewise)
Код: Выделить всё
Было (курсор на 'one'): one two three four five six seven
Нажимаем: 2d3w -> seven
(удалены 6 слов: one two three four five six)
Как из горстки кирпичей рождаются сотни команд
Сведём всё воедино. Возьмём матрицу: по одной оси операторы, по другой - движения и объекты. На пересечениях - готовые команды:
Код: Выделить всё
| w (слово) | $ (до конца строки) | } (абзац) | iw (внутр. слово) | i( (внутри скобок)
----+-------------+-----------------------+-------------+---------------------+--------------------
d | dw | d$ | d} | diw | di(
c | cw | c$ | c} | ciw | ci(
y | yw | y$ | y} | yiw | yi(
gU | gUw | gU$ | gU} | gUiw | gUi(
> | - | - | >} | - | -
Поэтому правильная стратегия обучения - не "запомнить, что gUiw делает слово заглавным", а понять: gU (верхний регистр) + iw (внутреннее слово). Тогда gUip (абзац в верхний регистр), guap (абзац в нижний) и десятки других команд вы соберёте сами, ни разу их предварительно не "изучив".
Практические примеры: было -> нажимаем -> стало
Код: Выделить всё
# Изменить слово целиком, начав с любого места слова - лучше через объект iw
Было: const userName = getValue(); (курсор на 'a' в userName)
Нажать: ciwuserId<Esc>
Стало: const userId = getValue();
Код: Выделить всё
# Скопировать тело функции до конца блока и вставить ниже
Было (курсор на первой строке тела):
return a + b;
// ...
Нажать: y} затем p -> дублирует абзац ниже
Код: Выделить всё
# Сделать заглавной всю строку-заголовок
Было: глава пятая
Нажать: gUU
Стало: ГЛАВА ПЯТАЯ
Код: Выделить всё
# Удалить три строки и сразу начать печатать замену
Было:
foo() <- курсор
bar()
baz()
qux()
Нажать: 3cc
Стало: (три строки удалены, вы в режиме вставки, отступ сохранён)
qux()
Код: Выделить всё
# Увеличить отступ абзаца кода
Нажать: >} -> сдвигает весь абзац вправо на один shiftwidth
Код: Выделить всё
# Поменять содержимое строки в кавычках
Было: title = "old value" (курсор где угодно на строке)
Нажать: ci"new value<Esc>
Стало: title = "new value"
Код: Выделить всё
# Отсортировать абзац через внешнюю команду
Было:
banana
apple
cherry
Нажать: !}sort<CR>
Стало:
apple
banana
cherry
Думайте "глагол + объект", а не сочетаниями клавиш. Единственное грамматическое правило Vim - оператор применяется к движению или текстовому объекту. Запомнив N движений и M операторов, вы получаете N x M команд; знание работает множителем, а не суммой. Учите паттерн (diw, ci(, gUap), а не таблицу готовых комбинаций.
Предпочитайте текстовые объекты движениям там, где важна повторяемость. Движения вроде dw, d$ зависят от позиции курсора внутри слова и плохо дружат с командой повтора . (точка). Текстовый объект diw, ci" работает одинаково из любой точки объекта, делая . предсказуемым. Это фундамент повторяемого редактирования - подробно в главах 06 (объекты) и 07 (dot-repeat).
Используйте count как множитель осознанно, а в своих маппингах берите v:count1, а не v:count. Два count перемножаются (3d2w == d6w). Когда вы пишете собственную функцию или маппинг, который должен учитывать счётчик, берите v:count1: при отсутствии счётчика он даёт 1 (разумный дефолт), тогда как v:count вернёт 0 и сломает поведение. Это типичная ошибка при написании Vimscript/Lua-маппингов (см. главу 22).
В Neovim делайте свои операторы dot-repeatable через operatorfunc + g@. Если вы создаёте собственный оператор-подобный маппинг, нативный путь без сторонних зависимостей - выставить operatorfunc на колбэк и вернуть из {expr=true}-маппинга строку g@. Тогда . будет повторять вашу операцию, а Neovim сам выставит метки '[ и '] на границы движения. Это убирает зависимость от классического плагина tpope/vim-repeat. Детали реализации - в главах 07 и 22.
Расширяйте грамматику плагинами, которые сами поддерживают . и count. Например, операции surround (добавить/удалить/заменить окружающие скобки и кавычки) от mini.surround или nvim-surround встраиваются в грамматику как новые "операторы". Современный mini.surround (организация nvim-mini) дополнительно поддерживает [count] и dot-repeat. Это естественное продолжение того же принципа композируемости. Подробнее об экосистеме - в главах 06 и 23.
Частые ошибки
Зубрить команды как отдельные сочетания. Самая дорогая ошибка. dw, de, d$, cc, yy - это не пять команд, которые надо заучить, а применения двух операторов к разным движениям. Пока вы воспринимаете их по отдельности, Vim кажется бесконечным списком клавиш. Как только увидите грамматику - список схлопнется в правило.
Путать символьные и линейные движения. Ожидать, что dj удалит "вниз на символ", - j линейное движение, поэтому dj удаляет две целые строки. Аналогично dG работает построчно. d} тоже обычно снимает целые строки, но по другой причине: } - exclusive charwise, повышаемое до linewise правилом exclusive-linewise (см. выше).
Не учитывать inclusive/exclusive. Удивляться, почему dw оставил последнюю букву слова на месте, а de - нет. w exclusive (не включает конечную позицию), e inclusive (включает символ под курсором). Это не баг, а определение движений.
Брать v:count вместо v:count1 в маппингах. При отсутствии счётчика v:count даёт 0, и операция выполняется ноль раз - то есть "ничего". Для повторяемого по умолчанию действия нужен v:count1.
Использовать движение там, где нужен повторяемый edit. dw повторённое точкой ведёт себя непредсказуемо из-за зависимости от позиции курсора. Если планируете повторять правку через ., берите текстовый объект (diw, di").
Забывать, что g-операторы - полноценные операторы. gU сам по себе ничего не делает и ждёт движение. Новички иногда жмут gU и недоумевают, почему "ничего не произошло" - Vim в operator-pending ждёт объект.
Ожидать, что cw симметричен dw. cw при курсоре внутри слова ведёт себя как ce (меняет до конца слова, не трогая пробел), тогда как dw пробел захватывает. Это не баг, а намеренное исключение. Чтобы не думать об этом - берите ciw.
Полагаться, что Y всюду равно yy. В Vim да, а в Neovim по умолчанию Y == y$. При переносе привычек между редакторами это источник тихих ошибок копирования.
Упражнения
- Матрица команд. Возьмите строку let result = compute(alpha, beta, gamma). Не подглядывая, выведите и выполните: удалить слово compute; изменить alpha на x; скопировать всё внутри скобок; сделать result заглавным. Для каждой задачи сначала проговорите "глагол + объект", потом нажмите.
- Inclusive vs exclusive. На строке foobar baz поставьте курсор на f и сравните результаты dw и de. Запишите, чем отличается итог и почему. Затем сравните df + пробел и dt + b.
- Линейные операции и count. В файле из 10 строк потренируйтесь: 3dd, 2yy затем p, 4>>, gUU. Убедитесь, что count и удвоение оператора работают так, как вы предсказываете до нажатия.
- Композиция count. На длинной строке из 8+ слов выполните 2d3w и убедитесь, что удалилось ровно 6 слов. Затем добейтесь того же результата другим способом (d6w) и сравните.
- Операторы с движением по абзацу. В коде с несколькими блоками примените >} (увеличить отступ абзаца), gq} (переформатировать абзац), =} (выровнять отступы). Понаблюдайте, как один и тот же объект } сочетается с разными операторами.
- Внешний фильтр. Создайте список из 5 неупорядоченных строк и отсортируйте абзац командой !}sort. Затем (на новых данных) пронумеруйте строки через !}nl или !}cat -n.
- Форсирование типа движения. На паре строк сравните dj и dvj: первое удалит две строки целиком, второе - charwise до того же столбца. Затем на строке foo (bar) поставьте курсор на f и сравните df) и dvf) - обратите внимание, что v инвертирует inclusive в exclusive (скобка останется). См. :help o_v.
- Ловушка cw. На строке hello world поставьте курсор на e в hello. Выполните dw (запомните результат), отмените (u), затем выполните cwX<Esc>. Убедитесь, что cw повёл себя как ce - изменил до конца слова, не тронув пробел. Повторите задачу через ciw и сравните удобство.
- Команда редактирования в Vim - это предложение: [count] оператор [count] движение. Оператор - глагол (что сделать), движение - объект (над чем).
- Основные операторы: d (удалить), c (изменить + insert), y (скопировать), >/< (отступ), = (выровнять), gu/gU/g~ (регистр), gq/gw (переформатировать), ! (внешний фильтр). g-операторы - полноценные операторы из двух символов.
- Движения бывают линейные (целые строки: j, k, G) и символьные; символьные делятся на exclusive (w, 0) и inclusive (e, f, $). Это определяет, попадает ли последний символ/строка в диапазон. Отдельно: }/{ - exclusive charwise, но с оператором обычно работают построчно по правилу exclusive-linewise.
- Удвоение оператора (dd, yy, >>, gUU) применяет его к текущей строке целиком. Внимание на различие: yy копирует строку и в Vim, и в Neovim, но Y - это yy только в Vim, а в Neovim по умолчанию Y == y$.
- Тип движения форсируется в operator-pending: v (charwise + инверсия inclusive/exclusive), V (linewise), <C-v> (blockwise). Например, dvj удалит charwise вниз, а не две строки. А cw - особый случай: внутри слова он работает как ce.
- Counts перемножаются: 2d3w == d6w. В своих маппингах берите v:count1, а не v:count.
- Сила Vim - в композируемости: N операторов x M объектов = N x M команд. Учите паттерн "глагол + объект", и сотни команд соберутся сами.
- Дальше эта грамматика раскроется через текстовые объекты (глава 06) и повторяемость через . (глава 07) - именно там операторы раскрывают полную мощь.