pub trait PickerSourceGenerator: Send + Sync {
// Required methods
fn spec(&self) -> &PickerSourceSpec;
fn init(
&self,
ctx: &PickerContext<'_>,
args: &[String],
) -> SourceResult<PickerInitResult>;
fn accept(
&self,
ctx: &PickerContext<'_>,
routing: &RoutingPayload,
) -> SourceResult<PickerAcceptOutcome>;
// Provided methods
fn accept_async(
&self,
_ctx: &PickerContext<'_>,
_routing: &RoutingPayload,
) -> Option<AcceptFuture> { ... }
fn on_query_changed(
&self,
_ctx: &PickerContext<'_>,
_query: &str,
) -> Option<SourceResult<PickerInitResult>> { ... }
fn descend(
&self,
_ctx: &PickerContext<'_>,
_candidate: &RawCandidate,
) -> Option<String> { ... }
fn ascend(&self, _query: &str) -> Option<String> { ... }
fn accept_navigates(
&self,
_ctx: &PickerContext<'_>,
_candidate: &RawCandidate,
) -> Option<String> { ... }
fn initial_query(&self, _args: &[String]) -> Option<String> { ... }
fn preview(
&self,
_ctx: &PickerContext<'_>,
_routing: &RoutingPayload,
) -> Option<PickerPreviewOutcome> { ... }
fn preview_debounce(&self) -> Option<Duration> { ... }
fn owner_plugin(&self) -> Option<u64> { ... }
}Expand description
Source generators implement this trait. Registered into
PickerRegistry at boot; the :picker <source>
dispatcher looks up by spec().id and calls init to
obtain candidates, then accept to translate the user’s
chosen routing payload into a typed outcome.
Lifetime story. init and accept borrow &self
(so the generator must be Sync) and the
PickerContext<'_> for the duration of the synchronous
call. The borrow is released the moment the method
returns; any captured async work owns its own clones
(extract URIs, positions, Arc handles, etc. into the
closure during the sync prelude).
Send + Sync. The registry stores generators as
Arc<dyn PickerSourceGenerator> and shares them across
the App + tokio task threads. Both bounds are required.
Required Methods§
Sourcefn spec(&self) -> &PickerSourceSpec
fn spec(&self) -> &PickerSourceSpec
Generator metadata. Returned by reference so the
registry can stamp it into :describe-picker /
:picker <Tab> listings without cloning.
Sourcefn init(
&self,
ctx: &PickerContext<'_>,
args: &[String],
) -> SourceResult<PickerInitResult>
fn init( &self, ctx: &PickerContext<'_>, args: &[String], ) -> SourceResult<PickerInitResult>
Build the candidate set. Sync prelude: read what’s
needed from ctx, clone into async captures if
necessary, return the appropriate PickerInitResult
variant. Synchronous errors (no active buffer when
one was required, args validation failure) return
Err; the host echoes the error and leaves the
picker closed.
args is the tokens the user typed after the source
id (:picker files /tmp/foo -> &["/tmp/foo"]).
Sources interpret them against their declared
PickerSourceSpec::args_schema; the grammar layer
doesn’t pre-validate because per-source arg shapes
vary too much.
Sourcefn accept(
&self,
ctx: &PickerContext<'_>,
routing: &RoutingPayload,
) -> SourceResult<PickerAcceptOutcome>
fn accept( &self, ctx: &PickerContext<'_>, routing: &RoutingPayload, ) -> SourceResult<PickerAcceptOutcome>
Translate the user’s chosen routing payload into a
typed PickerAcceptOutcome the host applies. The
generator owns the mapping from its emitted
routing-payload variant(s) to outcome(s); mismatch
returns Err, which the host echoes.
Provided Methods§
Sourcefn accept_async(
&self,
_ctx: &PickerContext<'_>,
_routing: &RoutingPayload,
) -> Option<AcceptFuture>
fn accept_async( &self, _ctx: &PickerContext<'_>, _routing: &RoutingPayload, ) -> Option<AcceptFuture>
Async-accept hook. Default None means “this source resolves accept
synchronously” — every native source takes this path and is unchanged.
A source whose accept translation requires off-thread work returns
Some(future); the host spawns it (never blocking the actor thread) and
applies the resolved outcome via the pending-accept drain, exactly the
way init’s PickerInitResult::Future is drained. The
motivating case is a WASM plugin source (lattice-plugin-host): its
guest accept export is async and bound to the plugin’s actor task, so
there is no synchronous path from the keystroke to plugin code
(paramount #4) — a slow/hostile plugin accept can never freeze the UI.
Like init, this is a synchronous prelude: read what’s needed from
ctx + routing, project/clone into the returned 'static future, and
release the borrows. A source that returns Some here still implements
the sync accept (the host prefers accept_async when
it returns Some, so that body is the fallback / tripwire only).
Sourcefn on_query_changed(
&self,
_ctx: &PickerContext<'_>,
_query: &str,
) -> Option<SourceResult<PickerInitResult>>
fn on_query_changed( &self, _ctx: &PickerContext<'_>, _query: &str, ) -> Option<SourceResult<PickerInitResult>>
Live-source hook: re-fetch candidates when the user’s
query changes. Default None means “static source –
host uses fuzzy refilter over the candidate set
returned by init”. Returning Some(result) from a
source whose spec().live == true replaces the
picker’s candidate set wholesale; the host wires up
debouncing and cancels any in-flight invocation when a
fresh query arrives.
The host calls this only after the picker has been
seated by init. Sources that opt in MUST set
spec().live = true; the two declarations stay
paired because the picker decides whether to bypass
fuzzy-refilter purely from spec().live, while the
host decides whether to invoke this hook from the same
flag.
Sourcefn descend(
&self,
_ctx: &PickerContext<'_>,
_candidate: &RawCandidate,
) -> Option<String>
fn descend( &self, _ctx: &PickerContext<'_>, _candidate: &RawCandidate, ) -> Option<String>
PC.10: <C-l> — refine the query from the selected candidate instead
of accepting it.
For a source whose candidates are containers — a directory, and later
perhaps a namespace or a tree node — “go into this” and “choose this”
are different questions, and a picker with only <CR> can ask one of
them. Returning Some(query) replaces the picker’s query and re-lists;
the picker stays open and nothing is accepted.
None is the default and means “this source has no notion of going
deeper”, which is every source but dir-pick today. <C-l> in those
pickers does nothing at all rather than doing something approximate —
a key that means “descend” in one picker and “almost descend” in
another is worse than one that means nothing in the second.
Only meaningful for a live source: a static source’s candidate set
comes from init and is fuzzy-refiltered, so rewriting the query
filters the same rows rather than fetching new ones. The default
keeps every such source out of this path entirely.
Sourcefn ascend(&self, _query: &str) -> Option<String>
fn ascend(&self, _query: &str) -> Option<String>
PC.10: <C-w> (<C-h> until PH.1) — descend’s peer, one level out.
Takes the QUERY rather than a candidate: going up is a statement about where you are, and the row you happen to have selected has nothing to do with it.
A hook rather than a generic query edit, which is a deviation from
this slice’s plan and the reason is grep. The plan reasoned that
“delete back through the previous /” needs no source involvement
because it is generic over any path-shaped query — true, but live is
not the same as path-shaped, and grep is live. A generic <C-h>
would silently truncate a grep pattern at a slash. Opting in keeps
<C-h> meaningless everywhere it has no meaning, which is the same
rule descend follows.
PP.3: <CR> on a row that is a SIGNPOST rather than a destination.
Returning Some(query) means “this row is navigation”: the host
replaces the query and re-lists, the picker stays open, and nothing is
accepted — no outcome resolved, no MRU recorded, no PickerAccepted
published, because none of that happened.
Distinct from descend, which it resembles.
descend answers for every row that contains things, and its key
(<C-l>) asks “go into the thing I have selected”. This answers for
rows that are not things at all. dir-pick’s ../ is the whole
motivating case: every other row in that picker is a directory you
might be choosing, and ../ is a way out — so <C-l> answers for all
of them and this answers only for ../.
The default None keeps <CR> meaning “take this row” everywhere it
already does, which is every picker but that one.
Only meaningful for a live source, for descend’s reason: a static
source’s rows come from init and rewriting its query would fuzzy-
filter them rather than fetch new ones.
Sourcefn initial_query(&self, _args: &[String]) -> Option<String>
fn initial_query(&self, _args: &[String]) -> Option<String>
PP.1: the query the picker opens WITH.
None — the default — leaves the host’s rule in place: a live source’s
first argument seeds the query (:picker grep foo opens on foo),
everything else opens empty.
A source overrides this when its query is not a filter but a
position. dir-pick’s query is the directory being listed, so an
empty one means the prompt cannot say where you are while every row
can — and the first <C-h> has nothing to take a level off. Seeding it
makes the prompt read dir-pick> ~/ and the ascend/descend keys work
from the first keystroke rather than the second.
Only consulted for a live source, for descend’s
reason: a static source’s rows come from init and a seeded query
would fuzzy-filter them instead of re-listing.
Sourcefn preview(
&self,
_ctx: &PickerContext<'_>,
_routing: &RoutingPayload,
) -> Option<PickerPreviewOutcome>
fn preview( &self, _ctx: &PickerContext<'_>, _routing: &RoutingPayload, ) -> Option<PickerPreviewOutcome>
T.12: live-preview hook — invoked as the picker SELECTION moves to
a candidate (before accept). Default None = no preview. A source
returns an outcome the host applies immediately for a live preview;
the host restores prior state on
This runs on the editor’s actor thread, synchronously. Read
what is already in hand and return; a source that must do
something to answer (spawn git, read a file, walk an index)
declares preview_debounce so the host
asks only once the selection has settled.
Sourcefn preview_debounce(&self) -> Option<Duration>
fn preview_debounce(&self) -> Option<Duration>
MG.54: how long the selection must sit still before the host
calls preview. None (the default) = call it
inline on every selection move, which is right for a source whose
preview is a cheap projection of data it already holds.
What this buys is not a faster call — it is no call at all. Arrowing through candidates restarts the timer; a source that spawns a subprocess to answer therefore spawns ZERO of them while the user is scrolling, and exactly one when they stop. That is what makes a synchronous, subprocess-backed preview viable: there is never an in-flight call to cancel and never a stale result to race, because the only call ever made is for the candidate the user is already sitting on.
The residual cost is real and deliberate: a keystroke arriving while the settled fetch is running waits for it. Bounded to one fetch per settle, never a queue. A source declaring this owes its user an option to turn the feature off, and a guard on how much work the fetch can be.
Sourcefn owner_plugin(&self) -> Option<u64>
fn owner_plugin(&self) -> Option<u64>
PH.1: the host-issued id of the plugin that contributed this source,
None for a native one.
Exists for <C-h>: a plugin’s help topics are namespaced
(project.picker-projects), so the host finds a plugin source’s page
by asking for a topic ending .picker-<id> registered by this same
plugin. The ownership check is the point — a suffix match alone
would let any plugin answer <C-h> for another plugin’s picker.
This is the id of the plugin’s PICKER seam; its help topics carry its
HELP seam’s id. The host compares the plugins both resolve to
(PluginMetaRegistry::primary_of), never the raw ids.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".