Skip to main content

PickerSourceGenerator

Trait PickerSourceGenerator 

Source
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§

Source

fn spec(&self) -> &PickerSourceSpec

Generator metadata. Returned by reference so the registry can stamp it into :describe-picker / :picker <Tab> listings without cloning.

Source

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.

Source

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§

Source

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).

Source

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.

Source

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.

Source

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.

Source

fn accept_navigates( &self, _ctx: &PickerContext<'_>, _candidate: &RawCandidate, ) -> Option<String>

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.

Source

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.

Source

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.

Source

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.

Source

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".

Implementors§