Skip to main content

lattice_mode/
refreshable_view_mode.rs

1//! `refreshable-view-mode` — the one place `gr` means "refresh this view".
2//!
3//! ## Why a shared minor and not a chord per mode
4//!
5//! `gr` refreshes a synthetic buffer in every synthetic buffer that has
6//! one. That is a property of synthetic views *as a class*, so by the
7//! "shared behaviour is a minor mode, never a copied keymap" standing
8//! rule the chord belongs here once.
9//!
10//! It did not start that way. As of 2026-08-10 three independent copies
11//! existed — `magit-core-mode` (`action:magit-refresh`),
12//! `compilation-mode` (`action:compilation-recompile`) and
13//! `providers::search` (`action:search-refresh`) — and the two synthetic
14//! views that landed most recently, `*problems*` and narrow, **had no
15//! `gr` at all**. Nobody noticed, because a gap in a copied set does not
16//! announce itself. That is the failure this mode closes.
17//!
18//! ## The split
19//!
20//! This mode owns the **chord**. Each view's mode owns the **body**, and
21//! declares which of its own actions is the refresh via
22//! [`Mode::refresh_action`](crate::Mode::refresh_action) — a target, not
23//! a body, so existing handlers keep working untouched.
24//!
25//! Resolution is host-side (`Editor::resolve_refresh_action`) because it
26//! needs the buffer's active-mode set, which lives on the editor rather
27//! than in the `ServiceRegistry`. Same split as
28//! [`invocation_runner`](crate::Mode::invocation_runner): the mode
29//! declares, the host walks and dispatches. `action:view-refresh` is
30//! therefore a *generic* host action — it carries no per-view logic, it
31//! only redirects to whatever the active modes named.
32//!
33//! ## Activation
34//!
35//! Automatic: a mode returning `Some` from `refresh_action()` pulls this
36//! minor in through the implies cascade (see
37//! `ModeRegistry::record_implies_cascade`). A mode author writes one
38//! line and gets the chord — there is no second thing to remember, which
39//! matters because forgetting it would kill the chord exactly as
40//! silently as the copied keymaps did.
41//!
42//! `ActivationPolicy::Manual` so it never auto-attaches to ordinary
43//! buffers: `gr` in a source buffer is LSP references and must stay that
44//! way.
45
46use std::sync::{Arc, OnceLock};
47
48use crate::registry::ModeRegistry;
49use crate::{
50    ActivationPolicy, Keymap, KeymapEntry, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind,
51    keymap_entry,
52};
53
54/// The canonical command name the shared `gr` resolves to. The host
55/// intercepts this id, resolves the active modes' declared refresh
56/// action, and dispatches *that*.
57pub const VIEW_REFRESH_ACTION: &str = "action:view-refresh";
58
59/// `refreshable-view-mode` minor. A marker mode: one keymap layer, no
60/// per-buffer resources (`Guard = ()`), and deliberately **no action
61/// handler** — the body it would hold lives in each view's own mode.
62pub struct RefreshableViewMode;
63
64impl RefreshableViewMode {
65    /// The canonical id, `"refreshable-view-mode"` — what [`Mode::id`](crate::Mode::id)
66    /// returns. Use it to name this mode without an instance (activation,
67    /// `implies`, keymap layers, tests).
68    pub fn mode_id() -> ModeId {
69        ModeId::new("refreshable-view-mode")
70    }
71}
72
73impl Mode for RefreshableViewMode {
74    type Guard = ();
75
76    fn id(&self) -> ModeId {
77        Self::mode_id()
78    }
79
80    fn kind(&self) -> ModeKind {
81        ModeKind::Minor
82    }
83
84    /// Never auto-activated by policy — it arrives through the implies
85    /// cascade when a mode declares `refresh_action()`. Keeping this
86    /// `Manual` is what stops `gr` shadowing LSP references on ordinary
87    /// document buffers.
88    fn activation_policy(&self) -> ActivationPolicy {
89        ActivationPolicy::Manual
90    }
91
92    /// Pushed once at boot under `MinorMode(refreshable-view-mode)`;
93    /// K.1.c's per-keystroke filter gates it to buffers where this mode
94    /// is active.
95    fn keymap(&self) -> Keymap {
96        Keymap::from_entries(refreshable_view_keymap_entries())
97    }
98
99    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
100        Box::pin(async { Ok(()) })
101    }
102}
103
104/// The single entry. `gr` — the chord every magit buffer, the
105/// compilation buffer and the search view already used, now declared
106/// once.
107fn refreshable_view_keymap_entries() -> &'static [KeymapEntry] {
108    static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
109    ENTRIES.get_or_init(|| {
110        vec![keymap_entry! {
111            mode: Normal,
112            chord: "gr",
113            doc: "Refresh this view",
114            cmd: "action:view-refresh"
115        }]
116    })
117}
118
119/// Register [`RefreshableViewMode`] in `registry`. Called by
120/// [`register_foundation_modes`](crate::register_foundation_modes); it must
121/// be registered for the implies cascade to pull it in (an unregistered
122/// shared minor is logged at `debug!` and the chord simply does not bind).
123///
124/// # Panics
125///
126/// If `refreshable-view-mode` is already registered.
127pub fn register_refreshable_view_mode(registry: &mut ModeRegistry) {
128    registry
129        .register(RefreshableViewMode)
130        .expect("refreshable-view-mode must register without conflict");
131}
132
133/// Register `action:view-refresh` so the mode's keymap `cmd` name
134/// resolves at boot.
135///
136/// The `apply` body is a dead `Effect::None`, like `repl-mode`'s: the
137/// host intercepts this `CommandId` in chord dispatch, resolves the
138/// active modes' declared refresh action, and dispatches *that* — so
139/// this body never runs. It exists so the `CommandId` resolves for the
140/// chord binding.
141pub fn register_refreshable_view_actions(registry: &mut lattice_grammar::CommandRegistry) {
142    use lattice_grammar::registry::ActionSpec;
143    registry.register_action(
144        VIEW_REFRESH_ACTION,
145        "Refresh this view (resolves to the active mode's declared refresh action).",
146        ActionSpec {
147            apply: Arc::new(|_| Ok(lattice_grammar::effect::Effect::None)),
148            args_schema: vec![],
149        },
150    );
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn mode_id_uses_the_mode_suffix() {
159        assert_eq!(
160            RefreshableViewMode::mode_id().as_str(),
161            "refreshable-view-mode"
162        );
163    }
164
165    #[test]
166    fn is_a_manual_minor() {
167        let m = RefreshableViewMode;
168        assert_eq!(m.kind(), ModeKind::Minor);
169        assert!(matches!(m.activation_policy(), ActivationPolicy::Manual));
170    }
171
172    #[test]
173    fn binds_gr_to_the_generic_action() {
174        let entries = refreshable_view_keymap_entries();
175        assert_eq!(entries.len(), 1, "one chord, or the shared-ness is a lie");
176        assert_eq!(entries[0].chord, "gr");
177        assert_eq!(entries[0].command, Some(VIEW_REFRESH_ACTION));
178    }
179
180    /// The mode must contribute no handler: the body belongs to each
181    /// view's own mode. A handler here would be the copied-keymap
182    /// problem wearing a different hat.
183    #[test]
184    fn contributes_no_action_handler() {
185        assert!(RefreshableViewMode.action_handlers().is_empty());
186    }
187
188    /// It does not declare a refresh of its own — otherwise it would
189    /// pull itself into the implies cascade.
190    #[test]
191    fn declares_no_refresh_action_itself() {
192        assert_eq!(RefreshableViewMode.refresh_action(), None);
193    }
194}