Навигация по коду: поиск, теги, символы и чтение незнакомого проекта
Весь трек до этого места был про инструменты: модальность, конфигурации, расширения, сравнение. Но настоящая работа в редакторе состоит не из набора текста. Она состоит из ответов на вопросы:
- Где это определено?
- Кто это вызывает и что сломается, если поменять сигнатуру?
- Где в этом репозитории живёт обработка платежей?
- Почему эта строка вообще появилась и кто её автор?
Скорость ответа на такие вопросы — главный практический навык, который переносится между редакторами полностью. Человек, который умеет за 20 секунд найти нужное место в чужом миллионе строк, одинаково эффективен в Vim, VS Code и IDEA. Человек, который этого не умеет, будет одинаково медленным везде, сколько бы плагинов ни поставил.
Эта статья — не про кнопки. Она про модель: три слоя поиска, у каждого своя цена, полнота и предел применимости. Разберём каждый слой до деталей реализации, потом соберём из них рабочий протокол чтения незнакомого проекта.
Почему это отдельная тема, а не строчка в настройках
Полевые исследования дают устойчивую картину: разработчик тратит на понимание кода заметно больше времени, чем на его написание. Работа Xin Xia и соавторов «Measuring Program Comprehension: A Large-Scale Field Study with Professionals» даёт около 58 % рабочего времени на активности понимания; исследование Роберто Минелли и соавторов «I Know What You Did Last Summer», основанное на записи взаимодействий с IDE, даёт близкие цифры и добавляет важную деталь: основная часть этого времени уходит не на чтение как таковое, а на навигацию — поиск того, что читать.
Посчитаем в лоб. Средний день — это порядка 30–60 вопросов «где/кто/почему». Если один вопрос стоит 10 секунд, это 5–10 минут в день. Если он стоит 2–3 минуты (открыть браузер, пойти в веб-интерфейс репозитория, поискать глазами, потерять контекст), это уже 1,5–3 часа. Разница — не в редакторе, а в том, каким слоем вы отвечаете на вопрос.
| Вопрос | Кто отвечает | Стоимость подготовки |
|---|---|---|
| Где встречается строка «payment_failed» | текстовый поиск | ноль |
Где определена функция charge_card |
теги или LSP | секунды / минуты индексации |
Кто вызывает charge_card |
LSP или GNU GLOBAL | минуты индексации |
| Что сломается, если поменять сигнатуру | LSP / модель IDE | индексация проекта |
| Где во всей организации используют этот API | серверный индекс | инфраструктура |
| Почему тут написано именно так | git log, git blame |
ноль |
Три слоя поиска
Слои различаются тем, что именно они понимают в тексте.
| Свойство | Слой 1: текст | Слой 2: теги | Слой 3: семантика |
|---|---|---|---|
| Что знает | байты и регулярки | имена и их определения | типы, области видимости, ссылки |
| Полнота | 100 % (найдёт всё) | только объявления | только то, что скомпилировалось |
| Точность | низкая (шум) | средняя (омонимы) | высокая |
| Работает на битом коде | да | почти да | нет или частично |
| Работает без настройки | да | нужен один запуск | нужен сервер и конфиг проекта |
| Цена на 1 млн строк | 0,1–1 с | 5–30 с индексации | 10 с – 10 мин индексации |
| Пределы | не различает user.id и User.ID |
не знает, кто вызывает | не видит рефлексию, DI, шаблоны конфигов |
Правило выбора звучит скучно, но экономит часы: начинайте с самого дешёвого слоя, который даёт приемлемую точность, и поднимайтесь выше, только если шум мешает.
строку или литерал?"} T -->|да| RG["Слой 1: ripgrep / git grep
ответ за секунду"] T -->|нет| S{"Нужно определение
символа?"} S -->|да| L{"Есть рабочий
LSP для языка?"} L -->|да| LSP["Слой 3: go-to-definition
точный ответ"] L -->|нет| TAGS["Слой 2: ctags / gtags
ответ без сервера"] S -->|нет| U{"Нужны все использования
и последствия правки?"} U -->|да| REF["Слой 3: find references,
call hierarchy"] U -->|нет| STR{"Нужен шаблон
по структуре кода?"} STR -->|да| AST["Слой 1.5: ast-grep / comby / Semgrep"] STR -->|нет| SCALE{"Искать за пределами
одного репозитория?"} SCALE -->|да| IDX["Серверный индекс:
Zoekt / Sourcegraph"] SCALE -->|нет| RG RG --> N{"Шума слишком много?"} N -->|да| LSP N -->|нет| DONE["Ответ получен"]
Слой 1: текстовый поиск, который стоит настроить один раз
Почему ripgrep быстрый
ripgrep стал стандартом де-факто не потому, что «написан на Rust». Разбор автора, «ripgrep is faster than {grep, ag, git grep, ucg, pt, sift}», объясняет реальные причины, и они полезны как модель того, из чего вообще состоит скорость поиска:
- Меньше файлов. Обход по умолчанию уважает
.gitignore, пропускает скрытые и бинарные файлы. Не искать вnode_modules— это не оптимизация, это отказ от 90 % работы. - Параллельный обход. Директории читаются в несколько потоков, поиск идёт по мере поступления файлов; вывод сериализуется отдельно, чтобы порядок оставался детерминированным.
- Быстрый поиск литералов. Регулярка разбирается, из неё извлекаются обязательные
литералы, и по ним работает
memchrна SIMD-инструкциях, а для нескольких литералов — алгоритм Teddy. Полный автомат запускается только на кандидатах. - Конечные автоматы без обратных ссылок. Синтаксис регулярок ограничен так, чтобы гарантировалось линейное время, — никакого катастрофического бэктрекинга. Про машинерию за этим — алгоритмы на строках.
Практический вывод: скорость поиска — это на 80 % то, сколько байт вы не прочитали. Любой инструмент станет быстрым, если правильно исключить мусор, и любой станет медленным на репозитории со сборочными артефактами внутри дерева.
Рабочий набор команд
# Базовое: поиск по проекту с уважением .gitignore
rg 'chargeCard'
# Только в файлах определённого типа (список типов: rg --type-list)
rg -t py -t go 'timeout'
# Только по путям, подходящим под glob (можно несколько -g, отрицание через !)
rg 'TODO' -g '*.ts' -g '!*.spec.ts'
# Целое слово, без учёта регистра, с контекстом в 3 строки
rg -w -i -C 3 'retry'
# Умный регистр: строчный запрос ищет без учёта регистра, запрос с заглавной — точно
rg -S 'UserRepository'
# Только имена файлов — дальше по конвейеру
rg -l 'deprecated' | xargs wc -l | sort -n | tail
# Фиксированная строка, а не регулярка (спасает при поиске "$", "(", "[")
rg -F 'config["db.host"]'
# Мультистрочный поиск: аннотация и следующая за ней сигнатура
rg -U --multiline-dotall '@Transactional.*?public .*?\('
# Заменить в выводе (только показать, файлы не трогает) — черновик правки
rg 'assertEquals\((.*?), (.*?)\)' -r 'assertThat($2).isEqualTo($1)'
# Смотреть даже в игнорируемых и скрытых файлах: -u -uu -uuu по нарастающей
rg -uu 'API_KEY' # включая .gitignore-файлы и скрытые
rg -uuu 'API_KEY' # плюс бинарные
# Машинный вывод для интеграций: одна JSON-строка на событие
rg --json 'panic!' | head -5
# Сколько совпадений по файлам — быстрый способ увидеть, где «горячо»
rg -c 'logger\.' | sort -t: -k2 -rn | head
Файл .ignore в корне репозитория работает как .gitignore, но только для поисковых
инструментов (rg, fd) и не попадает в правила Git.
Это правильное место для «не ищи здесь, но храни в репозитории»: снапшоты тестов,
сгенерированные клиенты API, вендоренные зависимости.
# .ignore — не влияет на git, влияет на rg/fd
/vendor/
/**/__snapshots__/
/api/generated/
*.min.js
*.lock
Измерьте свой репозиторий, а не чужой
# Сколько вообще кода: tokei честнее, чем find | wc -l
tokei
# Честное сравнение поиска на вашем дереве (hyperfine прогревает кэш ФС)
hyperfine --warmup 3 \
"rg 'chargeCard' --no-messages" \
"git grep -n 'chargeCard'" \
"grep -rn 'chargeCard' ."
# Где именно лежат байты — типичный ответ: сборочный каталог, который забыли исключить
du -sh -- * .[!.]* 2>/dev/null | sort -h | tail -10
Ориентир: на репозитории в 1 млн строк rg укладывается в 100–300 мс на холодном кэше и
в десятки миллисекунд на прогретом. Если у вас секунды — ищете по мусору.
Когда git grep лучше
git grep знает про индекс Git и умеет то, чего не
умеет обычный поиск по файлам:
# Искать в состоянии на конкретной ревизии, не переключая ветку
git grep -n 'legacy_flag' v2.14.0
# Искать во всех ветках сразу
git grep -n 'legacy_flag' $(git for-each-ref --format='%(refname)' refs/heads)
# Когда это появилось: поиск по истории изменений строки
git log -S 'legacy_flag' --oneline --pickaxe-regex
# Кто и когда трогал строку, игнорируя переформатирование и перемещения кода
git blame -w -C -C -L 120,140 src/billing.py
Пара -S (pickaxe) и git log -L закрывает вопрос «почему тут так написано» лучше любого
семантического индекса: код не объясняет намерение, а коммит и связанный с ним PR — часто
объясняют. Подробнее про эти инструменты — в статьях
ежедневного цикла Git.
Интеграция в редакторы
" Vim/Neovim: подменяем grep на rg, чтобы :grep наполнял quickfix
set grepprg=rg\ --vimgrep\ --smart-case
set grepformat=%f:%l:%c:%m
" дальше работает знакомый конвейер quickfix из статьи про продвинутый Vim
// VS Code: settings.json — исключения для поиска и для наблюдателя за ФС
{
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/*.min.js": true,
"**/__snapshots__": true
},
"search.followSymlinks": false,
"files.watcherExclude": {
"**/target/**": true,
"**/node_modules/**": true
}
}
Разница между тремя настройками VS Code, которую регулярно путают: files.exclude прячет
файлы из дерева проекта, search.exclude убирает их из поиска (наследует files.exclude),
files.watcherExclude снимает нагрузку с механизма слежения за файловой системой — именно
он чаще всего съедает процессор на больших монорепозиториях. В JetBrains тот же эффект
даёт «Mark Directory as → Excluded» и настройка Scopes, о чём подробнее было в статье про
JetBrains.
Предел слоя: почему grep-driven рефакторинг опасен
Классический сценарий: нужно переименовать метод run в одном классе. Текстовый поиск даёт
4000 совпадений, из которых нужны 12. Обратная ошибка страшнее: поиск по имени поля id
находит десятки тысяч мест и создаёт иллюзию, что «переименовать невозможно», хотя
семантический слой отвечает за 300 мс и показывает ровно 40 использований.
Текст не различает: одноимённые методы разных классов, строки в комментариях, совпадения в тестовых данных, имена в чужих языках внутри одного репозитория. Поэтому правило простое: текстовый поиск — чтобы найти, семантический — чтобы менять.
Слой 2: теги — переносимый индекс имён
Тегам сорок лет, и они регулярно оказываются единственным работающим инструментом там, где LSP не заводится. Идея проста: пройти по исходникам примитивным парсером и построить плоский файл «имя → файл → место».
# Установка: universal-ctags (не путать с древним exuberant-ctags)
ctags --version | head -1
# Генерация индекса по проекту
ctags -R --exclude=node_modules --exclude=.git --exclude='*.min.js' .
# Только нужные языки и с полями, которые понимает Vim
ctags -R --languages=Python,Go,TypeScript --fields=+n --extras=+q .
# Обновление в фоне после каждого переключения ветки: .git/hooks/post-checkout
# #!/bin/sh
# ctags -R -f .git/tags.new . && mv .git/tags.new .git/tags
Формат файла tags — текст, отсортированный, с поиском бинарным перебором:
chargeCard src/billing/gateway.go /^func chargeCard(ctx context.Context/;" f
ChargeError src/billing/errors.go /^type ChargeError struct/;" t
Ограничение принципиальное: теги знают только объявления. Вопрос «кто вызывает» они не закрывают. Его решает GNU GLOBAL, который строит двусторонний индекс, и старый cscope для C:
# GNU GLOBAL: индекс определений и ссылок
gtags # построить
global -x chargeCard # где определено
global -rx chargeCard # кто ссылается
global -gx 'TODO.*billing' # grep по индексу
# Инкрементальное обновление (быстрее полной пересборки)
global -u
Где теги в 2026 году по-прежнему правильный выбор:
- Языки без живого языкового сервера: внутренние DSL, Tcl, старый Perl, ассемблер, конфигурационные форматы с собственным синтаксисом.
- Огромные монорепозитории, где языковой сервер не выдерживает: индекс тегов линеен и строится за минуты даже на десятках миллионов строк.
- Работа по SSH на стенде, где нельзя ставить тулчейн:
ctagsесть в любом дистрибутиве, весит мегабайт и не требует компилятора. - Смешанные репозитории: один индекс на Python, Go, SQL-миграции и shell-скрипты, тогда как LSP пришлось бы поднимать четыре.
" Vim: переход по тегу и стек возвратов
" Ctrl-] — перейти к определению под курсором
" Ctrl-t — вернуться назад по стеку тегов
" :tselect — выбрать из нескольких одноимённых
" g Ctrl-] — сразу список, если совпадений больше одного
set tags=./tags,tags,.git/tags
В Emacs ту же роль играет etags и универсальный интерфейс xref (M-. / M-,),
причём xref одинаково работает поверх тегов и поверх LSP — редкий пример честной
абстракции над обоими слоями.
Слой 1.5: структурный поиск
Между «текст» и «полная семантика» есть промежуточный слой: поиск по синтаксическому дереву. Он не знает типов, но знает, что такое вызов функции, аргумент и блок.
# ast-grep: найти пустые catch-блоки в TypeScript
ast-grep --pattern 'try { $$$ } catch ($E) { }' --lang ts
# Переписать assertEquals в assertThat по всему проекту
ast-grep --pattern 'assertEquals($A, $B)' \
--rewrite 'assertThat($B).isEqualTo($A)' --lang java --update-all
# comby: то же самое на языке шаблонов, работает на десятках языков
comby 'assertEquals(:[a], :[b])' 'assertThat(:[b]).isEqualTo(:[a])' .java -i
# semgrep: правила как код, удобно вешать в CI
semgrep -e 'requests.get($URL, verify=False)' --lang python .
Структурный поиск закрывает ровно тот класс задач, где текст даёт слишком много шума, а семантика недоступна или слишком дорога: массовые миграции API, поиск антипаттернов, проверки в CI. Semgrep, ast-grep и comby отличаются деталями синтаксиса, но модель одна.
Внутри IDE тот же слой называется Structural Search and Replace — он разобран в статье про JetBrains; а о том, как устроены парсеры, дающие такие деревья, — в статье про инструменты языка.
Слой 3: семантика через LSP
Языковой сервер отвечает не на вопрос «где встречается строка», а на вопрос «что это за символ и где он ещё используется». Методы протокола, которые стоит знать по именам, потому что в разных редакторах они называются по-разному, а в логах — одинаково:
| Метод LSP | Что спрашивают руками | Типичная привязка |
|---|---|---|
textDocument/definition |
«где определено» | gd, F12, Ctrl+B |
textDocument/typeDefinition |
«какого это типа» | gy, Ctrl+Shift+B |
textDocument/implementation |
«кто реализует интерфейс» | gI, Ctrl+Alt+B |
textDocument/references |
«кто использует» | gr, Shift+F12, Alt+F7 |
textDocument/documentSymbol |
структура текущего файла | Ctrl+Shift+O, Ctrl+F12 |
workspace/symbol |
«найти класс/функцию по имени» | Ctrl+T, Ctrl+N |
callHierarchy/incomingCalls |
«цепочка вызовов сюда» | Ctrl+Alt+H |
typeHierarchy/supertypes |
«иерархия наследования» | Ctrl+H |
ответы приходят частичными или с задержкой
Где семантика врёт или молчит
Языковой сервер видит только то, что выражено в языке. Он не видит связей, созданных:
- рефлексией и внедрением зависимостей (Spring, декораторы Python, DI-контейнеры JS);
- строковыми идентификаторами: имена маршрутов, ключи конфигов, названия очередей, имена таблиц и колонок в SQL внутри строк;
- шаблонизаторами и генерацией кода:
.proto, OpenAPI, ORM-миграции; - динамическими обращениями
getattr(obj, name),obj[method]().
Именно поэтому серьёзный рефакторинг делают в два прохода: сначала семантический (переименование, изменение сигнатуры), потом текстовый — по строковым литералам, конфигам и шаблонам. Пропуск второго прохода — самый частый способ уронить прод «безопасным переименованием».
LSP на большом репозитории
// Пример: ограничиваем корни проектов, чтобы сервер не индексировал весь монорепо
// gopls (VS Code / Neovim)
{ "gopls": { "build.directoryFilters": ["-node_modules", "-vendor", "-third_party"] } }
-- rust-analyzer: явный список Cargo-проектов вместо автопоиска по всему дереву
require('lspconfig').rust_analyzer.setup({
settings = {
['rust-analyzer'] = {
linkedProjects = { 'services/billing/Cargo.toml', 'libs/core/Cargo.toml' },
cargo = { allFeatures = false },
-- проверка кластера крейтов при сохранении — самая дорогая операция
checkOnSave = { command = 'clippy', extraArgs = { '--no-deps' } },
},
},
})
Для TypeScript тот же приём — references в tsconfig.json и project references вместо
одного гигантского проекта. Практика такая же, как с индексами в базе данных: индексировать
всё дорого, индексировать нужное — дёшево. Про специфику больших репозиториев в целом —
монорепозитории в треке Git.
Масштаб: когда локальные инструменты кончаются
Порядок величин, на который стоит ориентироваться:
| Размер | Что работает | Что перестаёт работать |
|---|---|---|
| до 1 млн строк | rg, LSP, IDE — всё |
ничего |
| 1–10 млн строк | rg с исключениями, теги, LSP с ограниченными корнями |
полная индексация IDE «всего сразу» |
| десятки млн строк, один репозиторий | теги, серверный индекс, частичный чекаут | локальный LSP на весь репозиторий |
| тысячи репозиториев | только серверный индекс | всё локальное |
Серверные системы поиска строятся на триграммном индексе: для каждого трёхсимвольного фрагмента хранится список документов, где он встречается. Регулярное выражение транслируется в булев запрос по триграммам, который сначала отсекает 99,9 % документов, а полный автомат запускается только на оставшихся. Классическое объяснение — статья Расса Кокса «Regular Expression Matching with a Trigram Index», описывающая устройство Google Code Search. На этой же идее построены Zoekt, livegrep и Hound; GitHub описал свою реализацию в статье «The technology behind GitHub’s new code search».
Цена такого индекса — диск (обычно от 20 до 50 % от размера исходников) и свежесть: индекс всегда отстаёт от HEAD на минуты или часы. Для «где в компании ещё используют этот устаревший метод» это приемлемо; для «что сломает мой текущий коммит» — нет.
Fuzzy-поиск и путь чтения
Отдельный класс инструментов отвечает не на вопрос «где», а на вопрос «дай мне быстро добраться туда, где я уже был или про что я примерно помню».
# fzf: интерактивный фильтр над любым списком
# Ctrl-T — файлы, Ctrl-R — история команд, Alt-C — переход по каталогам
# Живой поиск по содержимому: перезапуск rg на каждое нажатие
INITIAL='rg --column --line-number --no-heading --color=always --smart-case'
fzf --ansi --disabled --query '' \
--bind "start:reload:$INITIAL {q}" \
--bind "change:reload:sleep 0.05; $INITIAL {q} || true" \
--delimiter : \
--preview 'bat --color=always --highlight-line {2} {1}' \
--preview-window 'up,60%,+{2}/2'
Механика скоринга у fzf и его аналогов одинакова по
смыслу: за совпадение в начале слова, после разделителя или на границе CamelCase даётся
бонус, за разрывы — штраф, за совпадение в имени файла — бонус больше, чем за совпадение
в пути. Отсюда практический приём: набирайте не подстроку, а «скелет» — usrepo находит
user/repository.py надёжнее, чем repo.
Не менее важен путь чтения — механизм возврата туда, где вы были:
| Механизм | Vim/Neovim | VS Code | JetBrains | Emacs |
|---|---|---|---|---|
| Назад по переходам | Ctrl-o / Ctrl-i |
Alt+← / Alt+→ | Ctrl+Alt+← | M-, / mark ring |
| Последние файлы | :browse oldfiles |
Ctrl+Tab (MRU) | Ctrl+E | recentf |
| Места последних правок | g; / g, |
Ctrl+K Ctrl+Q | Ctrl+Shift+Backspace | goto-last-change |
| Именованные закладки | marks ma / `a |
Bookmarks | Ctrl+F11 | registers |
| Быстрый набор рабочих файлов | harpoon | Pinned tabs | Favorites | bookmark+ |
Если из всей статьи вы возьмёте одну привычку — пусть это будет «назад по переходам». Ощущение «потерялся в чужом коде» почти всегда означает, что человек не умеет вернуться.
Протокол чтения незнакомого репозитория за 90 минут
# 1. Периметр: масштаб и языки
tokei # сколько кода и на чём
ls -1 | head -30 # верхний уровень как оглавление
fd -H -d 2 -e md # где документация, ADR, RFC
# 2. Форма: точки входа. Ищем не «главный файл», а границы.
rg -l 'func main\(|if __name__|public static void main|createApp|FastAPI\('
fd -e Dockerfile -e docker-compose.yml -e Makefile -e justfile
rg -n 'route|@app\.|@RestController|http\.HandleFunc' -g '!**/test/**' | head -40
# 3. История: где сосредоточена активность за год (hotspots)
git log --since='1 year ago' --pretty=format: --name-only \
| sed '/^$/d' | sort | uniq -c | sort -rn | head -20
# кто в теме — к кому идти с вопросами по этому модулю
git shortlog -sne --since='1 year ago' -- src/billing | head
# 4. Срез: один сценарий сверху вниз. Начинаем с теста, а не с кода.
rg -l 'billing|payment' --glob '**/test*/**' | head
Порядок здесь не случаен. Чтение «слева направо по алфавиту каталогов» проваливается всегда. Работает противоположное: сначала внешние границы (что система принимает и что отдаёт), потом один вертикальный срез — путь конкретного запроса от входа до базы данных, — и только потом вширь. Отладчик в роли навигатора недооценён: один прогон реального сценария с точкой останова даёт более точную карту вызовов, чем полчаса чтения, потому что показывает фактический путь, а не возможный.
Заметки на этом этапе — не бюрократия, а инструмент: через два дня вы забудете 70 % того, что поняли сегодня. Механику внешней памяти подробно разбирает трек про обучение — заметки и внешняя память и как учиться по чужому коду.
Бюджеты и диагностика медленного поиска
Ориентиры, при которых навигация не разрушает поток мышления:
| Операция | Приемлемо | Плохо |
|---|---|---|
| Поиск по проекту (текст) | до 300 мс | больше 1 с |
| Открытие файла по имени (fuzzy) | до 100 мс | больше 500 мс |
workspace/symbol |
до 200 мс | больше 1 с |
| Go to definition | до 150 мс | больше 500 мс |
| Find references на среднем символе | до 1 с | больше 5 с |
Чек-лист, когда медленно:
- Мусор в дереве.
du -sh */ | sort -h | tail— и в исключения:node_modules,target,build,dist,.venv,vendor, снапшоты, сгенерированные клиенты. - Гигантские файлы.
fd -S +5mнаходит пятимегабайтные JSON и минифицированный JS, на которых умирает и подсветка, и поиск. - Сетевая или виртуализованная ФС. Поиск по SMB/NFS или по смонтированному тому Docker на macOS медленнее локального на порядок; лечится переносом дерева внутрь виртуальной машины или контейнера — механика описана в статье про VS Code и devcontainers.
- Антивирус и системный индексатор. Внесите каталоги проектов в исключения Defender, Spotlight и аналогов — на Windows это регулярно даёт двукратную разницу.
- Watchers. На больших деревьях следящий за файлами процесс съедает CPU и упирается
в лимит
inotify:sysctl fs.inotify.max_user_watchesна Linux. - Языковой сервер индексирует лишнее. Ограничьте корни проектов, отключите анализ зависимостей, где он не нужен.
Типичные ошибки
- Начинать с самого умного слоя. Ждать, пока IDE проиндексирует 40 миллионов строк,
чтобы найти строковый литерал, который
rgнашёл бы за 200 мс. - Менять код по результатам текстового поиска. Работает до первого омонима; на
переименовании поля
idзаканчивается инцидентом. - Забывать про второй проход. Семантический рефакторинг не трогает строковые идентификаторы, конфиги, шаблоны и SQL — их надо искать текстом отдельно.
- Не исключать сгенерированный код. Половина совпадений в мёртвых файлах — и поиск бесполезен, и внимание израсходовано.
- Искать в вебе то, что есть локально. Веб-интерфейс репозитория удобен для ссылки коллеге, но по скорости проигрывает локальному поиску на порядок.
- Не уметь возвращаться. Без стека переходов человек боится «уйти далеко» и читает поверхностно.
- Полагаться на индекс, который отстал. Серверный поиск показывает состояние многочасовой давности; для проверки собственного изменения он не годится.
- Настраивать fuzzy-поиск и не настраивать исключения. Красивый интерфейс поверх
перебора
node_modulesостаётся переборомnode_modules.
Мини-итог
- Навигация — не настройка редактора, а отдельный навык, который переносится между инструментами целиком и определяет скорость работы больше, чем скорость набора.
- Слоёв три: текст (полнота, ноль подготовки, шум), теги (имена, дёшево, переносимо), семантика (точность, требует рабочего проекта). Плюс структурный поиск между первым и третьим.
- Скорость текстового поиска — это на 80 % то, сколько байт вы не читаете: исключения важнее выбора инструмента.
- Теги не устарели: языки без LSP, монорепозитории, работа по SSH и смешанные репозитории — их территория.
- Семантика не видит рефлексию, DI, строковые идентификаторы и шаблоны, поэтому любой массовый рефакторинг делается в два прохода.
- За пределами одного репозитория работает только серверный триграммный индекс — с ценой в виде диска и отставания от HEAD.
- Умение вернуться назад по стеку переходов даёт больше, чем любой плагин поиска.
- Незнакомый проект читается вертикальным срезом одного сценария, а не обходом каталогов по алфавиту.
Источники
- ripgrep is faster than {grep, ag, git grep, ucg, pt, sift} — разбор архитектуры поиска от автора ripgrep.
- Regular Expression Matching with a Trigram Index — Расс Кокс о том, как устроен индексируемый поиск по регуляркам.
- The technology behind GitHub’s new code search — инженерное описание поиска по масштабу всей платформы.
- Language Server Protocol Specification 3.17 — первоисточник по методам навигации.
- Universal Ctags и GNU GLOBAL — документация индексаторов имён.
- ast-grep, comby, Semgrep — структурный поиск и массовые переписывания.
- fzf и fd — интерактивный фильтр и быстрый обход файловой системы.
- Measuring Program Comprehension: A Large-Scale Field Study with Professionals — полевые данные о доле времени на понимание кода.
Что дальше
Мы научились быстро находить нужное место. Следующий естественный шаг — перестать делать руками то, что нашлось: превратить повторяющуюся последовательность действий в сниппет, задачу или собственную команду редактора. В следующей статье — лестница автоматизации от макроса до собственного расширения, честный расчёт окупаемости и правило, по которому логика живёт в CLI, а редактор остаётся тонкой обёрткой.
Автоматизация редактора: сниппеты, задачи, свои команды и плагины