Skip to main content

lattice_magit/
buffer_state.rs

1//! MG.13 — per-buffer mode state, published for boot-registered
2//! action handlers.
3//!
4//! **The problem this closes.** magit's per-buffer modes used to
5//! register their action handlers from inside `on_activate`, closing
6//! over an `Arc<Mutex<…State>>` built during activation. `on_activate`
7//! runs in the cascade future that `ModeRegistry::spawn_cascade`
8//! spawns, so there is a window after a magit buffer opens in which
9//! the chord resolves through the keymap, the mode reads as active,
10//! and **no handler exists** — the keypress does nothing. That is not
11//! a test artefact: it is what a user gets pressing `d` quickly after
12//! `:magit-branch`. It is also the exact bug MG.8 shipped
13//! (`MagitGlobalMode` registered from `on_activate` behind a
14//! `OnceLock`); the fix there was to move to `Mode::action_handlers()`,
15//! and this module finishes that migration for the modes that were
16//! left behind because they carry per-buffer state.
17//!
18//! **The shape.** Handlers move to `Mode::action_handlers()` —
19//! registered once at boot, for the lifetime of the app — and read
20//! their per-buffer state out of a [`BufferStates`] service keyed by
21//! `BufferId` at call time. `ActionContext` already carries both
22//! `buffer_id` and `services`, so the handler has everything it needs.
23//! Chord scoping is unchanged: K.1.c's per-keystroke filter only
24//! routes a mode's chords in buffers where that mode is active.
25//!
26//! **Why the state is there in time.** `spawn_cascade` polls the
27//! cascade future **once, synchronously, on the App thread** before
28//! spawning it (its try-sync-then-spawn arm). Everything in
29//! `on_activate` above its first `.await` therefore runs before
30//! `activate_major` returns. Publishing state there makes it visible
31//! to the very next keystroke — so each mode's `on_activate` must
32//! `publish` before it awaits anything. Fields that genuinely cannot
33//! be known until after an await (rebase's resolved `upstream`,
34//! commit's `diff_end_line`) are published with an inert initial value
35//! and filled in through the `Arc<Mutex<_>>` once known; their
36//! handlers already refuse to act on the inert value.
37//!
38//! **Caveat — cascade position.** The synchronous first poll reaches
39//! only as far as the first pending await in the *whole* cascade, so
40//! only the root step (the major mode) is guaranteed to publish
41//! synchronously. Implied minors run later. `magit-core-mode` is a
42//! minor and must therefore not depend on this guarantee — which it
43//! does not: its handlers read the buffer through `BufferStoreHandle`
44//! and `ctx.buffer_id`, so they need no published state at all.
45
46use std::collections::HashMap;
47use std::sync::{Arc, Mutex};
48
49use lattice_core::BufferId;
50use lattice_mode::ActionContext;
51
52/// Per-buffer state for one magit mode.
53///
54/// One instance per state type, registered as a service at install
55/// time. Entries are published by `on_activate` and removed when the
56/// buffer's mode guard drops, so a stale entry cannot outlive its
57/// buffer.
58pub struct BufferStates<S> {
59    map: Mutex<HashMap<BufferId, Arc<Mutex<S>>>>,
60}
61
62impl<S> Default for BufferStates<S> {
63    fn default() -> Self {
64        Self {
65            map: Mutex::new(HashMap::new()),
66        }
67    }
68}
69
70impl<S> BufferStates<S> {
71    /// Publish `state` for `buffer`, replacing any prior entry (a
72    /// re-activation on the same buffer supersedes the old state).
73    /// Returns the shared handle so the caller can keep mutating it
74    /// after an await — see the module note on late-resolved fields.
75    pub fn publish(&self, buffer: BufferId, state: S) -> Arc<Mutex<S>> {
76        let shared = Arc::new(Mutex::new(state));
77        if let Ok(mut map) = self.map.lock() {
78            map.insert(buffer, shared.clone());
79        }
80        shared
81    }
82
83    /// Publish an already-shared state handle.
84    ///
85    /// The peer of [`Self::publish`] for a mode that already owns an
86    /// `Arc<Mutex<S>>` (magit-status builds one for its fold source
87    /// before publishing).
88    pub fn publish_shared(&self, buffer: BufferId, state: Arc<Mutex<S>>) {
89        if let Ok(mut map) = self.map.lock() {
90            map.insert(buffer, state);
91        }
92    }
93
94    /// The state for `buffer`, or `None` when no magit mode of this
95    /// type is live on it. Handlers treat `None` as "not mine" and
96    /// no-op — the same outcome as before, minus the race.
97    pub fn get(&self, buffer: BufferId) -> Option<Arc<Mutex<S>>> {
98        self.map.lock().ok()?.get(&buffer).cloned()
99    }
100
101    pub fn remove(&self, buffer: BufferId) {
102        if let Ok(mut map) = self.map.lock() {
103            map.remove(&buffer);
104        }
105    }
106
107    /// Every live state of this type, in no particular order.
108    ///
109    /// MG.21c: for handlers that must reach their mode's buffers
110    /// *without* being able to name one. A prompt's `-finish` action
111    /// fires with the PROMPT buffer's `buffer_id` (see
112    /// `Editor::do_prompt_line_submit`), so [`Self::get`] and
113    /// [`state_for`] both return `None` there — but the work it just
114    /// did still has to show up. Services are reachable from any
115    /// context, so the finish handler refreshes through this instead.
116    pub fn all(&self) -> Vec<Arc<Mutex<S>>> {
117        match self.map.lock() {
118            Ok(map) => map.values().cloned().collect(),
119            Err(_) => Vec::new(),
120        }
121    }
122}
123
124/// Look up this mode's state for the buffer the action fired in.
125///
126/// `Handle` must be the same type used to register the service —
127/// `ServiceRegistry` keys on `TypeId`, so a mismatch silently returns
128/// `None` (see `feedback_servicesregistry_arc_typeid`). Each mode
129/// defines exactly one `…StatesHandle` alias and uses it for both.
130pub fn state_for<S: Send + Sync + 'static>(ctx: &ActionContext<'_>) -> Option<Arc<Mutex<S>>> {
131    let states = ctx.services.get::<Arc<BufferStates<S>>>()?;
132    states.get(BufferId(ctx.buffer_id.0 as u32))
133}
134
135/// Which tree a stretch of diff text was produced against — the
136/// answer [`MagitView::diff_source`] gives, and the only thing
137/// hunk-level `s` / `u` / `x` need to know beyond the hunk itself.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum DiffSource {
140    /// `git diff --cached` — HEAD vs the index. `u` reverses it out.
141    Staged,
142    /// `git diff` — the index vs the working tree. `s` applies it in,
143    /// `x` reverses it out of the worktree.
144    Unstaged,
145    /// MG.23g: a patch already in history — a commit's `git show`, a
146    /// stash's `git stash show -p`.
147    ///
148    /// Neither `s` nor `u` can act on it: the change is not sitting
149    /// between two of *this* checkout's trees, it is a description of
150    /// something that already happened. What it supports instead is
151    /// `a` — apply this one hunk to the working tree — and `-` —
152    /// reverse it back out. Cherry-picking or reverting one hunk of a
153    /// commit rather than the whole thing.
154    Committed,
155}
156
157/// A magit buffer's view behaviour, published per buffer alongside
158/// its state.
159///
160/// **Why this exists.** Several magit modes bind the *same* action —
161/// `action:magit-refresh` (`gr`) has five registrants (status, branch,
162/// stash, diff, log). Per-activation registration hid the collision:
163/// only the active buffer's mode had a handler installed at any
164/// moment. Boot-time registration does not, and
165/// `ActionHandlerRegistry::register` *inserts* — last writer wins — so
166/// five boot registrations would leave `gr` working in exactly one
167/// view and silently dead in the other four.
168///
169/// The fix is polymorphism rather than a central `match`: **one**
170/// handler for the shared action, owned by the mode that owns the
171/// chord (`magit-core-mode` owns `gr`), dispatching through this trait
172/// to whichever view is published for the buffer. Each view mode still
173/// owns its own refresh body — which is what mode-ownership requires —
174/// and no code branches on buffer kind.
175///
176/// **Do not mix registration styles for one action id.** Dropping an
177/// `ActionHandlerRegistration` unregisters *by action id*, so a mode
178/// that still registers `action:magit-refresh` from `on_activate` will,
179/// on deactivation, remove the boot-registered handler too and break
180/// `gr` everywhere. Any action reachable from more than one mode must
181/// be boot-registered exactly once and dispatched through here.
182pub trait MagitView: Send + Sync + 'static {
183    /// `gr` — rebuild this view's content in place.
184    fn refresh(&self) -> Option<lattice_grammar::Effect>;
185
186    /// MG.18d: rebuild after a mutation, then put the cursor back on
187    /// the work `restore` describes.
188    ///
189    /// Separate from [`Self::refresh`] because only a *mutation* has
190    /// something to restore to: a bare `gr` leaves the cursor where the
191    /// user parked it. The view resolves it because only the view knows
192    /// its buffer's shape — magit-status looks for an entry row, a diff
193    /// buffer for a `diff --git` header.
194    ///
195    /// The default is a plain refresh: a view that cannot say where the
196    /// work went rebuilds and leaves the cursor alone, which is the
197    /// pre-MG.18d behaviour.
198    fn refresh_restoring(
199        &self,
200        site: crate::cursor_restore::HunkSite,
201    ) -> Option<lattice_grammar::Effect> {
202        let _ = site;
203        self.refresh()
204    }
205
206    /// `s` — stage the entry at `cursor`.
207    ///
208    /// Bound by `magit-status-mode` and `magit-diff-mode`, which read
209    /// their own buffer's format to find the path (a status entry line
210    /// vs. the nearest `diff --git` header). Views that offer no
211    /// staging decline, which is what the default does — `magit-log`
212    /// has no `s` chord, so its view is never asked.
213    fn stage(
214        &self,
215        cursor: lattice_protocol::position::Position,
216    ) -> Option<lattice_grammar::Effect> {
217        let _ = cursor;
218        None
219    }
220
221    /// `s` in Visual mode — stage every entry the selection covers.
222    ///
223    /// `None` (the default) means this view has no range answer and the
224    /// caller falls back to the single-entry path at the cursor.
225    ///
226    /// **One call, not one per row.** Iterating [`Self::stage`] over
227    /// the selection would spawn a git process and a buffer refresh per
228    /// file, and those refreshes race each other — the last to land
229    /// wins, so the buffer can end up showing a state from the middle
230    /// of the batch. A view that answers this stages the whole set in
231    /// one task and refreshes once.
232    fn stage_rows(&self, rows: std::ops::RangeInclusive<u32>) -> Option<lattice_grammar::Effect> {
233        let _ = rows;
234        None
235    }
236
237    /// `u` in Visual mode. Peer of [`Self::stage_rows`].
238    fn unstage_rows(&self, rows: std::ops::RangeInclusive<u32>) -> Option<lattice_grammar::Effect> {
239        let _ = rows;
240        None
241    }
242
243    /// `u` — unstage the entry at `cursor`. Peer of [`Self::stage`].
244    fn unstage(
245        &self,
246        cursor: lattice_protocol::position::Position,
247    ) -> Option<lattice_grammar::Effect> {
248        let _ = cursor;
249        None
250    }
251
252    /// MG.18c: which diff the text at `cursor` came from.
253    ///
254    /// **Why staging has to ask.** A hunk's patch is only meaningful
255    /// against the tree it was diffed from: an unstaged hunk applies
256    /// forward into the index (`s`), a staged one reverses back out of
257    /// it (`u`). Pressing the wrong one produces a patch git refuses,
258    /// which reaches the user as `error: patch does not apply` —
259    /// indistinguishable from a missed keypress. Worse, `x` on a
260    /// staged hunk would reverse it out of the *worktree* while
261    /// leaving it in the index: the change vanishes from the file but
262    /// is still committed by the next `cc`.
263    ///
264    /// So the operation asks first and declines with a sentence.
265    /// Every view answers from what it already knows — magit-status
266    /// from the section header above the cursor, magit-diff from the
267    /// scope in its buffer name.
268    ///
269    /// `None` means "not classifiable here", and hunk-level staging is
270    /// refused rather than guessed: `*magit:diff*` (against HEAD)
271    /// mixes both sides in one hunk, and a commit's or stash's inline
272    /// patch in magit-status belongs to neither tree. File-level
273    /// staging is unaffected — it never needed this answer.
274    fn diff_source(&self, cursor: lattice_protocol::position::Position) -> Option<DiffSource> {
275        let _ = cursor;
276        None
277    }
278
279    /// MG.20: the commit this view describes at `cursor`, if any.
280    ///
281    /// Reset, revert and cherry-pick all mean "act on the commit under
282    /// the cursor", and every view that shows commits answers that
283    /// question differently — a log row, a `--stat` header, a rebase
284    /// todo line, a Recent-commits entry. Rather than a handler per
285    /// view (which the shared-action collision in MG.13 showed does not
286    /// work) or a `match buffer_kind` in the host (which the
287    /// everything-is-a-buffer rule forbids), each view answers here and
288    /// `magit-core-mode` owns one handler per operation.
289    ///
290    /// Views with no commits decline, which is what the default does.
291    fn commit_at_cursor(&self, cursor: lattice_protocol::position::Position) -> Option<String> {
292        let _ = cursor;
293        None
294    }
295
296    /// The stash index this view describes at `cursor`, if any.
297    ///
298    /// The peer of [`Self::commit_at_cursor`], and it exists for the
299    /// same reason: apply / pop / drop / show all mean "act on the
300    /// stash under the cursor", and more than one view shows stashes.
301    /// The stash-LIST buffer is the obvious one, but magit-status has a
302    /// Stashes section rendering byte-identical rows, and before this
303    /// the handlers resolved through `StashState` — so every stash
304    /// chord was dead in the status buffer, and the dispatch menu's
305    /// stash rows silently did nothing anywhere.
306    ///
307    /// Views with no stashes decline, which is what the default does.
308    /// A view that has them parses its own row format, exactly as it
309    /// does for commits.
310    fn stash_at_cursor(&self, cursor: lattice_protocol::position::Position) -> Option<usize> {
311        let _ = cursor;
312        None
313    }
314
315    /// MG.22: **which version** of `path` `<CR>` should open, for a
316    /// cursor sitting in this view's diff content.
317    ///
318    /// The split is the point. *Finding* the path is diff-text parsing
319    /// and identical everywhere, so it belongs to `magit-hunk-mode`
320    /// ([`crate::hunk::path_at_cursor`]) — three modes had a copy, and
321    /// one of them had a bug the other two did not. *Choosing the
322    /// version* is genuinely per-view and cannot be shared:
323    ///
324    /// | View | `<CR>` opens |
325    /// |---|---|
326    /// | magit-diff, staged scope | the index blob |
327    /// | magit-diff, unstaged / HEAD scope | the working-tree file |
328    /// | magit-commit | the index blob (its diff IS the index) |
329    /// | magit-revision | the file at that sha |
330    /// | magit-stash-show | the file as the stash left it |
331    ///
332    /// `None` means "this view has no answer for that path", and the
333    /// caller says so rather than guessing at a version — opening the
334    /// working-tree copy when the user asked for a historical one is
335    /// the mistake `magit-file-revision-mode` exists to prevent.
336    /// MG.50: `cursor` came with this in MG.50 because magit-status
337    /// needs it — which version of a file its inline diff describes is a
338    /// property of the SECTION the cursor sits under (Staged vs
339    /// Unstaged), not of the buffer. The views whose whole buffer has
340    /// one scope ignore it.
341    fn diff_target(
342        &self,
343        path: &std::path::Path,
344        cursor: lattice_protocol::position::Position,
345    ) -> Option<lattice_grammar::Effect> {
346        let _ = (path, cursor);
347        None
348    }
349
350    /// MG.22: what `<CR>` does when the cursor is **not** in diff
351    /// content.
352    ///
353    /// Only magit-status needs this, and it is why `<CR>` could not
354    /// simply move to `magit-hunk-mode` wholesale: there the chord is
355    /// context-aware over rows that are not diffs at all — a file
356    /// entry, a stash, a commit — and a minor's binding wins over a
357    /// major's, so taking the chord without carrying that behaviour
358    /// would have silently replaced it with a diff-only handler.
359    ///
360    /// Views whose buffer is entirely diff content never reach this.
361    fn visit_at_cursor(
362        &self,
363        cursor: lattice_protocol::position::Position,
364    ) -> Option<lattice_grammar::Effect> {
365        let _ = cursor;
366        None
367    }
368
369    /// The workdir this view's repository lives in — needed to run an
370    /// operation against it from a handler that holds only the view.
371    fn workdir(&self) -> Option<std::path::PathBuf> {
372        None
373    }
374
375    /// The rows `]f` / `[f` treat as "a file", when this view's answer
376    /// differs from magit-status's.
377    ///
378    /// `None` (the default) means the generic scan — indented entry
379    /// rows, which is magit-status's shape and was the ONLY shape until
380    /// now. In a buffer whose content is a unified diff that scan is
381    /// not merely useless, it is wrong: every indented *context* line
382    /// starts with two spaces too, so `]f` walked through arbitrary
383    /// lines of code while claiming to move between files. The same
384    /// class of bug MG.24a found in `]c` / `[c`, which were bound
385    /// universally and dead in the six majors with no hunks.
386    ///
387    /// Views whose content is a diff return the `diff --git` header
388    /// rows instead.
389    ///
390    /// The store and buffer are passed in rather than read off the
391    /// view: two of the diff views keep no store in their state, and
392    /// adding one just to answer this would be state carried for the
393    /// caller's convenience.
394    fn file_lines(
395        &self,
396        store: &lattice_mode::BufferStoreHandle,
397        buffer: BufferId,
398    ) -> Option<Vec<u32>> {
399        let _ = (store, buffer);
400        None
401    }
402
403    /// MG.23k: the git arguments this view can be re-run with — the
404    /// rows `D` offers.
405    ///
406    /// Empty (the default) means the view takes no arguments, and `D`
407    /// says so rather than opening a menu with nothing in it.
408    ///
409    /// **Why this is one chord and not magit's two.** Magit binds `D`
410    /// for diff arguments and `L` for log arguments. `D` is an editing
411    /// operator and therefore inert in a read-only magit buffer, so it
412    /// carries over unchanged — but `L` is the bottom-of-screen
413    /// *motion*, the same class as `M` and `B` that stay off chords
414    /// entirely. Rather than invent a second key, `D` asks the view
415    /// what arguments *it* has: the polymorphism this trait already
416    /// provides for `gr` is exactly the same shape.
417    fn argument_flags(&self) -> &'static [crate::magit_global_mode::RemoteFlag] {
418        &[]
419    }
420
421    /// Re-run this view with `extra` appended to its git invocation.
422    ///
423    /// The values come from the `D` menu, so they REPLACE whatever the
424    /// last run used rather than accumulating — the menu always opens
425    /// with its toggles clear, and "what the menu shows is what runs"
426    /// is the only reading that stays true after a refresh.
427    fn refresh_with_args(&self, extra: Vec<String>) -> Option<lattice_grammar::Effect> {
428        let _ = extra;
429        None
430    }
431}
432
433/// Per-buffer [`MagitView`] registry — the shared-action peer of
434/// [`BufferStates`].
435#[derive(Default)]
436pub struct MagitViews {
437    map: Mutex<HashMap<BufferId, Arc<dyn MagitView>>>,
438}
439
440/// Service alias — register and look up through this exact type
441/// (`feedback_servicesregistry_arc_typeid`).
442pub type MagitViewsHandle = Arc<MagitViews>;
443
444impl MagitViews {
445    pub fn publish(&self, buffer: BufferId, view: Arc<dyn MagitView>) {
446        if let Ok(mut map) = self.map.lock() {
447            map.insert(buffer, view);
448        }
449    }
450
451    pub fn get(&self, buffer: BufferId) -> Option<Arc<dyn MagitView>> {
452        self.map.lock().ok()?.get(&buffer).cloned()
453    }
454
455    pub fn remove(&self, buffer: BufferId) {
456        if let Ok(mut map) = self.map.lock() {
457            map.remove(&buffer);
458        }
459    }
460
461    /// Every live magit view, in no particular order.
462    ///
463    /// MG.21g: for repo-wide operations that invalidate *every* magit
464    /// buffer rather than one. A bisect mark checks out a different
465    /// commit, so the status buffer, any open log, and any open diff
466    /// are all stale at once — refreshing only the buffer the chord
467    /// fired in would leave the others confidently showing the previous
468    /// HEAD. The peer of [`BufferStates::all`], and reachable the same
469    /// way: through the service, so a handler with no buffer of its own
470    /// (a transient row, a prompt submit) can still use it.
471    pub fn all(&self) -> Vec<Arc<dyn MagitView>> {
472        match self.map.lock() {
473            Ok(map) => map.values().cloned().collect(),
474            Err(_) => Vec::new(),
475        }
476    }
477}
478
479/// Refresh every live magit view.
480///
481/// See [`MagitViews::all`] for why repo-wide operations need this
482/// rather than `view_for`.
483pub fn refresh_all_views(ctx: &ActionContext<'_>) {
484    let Some(views) = ctx.services.get::<MagitViewsHandle>() else {
485        return;
486    };
487    for view in views.all() {
488        // Each `refresh` spawns its own task and returns `None`; the
489        // effect channel is not how these land.
490        let _ = view.refresh();
491    }
492}
493
494/// The view for the buffer an action fired in.
495pub fn view_for(ctx: &ActionContext<'_>) -> Option<Arc<dyn MagitView>> {
496    let views = ctx.services.get::<MagitViewsHandle>()?;
497    views.get(BufferId(ctx.buffer_id.0 as u32))
498}
499
500/// Wraps a mode's existing Guard and additionally unpublishes its
501/// [`MagitView`] on drop.
502///
503/// Retained as the composition point for a mode whose Guard already
504/// carries other teardown (magit-status's fold-source registration)
505/// and which also publishes a view.
506pub struct ViewGuard<G> {
507    _inner: G,
508    views: MagitViewsHandle,
509    buffer: BufferId,
510}
511
512impl<G> ViewGuard<G> {
513    pub fn new(inner: G, views: MagitViewsHandle, buffer: BufferId) -> Self {
514        Self {
515            _inner: inner,
516            views,
517            buffer,
518        }
519    }
520}
521
522impl<G> Drop for ViewGuard<G> {
523    fn drop(&mut self) {
524        self.views.remove(self.buffer);
525    }
526}
527
528/// Drops a buffer's state entry when its mode deactivates.
529///
530/// Handler registrations are no longer per-activation anywhere in this
531/// crate, so what is left to unwind is the state entry, the published
532/// view, and (MG.14) the headerline's virtual-row provider.
533pub struct BufferStateGuard<S: Send + Sync + 'static> {
534    states: Arc<BufferStates<S>>,
535    /// Set when the mode also published a [`MagitView`]; dropped
536    /// together with the state so a dead buffer's `gr` cannot resolve.
537    views: Option<MagitViewsHandle>,
538    /// MG.14: the headerline provider registration. Its own `Drop`
539    /// unregisters — holding it here just ties its lifetime to the
540    /// mode's, so the sticky row disappears with the mode.
541    _headerline: Option<crate::headerline::HeaderlineRegistration>,
542    buffer: BufferId,
543}
544
545impl<S: Send + Sync + 'static> BufferStateGuard<S> {
546    pub fn new(states: Arc<BufferStates<S>>, buffer: BufferId) -> Self {
547        Self {
548            states,
549            views: None,
550            _headerline: None,
551            buffer,
552        }
553    }
554
555    /// Also unpublish this buffer's [`MagitView`] on drop.
556    pub fn with_views(mut self, views: MagitViewsHandle) -> Self {
557        self.views = Some(views);
558        self
559    }
560
561    /// Also tear down this buffer's headerline on drop. `None` (a
562    /// harness with no virtual-row registrar) is a no-op.
563    pub fn with_headerline(
564        mut self,
565        registration: Option<crate::headerline::HeaderlineRegistration>,
566    ) -> Self {
567        self._headerline = registration;
568        self
569    }
570}
571
572impl<S: Send + Sync + 'static> Drop for BufferStateGuard<S> {
573    fn drop(&mut self) {
574        self.states.remove(self.buffer);
575        if let Some(views) = &self.views {
576            views.remove(self.buffer);
577        }
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[derive(Debug, PartialEq)]
586    struct Probe(u32);
587
588    #[test]
589    fn published_state_is_readable_for_its_buffer_only() {
590        let states: BufferStates<Probe> = BufferStates::default();
591        states.publish(BufferId(1), Probe(7));
592        assert_eq!(states.get(BufferId(1)).unwrap().lock().unwrap().0, 7);
593        assert!(
594            states.get(BufferId(2)).is_none(),
595            "a handler firing in another buffer must not see this state"
596        );
597    }
598
599    /// The late-resolved-field path: `on_activate` publishes before it
600    /// awaits, then fills in what it learns afterwards. A handler that
601    /// ran in between sees the inert initial value, never a missing
602    /// entry.
603    #[test]
604    fn state_published_before_an_await_is_mutable_afterwards() {
605        let states: BufferStates<Probe> = BufferStates::default();
606        let shared = states.publish(BufferId(1), Probe(0));
607        shared.lock().unwrap().0 = 42;
608        assert_eq!(states.get(BufferId(1)).unwrap().lock().unwrap().0, 42);
609    }
610
611    /// Re-activating on the same buffer must supersede, not
612    /// accumulate — otherwise a reopened magit buffer's chords would
613    /// act on the previous session's state.
614    #[test]
615    fn republishing_supersedes_the_previous_entry() {
616        let states: BufferStates<Probe> = BufferStates::default();
617        states.publish(BufferId(1), Probe(1));
618        states.publish(BufferId(1), Probe(2));
619        assert_eq!(states.get(BufferId(1)).unwrap().lock().unwrap().0, 2);
620    }
621
622    #[test]
623    fn guard_drop_removes_the_entry_so_it_cannot_outlive_the_buffer() {
624        let states: Arc<BufferStates<Probe>> = Arc::new(BufferStates::default());
625        states.publish(BufferId(1), Probe(1));
626        let guard = BufferStateGuard::new(states.clone(), BufferId(1));
627        drop(guard);
628        assert!(states.get(BufferId(1)).is_none());
629    }
630}