rq — Reference Query
rq finds the code you're looking for. Name a class, method, function, struct or constant, and rq ranks its definition first.
In a Rails checkout:
rq finds names, not behaviour. If you know what the code does but not what
it's called, use semantic search (contour) or
text search (rg) instead.
Why not grep / ctags / an LSP?
- grep / rg give every textual mention; rq gives the one place a symbol is defined, ranked.
- ctags is static and relevance-blind; rq ranks by match quality, your current repo, recency, and the branch you're on.
- an LSP is heavy — per-language, per-project, slow to warm. rq is one binary across all your repos: sub-millisecond search, warms itself on first use, and self-heals on edits.
Definitions come from Tree-sitter for Ruby, Rust, Go, Python, TypeScript, and JavaScript.
Install
Or build it yourself — rq needs Rust only at build time:
Usage
# (prefix-matched; r=ruby+rust; aliases rb/rs/ts/js)
Opening results
rq -o <query> jumps to the best match in your editor. On a terminal with
several matches it asks you to choose; otherwise it takes the top hit. The
launcher is, in order:
RQ_OPEN, a command template:{file}is the absolute path,{line}the line,{}both aspath:line. A template with none of them getspath:lineappended as its last argument.- VS Code (
code). $VISUAL, then$EDITOR.- Nothing found: rq prints
path:line.
RQ_OPEN='vim +{line} {file}' RQ_OPEN=subl
rq -w <query> does the same in the browser. It opens the match on the repo's
git host (https://<remote>/blob/<sha>/<file>#L<line>), pinned to a commit so
the link survives the branch moving: HEAD, or if HEAD isn't pushed yet, the
newest pushed commit in its history. A result from another repo (-a) links to
that host's default branch, since rq doesn't know what's checked out there. The
launcher is $BROWSER, then open/xdg-open, else rq prints the URL.
For an interactive fzf picker, script/rq-open is a small wrapper around rq.
docs/EDITORS.md covers it, the VS Code extension
(Cmd-click Go to Definition) and Neovim.
Ranking from where you are
--anchor FILE:LINE[:COL] tells rq where the question comes from — an editor's
cursor, or the file an agent is reading. Definitions in the classes and modules
enclosing that line rank first, then the same file and nearby directories.
It reorders and never filters, so it's safe whenever you know the file. FILE
is relative to the current directory; COL is accepted and ignored. rq records
no inheritance, so an inherited method gets no credit from the enclosing class.
The VS Code extension passes --anchor on every click.
For agents / scripts
-j/--json (array) and -J/--ndjson (one object per line) are the structured
surface for editors, scripts, and AI agents. Every command honors them, not just
search.
Result fields
A search result, a --show result and a --symbols row share one shape. A field
that doesn't apply is omitted, never null.
| Field | Present | Meaning |
|---|---|---|
name |
always | The symbol's name. |
kind |
always | class, module, method, function, struct, enum, trait or constant. |
language |
always | ruby, rust, go, python, typescript or javascript. |
file |
always | Path relative to root. |
root |
when rq knows a checkout for the repo (always for --symbols) |
Absolute checkout root. Per result, because -a spans repos: join root and file to read it. |
line |
always | 1-based first line of the definition. |
end_line |
when known | Last line: line..=end_line is the whole definition. |
parent |
when nested | The enclosing scope, e.g. ActiveRecord::Migration. |
visibility |
when the language expresses one | public, crate, private, protected or local (a function nested in another). |
repo |
always | Repo identity: github.com/org/repo, or local:/abs/path. |
source |
search | index, or live when the result came from a live scan of files on disk because no index pass has finished for this directory yet: one rq doesn't track, or a repo asked with --no-wait before its first index (or after --drop). The hit is real; only its ranking is provisional. See Staying current. |
confidence |
search | 0–1: match quality × how far it leads the runner-up. Near 1 means take it. |
features |
search | The scoring signals that fired, strongest first. |
signature |
when the line is non-empty | The definition's first source line, trimmed. |
body |
--show, confident match |
The full line..=end_line source. |
declarations |
when more than one | How many places declare this name (a reopened module, impl blocks across files), folded into one result. |
also_in |
with declarations |
file:line of the other declarations. |
total |
search | Matches the window was drawn from, before --limit. |
explain |
--explain |
Feature name → score contribution, in whole points. |
query |
batch mode | The stdin line this row answers. |
Misses and exit codes
A miss is one {"status": …, "query": …} object instead of results:
status |
Exit | Meaning |
|---|---|---|
no_match |
1 | Definitive: nothing by that name. |
scope_not_found |
1 | Nothing in the scope you named; found_in says where the name does live. |
warming |
2 | The index is incomplete; retry. Mostly with --no-wait, since otherwise a cold repo blocks until it can answer. |
interrupted |
2 | Indexing was stopped (Ctrl-C) before it could answer; run again. |
A match exits 0. Every miss is non-zero, so rq … && … reads as "found
something".
Errors
When a run fails under --json/--ndjson — a bad flag or value, an empty
query, an index that can't be opened, a --symbols file that doesn't exist —
stdout carries one object instead of results:
kind is stable, and code is the exit code. The message also goes to stderr.
Errors take codes from sysexits(3), so none is ever mistaken for a miss or a
retry:
kind |
Exit | Meaning |
|---|---|---|
usage |
64 | The command line is wrong: an unknown or conflicting flag, a bad value, an empty query — including flags before --json. Fix the command; it won't succeed on retry. |
not_found |
66 | A file the command names doesn't exist (--symbols). |
no_remote, launch |
69 | Nothing to hand off to: -w on a repo with no git host, or an editor or browser that won't start. |
internal |
70 | rq couldn't render its own output — a bug. |
database, index |
74 | The index can't be opened, read or written. |
Every code means one thing, so a script can branch on the number: 1 is
absent, 2 is ask again, and anything else is an error, which kind names.
rq --help lists the same table.
Batch mode
Pipe queries on stdin, one per line, with -J. rq resolves the repo and opens
the index once instead of per query, which on a large repo is most of what a
lookup costs:
|
Each row carries its query; a miss row carries its own status. The run exits
0 if any query matched, non-zero only if every one missed. --json can't frame
several result sets, so batch needs -J, and --show/--open/--web don't
apply. A cold repo is indexed up front, within the --wait budget, before the
first answer.
Reading the source
--show locates and reads in one call. When the top match's confidence is at
least 0.85 it prints the full line..=end_line span (body in JSON); otherwise
it prints the ranked list, so it never dumps a definition it isn't sure about.
Other commands
rq --status --json emits coverage rows (repo, status, files, symbols).
status is complete, or warming while the index is partial — a first index
still running, or a pass cut short that the next query continues. files and
symbols count what's indexed so far. A dropped repo is gone from --status
until a query or --index starts rebuilding it, and then reads warming.
rq --index --json emits this run's counts (files_added, symbols_added)
plus the repo's totals. rq --drop --json reports what it removed (repo,
files, symbols, dropped). Single-result commands emit one object.
These commands exit 0 whenever they ran, including when there's nothing to
report: an empty --status, an --index that found nothing new, or a --drop
of a repo that isn't indexed ("dropped": false tells you). They exit non-zero
only with an error code from the table above. --usage is the exception: with
nothing recorded yet it exits 1.
Waiting on the index
--no-wait answers from whatever's already indexed instead of waiting on a
warming repo — say, right after a branch switch on a huge repo. A miss reports
warming (exit 2) so you can retry; warming continues in a detached background
process. On a repo with no index yet it answers from a quick live scan
("source": "live"), and misses only what that scan can't reach. On a small or
fully indexed repo it answers the same as without the flag: the difference
shows only while a large index is being built.
--wait <dur> caps how long a query may wait: 50ms, 2s, 1m, or bare
seconds (--wait 0 is --no-wait). It overrides RQ_WAIT_BUDGET_MS (default 1
minute) for that call.
File outline
rq --symbols <file> lists every definition in a file, in line order — a
structural outline, not a ranked search. Honors -k/--kind and -x/--lang, and
emits the same fields as a search result, minus the scoring ones.
A file with no definitions — none at all, none of the kinds asked for, or in a
language rq doesn't parse — is a miss like a search's: {"status": "no_match"},
exit 1, so rq --symbols f && … reads as "it defines something". A file that
doesn't exist is an error (not_found, 66).
Ranking
A query is matched and scored by an additive sum of named signals, and
--explain shows the sum for each result:
)
- match quality — exact > prefix > camel/underscore abbreviation > subsequence
- visibility — public API edges out private/protected helpers (Rust
pub, Rubyprivatesections, Python_underscore, Go capitalization, TypeScript member modifiers and ESMexport), and a function nested in another ranks below every same-named definition that isn't (local) - qualifier — a scoped query (
Foo::Bar,Foo#bar,Foo.bar) keeps only the definitions inside that scope;Foo.newfinds the constructor, or the class itself when it inherits one. A package or module scope is read off the file's path, sohugolib.HugoSites,models.QuerySetandmpsc::Senderwork too - path — the query also matches the file's name
- current repo — results are scoped to the repo you're in by default
(
-a/--all-reposto search every indexed repo) - recency — symbols in recently edited or committed files
- branch — on a feature branch, files you're changing vs the trunk (and their directory neighbors) — where you're most likely working
- anchor — with
--anchor, definitions enclosing that line (enclosing), then those in the same file and nearby directories (proximity)
Fewer, better, ranked results are the goal — not completeness.
Staying current
You rarely run rq --index by hand. The first query in a git repo warms the
index, files you're changing on this branch first, and once your answer prints
a detached, low-priority process finishes the sweep in the background. A
cold repo is the exception: the first query indexes until it can answer
rather than report a false miss. On a terminal, a progress line appears if that
takes longer than half a second, and Ctrl-C stops it. It's a one-time cost — the
index persists and self-heals as you search, re-reading edited files and
reconciling added and removed ones.
A non-git directory isn't warmed on a stray query, but rq --index <dir> tracks
it like any repo under a local:<path> identity; otherwise rq live-scans it, so
it still answers at zero coverage. A --no-wait query live-scans a git repo
whose first index hasn't finished the same way. A live answer is marked
"source": "live" in JSON (text output doesn't mark it), -v notes the files it scanned and how long it took against its budget
(RQ_FALLBACK_BUDGET_MS, default 250 ms), and --usage counts these answers
apart. The index is a SQLite file at $RQ_DB
(default ~/.local/share/rq/rq.db; an empty RQ_DB means the default).
RQ_DB must be an absolute path to a file: a relative one would resolve
against each command's working directory and split the index, so rq refuses it
with a usage error (exit 64), as it does a directory, or a default path under an
unset or relative HOME.
Shell completions
Homebrew installs bash/zsh completions automatically.
Using with Claude Code
rq ships with a Claude Code skill (claude/rq-skill.md) so Claude reaches for it when locating a definition instead of grepping the tree. Install the marketplace plugin, which updates itself and brings the sibling skills:
/plugin marketplace add dpep/claude
/plugin install code@dpep
Or copy the one file:
The plugin is the better default; claude/INSTALL.md covers when it isn't.
Performance
The in-process search pipeline measures p50 ~160 µs, max < 0.25 ms on a mid-size
library (a few hundred symbols) — microseconds against a 50 ms first-answer
budget. Benchmark your own tree: make bench REPO=/path/to/repo.
Scope
rq indexes definitions — classes, modules, methods, functions. It does
not do call graphs, type inference, reference tracking, inheritance, or LSP
features. It's built for many repositories and millions of symbols, and never
assumes everything belongs to one project. Repository identity is normalized
from the git remote (github.com/org/repo), falling back to
local:/absolute/path.
See docs/ARCHITECTURE.md for the full design.
License
MIT © Daniel Pepper.