lattice_picker/context.rs
1//! Host-built snapshot the picker primitive hands to source
2//! generators on every `:picker <source>` open.
3//!
4//! `PickerContext` is the *runtime-varying* "where am I"
5//! state every picker source needs: the active buffer, the
6//! workspace root, the recent-files MRU, marks, registers,
7//! and the position-history ring. It does NOT carry feature-
8//! specific facades (LSP supervisor handle, snippet registry,
9//! grammar registry) -- those are captured by source
10//! generators at construction time. See
11//! `docs/dev/architecture/picker.md` for why.
12//!
13//! The struct is borrow-heavy by design: sync sources read
14//! straight through the borrows, async sources clone what
15//! they need into their future's captures before the
16//! synchronous `init` call returns. Three fields are owned
17//! vecs (`buffers`, `marks`, `registers`) -- the App rebuilds
18//! these fresh on each picker-open because their backing
19//! state lives in non-borrow-friendly types (HashMaps with
20//! transient liveness). Allocation cost is trivial at our
21//! sizes (<100 buffers, <26 marks, <40 registers).
22
23use std::path::{Path, PathBuf};
24use std::sync::Arc;
25
26use lattice_core::Buffer;
27use lattice_protocol::Position;
28
29/// Snapshot the host passes to `PickerSourceGenerator::init`
30/// (and `accept`) on each picker invocation.
31///
32/// Sources access the borrowed fields directly during the
33/// synchronous prelude; the moment `init` returns, the
34/// borrow is released and any captured async work owns its
35/// own clones.
36pub struct PickerContext<'a> {
37 pub active_buffer: ActiveBufferSnapshot<'a>,
38 /// The active buffer's **project root** (PR.4), resolved by the
39 /// host through `ProjectResolverHandle::for_path`.
40 ///
41 /// NOT the process working directory, and not the active
42 /// document's parent — both were tried and both were wrong in
43 /// opposite directions. The parent "behaved unintuitively for
44 /// projects spread across many subdirectories"; the cwd is right
45 /// only if you launched the editor inside the tree you are
46 /// editing. A source wanting "where does this file's project
47 /// live" reads this and needs nothing else.
48 ///
49 /// Degrades to the process cwd (then `.`) only when no
50 /// `ProjectResolverHandle` is registered — a test harness, not a
51 /// real boot. Owned because that fallback is fallible and the
52 /// PathBuf needs somewhere stable to live.
53 pub workspace_root: PathBuf,
54 pub recent_files: &'a [PathBuf],
55 /// Position-history ring, translated from the App's richer
56 /// `PositionEntry` (which carries `BufferKind`) to this
57 /// crate's picker-friendly view. Owned vec because the
58 /// translation step produces fresh entries -- consistent
59 /// with `buffers` / `marks` / `registers` below.
60 pub position_history: Vec<PositionEntry>,
61 pub buffers: Vec<BufferEntry>,
62 pub marks: Vec<(char, Position)>,
63 /// Registers as `(name, contents)` pairs, with the **full**
64 /// contents.
65 ///
66 /// It used to carry a 40-char preview with newlines replaced by
67 /// `\u{21B5}`, which was harmless while the only consumer routed
68 /// `PasteRegister { name }` and the host re-read the register itself.
69 /// `yank-ring` returns the text directly, so a preview here meant
70 /// picking a long register inserted a truncation — and one with `↵`
71 /// where its newlines had been, so corrupt rather than merely short.
72 ///
73 /// Consumers truncate for display; only display should be lossy.
74 pub registers: Vec<(String, String)>,
75 /// YR.4: the yank ring, newest first, as
76 /// `(content, linewise)` pairs.
77 ///
78 /// The full content, not a preview — the picker's accept has to
79 /// return the real text, and truncating here would mean pasting a
80 /// truncation. `registers` above carries previews because its accept
81 /// is `PasteRegister { name }`, which re-reads the register host-side
82 /// rather than carrying its text.
83 ///
84 /// `linewise` rides along because a linewise and a charwise entry
85 /// paste differently, and a picker that hides that makes paste
86 /// unpredictable exactly when the user is choosing between two
87 /// similar-looking rows.
88 pub yank_ring: Vec<(String, bool)>,
89 /// MARG.3 (2026-07-15): active minor+major mode names for the
90 /// current buffer. The command completion margin uses this to
91 /// filter out keybindings whose source minor/major mode is not
92 /// currently active (see [`KeybindingSource::Mode`]).
93 pub active_modes: Vec<Arc<str>>,
94 /// MB.3: the App's command-line history ring (`command_history`),
95 /// oldest-first as stored host-side. The `history` picker source
96 /// (`q:` / `:history`) walks this reversed (newest first) and
97 /// `<CR>` loads the chosen entry into the `:` line without
98 /// executing. Owned vec, rebuilt per picker-open like the other
99 /// snapshot fields.
100 pub command_history: Vec<String>,
101 /// MB.5: the App's search-line history ring (`search_history`),
102 /// oldest-first as stored host-side. The `search-history` picker
103 /// source (`q/` / `q?` / `:history search`) walks this reversed
104 /// (newest first) and `<CR>` loads the chosen entry into the `/`
105 /// search line without executing.
106 pub search_history: Vec<String>,
107 /// PBH.5: the ACTIVE pane's buffer trail, oldest-first, as
108 /// `(label, is_current)` per entry. The `pane-buffer-history`
109 /// source (`:history pane-buffers`) walks this reversed so the most
110 /// recent stop is on top, and marks the entry the walk cursor is
111 /// currently on.
112 ///
113 /// Per-pane by construction: the host snapshots only the active
114 /// pane's trail, so the picker cannot accidentally show a global
115 /// or cross-pane view.
116 pub pane_buffer_history: Vec<PaneHistoryRow>,
117}
118
119/// PBH.5: one row of the active pane's buffer trail, flattened for the
120/// picker.
121///
122/// Carries the trail **index** rather than a buffer id: the same buffer
123/// can appear at several points in a trail, and accepting the third
124/// stop must land on the third stop, not the first occurrence.
125pub struct PaneHistoryRow {
126 /// Position in the trail, oldest-first. The accept payload.
127 pub index: u32,
128 /// Display label — the buffer's name or path.
129 pub label: String,
130 /// Line the trail recorded for this stop (1-based for display).
131 pub line: u32,
132 /// Whether the walk cursor currently sits on this entry.
133 pub is_current: bool,
134}
135
136/// Snapshot of the active document buffer at the moment the
137/// picker opened. Carries enough state for line / mark /
138/// outline / grep / LSP-position sources to do their work
139/// without a second App round-trip.
140pub struct ActiveBufferSnapshot<'a> {
141 pub buffer_id: u32,
142 pub path: Option<&'a Path>,
143 /// Language id (`"rust"`, `"markdown"`, ...). Snippet and
144 /// outline sources filter on this; absent for unknown /
145 /// untyped buffers.
146 pub language: Option<&'a str>,
147 pub cursor: Position,
148 /// Visual-mode selection extent at picker-open, if any.
149 /// `:picker grep` defaults its pattern to the selected
150 /// text when present.
151 pub selection: Option<(Position, Position)>,
152 /// Read-only borrow of the rope. Line / outline / grep
153 /// sources walk this directly; long-running async work
154 /// must extract what it needs before `init` returns.
155 pub buffer: &'a Buffer,
156 /// Tree-sitter symbol locations (name, line, byte-col)
157 /// for the active buffer when syntax is available.
158 /// Pre-collected by the host via
159 /// `Syntax::collect_symbol_locations`; empty when there's
160 /// no parser registered for the buffer's language. Drives
161 /// `:picker outline`.
162 pub syntax_symbols: Vec<(String, u32, u32)>,
163 /// PH.2: per-line syntax-highlight spans for the active
164 /// buffer, one inner `Vec` per buffer line, each span's
165 /// `range` a *line-relative* byte range. Pre-collected by
166 /// the host via `SyntaxSnapshot::highlight_lines` (a
167 /// read-only tree query run off the render thread, mirroring
168 /// the `syntax_symbols` precedent) and already mapped to
169 /// `DisplaySpan` host-side so this crate needs no
170 /// `lattice-cells` dependency. Empty when no grammar is
171 /// registered / the snapshot is stale → plain previews.
172 /// `LinesSource` clones the matching line's spans into a
173 /// candidate's `display_spans`; `OutlineSource` clips them
174 /// to the symbol-name column. See
175 /// `docs/dev/architecture/picker-preview-highlight.md` §6.
176 pub syntax_highlights: Vec<Vec<lattice_completion::DisplaySpan>>,
177}
178
179/// One buffer in the registry, projected for picker rows.
180/// `kind_label` is a display string -- the picker primitive
181/// stays oblivious to the actual `BufferKind` enum so the
182/// "no kind-specific logic" rule (CLAUDE.md memory) holds at
183/// this seam too.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct BufferEntry {
186 pub id: u32,
187 pub kind_label: String,
188 pub path: Option<PathBuf>,
189 pub title: String,
190 pub dirty: bool,
191}
192
193/// Picker-friendly view of one entry in the App's
194/// position-history ring (§5.1.1 unified jump list + mark
195/// ring). The App's richer `PositionEntry` carries
196/// `BufferKind` + framework-internal fields; the picker only
197/// needs `(buffer_id, line, col, source)` to render a row
198/// and emit a jump outcome.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub struct PositionEntry {
201 pub buffer_id: u32,
202 pub line: u32,
203 pub col: u32,
204 pub source: PositionSource,
205}
206
207/// Why this entry was pushed onto the position history.
208/// Mirror of `lattice_ui_tui::app::PositionSource` but kept
209/// here so `lattice-picker` doesn't depend on the host
210/// crate. The App translates between the two at
211/// PickerContext-build time.
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum PositionSource {
214 /// Big motions: `gg`, `G`, search, `*`, `#`, `%`, mark jump.
215 AutoJump,
216 /// Explicit user "remember here" push (reserved for
217 /// `g<C-o>` style emacs-`set-mark` equivalents).
218 ExplicitMark,
219 /// LSP / fuzzy-finder / plugin-pushed jumps.
220 PluginPush,
221 /// Named mark (`mX`). Walks via `g;` / `g,`.
222 NamedMark(char),
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn buffer_entry_clone_and_eq() {
231 let a = BufferEntry {
232 id: 7,
233 kind_label: "doc".into(),
234 path: Some("/tmp/foo.rs".into()),
235 title: "foo.rs".into(),
236 dirty: false,
237 };
238 let b = a.clone();
239 assert_eq!(a, b);
240 }
241
242 #[test]
243 fn position_entry_carries_source_variant() {
244 let e = PositionEntry {
245 buffer_id: 3,
246 line: 12,
247 col: 4,
248 source: PositionSource::NamedMark('a'),
249 };
250 assert_eq!(
251 e.source,
252 PositionSource::NamedMark('a'),
253 "NamedMark name survives copy"
254 );
255 assert_ne!(e.source, PositionSource::AutoJump);
256 }
257}