Skip to main content

lattice_mode/modes/
display.rs

1//! Display minor modes -- thin user-toggleable wrappers around
2//! the typed display options that already shipped (DESIGN.md
3//! §5.12 + mode-architecture.md §4.2.2 / M.7).
4//!
5//! Each mode contributes its corresponding typed option's
6//! "on" value via `Mode::options()`. Activating the mode flips
7//! the option in the buffer's resolved state via the
8//! mode-contribution layer; deactivating removes the
9//! contribution and the resolver falls back to the typed-option
10//! layer (TOML / `:set` / default).
11//!
12//! ## Two surfaces, one underlying state
13//!
14//! `:set number=true` and `:line-numbers-mode` both flip the
15//! resolved value of `Number` for the active buffer. They sit at
16//! different layers of the resolver:
17//!
18//! - **Typed-option layer** (`:set`): the persistent global /
19//!   per-project value; user-typed config.
20//! - **Mode-contribution layer** (`:line-numbers-mode`): the
21//!   per-buffer mode-driven override.
22//!
23//! Layer priority is `mode-contribution > typed-option`, so when
24//! the mode is active it wins regardless of `:set` state. The
25//! M.7.1 follow-up adds a cascade so `:set number=false` also
26//! deactivates `line-numbers-mode`, giving full bidirectional
27//! convergence; for v1 the mode is a one-way ratchet --
28//! activating it forces the on-value, deactivating it removes
29//! the contribution and the typed-option layer takes over.
30//!
31//! ## What's NOT here
32//!
33//! - `whitespace-show-mode` and `current-line-highlight-mode`:
34//!   their backing typed options don't exist yet (the renderer
35//!   has no `:set list` / `:set cursorline` plumbing). These
36//!   land alongside the option in M.7.2.
37
38use lattice_config::OptionOverrideSet;
39
40use crate::{CapabilitySet, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind};
41
42/// Macro: declare a display minor mode that contributes a
43/// single typed-option override when active.
44///
45/// `$option_path` is the option's type path (e.g.
46/// `lattice_config::Number`); `$on_value` is the literal value
47/// the mode contributes (typically `true` for booleans).
48macro_rules! display_minor_mode {
49    (
50        $struct_name:ident,
51        $mode_name:literal,
52        $(mirrors $mirrors_name:literal,)?
53        contributes $option_path:path = $on_value:expr,
54        $($extra_overrides:tt)*
55    ) => {
56        #[doc = concat!(
57            "`", $mode_name, "` — a display minor mode that overrides `",
58            stringify!($option_path), "` to `", stringify!($on_value),
59            "` (plus any further overrides) while active. `Manual` activation; ",
60            "a user toggle for the option, kept in sync with it by the host's ",
61            "option-mirror cascade when a `mirrors` name is declared."
62        )]
63        pub struct $struct_name;
64
65        impl $struct_name {
66            #[doc = concat!("The canonical id, `\"", $mode_name, "\"`.")]
67            pub fn mode_id() -> ModeId {
68                ModeId::new($mode_name)
69            }
70        }
71
72        impl Mode for $struct_name {
73            type Guard = ();
74            fn id(&self) -> ModeId {
75                Self::mode_id()
76            }
77            fn kind(&self) -> ModeKind {
78                ModeKind::Minor
79            }
80            fn options(&self) -> OptionOverrideSet {
81                lattice_config::overrides! {
82                    $option_path = $on_value,
83                    $($extra_overrides)*
84                }
85            }
86            fn required_capabilities(&self) -> CapabilitySet {
87                CapabilitySet::empty()
88            }
89            $(
90                fn mirrors_option(&self) -> Option<&'static str> {
91                    Some($mirrors_name)
92                }
93            )?
94            fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
95                Box::pin(async { Ok(()) })
96            }
97        }
98    };
99}
100
101display_minor_mode!(
102    LineNumbersMode,
103    "line-numbers-mode",
104    mirrors "number",
105    contributes lattice_config::Number = true,
106);
107
108// `relative-line-numbers-mode` mirrors vim's `:set rnu` cascade:
109// rnu implies nu (so the gutter renders at all). We contribute
110// both overrides directly. The `mirrors "relativenumber"` hint
111// keeps the mode's active state and the option's value in sync
112// via the host's option-mirror cascade -- the App's hardcoded
113// per-mode special case in `apply_option_cascade` is gone,
114// replaced with one declarative loop driven by this hint.
115display_minor_mode!(
116    RelativeLineNumbersMode,
117    "relative-line-numbers-mode",
118    mirrors "relativenumber",
119    contributes lattice_config::RelativeNumber = true,
120    lattice_config::Number = true,
121);
122
123display_minor_mode!(
124    WrapMode,
125    "wrap-mode",
126    mirrors "wrap",
127    contributes lattice_config::Wrap = true,
128);
129
130// `read-only-mode` -- user-toggleable surface for the same
131// `ReadOnly` option that `help-mode` / `file-tree-mode` /
132// LSP-log-mode contribute via M.3.1. Those majors are
133// kind-driven (every help buffer gets read-only); this minor
134// is the user gesture for "make THIS buffer read-only" on
135// arbitrary buffer kinds. `ReadOnly` is `customizable = false`
136// (mode-only); this is the only user-typed pathway. No
137// `mirrors` hint because `ReadOnly` has no `:set` surface.
138//
139// PD.4: written out rather than declared through
140// `display_minor_mode!` because it is the one mode here that is
141// not purely a display toggle -- it also declares an
142// **invocation runner**, and the option alone does not make a
143// buffer read-only. `ReadOnly` is read by the host's
144// `read_only_edit_rejected`, which guards `apply_edit_blocking`
145// and its batch peer -- the insert-mode char path, and only
146// that. Operators never pass through it: a `Document`'s grammar
147// dispatch applies its own edits and hands the host an
148// already-applied `Effect::Edits`. Contributing the option and
149// stopping there gated typing and left `x` / `dd` / `cw`
150// working, which is worse than not gating at all, because the
151// buffer looks protected and is not.
152/// `read-only-mode` — makes a buffer read-only in BOTH ways that matter:
153/// it contributes `ReadOnly = true` (which gates insert-mode typing) and
154/// declares an invocation runner that refuses operators (`x`, `dd`, `cw`,
155/// `p`), which never pass through the option's gate.
156///
157/// A user toggle on any buffer, and the mode a read-only major pulls in
158/// through [`Mode::implies`](crate::Mode::implies) — declaring `ReadOnly`
159/// alone leaves operators working.
160pub struct ReadOnlyMode;
161
162impl ReadOnlyMode {
163    /// The canonical id, `"read-only-mode"` — what [`Mode::id`](crate::Mode::id)
164    /// returns. Use it to name this mode without an instance (activation,
165    /// `implies`, keymap layers, tests).
166    pub fn mode_id() -> ModeId {
167        ModeId::new("read-only-mode")
168    }
169}
170
171impl Mode for ReadOnlyMode {
172    type Guard = ();
173    fn id(&self) -> ModeId {
174        Self::mode_id()
175    }
176    fn kind(&self) -> ModeKind {
177        ModeKind::Minor
178    }
179    fn options(&self) -> OptionOverrideSet {
180        lattice_config::overrides! {
181            lattice_config::ReadOnly = true,
182        }
183    }
184    fn required_capabilities(&self) -> CapabilitySet {
185        CapabilitySet::empty()
186    }
187    /// The host registers `Editor::run_read_only_motion` under this id
188    /// (see `editor_boot.rs`). Motions still move the cursor, `:` and
189    /// `/` still fall through to the central dispatcher, and mutating
190    /// operators echo "buffer is read-only" rather than silently doing
191    /// nothing -- a buffer that ignores keystrokes without saying so
192    /// reads as a hang.
193    fn invocation_runner(&self) -> Option<ModeId> {
194        Some(Self::mode_id())
195    }
196    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
197        Box::pin(async { Ok(()) })
198    }
199}
200
201// M.7.2: `whitespace-show-mode` -- backing for `:set list`
202// (vim convention). Renderer's whitespace-glyph plumbing
203// lands in M.7.3; today the mode + option exist as the
204// declarative + cascade surface, ready for the renderer hook.
205display_minor_mode!(
206    WhitespaceShowMode,
207    "whitespace-show-mode",
208    mirrors "whitespace",
209    contributes lattice_config::Whitespace = true,
210);
211
212// M.7.2: `current-line-highlight-mode` -- backing for
213// `:set cursorline`. Same M.7.3 deferral note as
214// whitespace-show-mode: option + mode declared today,
215// renderer's current-line-highlight pipeline lands later.
216display_minor_mode!(
217    CurrentLineHighlightMode,
218    "current-line-highlight-mode",
219    mirrors "current-line-highlight",
220    contributes lattice_config::CursorLine = true,
221);
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn each_display_mode_has_distinct_id() {
229        let ids = [
230            LineNumbersMode::mode_id(),
231            RelativeLineNumbersMode::mode_id(),
232            WrapMode::mode_id(),
233            ReadOnlyMode::mode_id(),
234            WhitespaceShowMode::mode_id(),
235            CurrentLineHighlightMode::mode_id(),
236        ];
237        for (i, a) in ids.iter().enumerate() {
238            for b in &ids[i + 1..] {
239                assert_ne!(a, b);
240            }
241        }
242    }
243
244    #[test]
245    fn each_display_mode_is_minor_with_no_caps() {
246        use crate::DynMode;
247        let modes: Vec<&dyn DynMode> = vec![
248            &LineNumbersMode,
249            &RelativeLineNumbersMode,
250            &WrapMode,
251            &ReadOnlyMode,
252            &WhitespaceShowMode,
253            &CurrentLineHighlightMode,
254        ];
255        for m in modes {
256            assert_eq!(m.kind(), ModeKind::Minor, "{} not minor", m.id());
257            assert_eq!(
258                m.required_capabilities(),
259                CapabilitySet::empty(),
260                "{} declared caps",
261                m.id(),
262            );
263        }
264    }
265
266    #[test]
267    fn line_numbers_mode_contributes_number_true() {
268        let opts = <LineNumbersMode as Mode>::options(&LineNumbersMode);
269        // Single contribution.
270        assert_eq!(opts.iter().count(), 1);
271    }
272
273    #[test]
274    fn relative_line_numbers_mode_contributes_both_options() {
275        // Mirrors vim's `:set rnu` ⇒ `:set nu` cascade by
276        // contributing both directly. Lets a user have
277        // `relative-line-numbers-mode` active without needing
278        // `line-numbers-mode` separately.
279        let opts = <RelativeLineNumbersMode as Mode>::options(&RelativeLineNumbersMode);
280        assert_eq!(opts.iter().count(), 2, "expected RelativeNumber + Number",);
281    }
282
283    #[test]
284    fn wrap_mode_contributes_wrap_true() {
285        let opts = <WrapMode as Mode>::options(&WrapMode);
286        assert_eq!(opts.iter().count(), 1);
287    }
288
289    #[test]
290    fn read_only_mode_contributes_read_only_true() {
291        let opts = <ReadOnlyMode as Mode>::options(&ReadOnlyMode);
292        assert_eq!(opts.iter().count(), 1);
293    }
294
295    #[test]
296    fn whitespace_show_mode_contributes_whitespace_true() {
297        let opts = <WhitespaceShowMode as Mode>::options(&WhitespaceShowMode);
298        assert_eq!(opts.iter().count(), 1);
299    }
300
301    #[test]
302    fn current_line_highlight_mode_contributes_cursorline_true() {
303        let opts = <CurrentLineHighlightMode as Mode>::options(&CurrentLineHighlightMode);
304        assert_eq!(opts.iter().count(), 1);
305    }
306}