rq — Reference Query
rq finds where a symbol is defined and ranks the one you meant to the top. Ask for a name and you get the single most-likely definition first — a class, method, function, struct — not every line that mentions it. Navigation, not enumeration.
Search is the default action — rq <query>, no subcommand. Every operation is a flag (--index, --status, --symbols), so no word is reserved: rq index searches for a symbol named "index" like any other query. The feel is rg/fd: type a name, get an answer.
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 what you've opened before.
- an LSP is heavy — per-language, per-project, slow to warm. rq is one fast binary across all your repos: in-process search at
rgspeed (sub-millisecond), warms itself on first use, self-heals on edits, and learns from the results you actually open.
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 straight to the best match in your editor and records the
pick, so ranking learns which result you actually wanted. On a terminal with
several matches it prompts you to choose; otherwise it takes the top hit. The
launcher is resolved in order: RQ_OPEN (a command template with {file},
{line}, or {} = path:line), then VS Code (code), then $VISUAL/$EDITOR,
and failing all that it just prints the resolved path:line.
RQ_OPEN='vim +{line} {file}'
For an interactive fzf picker (or to wire a custom flow), script/rq-open is a
small reference wrapper around rq + rq --record.
For agents / scripts
-j/--json (array) and -J/--ndjson (one object per line) are the structured
surface for editors, scripts, and AI agents. Each result is an object with
name, kind, language, file, line, end_line (the definition's last
line — read line..=end_line for the whole span), parent, repo,
confidence (0–1: match quality × how much it leads the runner-up), features
(the scoring signals, strongest first), and signature (the definition's source
line, so you can judge a result without opening the file). On a miss, JSON
returns a {"status": …} object instead of results — no_match (definitive),
warming (index incomplete, retry), or interrupted. Exit codes mirror it: 0
matched, 1 no match, 2 no match yet (warming). All non-zero, so
rq … && … is unchanged.
--show fetches the definition's source in one step: when the top match is
confident it prints the full line..=end_line span (a body field in JSON),
else it falls back to the ranked list — so it never dumps a definition it isn't
sure about.
Pass --no-wait for a strictly non-blocking query — it answers from whatever's
already committed instead of waiting on a warming repo. Handy for agents/scripts:
a query issued while a background reindex is rewriting the index (say, right
after a branch switch on a huge repo) returns at once rather than blocking up to
the wait budget; a miss reports warming (exit 2) so the caller can retry, and
leftover warming still continues in a detached background child.
--wait <dur> sets how long a query may wait for the index to warm before
answering with whatever's committed — a duration like 50ms, 2s, 1m, or a
bare number of seconds (--wait 0 is the same as --no-wait). It overrides
RQ_WAIT_BUDGET_MS (default 1 minute) for that one call.
--json/--ndjson work for every command, not just search: rq --status --json
emits the coverage rows (repo, status, files, symbols), rq --index --json
emits this run's counts plus the index totals, and rq --drop --json reports what
it removed (repo, files, symbols, dropped). Single-result commands emit
one object; --ndjson is the compact one-line form.
Narrow with --path when you know the area:
Pass --no-record for speculative/agent searches so they don't perturb the
learned ranking (which is meant to reflect deliberate, human picks).
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 --json/--ndjson like everything else.
Each result is a navigable path:line. --explain shows the additive score:
Ranking
Symbols come from Tree-sitter (Ruby, Rust, Go, Python, TypeScript, JavaScript; the core is language-agnostic). A query is matched and scored by an additive, explainable sum of signals:
- 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) - qualifier — a scoped query (
Foo::Bar) prefers the definition inside that scope - path — the query also matches the file's name
- current repo — results are scoped to the repo you're in by default
(
--all-reposto search every indexed repo) - recency — symbols in recently-edited or recently-committed files
- branch — on a feature branch, files you're changing vs the trunk (and their directory neighbors) — where you're most likely working
- learned — results you've opened before for this query (see below)
Returning fewer, better, ranked results is the goal — not completeness.
Staying current
You rarely run rq --index by hand. The first query in a git repo warms the
index opportunistically — files you're changing on this branch first — and once
your answer prints, a detached, low-priority child finishes the sweep in the
background, so coverage completes without delaying your shell. A cold repo
is the exception: the first query blocks and indexes until it can answer
(progress shown, Ctrl-C to stop) rather than lie with a false miss. It's a
one-time cost — the index persists and self-heals as you search, re-reading
edited files and reconciling added/removed ones on the warm sweep.
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. The index is a SQLite file at $RQ_DB
(default ~/.local/share/rq/rq.db).
Learning from what you pick
Ranking improves as you use it. rq logs each search; a thin hook reports which
result you opened, so a learned boost lifts it next time:
A pick for a shorter query (ref) also informs longer ones (refund), and
repeating a search without opening anything is read as a miss — that query's
learned boost decays so a stale favorite stops dominating.
The wrapper script/rq-open does search → pick → open →
record in one step. See docs/EDITORS.md for VS Code and
Neovim — it's just rq plus rq --record, no socket.
Shell completions
Homebrew installs bash/zsh completions automatically.
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 useful with definitions alone. 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.