В этой главе мы разберём весь поисковый арсенал: от простого /foo до very magic-паттернов, текстового объекта gn и связки cgn + ., которая в 2026 году остаётся одним из самых элегантных способов "найти и заменить руками, но молниеносно".
Зачем это нужно
Представьте задачу: в файле на 400 строк нужно переименовать переменную userId в accountId, но не везде - только там, где она встречается как самостоятельное слово, и не трогая userIdList. Можно открыть :%s/, выстроить хитрый паттерн с границами слов, добавить флаг c и нажимать y/n на каждом совпадении. А можно поставить курсор на слово, нажать *, затем cgn, ввести новое имя - и дальше жать . ровно там, где замена нужна. Второй путь короче, нагляднее и не требует выходить из normal-режима.
Поиск также лежит в основе навигации: * мгновенно перепрыгивает к следующему вхождению слова под курсором, n/N гоняют по совпадениям, а /pattern с оффсетом /e ставит курсор ровно в нужную точку. Освоив это, вы перестанете "листать глазами".
Базовый поиск: / и ?
Два направления, две клавиши:
- /pattern <CR> - искать вперёд (вниз по файлу) от курсора.
- ?pattern <CR> - искать назад (вверх по файлу).
- n - повторить поиск в том же направлении.
- N - повторить поиск в обратном направлении.
Поиск по умолчанию зацикливается ('wrapscan' включён): дойдя до конца файла, он продолжит с начала. Отключить можно через :set nowrapscan, но большинство оставляет дефолт.
Ещё один практичный момент: пустой паттерн повторяет последний. / <CR> (просто слэш и Enter) повторит предыдущий поиск вперёд, а ? <CR> - назад. Последний паттерн хранится в специальном регистре "/ (см. главу 8 о регистрах), и его можно вставить или присвоить программно - этим мы воспользуемся ниже.
История поиска
Командная строка поиска имеет собственную историю. Нажав / и затем стрелку вверх (или <C-p>), вы листаете прошлые запросы. А q/ открывает окно истории поиска - полноценный буфер, где каждую строку можно отредактировать как обычный текст и выполнить нажатием <CR>. Это особенно удобно для длинных регулярок: проще поправить опечатку в буфере, чем перенабирать.
Поиск слова под курсором: *, #, g*, g#
Самый частый сценарий - "найти, где ещё используется вот это слово". Для него есть отдельные клавиши, не требующие набирать паттерн вообще:
Код: Выделить всё
Клавиша | Действие
---------+---------------------------------------------------------------------------
* | Искать следующее вхождение слова под курсором (вперёд), с границами слова
# | То же, но назад
g* | Как *, но без границ слова (частичные совпадения тоже считаются)
g# | Как #, но без границ слова
После нажатия * паттерн оседает в регистре "/, поисковая подсветка зажигается, а n/N начинают работать с этим словом. Фактически * - это "настроить поиск по слову и сразу прыгнуть".
Код: Выделить всё
Было (курсор на слове userId в строке 1):
userId = req.userId; <- курсор здесь
const userIdList = [];
log(userId);
Нажимаем: *
Стало: курсор прыгнул на userId в строке 3 (log(userId)),
строку с userIdList ПРОПУСТИЛО (это другое слово).
Дальше n идёт по вхождениям userId как целого слова.
Две опции, без которых поиск в 2026 году ощущается архаично. В Neovim 'incsearch' и 'hlsearch' включены по умолчанию; в классическом Vim их стоит включить явно в конфиге (см. главу 20).
'incsearch' (incremental search) - по мере набора паттерна Vim сразу подсвечивает и прыгает к первому совпадению. Вы видите результат ещё до нажатия <CR>. Если передумали - <Esc> отменяет поиск и возвращает курсор на исходное место.
'hlsearch' - подсвечивает все совпадения последнего паттерна, а не только текущее. Это превращает поиск в инструмент "обзора": нажали * на имени функции - и видите все её вызовы, разбросанные по экрану.
Проблема hlsearch: подсветка остаётся после того, как поиск стал не нужен, и мешает читать. Решение - команда сброса:
- :noh (полностью :nohlsearch) - убирает подсветку до следующего поиска. Опцию hlsearch она не выключает, просто гасит текущее свечение.
Код: Выделить всё
-- Neovim, init.lua
vim.keymap.set('n', '<Esc>', '<cmd>noh<CR>', { desc = 'Clear search highlight' })
Регистр символов: ignorecase и smartcase
По умолчанию поиск чувствителен к регистру: /foo не найдёт Foo. Две опции делают поведение умным:
- :set ignorecase - поиск игнорирует регистр полностью. /foo найдёт Foo, FOO, foo.
- :set smartcase - работает только вместе с ignorecase и уточняет его: если в паттерне есть хотя бы одна заглавная буква, поиск снова становится чувствительным к регистру.
Код: Выделить всё
vim.opt.ignorecase = true
vim.opt.smartcase = true
Важный нюанс: * и # подчиняются ignorecase, но НЕ smartcase (см. :help star) - даже при включённом smartcase команда * на слове со строчными буквами останется регистронезависимой. А ещё в любой паттерн можно встроить флаг прямо посимвольно: \c форсирует игнорирование регистра в этом конкретном поиске, \C - форсирует чувствительность, независимо от опций. Например, /foo\c найдёт FOO даже при noignorecase.
Магия паттернов: режим very magic (\v)
Поиск в Vim - это регулярные выражения, но синтаксис у них исторически своеобразный. По умолчанию (magic) часть метасимволов требует экранирования обратным слешем: чтобы сгруппировать, пишут \(...\), чтобы поставить "один или больше" - \+, альтернатива - \|. Это раздражает всех, кто привык к PCRE/Perl/JavaScript.
Решение - very magic, префикс \v. После него синтаксис становится почти как в обычных регэкспах: (, ), +, ?, |, {} работают без бэкслешей, а экранировать нужно только литералы.
Код: Выделить всё
Что нужно | Обычный режим (magic) | Very magic (\v)
-----------------+-------------------------+-------------------+-------+-----
Группа | \(abc\) | (abc)
Один или больше | a\+ | a+
Ноль или один | a\? | a?
Альтернатива | foo\ | bar | foo | bar
Квантификатор | a\{2,4\} | a{2,4}
Граница слова | \<word\> | <word>
Код: Выделить всё
magic: /(\d\+) <- скобки литеральные, \d\+ метасимволы
very magic: /\v\(\d+\) <- скобки экранированы, \d+ чисто
- \m - magic (дефолт).
- \M - nomagic, экранирования требует больше символов.
- \V - very nomagic ("very literal"): почти всё трактуется буквально. Идеально для поиска путей, URL и текста со спецсимволами: /\V/usr/local/bin найдёт буквальную строку, не интерпретируя . или /.
Классы символов и границы слов
Полезный минимум, который покрывает 90% поисков (показан в \v-стиле, но классы \d, \w и т.п. работают во всех режимах):
Код: Выделить всё
Конструкция | Значение
--------------+-----------------------------------------------
. | Любой символ (кроме перевода строки)
\d / \D | Цифра / не цифра
\w / \W | Слово-символ ([0-9A-Za-z_]) / не слово-символ
\s / \S | Пробельный / не пробельный
\a / \l / \u | Буква / строчная / заглавная
[abc] | Любой из перечисленных
[^abc] | Любой, кроме перечисленных
< и > (в \v) | Граница начала / конца слова (\< и \> вне \v)
Код: Выделить всё
Текст: account counter count counting
/count -> найдёт ВСЕ четыре (count внутри каждого)
/\v<count> -> найдёт только третье слово (count целиком)
/\v<count -> найдёт count и counter и counting (начало слова count)
Вот где поиск раскрывается. Раз /foo - это движение к началу совпадения, его можно ставить после оператора:
- d/foo <CR> - удалить от курсора до начала следующего foo.
- c/foo <CR> - изменить от курсора до foo.
- y/foo <CR> - скопировать до foo.
Код: Выделить всё
Было: const value = getUserData() + computeTotal();
^курсор в начале строки
Нажимаем: d/comp <CR>
Стало: computeTotal();
(удалено всё от начала до начала "comp")
Оффсеты поиска: /foo/e, /foo/s, /foo/+2
Поиск умеет ставить курсор не на начало совпадения, а со смещением. Оффсет добавляется через второй слэш:
Код: Выделить всё
Оффсет | Куда встанет курсор
-------------------+---------------------------------------------------------
/foo/e | На последний символ совпадения (end)
/foo/e+1 | На символ после совпадения
/foo/s или /foo/b | На первый символ совпадения (begin) - это и есть дефолт
/foo/s+2 | На 2 символа правее начала
/foo/+2 | На 2 строки ниже строки совпадения, в колонку 1
/foo/-1 | На строку выше, в колонку 1
Код: Выделить всё
Было: foo = calculate(a, b, c);
^курсор
Нажимаем: d/c)/e <CR> (искать "c)", курсор на ")")
Стало: ;
(удалено от начала до закрывающей скобки включительно)
Обратите внимание на разницу между линейными и символьными оффсетами: линейные (+N/-N, без e/s/b) ставят курсор всегда в колонку 1 соответствующей строки - горизонтальная позиция не сохраняется (см. :help search-offset: "[num] lines downwards, in column 1"). Символьные же (e, s, b с числом) работают относительно самого совпадения.
gn и gN: поиск как текстовый объект
Это, возможно, самая недооценённая фича поиска. gn - это текстовый объект (про текстовые объекты - глава 6), который выделяет текущее или следующее совпадение последнего поискового паттерна.
- gn - в normal-режиме входит в visual и выделяет следующее совпадение; в комбинации с оператором применяет его к этому совпадению.
- gN - то же, но в обратную сторону.
- dgn - удалить следующее совпадение.
- cgn - изменить (удалить и войти в insert) следующее совпадение.
- ygn - скопировать его.
Код: Выделить всё
Задача: заменить foo на bar выборочно.
1. Ставим паттерн. Проще всего: курсор на foo, жмём *
(теперь @/ = \<foo\>, и hlsearch показывает все foo)
2. n возвращает на первое foo (или вручную дойти до нужного)
3. cgn -> foo выделилось и стёрлось, мы в insert
4. печатаем bar, <Esc>
5. Теперь на КАЖДОМ следующем foo:
- n -> перейти к нему (если хотим ПРОПУСТИТЬ - просто n)
- . -> заменить (точка повторяет ВЕСЬ cgn+ввод!)
- остаётесь в normal-режиме и видите контекст в полном окне;
- решение "менять или нет" делаете естественным . (заменить) или n (пропустить);
- можете в любой момент сделать что угодно ещё и вернуться;
- замена попадает в точку-команду, так что её легко перенести в другой файл.
Код: Выделить всё
Было:
foo(); <- курсор, жмём * затем cgn -> bar(); <Esc>
log(foo);
foo.bar = foo;
fooBar(); <- это НЕ совпадёт (* ищет \<foo\>)
Последовательность: cgn -> "bar" <Esc> -> n -> . -> . -> n -> .
(где . меняет, n пропускает по вашему усмотрению)
Стало (если меняли всё):
bar();
log(bar);
bar.bar = bar;
fooBar(); <- осталось нетронутым
Код: Выделить всё
-- Neovim: <Leader>c - заменить слово под курсором через cgn
vim.keymap.set('n', '<Leader>c',
[[:let @/='\<'.expand('<cword>').'\>'<CR>cgn]],
{ desc = 'Change word under cursor (repeat with .)' })
-- Визуальный режим: взять выделение как паттерн
vim.keymap.set('x', '<Leader>c',
[["sy:let @/=@s<CR>cgn]],
{ desc = 'Change selection (repeat with .)' })
Ещё одна важная оговорка про визуальный маппинг: он кладёт выделение в @/ как есть, без экранирования regex-метасимволов. Поэтому он корректен только для текста без спецсимволов (простые идентификаторы вроде foo, myVar). Если в выделении окажутся ., *, [, ], \, /, ~ и т.п., они будут истолкованы как regex - паттерн сломается или сработает не так. Для произвольного выделения нужно либо префиксовать паттерн \V (very nomagic, тогда литеральными станут почти все символы, кроме \ и разделителя), либо экранировать через escape(). Например, более надёжный вариант: [["sy:let @/='\V'.escape(@s,'\/')<CR>cgn]].
Лучшие практики 2026
- Включите incsearch, hlsearch, ignorecase + smartcase. В Neovim первые два - дефолт; регистровые опции добавьте сами. Это базовый "sane defaults"-набор (глава 20), без которого поиск ощущается сырым.
- Начинайте сложные паттерны с \v. Меньше бэкслешей - меньше ошибок. В скриптах фиксируйте режим явно, не полагаясь на чужую опцию 'magic'.
- Предпочитайте * + cgn + . для рефакторинга в одном файле. Это нагляднее и контролируемее, чем :%s//gc, и кладёт правку в точку-команду. К :substitute переходите, когда правок много и они однотипны (глава 11), а для всего проекта - quickfix + :cfdo (глава 14).
- Сбрасывайте подсветку маппингом. Повесьте :noh на <Esc> или leader-клавишу, чтобы hlsearch не превращался в визуальный шум.
- Используйте \V для "грязного" текста. Пути, URL, строки с ./*// ищите в very nomagic - не придётся экранировать каждый спецсимвол.
- Запоминайте оффсет /e для inclusive-движений с операторами и точного позиционирования курсора в конец совпадения.
- q/ для длинных регулярок. Окно истории поиска редактируется как обычный буфер - это удобнее, чем перенабирать сложный паттерн в командной строке.
- Путать направление n/N после ?. n повторяет поиск в направлении исходного запроса. После ?foo клавиша n идёт вверх. Если запутались - / или ? без паттерна задаёт направление заново.
- Забыть, что * ставит границы слова. Ожидать, что * на count найдёт counter - не найдёт. Для частичных совпадений нужен g*.
- smartcase без ignorecase. smartcase сам по себе не работает - он только модифицирует ignorecase. Включать надо обе опции.
- Лишние бэкслеши в \v-паттерне. Перейдя на very magic, легко по привычке написать \( - а там это означает буквальную скобку. В \v экранируются литералы, а не метасимволы.
- cgn без заданного паттерна. Если "/ пуст или содержит не то, что вы думаете, cgn поменяет неожиданное место. Всегда сначала * или маппинг, выставляющий @/.
- Считать, что :noh выключает hlsearch. Она лишь гасит текущую подсветку до следующего поиска; сама опция остаётся включённой. Чтобы отключить совсем - :set nohlsearch.
- Поиск спецсимволов без \V или экранирования. /a.b найдёт и axb, потому что . - метасимвол. Для буквальной точки нужен /a\.b или \Va.b.
- Границы слова. В любом коде найдите идентификатор, который является подстрокой другого (например id внутри idList). Поставьте курсор на короткий, нажмите * и убедитесь, что n пропускает idList. Затем попробуйте g* и сравните поведение.
- Оператор + поиск с оффсетом. Возьмите строку result = compute(a) + render(b);. Поставьте курсор в начало и удалите всё до закрывающей скобки первого вызова включительно одной командой d/)/e. Затем повторите с c вместо d.
- Very magic. Напишите один поисковый паттерн в режиме \v, который найдёт "слово из 3-5 цифр, заключённое в круглые скобки" (например (12345), но не (7) и не 123). Подсказка: \v\(\d{3,5}\). Проверьте на тестовых строках.
- cgn + .. Создайте файл с пятью строками, в каждой встречается слово temp. Через * задайте паттерн, затем cgn, замените на cache, и пройдите по остальным, выборочно применяя . и пропуская через n. Оставьте одно вхождение нетронутым.
- smartcase на практике. Включите ignorecase + smartcase. В тексте, где есть Error, error и ERROR, убедитесь, что /error находит все три, а /Error - только вариант с заглавной E.
- Окно истории. Сделайте 3-4 разных поиска. Нажмите q/, найдите в открывшемся буфере один из паттернов, отредактируйте его прямо в строке и выполните нажатием <CR>. Закройте окно через :q, если передумали.
- / и ? ищут вперёд и назад; n/N повторяют поиск в исходном и обратном направлении; пустой паттерн повторяет последний.
- * и # ищут слово под курсором с границами слова; g*/g# - без границ (частичные совпадения).
- 'incsearch' показывает совпадение по мере набора, 'hlsearch' подсвечивает все, :noh гасит подсветку.
- 'ignorecase' + 'smartcase' - рекомендуемый дефолт: регистр игнорируется, пока в паттерне нет заглавных; \c/\C форсируют режим точечно.
- \v (very magic) даёт PCRE-подобный синтаксис без лишних бэкслешей; \V - для буквального текста. В скриптах режим фиксируется явно.
- Поиск - это движение: d/foo, c/foo, y/foo работают с любым оператором; оффсет /e делает движение inclusive и точно позиционирует курсор.
- gn/gN - текстовый объект совпадения; связка * -> cgn -> ввод -> . даёт выборочную замену в normal-режиме, повторяемую точкой, - флагманский приём этой главы.