Ответ Vim на этот вопрос - quickfix list и location list. Это структурированные списки позиций "файл:строка:колонка", по которым можно прыгать одной клавишей и применять команды массово. Quickfix - едва ли не самый недооценённый механизм Vim: освоив его, вы перестанете открывать файлы вручную и начнёте обрабатывать результаты поиска как конвейер. Именно здесь связываются воедино внешние инструменты (ripgrep, компиляторы, линтеры) и редактор.
Зачем это нужно
Представьте задачу: переименовать getUserData в fetchUserData по всему проекту. Открывать каждый файл, искать, заменять, сохранять - это десятки ручных действий и гарантированный риск что-то пропустить. С quickfix задача сводится к двум командам: наполнить список всеми вхождениями и прогнать по нему массовую замену. Весь проект обрабатывается за секунды, причём детерминированно.
Quickfix - это не "ещё одно окно". Это рабочая память для пакетных операций: компилятор, grep, линтер и LSP складывают туда свои результаты в едином формате, а вы единообразно по ним навигируете и редактируете.
Quickfix list против location list
Это два почти идентичных механизма, и важно понимать разницу с самого начала.
Quickfix list - один на всю сессию Vim (точнее, глобальный для всего инстанса). Его наполняют команды вроде :make, :grep, :vimgrep. Логика проста: список один, и любая команда, кладущая в него результаты, перезаписывает предыдущий.
Location list - привязан к конкретному окну. У каждого окна свой location list. Команды с префиксом l (:lmake, :lgrep, :lvimgrep) работают именно с ним.
Почему два механизма? Потому что у них разные жизненные циклы. Quickfix хорош для "глобальной задачи": ошибки сборки всего проекта, результаты grep по репозиторию. Location list хорош для "локальной задачи, привязанной к окну": например, LSP-диагностика конкретного буфера или список вхождений в одном файле. Можно держать оба одновременно: в одном окне location list с диагностикой текущего файла, а quickfix - с результатами проектного поиска.
Практическое правило: по умолчанию используйте quickfix, а location list берите, когда нужно несколько независимых списков одновременно или когда список логически принадлежит окну. Все команды quickfix имеют зеркальный location-аналог с буквой l в начале.
Код: Выделить всё
Назначение | Quickfix | Location list
-----------------------------+---------------------+-------------------
Наполнить из make | :make | :lmake
Наполнить из grep | :grep | :lgrep
Внутренний grep | :vimgrep | :lvimgrep
Открыть окно | :copen | :lopen
Закрыть окно | :cclose | :lclose
Следующая запись | :cnext (:cn) | :lnext (:lne)
Предыдущая | :cprevious (:cp) | :lprevious (:lp)
Первая / последняя | :cfirst / :clast | :lfirst / :llast
Команда на каждую запись | :cdo | :ldo
Команда на каждый файл | :cfdo | :lfdo
Предыдущий/следующий список | :colder / :cnewer | :lolder / :lnewer
Команда :make запускает программу, заданную опцией 'makeprg' (по умолчанию make), перехватывает её вывод и парсит его в quickfix list. Магия в том, как именно Vim понимает, где в выводе компилятора спрятаны "файл, строка, сообщение". За это отвечает опция 'errorformat'.
'errorformat' - это набор шаблонов в стиле scanf, описывающих формат строк ошибок. Ключевые спецификаторы:
Код: Выделить всё
Спецификатор | Значение
--------------+----------------------------------
%f | имя файла
%l | номер строки
%c | номер колонки
%m | текст сообщения
%t | тип (e/w/i - error/warning/info)
%% | литеральный символ %
Код: Выделить всё
set errorformat=%f:%l:%c:\ %t%*[^:]:\ %m
Код: Выделить всё
:set makeprg=cargo\ build
:make " запускаем сборку, ошибки попадают в quickfix
:copen " смотрим список ошибок
:grep/:lgrep против :vimgrep/:lvimgrep
Есть два принципиально разных способа наполнить quickfix результатами поиска по файлам.
:vimgrep использует встроенный движок регулярных выражений Vim. Синтаксис паттерна - тот же, что в / (см. главу 10), включая \v (very magic).
Код: Выделить всё
:vimgrep /\vfetchUser/ **/*.js
Полезные флаги: :vimgrep /pat/gj **/*.py. Здесь g означает "все совпадения в строке" (а не только первое), а j (jump) - "не прыгать сразу на первый результат", что критично для пакетной работы: вы хотите сначала собрать список, а потом решать. Есть и флаг f - fuzzy matching (нечёткий поиск вместо regex).
Важная механика g-флага: с ним :vimgrep добавляет в quickfix несколько записей с одной строки (по записи на каждое совпадение). Именно поэтому при последующем массовом редактировании для замены s///g берут :cfdo (раз на файл), а не :cdo: иначе :cdo повторно обработает одну и ту же строку столько раз, сколько на ней совпадений (подробнее про :cdo/:cfdo ниже).
:grep запускает внешнюю программу, заданную опцией 'grepprg' (по умолчанию системный grep), и парсит её вывод через 'grepformat' - ровно как :make через errorformat. Это быстрее на порядки и позволяет подключить современные инструменты вроде ripgrep.
Код: Выделить всё
Критерий | :vimgrep | :grep
---------------------+------------------+-------------------------------------------------------------------------------------------------------------
Движок regex | встроенный Vim | движок внешней программы (по умолчанию у rg - Rust regex, без lookaround/backref; PCRE2 только с флагом -P)
Скорость | медленно | очень быстро
Зависимость | нет | внешний бинарник
Уважение .gitignore | нет | да (у ripgrep)
Синтаксис паттерна | как в / | как у внешней программы
Настройка grepprg на ripgrep
ripgrep (rg) - самый быстрый CLI-поисковик, по умолчанию уважающий .gitignore и поддерживающий smart-case. Чтобы Vim корректно парсил его вывод, нужны согласованные grepprg и grepformat. Ключ - флаг --vimgrep, который выдаёт строки в формате file:line:col:text.
Для Vim (в .vimrc):
Код: Выделить всё
set grepprg=rg\ --vimgrep\ --no-heading\ --smart-case
set grepformat=%f:%l:%c:%m
Код: Выделить всё
vim.o.grepprg = 'rg --vimgrep --no-heading --smart-case'
vim.o.grepformat = '%f:%l:%c:%m'
- --vimgrep - формат file:line:col:text, который точно парсится grepformat;
- --no-heading - не группировать результаты по файлам (нужен плоский формат);
- --smart-case - регистронезависимо, если паттерн в нижнем регистре, иначе с учётом регистра.
Код: Выделить всё
:grep! 'TODO' src/
:copen
Код: Выделить всё
:cexpr systemlist('rg --vimgrep TODO')
Навигация по quickfix
Список собран - теперь по нему нужно ходить. Базовый набор команд:
Код: Выделить всё
Команда | Сокращение | Действие
------------+--------------+-----------------------------
:copen | :cope | открыть окно quickfix
:cclose | :ccl | закрыть окно quickfix
:cnext | :cn | перейти к следующей записи
:cprevious | :cp / :cN | перейти к предыдущей записи
:cfirst | :cfir | первая запись
:clast | :cla | последняя запись
:cc [N] | | перейти к записи номер N
:colder | :col | предыдущая версия списка
:cnewer | :cnew | следующая версия списка
Голые :cnext/:cprev неудобно набирать в цикле, поэтому почти все настраивают маппинги. Классический вариант (см. главу 21 о маппингах):
Код: Выделить всё
vim.keymap.set('n', ']q', '<cmd>cnext<cr>', { desc = 'Quickfix: следующая' })
vim.keymap.set('n', '[q', '<cmd>cprevious<cr>', { desc = 'Quickfix: предыдущая' })
vim.keymap.set('n', '<leader>co', '<cmd>copen<cr>', { desc = 'Quickfix: открыть' })
vim.keymap.set('n', '<leader>cc', '<cmd>cclose<cr>', { desc = 'Quickfix: закрыть' })
:colder и :cnewer - недооценённая пара. Vim хранит историю из последних quickfix-списков (по умолчанию 10, настраивается опцией 'chistory'; для location list - 'lhistory'). Сделали :grep по одному паттерну, потом по другому - первый список не потерян. :colder ("older") возвращает к предыдущему, :cnewer - вперёд. Это спасает, когда вы случайно перезаписали важный результат поиска новым.
'switchbuf': как открываются записи
Частый источник путаницы при пакетной работе - как именно :cnext/Enter в окне quickfix открывают запись: в текущем окне, в существующем, в новом сплите? Этим управляет опция 'switchbuf'. Значения комбинируются через запятую:
- useopen - если буфер уже открыт в каком-то окне, перейти в него, а не загружать поверх текущего;
- uselast - использовать последнее использованное окно (удобно с открытым окном quickfix снизу);
- split / vsplit - открывать запись в новом гори-/вертикальном сплите;
- newtab - в новой вкладке.
Код: Выделить всё
set switchbuf=useopen,uselast
Авто-открытие quickfix через QuickFixCmdPost
Вокруг наполнения списка Vim генерирует события QuickFixCmdPre и QuickFixCmdPost (для :make, :grep, :vimgrep, :cexpr и т.п.). Стандартный приём - повесить на QuickFixCmdPost авто-открытие окна, чтобы :make/:grep сами показывали результат:
Код: Выделить всё
augroup AutoQuickfix
autocmd!
autocmd QuickFixCmdPost [^l]* cwindow " после :grep/:make - открыть quickfix, если есть записи
autocmd QuickFixCmdPost l* lwindow " после :lgrep/:lmake - открыть location list
augroup END
Массовое редактирование: :cdo, :cfdo, :ldo, :lfdo
Вот ради чего всё затевалось. Эти команды выполняют произвольную Ex-команду по всем записям quickfix/location list.
- :cdo {cmd} - выполняет {cmd} на каждой записи списка (один раз на каждое совпадение).
- :cfdo {cmd} - выполняет {cmd} на каждом файле из списка (один раз на файл, независимо от числа совпадений в нём).
- :ldo / :lfdo - то же самое для location list.
Откуда вообще берётся "пять записей в одном файле"? Здесь смыкаются две вещи. Во-первых, :grep/ripgrep и :vimgrep с флагом g кладут в quickfix отдельную запись на каждое совпадение, а не на строку: две совпавшие подстроки на одной строке - это две записи (а с g даже на одной строке их может быть несколько). Во-вторых, :cdo идёт именно по записям. Значит, на строке с двумя совпадениями :cdo встанет дважды - и s///g, который уже в первый проход заменил оба, во второй проход либо ничего не найдёт (отсюда обязательный флаг e), либо, если паттерн пересекается с заменой, испортит текст. :cfdo же берёт каждый файл ровно один раз - это и есть правильная гранулярность для s///g.
Используйте :cdo, когда команда привязана к конкретной строке записи (например, что-то сделать именно на строке совпадения). Используйте :cfdo, когда команда сама обходит весь файл (:substitute с g, :global, :normal по диапазону).
Ещё две критичные детали:
- :cdo/:cfdo НЕ сохраняют файлы автоматически. Изменения остаются в незаписанных буферах. Добавляйте | update в саму команду (сохранит только реально изменённые буферы) либо выполните :wall после.
- Без ! команда упадёт на изменённом несохранённом буфере, который нельзя оставить. И флаг e в s///e подавляет ошибку E486: Pattern not found - без него :cfdo прервётся на первом файле, где паттерн не нашёлся.
Это канонический воркфлоу проектного рефакторинга. Разберём по шагам на задаче "переименовать getUserData в fetchUserData".
Шаг 1. Наполняем quickfix всеми вхождениями (:grep! с !, чтобы не прыгать):
Код: Выделить всё
:grep! 'getUserData'
Код: Выделить всё
:copen
Код: Выделить всё
:cfdo s/getUserData/fetchUserData/ge | update
- :cfdo - раз на файл (не на запись!);
- s/.../.../g - заменить все вхождения в файле;
- e - не падать, если в файле паттерна нет (бывает при пограничных случаях парсинга);
- | update - сохранить буфер, если он изменился.
Код: Выделить всё
src/api.js:12: const data = getUserData(id);
src/profile.js:8: return getUserData(user.id);
Стало:
Код: Выделить всё
src/api.js:12: const data = fetchUserData(id);
src/profile.js:8: return fetchUserData(user.id);
Сужение списка через :Cfilter
Перед массовой заменой почти всегда нужно убрать ложные срабатывания. Vim и Neovim поставляются со встроенным плагином cfilter, который надо один раз подключить:
Код: Выделить всё
:packadd cfilter
:Cfilter /pattern/
Код: Выделить всё
:grep! 'getUserData'
:packadd cfilter
:Cfilter! /test/ " выбросить вхождения в тестах
:cfdo s/getUserData/fetchUserData/ge | update
Для нетекстовых замен (когда :substitute недостаточно) :cdo отлично сочетается с :normal и макросами (главы 12 и 16):
Код: Выделить всё
:grep! 'console.log'
:cdo normal! dd | update " удалить каждую строку с отладочным выводом
Quickfix в Neovim: словарный API и quickfixtextfunc
До сих пор мы наполняли список командами (:grep, :vimgrep, :cexpr). Но quickfix можно собрать и программно - это особенно удобно в Neovim, где список естественно строится из Lua. Базовый инструмент общий с Vim - функция setqflist() (и setloclist() для location list), которой передают список словарей с богатыми полями:
Код: Выделить всё
vim.fn.setqflist({
{ filename = 'src/api.js', lnum = 12, col = 17, text = 'getUserData here' },
{ filename = 'src/profile.js', lnum = 8, col = 10, text = 'and here', type = 'W' },
}, ' ') -- ' ' создаёт новый список, 'a' - добавляет, 'r' - заменяет текущий
В Neovim поверх этого есть собственный диагностический мост: vim.diagnostic.setqflist() и vim.diagnostic.setloclist() сами собирают LSP-диагностику в quickfix/location list - это и есть "правильный" способ получить ошибки линтеров и языковых серверов вместо :make. Отсюда и упомянутое в начале правило: проектные ошибки сборки - в quickfix, диагностика текущего буфера - в location list (setloclist).
quickfixtextfunc - кастомное форматирование окна. По умолчанию строка в окне quickfix выглядит как file|line col| text. Опция 'quickfixtextfunc' позволяет задать функцию, которая полностью определяет, как рендерится каждая строка списка, - например, скрыть путь, выровнять колонки, добавить иконку типа:
Код: Выделить всё
vim.o.quickfixtextfunc = 'v:lua.MyQfText'
function MyQfText(info)
local items = vim.fn.getqflist({ id = info.id, items = 0 }).items
local lines = {}
for i = info.start_idx, info.end_idx do
local it = items[i]
local name = vim.fn.bufname(it.bufnr)
lines[#lines + 1] = string.format('%s:%d %s', vim.fn.fnamemodify(name, ':t'), it.lnum, it.text)
end
return lines
end
Редактируемый quickfix: quickfix-reflector и современные плагины
Стандартное окно quickfix доступно только для чтения. Идея quickfix-reflector (stefandtw/quickfix-reflector.vim) проста и мощна: оно делает окно quickfix редактируемым. Вы открываете :copen, правите текст совпадений прямо в окне списка как обычный буфер, сохраняете через :w - и плагин разносит изменения обратно по исходным файлам. Это превращает результаты :grep в один редактируемый "срез проекта".
В экосистеме Neovim 2026 года этот подход развился в более мощные инструменты:
- nvim-bqf (kevinhwang91/nvim-bqf) - улучшенное окно quickfix: превью под курсором, fzf-фильтрация записей, более удобная навигация. Незаменимо, когда :grep вернул сотни результатов и их надо просмотреть перед :cfdo.
- grug-far.nvim (MagicDuck/grug-far.nvim) - де-факто стандарт интерактивного find & replace в Neovim 2026. Единый движок (ripgrep по умолчанию, опционально ast-grep для структурного поиска) и для поиска, и для замены; свободное редактирование результатов с синком обратно в файлы; интеграция с quickfix. Требует Neovim 0.11+ и ripgrep 14+. Для многих сценариев grug-far заменяет ручную связку :grep + :cfdo, давая живой предпросмотр замены. Учтите: синк правок в файлы работает только с движком ripgrep и недоступен в multiline-режиме.
Лучшие практики 2026
- Настройте grepprg на ripgrep с --vimgrep и согласуйте grepformat=%f:%l:%c:%m. Это база для :grep, :cexpr и многих плагинов. Без --vimgrep (или при рассогласованном grepformat) записи quickfix не парсятся - теряются строка и колонка, переходы ломаются.
- Используйте :grep! (с !), а не :grep для пакетной работы - иначе Vim сразу прыгнет на первый результат и неожиданно сменит буфер. Собирайте список, открывайте :copen, потом обрабатывайте.
- Для массового :substitute берите :cfdo, а не :cdo. :cfdo - раз на файл, :cdo - раз на запись. Для s///g нужен именно :cfdo, иначе файл с N совпадениями обрабатывается N раз.
- Всегда добавляйте e и | update: :cfdo s/old/new/ge | update. Флаг e подавляет E486 на файлах без совпадений, | update сохраняет изменённые буферы (сами :cdo/:cfdo не сохраняют).
- Сужайте список через :Cfilter перед заменой (:packadd cfilter) - это безопаснее ручной чистки и снимает ложные срабатывания.
- Освойте :colder/:cnewer - Vim хранит прошлые quickfix-списки (по умолчанию 10, настраивается 'chistory'; для location list - 'lhistory'). Не теряйте результат поиска, случайно перезаписав его новым :grep.
- В Neovim рассмотрите nvim-bqf для превью и фильтрации больших списков и grug-far.nvim для интерактивной замены с предпросмотром (Neovim 0.11+, ripgrep 14+).
- Настройте 'switchbuf' (useopen,uselast), чтобы :cnext/Enter не загружали каждый файл поверх текущего окна, и повесьте QuickFixCmdPost ... cwindow, чтобы :grep/:make сами открывали список.
- В Neovim стройте списки через setqflist()/setloclist() и vim.diagnostic.set*list() - словарный API даёт полный контроль над записями, а quickfixtextfunc - над форматом окна.
- На серверах и в скриптах опирайтесь на встроенный механизм (:grep/:cexpr/:cfdo/cfilter) - он есть везде и не требует плагинов. Это согласуется с практикой держать беглость в "голом" Vim там, где нельзя ставить софт.
- Помните про различие quickfix и location list: quickfix глобален, location list привязан к окну. LSP-диагностику и пер-файловые списки удобно держать в location list, проектный поиск - в quickfix.
- Путать :cdo и :cfdo. Самая частая и дорогая ошибка. s///g через :cdo обрабатывает каждый файл столько раз, сколько в нём совпадений - лишняя работа и риск повторных замен. Берите :cfdo для :substitute.
- Забыть сохранить. После :cdo/:cfdo изменения висят в незаписанных буферах. Без | update или последующего :wall они не на диске. А если буфер изменён и не может быть оставлен - команда без ! вообще упадёт.
- Пропустить флаг e. Без s///e команда :cfdo прервётся на первом файле, где паттерна нет (E486: Pattern not found), и часть файлов останется необработанной.
- grepprg без --vimgrep. Записи quickfix получаются неразбираемыми, колонка и строка теряются, :cnext не переходит куда надо.
- Запускать :grep без !. Vim прыгает на первый результат и меняет текущий буфер - неожиданно и мешает собрать полный список.
- Шелл-экранирование в :grep/:cexpr. Спецсимволы regex и пробелы в shell отличаются от того, что ждёт grepprg. В скриптах используйте shellescape().
- Ожидать, что окно quickfix редактируемо. По умолчанию оно read-only. Для правок прямо в списке нужен quickfix-reflector или grug-far.
- Удалять строки через :cdo normal dd без оглядки на сдвиг нумерации. Удаление строк смещает позиции записей; для удаления надёжнее :cfdo g/pat/d.
- Настройка. Пропишите grepprg/grepformat под ripgrep в своём конфиге. Проверьте: :grep! 'function' в любом проекте должен наполнить quickfix без прыжка, а :copen - показать плоский список file:line:col:text.
- Базовая навигация. Выполните :vimgrep /TODO/gj **/* по учебному проекту. Откройте :copen, пройдитесь по записям через :cnext/:cprevious, прыгните к конкретной через :cc 5. Настройте себе маппинги ]q/[q.
- Проектная замена. Найдите по проекту любой идентификатор и переименуйте его одной командой: :grep! 'oldName' -> :cfdo s/oldName/newName/ge | update. Сравните результат с тем, что было бы при :cdo (для файла с несколькими вхождениями).
- Фильтрация. После :grep! подключите :packadd cfilter и сузьте список: оставьте только файлы определённого каталога (:Cfilter /src/), затем выбросьте тесты (:Cfilter! /test/). Убедитесь, что :cfdo теперь затрагивает только нужные файлы.
- История списков. Сделайте два разных :grep подряд. Вернитесь к первому результату через :colder, затем снова к свежему через :cnewer. Это должно стать рефлексом.
- Location list. Повторите упражнение 2, но с :lvimgrep, :lopen, :lnext. Откройте два окна и убедитесь, что у каждого свой независимый location list, тогда как quickfix остаётся общим.
- (Продвинутое) :cdo + макрос. Запишите макрос, который что-то делает со строкой совпадения (например, оборачивает значение в вызов функции), и прогоните его через :cdo normal @q | update. Подумайте, где здесь уместен :cdo, а где :cfdo.
- Quickfix list - глобальный список позиций "файл:строка", location list - то же самое, но привязано к окну. Команды зеркальны: c для quickfix, l для location list.
- Наполняют список: :make (вывод сборки через errorformat), :grep/:lgrep (внешний поиск через grepprg/grepformat), :vimgrep/:lvimgrep (встроенный движок regex), а также :cexpr напрямую из вывода команды.
- Настройте grepprg на ripgrep с --vimgrep и grepformat=%f:%l:%c:%m - это база быстрого проектного поиска 2026 года.
- Навигация: :copen/:cclose, :cnext/:cprevious, :colder/:cnewer (история списков, по умолчанию 10 - 'chistory'/'lhistory'). Маппинги ]q/[q - стандарт.
- Массовое редактирование: :cdo - раз на запись, :cfdo - раз на файл. Для :substitute с g нужен :cfdo. Всегда добавляйте e (подавить ошибку) и | update (сохранить).
- Канонический рецепт рефакторинга: :grep! 'pat' -> (опционально :Cfilter) -> :cfdo s/old/new/ge | update.
- Для интерактива в Neovim 2026: nvim-bqf (превью/фильтрация больших списков), grug-far.nvim (find & replace с живым предпросмотром и синком в файлы); классический quickfix-reflector делает само окно quickfix редактируемым.