Новое в журнале. Маленькие pull request: почему их быстрее ревьюят и как резать задачу
Инструменты

Поиск в Vim: от /слова до замены по всему проекту

Разбираем, как в Vim устроен поиск: от простого / в открытом файле до замены тысяч вхождений в проекте с ripgrep и quickfix. Без магии, только привычки, которые экономят часы работы.

11 ноября 20257 мин чтенияПрактикам
Поиск в Vim: от /слова до замены по всему проекту
5 командбазовый поиск в файле
1 утилитапоиск по всему проекту

Поиск в редакторе кажется мелочью, пока не приходится искать одно и то же слово в полутысяче файлов проекта на PHP или JS. В Vim и Neovim поиск устроен слоями: от простого поиска по текущему файлу до запросов по всему рабочему каталогу через внешние утилиты. Разберём все уровни по порядку, без лишних плагинов, которые не нужны большинству разработчиков.

Поиск в открытом файле: /, ?, * и навигация n/N

В нормальном режиме клавиша / переводит Vim в режим поиска вперёд по файлу, ? ищет назад. После ввода шаблона и нажатия Enter курсор перескакивает на первое совпадение, а клавиши n и N двигают его к следующему и предыдущему результату. Если курсор стоит на нужном слове, звёздочка * ищет его вперёд по всему файлу целиком, а g* делает то же самое, но ищет слово как часть более длинных слов.

  • :set hlsearch включает подсветку всех совпадений (в Neovim включено по умолчанию)
  • :noh снимает подсветку без сброса самого поиска
  • /слово\c ищет без учёта регистра, \C принудительно с учётом
Повесьте :noh на Esc в конфиге (nnoremap <esc> :noh<cr>): подсветка после десятого поиска в файле начинает мешать больше, чем помогает.
Главная мысльЛюбой поиск, который требует мыши, в Vim уже избыточен: клавиатура быстрее на любом масштабе, от строки до тысячи файлов.

Регулярные выражения: чем синтаксис Vim отличается от PCRE

Поиск в Vim по умолчанию работает в собственном диалекте регулярных выражений, который местами не совпадает с привычным PCRE из PHP, JS или ripgrep. Главное отличие: группы и квантификаторы нужно экранировать обратным слэшем, если не включён режим \v (very magic), который делает синтаксис почти обычным.

ЗадачаVim (magic)Vim (\v) / PCRE
Группа\(php\|js\)(php|js)
Один или большеa\+a+
Любая цифра\d\d
Начало слова\<term\<term

Если регулярка не ищется так, как ожидалось, первое, что стоит проверить: не забыт ли режим \v в начале шаблона. С ним большинство привычек из PHP и JS переносятся почти без изменений.

