Представьте: вам нужно переименовать переменную tmp в result в десяти местах файла. Новичок выделит мышью первое вхождение, наберёт новое слово, потом проскроллит ко второму, снова выделит, снова наберёт - и так десять раз. Опытный пользователь Vim сделает первую правку, а затем девять раз нажмёт одну-единственную клавишу: . (точка).
Команда . повторяет последнее изменение буфера. Это одна из тех вещей, которые отличают человека, "знающего хоткеи", от человека, который действительно думает на языке Vim. Точка превращает любую разовую правку в шаблон, готовый к применению где угодно. В сочетании с движениями поиска (n,
Эта глава - про то, как осознанно строить повторяемые правки. Мы разберём, почему текстовые объекты дружат с точкой лучше, чем движения; чем точка отличается от макросов и когда что выбирать; как точка взаимодействует с регистрами; и как заставить плагинные команды тоже подчиняться . через vim-repeat и нативный механизм operatorfunc.
Что считается "последним изменением"
Прежде чем нажимать ., нужно понимать, что именно она повторит. Точка повторяет последнее изменение текста в normal-режиме - единый "акт редактирования" от начала команды до возврата в normal-режим.
Ключевые правила:
- Команда вставки i + набранный текст + <Esc> - это одно изменение. . повторит "вставить ровно эти символы".
- cw + новое слово + <Esc> - одно изменение: "удалить слово и впечатать вот это".
- Чистые движения (w, j, /foo<CR>, n) не являются изменениями - точка их игнорирует и хранит предыдущую правку. Это и есть фундамент паттерна "двигайся + повторяй": перемещение поиском не сбивает то, что повторит ..
- Команды normal-режима, меняющие текст (например x, dd, p, J, ~, >>), - это изменения, и точкой повторяются.
- Команды ex-режима (:s, :g, :d) точкой не повторяются. Для повтора последней :substitute есть отдельная команда & (и g&), а не ..
Не путайте с insert-режимом. Внутри вставки есть "родственники" точки: <C-a> вставляет текст, набранный в предыдущей вставке, а <C-@> - вставляет его и сразу выходит в normal-режим. Это не dot-repeat: они работают только в insert-режиме, повторяют лишь последний вставленный текст (а не команду целиком, как .), и не учитывают счётчик. Точка же повторяет всё изменение - i/A/ciw вместе с движением входа и набранным текстом.
Почему текстовые объекты повторяются лучше движенийТочка не возвращает курсор сама. . повторяет правку там, где сейчас стоит курсор; навигация к следующей цели - на вас (через n, ;, j). Это сознательный контраст с :normal, который применяет команду к каждой строке диапазона автоматически (см. таблицу "точка против макросов"): для блока строк подряд точка требует ручного j . j ., тогда как :%normal или макрос по диапазону проходят всё сами.
Это центральная идея главы, и её стоит прочувствовать. Сравним два способа изменить слово.
Способ 1 - движение cw:
Код: Выделить всё
Курсор стоит в начале слова "tmp":
|tmp = compute()
Нажимаем: cw result <Esc>
result| = compute()
Код: Выделить всё
tm|p = compute()
Нажимаем: cw result <Esc>
tmresult| = compute() <- не то, что хотели!
Код: Выделить всё
tm|p = compute()
Нажимаем: ciw result <Esc>
result| = compute() <- правильно, откуда бы ни стоял курсор
Правило-вывод: для повторяемых правок предпочитайте текстовые объекты (ciw, ci", dap) движениям (cw, d$). Подробно про текстовые объекты и пару inner/around (i/a) - в главе 06; здесь нам важно лишь то, что они делают точку надёжной.
Паттерн "двигайся и повторяй"
Соберём всё вместе на каноническом примере: переименовать все вхождения tmp в result.
Код: Выделить всё
Исходный текст:
tmp = compute()
log(tmp)
return tmp + 1
1. Ставим курсор на первое "tmp".
2. Нажимаем * -> Vim ищет слово под курсором, прыгает
к следующему "tmp" и запоминает паттерн.
(можно сначала cgn - см. ниже; для классики идём через * и N)
3. Возвращаемся к первому: N
4. Делаем правку: ciw result <Esc>
5. Идём к следующему: n
6. Повторяем правку: .
7. Снова: n .
Тот же ритм работает для внутристрочной навигации с f/t и повтором ;:
Код: Выделить всё
Заменить все запятые на точку с запятой в строке:
a, b, c, d
1. f, прыгаем к первой запятой
2. r; заменяем на ';' (это изменение)
3. ; прыгаем к следующей запятой (повтор f,)
4. . повторяем замену
5. ; . ; . до конца строки
В Vim 7.4+ и Neovim есть команда gn - текстовый объект, который выделяет следующее совпадение последнего поискового паттерна (@/). Связка cgn идеально заточена под точку:
Код: Выделить всё
1. Установите паттерн: наведитесь на слово и нажмите *
(или :let @/='\<tmp\>')
2. Вернитесь: N (чтобы * не "съел" первое вхождение)
3. cgn result <Esc> меняем первое совпадение
4. . точка ОДНА переходит к следующему
совпадению И повторяет правку
5. . . . до конца файла; ненужное пропускаем через n
Таблица ключевых командcgn + . - не полный аналог :s//gc. gn ищет только вперёд от курсора и оборачивается через конец файла лишь при включённом 'wrapscan' (по умолчанию он включён, но в чужом конфиге может быть set nowrapscan - тогда совпадения до курсора вы пропустите). :%s//gc же гарантированно проходит весь файл сверху вниз. Поэтому при cgn начинайте с самого верха (или с первого вхождения через * N), а если важно покрыть всё независимо от позиции и 'wrapscan' - берите :%s//gc.
Код: Выделить всё
Команда | Что делает
-----------------+--------------------------------------------------------------------------------------------------------------------------------
. | Повторить последнее изменение
3. / 5. | Повторить с новым счётчиком (переопределяет исходный)
u | Отменить (часто идёт в паре с . - повторил не туда, откатил)
cw | Изменить от курсора до конца слова (движение - повторяется хуже)
ciw | Изменить слово целиком (объект - повторяется надёжно)
ci" / ci( / cit | Изменить внутри кавычек / скобок / тега
gn | Текстовый объект: следующее совпадение @/
cgn | Изменить следующее совпадение (точка переходит + повторяет)
dgn / ygn | Удалить / скопировать следующее совпадение
n / N | Следующее / предыдущее совпадение поиска (не изменение)
; / , | Повторить f/t/F/T вперёд / назад (не изменение)
* / # | Искать слово под курсором вперёд / назад
& | Повторить последнюю :s на текущей строке без флагов (:&& или g& сохраняют флаги)
g& | Повторить :s на всём файле с флагами; берёт последний поисковый паттерн (//), который может отличаться от паттерна исходной :s
Здесь живёт тонкая ловушка. Точка повторяет полную команду, включая использованный регистр.
Если вы сделали "ayiw (скопировать слово в регистр a), то . не повторит это - yiw не меняет текст. А вот вставка "ap - это изменение, и . повторит вставку из того же регистра a. Это удобно: содержимое регистра не "протухает" между повторами.
Опаснее другое взаимодействие - с безымянным регистром при замене. Классический сценарий:
Код: Выделить всё
1. Скопировали слово: yiw (слово ушло в регистр "" и в "0)
2. Перешли к цели и вставили поверх через визуальное выделение:
viw p
3. Результат: цель заменена... но безымянный регистр теперь
содержит ТО, ЧТО БЫЛО под выделением - не исходное слово!
4. Идём к следующей цели, viw p - вставляется не то.
Код: Выделить всё
1. yiw слово в "0
2. viw "0p вставить из "0 поверх выделения
3. Теперь "0 цел, можно повторять
Точка против макросов
Точка и макросы (глава 16) решают пересекающиеся, но разные задачи. Понимание границы экономит много времени.
Код: Выделить всё
| Точка . | Макрос @q
-------------------------+--------------------------------------+--------------------------------------------------------------------------
Что повторяет | Одно последнее изменение | Произвольную последовательность нажатий (движения + правки + ex-команды)
Сложность правки | Одна атомарная правка | Многошаговый сценарий
Включает движения? | Нет (двигаетесь вы сами через n/;) | Да (движения записаны внутрь)
Хранилище | Неявное, всегда одно | Именованный регистр (q, a...)
Применение к диапазону | Вручную, штрих за штрихом | :%normal @q, 5@q, рекурсия
Контроль на каждом шаге | Полный (видите каждое вхождение) | Меньше (макрос летит сам)
- Правка одна и простая (изменить слово, дописать ; в конце строки, обернуть в кавычки) -> точка. Двигайтесь поиском, тиражируйте ..
- Правка из нескольких разнородных шагов (зайти в строку, переставить два поля, удалить хвост, перейти на строку ниже) -> макрос. Запишите один раз и прогоните.
- Нужен полный визуальный контроль над каждым вхождением -> точка (или cgn + .), потому что вы решаете на каждом шаге, жать . или пропустить.
- Количество элементов известно или их много подряд -> макрос с :normal/счётчиком/рекурсией.
Делаем правку "точко-совместимой" заранее
Опытный пользователь продумывает первую правку так, чтобы её можно было повторить. Несколько приёмов:
Дописать символ в конец строки. Вместо $a;<Esc> (зависит от того, где курсор) используйте A;<Esc> - A всегда идёт в конец строки, поэтому . сработает на любой строке независимо от позиции курсора:
Код: Выделить всё
foo() <-
bar(x)
baz(1, 2)
A;<Esc> затем j. j.
foo();
bar(x);
baz(1, 2);
Обернуть слово. ciw"<C-r>""<Esc> тиражируется хуже, чем плагинная команда ysiw" из vim-surround/mini.surround, которая поддерживает dot-repeat "из коробки" (см. ниже).
Общий принцип: выбирайте курсор-независимые команды (A, I, ciw, ci", dap), а движение к цели выносите наружу - в n, ;, j. Тогда . остаётся чистой, переиспользуемой правкой.
Лучшие практики 2026
- Стройте редактирование по ритму "двигайся -> повторяй". Идеальный цикл: первое изменение текстовым объектом, затем n . / ; . / j .. Это и есть "one keystroke to move, one to repeat". Если ловите себя на ручном выделении каждой цели - остановитесь и поищите паттерн поиска.
- Предпочитайте текстовые объекты движениям ради предсказуемости точки. ciw, ci", dap курсор-независимы; cw, d$ - нет. Это прямая рекомендация из практик грамматики Vim: объекты дружат с dot-repeat, движения подводят.
- Для выборочной замены в файле берите cgn + . вместо :%s//gc. cgn даёт визуальный контроль над каждым вхождением и при этом одну . на правку - без флага подтверждения и без переключения в command-line. Установите паттерн через * или let @/, и точка сама перейдёт к следующему совпадению.
- Не давайте регистрам портить серию правок. При "вставить поверх" используйте "0p или маппинг xnoremap <leader>p "_dP, чтобы безымянный регистр не перезаписывался и . повторяла именно то, что нужно (детали - глава 08).
- На современном Neovim делайте свои маппинги dot-repeatable нативно, без vim-repeat. Механизм operatorfunc + g@ встроен в ядро: ваш {expr=true}-маппинг возвращает g@l, предварительно выставив vim.o.operatorfunc на колбэк. Neovim принимает оба варианта: ссылку на Lua-функцию (vim.o.operatorfunc = my_op, нативный путь) и строку 'v:lua.my_op' - выберите один и держитесь его. При . Neovim заново вызывает g@, восстанавливая марки '[ и ']. Это убирает зависимость от плагина в новых конфигах. Для классического .vimrc на Vimscript по-прежнему уместен tpope/vim-repeat.
- Знайте границу с макросами. Точка - для одной атомарной правки с ручной навигацией; макрос - для многошаговых сценариев и пакетного прогона через :normal. Не записывайте макрос там, где хватает ..
По умолчанию . повторяет только встроенные изменения. Команда плагина, реализованная как маппинг (например ysiw) из vim-surround - "обернуть слово в круглые скобки"), по умолчанию точкой не повторится: для Vim это просто цепочка нажатий, а "последним изменением" окажется лишь её внутренний кусок.
Решений два.
1. Классический путь - tpope/vim-repeat. Это библиотека-плагин: автор другого плагина вызывает в конце своей команды silent! call repeat#set("\<Plug>MyMapping", v:count), и тогда . повторяет всю команду целиком. Многие популярные плагины (vim-surround, vim-commentary, vim-unimpaired) уже интегрированы с vim-repeat, поэтому достаточно его установить - и ds", cs"', ysiw} начинают слушаться точки. Это стандарт для .vimrc-экосистемы на Vimscript.
Код: Выделить всё
Без vim-repeat:
"hello"
ds" -> hello (удалили кавычки)
. -> НЕ работает как ожидалось
С установленным vim-repeat:
cs"' -> 'hello' (заменили " на ')
. -> повторит замену кавычек на следующей цели
Код: Выделить всё
-- Набросок dot-repeatable действия на нативном API
function _G.my_op(motion_type)
-- здесь работаем с диапазоном между марками '[ и ']
-- ...
end
vim.keymap.set('n', '<leader>x', function()
vim.o.operatorfunc = my_op -- ссылка на Lua-функцию (нативный путь);
-- эквивалент строки 'v:lua.my_op'
return 'g@l' -- 'l' = применить к одному символу-движению
end, { expr = true })
-- теперь <leader>x повторяется через '.' без vim-repeat
Практический вывод 2026: на чистом Neovim-конфиге тяните operatorfunc/g@ или плагины с встроенным dot-repeat; vim-repeat оставьте для Vim и legacy-Vimscript.
Частые ошибки
- Использовать cw/d$ там, где нужен повторяемый edit. Точка ведёт себя непредсказуемо из-за зависимости от позиции курсора. Берите ciw/di" - объекты, а не движения.
- Ждать, что . повторит ex-команду. :s, :g, :d точкой не повторяются. Для последней замены используйте & (строка) или g& (файл), для команд - макрос или историю command-line (q:).
- Забыть, что движение между правками сбрасывает счётчик, но не саму правку. n, /foo, ; - не изменения, они не трогают то, что повторит .. Это не баг, а фича - на ней держится весь паттерн "двигайся и повторяй".
- Дать p/визуальной вставке перезаписать безымянный регистр посреди серии. После viw p буфер уже не тот - следующая . (или ручной повтор) вставит мусор. Лечится "0p или "_dP.
- Полагаться на курсор-зависимые команды первого шага. $a;<Esc> повторится криво, если на следующей строке курсор окажется не в конце. Берите A;<Esc>.
- Ожидать dot-repeat от плагинной команды без поддержки. Без vim-repeat (или нативного g@) . повторит лишь внутренний фрагмент маппинга, а не всю операцию плагина. Поставьте vim-repeat или используйте плагины с нативным dot-repeat.
- Записывать макрос там, где хватило бы точки. Лишняя церемония: для одной правки с навигацией поиском cgn + . короче и нагляднее любого @q.
- Переименование через cgn. Возьмите файл, где слово foo встречается 6-8 раз. Наведитесь на первое вхождение, нажмите *, затем N. Выполните cgnbar<Esc>, после чего пройдите весь файл одной только точкой ., пропуская ненужные вхождения через n. Сравните число нажатий с тем, что было бы при ручном выделении.
- Дописать точку с запятой. Дан блок из пяти строк кода без ; в конце. Сделайте A;<Esc> на первой строке, затем добейте остальные ритмом j.. Затем попробуйте "неправильный" вариант $a;<Esc> и убедитесь, что он ломается, если курсор не в конце строки.
- Объект против движения. Поставьте курсор в середину слова и попробуйте сначала cw, отмените (u), затем ciw. Зафиксируйте разницу. Повторите обе команды точкой на следующем слове и объясните, почему ciw ведёт себя предсказуемо, а cw - нет.
- Замена в кавычках. В файле с десятком строк вида name = "old_value" поменяйте все значения внутри кавычек на NEW. Подсказка: ci"NEW<Esc> плюс навигация /" <CR> и .. Где . оказывается удобнее, чем :%s/"[^"]*"/"NEW"/?
- Регистровая ловушка. Скопируйте слово (yiw), затем замените им три других слова через визуальное выделение и p. Понаблюдайте, как со второго раза вставляется "не то". Исправьте подход, используя "0p, и тиражируйте правку без сбоев.
- (Neovim, продвинутое) Свой dot-repeatable маппинг. Напишите на Lua маппинг, который оборачивает слово под курсором в обратные кавычки, используя operatorfunc + g@, и убедитесь, что он повторяется точкой без vim-repeat. Сравните с тем, как ту же задачу решает mini.surround.
- . повторяет последнее изменение текста; движения (n, ;, /) изменениями не считаются и точку не сбивают - на этом стоит паттерн "один штрих, чтобы переместиться, один, чтобы повторить".
- Для надёжной повторяемости берите текстовые объекты (ciw, ci", dap), а не курсор-зависимые движения (cw, d$).
- Связка cgn + . - современный способ выборочной замены по файлу с полным визуальным контролем, часто удобнее :%s//gc.
- Точка повторяет команду вместе с регистром; следите, чтобы безымянный регистр не перезаписывался посреди серии (используйте "0p или "_dP).
- Точка - для одной атомарной правки; макрос (глава 16) - для многошаговых сценариев и пакетного прогона.
- Плагинные команды повторяются точкой через tpope/vim-repeat (классика Vim) или нативный operatorfunc + g@ (предпочтительно в Neovim 2026); многие современные Lua-плагины поддерживают dot-repeat "из коробки".