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}