Skip to main content

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}