Skip to main content

lattice_multibuffer/providers/
narrow.rs

1//! N.1.1 (2026-06-10): **narrow mode** — a focused, editable
2//! one-excerpt multibuffer view of a region of a source buffer.
3//!
4//! Design: `docs/dev/architecture/narrow-mode.md`. Slice plan:
5//! `docs/dev/operations/slice-plans/narrow-mode.md` (N.1.1).
6//!
7//! Narrow is a *one-excerpt* multibuffer + a marker minor mode.
8//! There is no `BufferKind::Narrow` — the view is a regular
9//! `BufferKind::Multibuffer` and reuses every M-series primitive
10//! (M.3 edit propagation, M.4 live source updates, K.4.7 per-excerpt
11//! syntax). Edits in the narrow view propagate to the source buffer;
12//! `:widen` closes the view, leaving the source (with its edits)
13//! open.
14//!
15//! Unlike `providers::search`, narrow is **not** feature-gated — it's
16//! a first-class built-in primitive — and it carries no async scan,
17//! no service, and no per-view state beyond the excerpt itself.
18//!
19//! Entry is the `:narrow [{range}]` ex-command (and, from N.1.3, the
20//! `zn` operator). The ex-command emits `AppEffect::NarrowTrigger`;
21//! the host arm resolves the range, fetches the active buffer's
22//! handle, and calls [`create_narrow_view`].
23
24use std::collections::HashMap;
25use std::sync::Arc;
26
27use lattice_config::OptionOverrideSet;
28use lattice_core::{BufferFlags, BufferId};
29use lattice_grammar::{CommandRegistry, CommandRegistryHandle};
30use lattice_mode::{
31    CapabilitySet, Keymap, LifecycleFuture, Mode, ModeActivator, ModeContext, ModeId, ModeKind,
32    ModeRegistry,
33};
34use lattice_runtime::Document;
35use lattice_syntax::LangRegistry;
36
37use crate::registry::MultibufferRegistryHandle;
38use crate::view::create_multibuffer_view;
39use crate::{Excerpt, HeaderlineStatus};
40
41// ─────────────────────────────────────────────────────────────────
42// NarrowMode — identity marker for narrow views
43// ─────────────────────────────────────────────────────────────────
44
45/// `narrow-mode` — the provider-minor activated on a narrow
46/// view. In N.1.1 it is a pure identity marker: a multibuffer with
47/// this minor active IS a narrow view (distinguished from search /
48/// diff multibuffers), which the host's `:widen` guard and N.1.5's
49/// stacking logic read.
50///
51/// N.1.1.b adds the in-view surface (`q` → widen chord, `:w`
52/// source-save override) here; for now `on_activate` is a no-op so
53/// the marker is cheap.
54pub struct NarrowMode;
55
56impl NarrowMode {
57    pub fn mode_id() -> ModeId {
58        ModeId::new("narrow-mode")
59    }
60}
61
62/// RAII guard for `NarrowMode`. Unit in N.1.1 (no
63/// subscriptions / action handlers yet); becomes a `Vec` of
64/// `ActionHandlerRegistration` when the `q` / `:w` surface lands.
65pub struct NarrowModeGuard;
66
67impl Mode for NarrowMode {
68    type Guard = NarrowModeGuard;
69
70    fn id(&self) -> ModeId {
71        Self::mode_id()
72    }
73    fn kind(&self) -> ModeKind {
74        ModeKind::Minor
75    }
76    fn options(&self) -> OptionOverrideSet {
77        // Narrow views are EDITABLE — edits propagate to the source
78        // via M.3. No ReadOnly override.
79        OptionOverrideSet::new()
80    }
81    fn required_capabilities(&self) -> CapabilitySet {
82        CapabilitySet::empty()
83    }
84    fn keymap(&self) -> Keymap {
85        // N.1.1: no contributed chords yet (the `q` → widen binding
86        // lands in N.1.1.b once `action:narrow-widen` is registered).
87        Keymap::default()
88    }
89
90    // RV.3 (2026-08-10): narrow declares NO refresh action, and that is
91    // a decision rather than an omission.
92    //
93    // A narrow view is one excerpt over a **live** buffer, and
94    // `create_multibuffer_view` subscribes it to the source's
95    // `DocumentChanged` events — so it recomposes as the source is
96    // edited and is never stale. There is nothing a refresh could do
97    // that has not already happened.
98    //
99    // Contrast `providers::problems`, which reads its sources from disk
100    // into fresh handles at open time and renders a snapshot of the
101    // error list: both can drift, so it declares one.
102    //
103    // Leaving this `None` is now safe to state plainly, because the
104    // shared `gr` echoes "nothing to refresh here" instead of
105    // swallowing the key — the absence is spoken, which is precisely
106    // what pre-RV.1 narrow could not do.
107
108    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
109        Box::pin(async move { Ok(NarrowModeGuard) })
110    }
111}
112
113// ─────────────────────────────────────────────────────────────────
114// create_narrow_view — the shared sink
115// ─────────────────────────────────────────────────────────────────
116
117/// Allocate a one-excerpt multibuffer view over
118/// `[start_line, end_line]` (inclusive, 0-based) of `source_id` and
119/// activate [`NarrowMode`] on it. Returns the new view's
120/// `BufferId`.
121///
122/// `source_handle` is the *existing* source buffer's document handle
123/// (an `Arc` clone) — the narrow view shares it, so edits in the
124/// narrow view and the original pane stay live-synced (M.4). `label`
125/// is shown in the headerline (`"[narrow] <label> L<start+1>–<end+1>"`);
126/// pass the file name or a symbol name when known, else `""`.
127///
128/// Every entry surface (`:narrow`, Visual, the `zn` operator) funnels
129/// through here.
130pub fn create_narrow_view(
131    activator: &mut dyn ModeActivator,
132    source_id: BufferId,
133    source_handle: Arc<dyn Document>,
134    start_line: u32,
135    end_line: u32,
136    label: &str,
137    registry: CommandRegistryHandle,
138    lang_registry: Option<Arc<LangRegistry>>,
139) -> BufferId {
140    let mut sources: HashMap<BufferId, Arc<dyn Document>> = HashMap::new();
141    sources.insert(source_id, source_handle);
142    let excerpt = Excerpt::new(source_id, start_line, end_line);
143
144    let name = if label.is_empty() {
145        format!("*narrow:L{}–{}*", start_line + 1, end_line + 1)
146    } else {
147        format!("*narrow:{label}*")
148    };
149
150    let view_id = create_multibuffer_view(
151        activator,
152        sources,
153        vec![excerpt],
154        Some(name),
155        BufferFlags::default(),
156        registry,
157        lang_registry,
158        crate::FoldGrouping::SourceFile,
159    );
160
161    // Set the sticky headerline. Narrow is instantaneous — no
162    // InProgress phase, straight to Complete.
163    if let Some(mb_reg) = activator.services().get::<MultibufferRegistryHandle>()
164        && let Some(view) = mb_reg.handle(view_id)
165    {
166        let summary = if label.is_empty() {
167            format!("[narrow] L{}–{}", start_line + 1, end_line + 1)
168        } else {
169            format!("[narrow] {label} L{}–{}", start_line + 1, end_line + 1)
170        };
171        view.set_headerline(HeaderlineStatus::Complete {
172            summary,
173            emphasis: None,
174        });
175    }
176
177    activator.activate_minor_by_id(view_id, NarrowMode::mode_id());
178    view_id
179}
180
181// ─────────────────────────────────────────────────────────────────
182// Boot integration
183// ─────────────────────────────────────────────────────────────────
184
185/// Boot helper — register the narrow provider-minor mode. Called
186/// from `lattice-host::editor_boot` alongside the other multibuffer
187/// mode registrations.
188pub fn register_narrow_mode(mode_registry: &mut ModeRegistry) {
189    mode_registry
190        .register(NarrowMode)
191        .expect("narrow-mode registers without conflict at boot");
192}
193
194/// Boot helper — register the `:narrow` + `:widen` ex-commands.
195///
196/// `:narrow` accepts an optional `{start},{end}` line range
197/// (`accepts_range`). The host resolves the range against the active
198/// document (the apply context has no document), so the apply just
199/// forwards the raw [`lattice_grammar::range::Range`] through
200/// `AppEffect::NarrowTrigger`. `:widen` emits `AppEffect::NarrowWiden`,
201/// which the host guards to narrow views before closing.
202pub fn register_narrow_ex_commands(registry: &mut CommandRegistry) {
203    use lattice_grammar::app_effect::AppEffect;
204    use lattice_grammar::args::Args;
205    use lattice_grammar::command::LatencyClass;
206    use lattice_grammar::effect::Effect;
207    use lattice_grammar::registry::{ExCommandSpec, SurfaceForm};
208
209    registry.register_ex_command(
210        "narrow",
211        "Narrow the editing surface to a region: `:narrow` (the cursor's \
212         paragraph) or `:{start},{end}narrow`. The region opens as a focused, \
213         editable view; edits propagate to the source file. `:widen` restores \
214         the full buffer.",
215        ExCommandSpec {
216            latency_class: LatencyClass::Reflex,
217            accepts_bang: false,
218            accepts_range: true,
219            parse_args: Arc::new(|_s: &str, _bang: bool| Ok(Args::None)),
220            apply: Arc::new(|ctx| {
221                Ok(Effect::AppAction(AppEffect::NarrowTrigger {
222                    range: ctx.range.clone(),
223                }))
224            }),
225            args_schema: vec![],
226            surface_form: SurfaceForm::Keyword,
227        },
228    );
229
230    registry.register_ex_command(
231        "widen",
232        "Close the active narrow view, restoring the full source buffer.",
233        ExCommandSpec {
234            latency_class: LatencyClass::Reflex,
235            accepts_bang: false,
236            accepts_range: false,
237            parse_args: Arc::new(|_s: &str, _bang: bool| Ok(Args::None)),
238            apply: Arc::new(|_ctx| Ok(Effect::AppAction(AppEffect::NarrowWiden))),
239            args_schema: vec![],
240            surface_form: SurfaceForm::Keyword,
241        },
242    );
243}
244
245/// N.1.3 (2026-06-10): register the **`zn` narrow operator** into the
246/// grammar's `CommandRegistry` and return its `OperatorId`.
247///
248/// The operator is *universal* — you narrow from any editable buffer,
249/// like `d` / `y` / `c`. Per the mode-ownership split, this crate owns
250/// the operator SPEC + `apply`; the host wires the `zn` chord to the
251/// returned `OperatorId` at the universal operator-pending layer
252/// (operator-pending composition needs the host-resolved `Builtins`,
253/// which this crate can't reach). The narrow-VIEW surface
254/// (`:widen` / `:w` / `q`) stays with `NarrowMode`.
255///
256/// `apply` reads the resolved `OperatorContext.range` (the span the
257/// following motion / text object produced), converts it to an
258/// inclusive whole-line span, and emits
259/// `Effect::AppAction(AppEffect::NarrowLines { .. })`; the host arm
260/// narrows the active buffer to that span via `create_narrow_view`.
261pub fn register_narrow_operator(registry: &mut CommandRegistry) -> lattice_grammar::OperatorId {
262    use lattice_grammar::app_effect::AppEffect;
263    use lattice_grammar::effect::Effect;
264    use lattice_grammar::registry::{OperatorContext, OperatorSpec};
265
266    registry.register_operator(
267        "operator:narrow",
268        "Narrow the editing surface to the {motion}/{text-object} that follows \
269         `zn` — a focused, editable view of that region. `znn` narrows the \
270         current line; `znip` a paragraph; `znaf` a function. `:widen` restores.",
271        OperatorSpec {
272            repeatable: false,
273            blockwise_per_row: false,
274            post_motion_char: false,
275            args_schema: vec![],
276            apply: Arc::new(|ctx: &mut OperatorContext| {
277                let (start_line, end_line) = lattice_grammar::range::span_to_whole_lines(
278                    ctx.range.start.line,
279                    ctx.range.start.byte,
280                    ctx.range.end.line,
281                    ctx.range.end.byte,
282                );
283                Ok(Effect::AppAction(AppEffect::NarrowLines {
284                    start_line,
285                    end_line,
286                }))
287            }),
288        },
289    )
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn register_narrow_operator_registers_the_operator() {
298        let mut registry = CommandRegistry::new();
299        let _op = register_narrow_operator(&mut registry);
300        assert!(registry.id_by_name("operator:narrow").is_some());
301    }
302}