pub struct Picker {Show 16 fields
pub title: String,
pub root_label: Option<String>,
pub query: String,
pub query_cursor: usize,
pub candidates: Vec<RenderedCandidate>,
pub selected: usize,
pub source: PickerSource,
pub on_accept: PickerAction,
pub source_id: Option<String>,
pub loading: bool,
pub revision: u64,
pub transient: Option<Arc<TransientSpec>>,
pub transient_state: TransientState,
pub transient_stack: Vec<(Arc<TransientSpec>, TransientState, usize)>,
pub transient_selected: usize,
pub transient_prefix: String,
/* private fields */
}Expand description
One open vertico-style picker. Lives on App.picker while
active; the input and render layers route to / from it via
the Action::Picker* family.
Fields§
§title: String§root_label: Option<String>PP.2: the root these results are scoped to, ready to display —
home-contracted (~/src/lattice) by whoever set it, because the
renderers have one line and the home prefix is the least informative
part of a path.
None for every picker whose results are not root-scoped, which is
most of them: buffers spans every project you have open, commands
is registry-wide, lines is one buffer. A root on those is noise on
the one line the user reads to know what they are looking at.
Set at seat time, from the seating source’s
PickerSourceSpec::rooted — or
directly, for the LSP pickers, which are seated by hand and never had
a spec.
query: String§query_cursor: usizeByte offset within query where the cursor sits. Today
the picker only appends / backspaces at end-of-query so
this equals query.len(); reserved for future left/right
editing.
candidates: Vec<RenderedCandidate>Candidates that pass the current query filter. Re-built
on every refilter call.
selected: usizeIndex into candidates. Clamped to 0..candidates.len()
on every refilter.
source: PickerSource§on_accept: PickerAction§source_id: Option<String>Picker-registry source id that seated this picker. Some
when the picker was seated via the trait-driven path
(:picker <source>); None for legacy imperative
pickers (:b, :lsp-log, multi-result LSP locations).
do_picker_accept reads this to decide whether to
delegate accept to the source’s PickerSourceGenerator
or fall back to the legacy per-routing dispatch.
loading: boolTrue while an async fetch for this picker is in flight
(the initial grep on :picker grep <pat>, or a live
re-query after a keystroke). Both renderers surface it as
a searching… indicator in the prompt so a slow grep
reads as “working”, not “nothing happened”. The host sets
it when it spawns a fetch future and clears it when
results seat (a fresh Picker defaults to false) or the
fetch errors. See seat_picker_from_pairs /
open_picker / fire_live_picker_query_changed.
revision: u64Globally monotonic stamp, re-taken by Self::refilter — i.e. by every
change to candidates, because refilter is the only thing that writes
them and every raw assignment calls it.
Exists for the host’s paint gate. compute_paint_revision used to fold
in picker.is_some() and nothing else, on a comment that read “async
result GROWTH inside an open picker still rides a keystroke today; if
that changes, fold a content count here.” PC.10’s <C-l> / <Tab>
descend is when that changed: it re-queries the source off-keystroke, so
the new listing replaced candidates while the gate reported nothing
moved, paint_request never fired, and the rows on screen stayed the
ones from before the descend until the user typed.
A stamp rather than hashing the rows: the gate runs on every publish and
a picker may hold tens of thousands of candidates (paramount #1). A
stamp rather than candidates.len(): a re-query returning the same
NUMBER of different rows is exactly what a length cannot see.
GLOBAL rather than per-picker, which is the whole of why the first
attempt at this did not work. A live re-query does not mutate the open
picker — seat_picker_from_pairs builds a FRESH Picker and swaps it
in. A per-instance counter therefore read 1 both before and after the
descend, the hash did not move, and the gate was as blind as before. A
process-wide counter is unrepeatable by construction, so re-seating
cannot alias.
transient: Option<Arc<TransientSpec>>Active transient-mode specification + live state. When
Some, the renderer switches to grouped section layout
with single-key chord dispatch, and the input layer routes
keystrokes through transient chord matching rather than
the query → filter path. See transient.rs.
transient_state: TransientState§transient_stack: Vec<(Arc<TransientSpec>, TransientState, usize)>Stack of parent transient specs for BS/DEL back
navigation through nested submenus. The usize is the
parent’s transient_selected at the moment its submenu opened —
popping restores it instead of leaving the parent’s selection
wherever the submenu happened to leave the shared field.
transient_selected: usizeWhich of the transient’s items <C-n> / <C-p> have walked to
— an index over TransientSpec::selectable_count, NOT a
scroll offset.
The distinction is the whole fix: the host can bound an item
index from the spec alone, whereas a scroll offset’s true
maximum depends on a viewport height only the renderer knows.
The offset version grew unbounded and each renderer clamped it
privately at paint time, so the stored value drifted tens of
rows past anything renderable and <C-p> did nothing visible
until the overshoot had been walked back off. Renderers now
derive their scroll from this every frame
(TransientSpec::scroll_for), leaving no scroll state to
drift.
transient_prefix: StringKeys typed at this level that begin some row’s key but do not
complete one yet — magit’s , k / , r / = f rows.
Empty whenever no multi-key row is part-way typed, which is
almost always. The host used to compare a single typed char
against each row’s key string, so a multi-key row rendered,
could be walked to with <C-n> and fired with <CR>, and did
nothing at all when its own keys were pressed.
Implementations§
Source§impl Picker
impl Picker
pub fn new( title: impl Into<String>, source: PickerSource, on_accept: PickerAction, ) -> Self
Sourcepub fn set_create_label(&mut self, label: Option<String>)
pub fn set_create_label(&mut self, label: Option<String>)
OR.5: declare that this picker offers to create what the query names.
Called by the host at seat time from the seating source’s
PickerSourceSpec::create_label. %s in label is replaced by the
query on every render.
Sourcepub fn create_label(&self) -> Option<&str>
pub fn create_label(&self) -> Option<&str>
OR.5: this picker’s create label, if it has one.
Sourcepub fn arm_preview_settle(&mut self, delay: Duration)
pub fn arm_preview_settle(&mut self, delay: Duration)
MG.54: (re)start this picker’s preview settle window.
Called on every selection move for a source that declares a window. Each call pushes the deadline out, so a burst of moves leaves exactly one due time — the moment the user stopped.
The host still schedules the wake (it owns the runtime), but the policy and the state are the picker’s, so every picker — first party, plugin, one written next year — gets the same behaviour from declaring the window alone.
Sourcepub fn take_due_preview_settle(&mut self, now: Instant) -> bool
pub fn take_due_preview_settle(&mut self, now: Instant) -> bool
MG.54: has the settle window elapsed? Consumes the deadline when it has, so the preview runs once per settle no matter how many wakes arrive — a burst of N moves schedules N wakes, and the N-1 superseded ones find the deadline still in the future.
Sourcepub fn preview_settle_pending(&self) -> bool
pub fn preview_settle_pending(&self) -> bool
MG.54: whether a deferred preview is waiting on this picker. Read by tests and by the host’s “is anything pending” checks; the deadline itself is deliberately not exposed.
Sourcepub fn filtered_routing(&self) -> Vec<&RoutingPayload>
pub fn filtered_routing(&self) -> Vec<&RoutingPayload>
Unwind one level of transient state: a half-typed multi-key row first, then one submenu off the stack.
Returns false when there was nothing to unwind — the caller
(<Esc> / BS) then means “close”, because this is the root
menu with no pending keys.
The precedence is the point. A part-typed row (, waiting
for k) is what the key most likely means to undo, so it goes
first; only once nothing is pending does the same key leave the
menu you are in. Vim gives <Esc> the same precedence over a
partial chord.
Lives here rather than in the host’s dispatch arm because BS
and <Esc> both need it, and two copies of a precedence rule
drift.
LR.5: the routing payloads of the candidates that survived the
current query — the FILTERED set, which is what <C-q> means.
Sending the unfiltered list would discard the work the user just
did typing a query (telescope’s send_to_qflist semantics).
Sourcepub fn filtered_entries(&self) -> Vec<(&str, &RoutingPayload)>
pub fn filtered_entries(&self) -> Vec<(&str, &RoutingPayload)>
PE.2: the filtered candidates paired with their routing payload AND
the visible row text (display) — the peer of
Self::filtered_routing for a send that wants to carry the matched
line as the error-list entry’s message, not just its coordinates.
<C-q>’s result should read the way the rows looked in the picker
(the grep match text, the symbol name, the reference preview), so the
error list and *problems* are a column of meaningful lines rather
than bare file:lines the user must jump to one by one to tell apart.
pub fn transient_unwind(&mut self) -> bool
Sourcepub fn transient_select_next(&mut self)
pub fn transient_select_next(&mut self)
Walk the transient’s selection one item forward, wrapping at the
end — the same wrap Self::select_next gives the candidate
list, and the reason <C-n> can no longer overshoot: the index
is taken modulo the item count, so there is no out-of-range
value to represent.
No-op when no transient is open or it has no items.
Sourcepub fn transient_select_prev(&mut self)
pub fn transient_select_prev(&mut self)
The peer of Self::transient_select_next, wrapping the other
way.
Sourcepub fn transient_selected_item(&self) -> Option<&TransientItem>
pub fn transient_selected_item(&self) -> Option<&TransientItem>
The item <CR> would fire — the selection resolved against the
open transient. None when there is no transient, it is empty,
or the selection is somehow past its end.
Sourcepub fn set_live_source_mode(&mut self, live: bool)
pub fn set_live_source_mode(&mut self, live: bool)
Toggle live-source mode (spec().live == true). When
on, Self::refilter renders raw verbatim instead
of running fuzzy matching. The host calls this once
at picker-open time after consulting the source’s
spec; the flag stays on for the picker’s lifetime.
Sourcepub fn is_live_source_mode(&self) -> bool
pub fn is_live_source_mode(&self) -> bool
Query accessor for the host (and tests) – mirrors the other state predicates.
Sourcepub fn set_orderless(&mut self, orderless: bool)
pub fn set_orderless(&mut self, orderless: bool)
Mirror the picker.orderless option onto this picker. The host
calls this once at picker-open time, alongside
Self::set_live_source_mode; the flag stays put for the
picker’s lifetime so the result list cannot change semantics
underneath a user who is mid-query.
Sourcepub fn is_orderless(&self) -> bool
pub fn is_orderless(&self) -> bool
Whether this picker splits its query into orderless components.
Sourcepub fn set_mru_bonuses(&mut self, bonuses: Vec<f64>)
pub fn set_mru_bonuses(&mut self, bonuses: Vec<f64>)
Stamp the parallel mru_bonuses vec the matcher reads
during refilter. Must be called after
Self::set_raw_candidates_with_routing and must match
routing_meta.len() in length; mismatched lengths reset
to zero-bonus so the picker stays in a sane state if a
caller miscounts (mostly a guardrail for tests).
Callers that already have both pairs + bonuses in hand
should prefer
Self::set_raw_candidates_with_routing_and_bonuses –
it sets all three vecs and refilters once, vs. this
path which leaves a wasted refilter behind from the
preceding set_raw_candidates_with_routing call.
Sourcepub fn set_raw_candidates_with_routing_and_bonuses(
&mut self,
items: Vec<(RawCandidate, RoutingPayload)>,
bonuses: Vec<f64>,
)
pub fn set_raw_candidates_with_routing_and_bonuses( &mut self, items: Vec<(RawCandidate, RoutingPayload)>, bonuses: Vec<f64>, )
Single-pass seat: mutate raw + routing_meta +
mru_bonuses and refilter exactly once. The
fast-path replacement for the
set_raw_candidates_with_routing + set_mru_bonuses
pair the host’s trait-driven seat path used before
this method existed – which refiltered twice, with
the first pass entirely wasted because the bonuses
were about to replace the same data.
Mismatched-length bonuses zero out (same guardrail as
Self::set_mru_bonuses). Legacy callers that don’t
have bonuses yet keep using set_raw_candidates_with_routing
set_mru_bonuses; the new method is opt-in.
Sourcepub fn mru_bonus_for(&self, candidate: &RenderedCandidate) -> f64
pub fn mru_bonus_for(&self, candidate: &RenderedCandidate) -> f64
Borrow the candidate’s MRU bonus by its routing-payload index. Returns 0.0 for candidates without a registered bonus (slice 12 pickers, legacy LSP pickers, anything pre-MRU-snapshot). Public so the refilter path can read it; not intended for downstream callers.
Sourcepub fn set_raw_candidates(&mut self, raw: Vec<RawCandidate>)
pub fn set_raw_candidates(&mut self, raw: Vec<RawCandidate>)
Replace the raw candidate list. Host-built (e.g. the TUI
host walks BufferRegistry for the buffer switcher);
picker just stores + refilters. The single mutation entry
point: every other “set the candidates” helper (e.g.
Self::set_lsp_instances) routes through this.
Sourcepub fn set_raw_candidates_with_routing(
&mut self,
items: Vec<(RawCandidate, RoutingPayload)>,
)
pub fn set_raw_candidates_with_routing( &mut self, items: Vec<(RawCandidate, RoutingPayload)>, )
Replace the raw candidate list AND the typed routing
sidecar (Phase 4.2.g.7 polish). Each input pair is a
(RawCandidate, RoutingPayload) – the picker stores
the payload at index i in routing_meta and stamps
the candidate’s data with Extension { kind_id: PICKER_ROUTING_KIND_ID, payload: i.to_le_bytes() }. The
accept dispatch reads the index back, indexes the
sidecar, and matches on the typed enum variant. Replaces
the prior text-tab-encoded string-parsing path.
Sourcepub fn routing_for(
&self,
candidate: &RenderedCandidate,
) -> Option<&RoutingPayload>
pub fn routing_for( &self, candidate: &RenderedCandidate, ) -> Option<&RoutingPayload>
Look up the routing payload for candidate – returns
None for candidates that don’t carry a picker-routing
Extension payload (defensive; the picker only ever
builds candidates through Self::set_raw_candidates_with_routing
in the new world). Used by the accept dispatch.
Sourcepub fn set_lsp_locations(&mut self, rows: Vec<LspLocationRow>)
pub fn set_lsp_locations(&mut self, rows: Vec<LspLocationRow>)
Replace the raw candidate list with externally-built LSP
location rows – multi-result navigation, references,
diagnostics. Caller builds + sorts + dedups the Vec
host-side; the picker just stores + refilters.
Sourcepub fn set_lsp_instances(&mut self, rows: Vec<LspInstanceRow>)
pub fn set_lsp_instances(&mut self, rows: Vec<LspInstanceRow>)
Replace the raw candidate list with externally-built LSP
instance rows. Caller (App::open_lsp_picker) snapshots the
supervisor under its lock and hands the resulting tuples
here. Refreshes the filter.
Slice 15: stamps each candidate’s accept_action based
on the picker’s on_accept (OpenLspLog vs
OpenLspTraceLog) so the typed dispatch (7d.0) fires
instead of the legacy PickerAction match.
Sourcepub fn set_ai_sessions(&mut self, rows: Vec<AiSessionRow>)
pub fn set_ai_sessions(&mut self, rows: Vec<AiSessionRow>)
Replace the raw candidate list with externally-built AI
session rows. Caller (App::do_open_ai_log) snapshots the
AiLogger service’s known_sessions() and hands the rows
here. Honors the source’s optional provider prefilter.
Mirrors Self::set_lsp_instances, minus the
accept_action stamping (the AI picker rides the legacy
RoutingPayload::AiSession accept dispatch).
Sourcepub fn refilter(&mut self)
pub fn refilter(&mut self)
Filter raw against the current query and write the
matches into candidates. Routes through
[lattice_completion::fuzzy_match] – the same 5-tier
algorithm Insert-mode completion uses (exact / prefix /
word-boundary / substring / subsequence). Picker rows
match against display because their text field
often carries a routing payload (e.g.
"<server_id>\t<workspace>") the user never sees.
Empty query yields a uniform score so every candidate
passes through; the rust stdlib’s stable sort preserves
the host-supplied insertion order on ties (callers like
the buffer switcher depend on this – alternate-buffer
floats to the top via insertion order).
pub fn append_query(&mut self, c: char)
Sourcepub fn paste_query(&mut self, text: &str) -> bool
pub fn paste_query(&mut self, text: &str) -> bool
Append a whole pasted burst to the query.
Newlines are flattened to spaces rather than dropped or
honoured. The query is a single line, so a multi-line paste has
to become one — and joining with nothing would weld the last word
of each line to the first of the next (foo.rs + bar.rs →
foo.rsbar.rs), which matches nothing and looks like the paste
was corrupted. Other control characters are dropped: they cannot
be typed into the query, so they cannot be intended in it, and a
stray \t or \r from a terminal round-trip would silently make
the filter match nothing.
Returns false when the burst contributes nothing, so the caller
can skip the refilter and the preview.
pub fn backspace_query(&mut self)
Sourcepub fn delete_word_backward(&mut self) -> bool
pub fn delete_word_backward(&mut self) -> bool
PH.1: <C-w> — vim’s c_CTRL-W. Drop trailing whitespace, then the
run of characters of the class before it: a word (alphanumerics and
_) or a run of punctuation. foo bar → foo ; src/lib → src/.
Returns whether anything was deleted, so the host can skip the re-query tail on an empty query.