Отдельно стоит запомнить пару меток \zs и \ze, у них нет прямого аналога в PCRE, и именно их отсутствие чаще всего ломает перенесённые из других языков регулярки. \zs отмечает начало фактического совпадения внутри более длинного шаблона, а \ze отмечает конец, то есть всё, что окружает эти метки, участвует в поиске контекста, но не входит в подсвеченное и заменяемое совпадение. Например, шаблон function\zs.*\ze( найдёт только имя функции между словом function и открывающей скобкой, не трогая саму скобку и ключевое слово, и это ровно то, что в PCRE обычно делают через lookahead и lookbehind.

vimgrep и quickfix: поиск по проекту без единого плагина

Команда :vimgrep ищет шаблон во всех переданных ей файлах и складывает результаты в quickfix-список, встроенный механизм Vim для любых списков вида «файл плюс строка».

:vimgrep /TODO/ **/*.php
:copen

Команда выше ищет TODO во всех PHP-файлах рабочего каталога и подкаталогов, а :copen открывает окно со списком найденных мест. Внутри quickfix-окна :cnext и :cprev двигают курсор по результатам, Enter на строке открывает файл в нужном месте. У :vimgrep есть честный минус: он построчно читает каждый файл сам, на репозитории в несколько тысяч файлов это заметно медленнее внешнего поисковика.

Вы в начале разбораДальше: разбор компромиссов, чек-лист и ответы на частые вопросы.

:grep с ripgrep вместо встроенного движка

Для больших проектов Vim умеет делегировать поиск внешней программе через :grep, которая по умолчанию зовёт системный grep, но её легко переключить на ripgrep: быстрее, с поддержкой .gitignore из коробки и без лишних системных файлов в результатах.

set grepprg=rg\ --vimgrep\ --smart-case
set grepformat=%f:%l:%c:%m

После такой настройки :grep TODO отработает за доли секунды даже на монорепозитории, а результат попадёт в тот же quickfix и откроется через :copen. Если ripgrep не установлен, в Debian и Ubuntu он ставится одной командой apt install ripgrep, в Arch пакет называется так же.

Держите в quickfix только текущий поиск: :grep каждый раз перезаписывает список, а не дополняет его, так что старые результаты не потеряются случайно, зато легко забыть, что искали последним.

Замена по всему проекту: argdo, bufdo и cgn

Классический способ заменить в одном файле: :%s/старое/новое/g. Буква g в конце означает замену всех вхождений в строке, без неё заменится только первое. Для проекта из нескольких файлов Vim использует arglist, список файлов, с которым можно работать как с одним буфером.

:args **/*.php
:argdo %s/old_name/new_name/ge | update

Флаг e в конце команды подстановки подавляет ошибку на файлах, где совпадений не нашлось, без него :argdo остановится на первом же таком файле. :bufdo работает так же, но по уже открытым буферам, а не по arglist.

  • Нужна замена с подтверждением на каждое вхождение: добавьте флаг c к :s
  • Нужно заменить только выделенные вручную вхождения в файле: встаньте на первое совпадение и используйте cgn, затем n и точку для повтора на следующих
  • Собрали список мест через :vimgrep и хотите заменить только в них: :cfdo %s/.../.../ge | update пройдёт по файлам из quickfix, а не из arglist

Что изменилось с приходом LSP и Telescope в 2026 году

Встроенный LSP-клиент в Neovim и плагины вроде Telescope или fzf-lua не заменяют описанные выше команды, они решают другую задачу: поиск по смыслу (где объявлена функция, кто её вызывает), а не по тексту. Поиск по строке остаётся самым быстрым способом найти текстовое совпадение, когда вы точно знаете, что искать, и не хотите ждать индексации проекта. На практике разработчики комбинируют оба подхода: LSP для навигации по коду, :grep с ripgrep для текстового поиска и массовой замены.

Telescope добавляет к этому удобный интерактивный слой: его модуль live_grep запускает тот же ripgrep, но результаты обновляются на каждое нажатие клавиши и показываются в отдельном окне с предпросмотром файла. Для разового поиска это удобнее голого :grep, который требует сначала набрать весь запрос и только потом увидеть результат. Для автоматизированной замены по десяткам файлов quickfix и :cfdo остаются быстрее, потому что не требуют ручного подтверждения на каждый файл.

Полезные настройки в vimrc, которые экономят время каждый день

Несколько строк в конфиге убирают большую часть ручной рутины поиска. Опция incsearch показывает совпадение прямо во время набора шаблона, не дожидаясь Enter, это особенно удобно при составлении регулярки, когда проще увидеть промежуточный результат, чем держать синтаксис в голове. Опция wrapscan, включённая по умолчанию, заставляет поиск переходить в начало файла после последнего совпадения, вместо того чтобы останавливаться на границе.

  • set incsearch показывает совпадения во время набора шаблона, до нажатия Enter
  • set ignorecase и set smartcase вместе дают разумное поведение по умолчанию для большинства запросов
  • nnoremap <leader>r :%s/<C-r><C-w>/ подставляет слово под курсором прямо в команду замены, не набирая его вручную

Эти три настройки не требуют никаких внешних зависимостей и работают одинаково в Vim и Neovim без установки единого плагина, что делает их первым кандидатом в минимальный конфиг на новом сервере.

Частые вопросы

Почему :vimgrep работает медленнее, чем :grep с ripgrep?

Vimgrep читает и разбирает каждый файл средствами самого Vim, это универсально, но медленно на больших деревьях файлов. Ripgrep написан на компилируемом языке, параллелит обход директорий и сразу уважает .gitignore, поэтому на том же проекте он отрабатывает в разы быстрее.

Как искать с учётом регистра по умолчанию, но игнорировать его для запросов с маленькими буквами?

Добавьте в конфиг set ignorecase и set smartcase. Это включит поиск без учёта регистра, но если в шаблоне встретится хотя бы одна заглавная буква, поиск автоматически станет точным по регистру.

Чем argdo отличается от cfdo при замене по нескольким файлам?

Argdo проходит по arglist, списку файлов, который вы явно задали командой args. Cfdo проходит по файлам из текущего quickfix-списка, то есть по результатам последнего vimgrep или grep. Если список файлов для замены совпадает со списком результатов поиска, cfdo удобнее, не нужно собирать arglist вручную.

Код, который не стыдно передать

Начните с принципов, а дальше двигайтесь по рубрикам. Каждый разбор заканчивается чек-листом.

Случайный разбор