Skip to main content

lattice_snippet/
session.rs

1//! SN.2: the live snippet-expansion session, relocated off the host
2//! `Editor` so the mode-owned placeholder-navigation handlers can
3//! reach it.
4//!
5//! While a snippet is expanding, exactly one [`ActiveSnippet`] is
6//! live (the tabstop groups + the focused index). Before SN.2 this
7//! lived as `Editor.active_snippet: Option<ActiveSnippet>` and the
8//! `<Tab>` / `<S-Tab>` handlers were `Editor::do_snippet_*` methods.
9//! Per `feedback_mode_owns_its_surface`, the handler bodies belong to
10//! the mode that owns the chords (`SnippetActiveMode`). The
11//! `ActionHandlerRegistry` seam hands a handler only an
12//! [`ActionContext`](lattice_mode::ActionContext) (no `&mut Editor`),
13//! so the session it mutates must live somewhere both the host (which
14//! still creates the session on expand — SN.3 moves that too) and the
15//! mode handler can reach: this service, registered in the
16//! `ServiceRegistry` under [`SnippetSessionHandle`].
17//!
18//! SN.3e: the session is keyed **by buffer**
19//! (`HashMap<BufferId, ActiveSnippet>`), not a single global slot.
20//! Before SN.3e a snippet started in buffer A then switching to B lit
21//! `active-snippet-mode` on B and routed `<Tab>` to A's tabstops
22//! against B's cursor (the predicate + slot were buffer-agnostic).
23//! Snippet state is buffer-local (everything-is-a-buffer ⇒ per-buffer,
24//! not a singleton), so every operation names its buffer: handlers use
25//! `core_buffer_id(ctx.buffer_id)`, host paths use the document buffer.
26//!
27//! One `Mutex` guards the map. Snippet operations are rare (per
28//! `<Tab>`, never per render), so the lock is uncontended and off any
29//! hot path.
30
31use std::collections::HashMap;
32use std::sync::{Arc, Mutex};
33
34use lattice_core::BufferId;
35
36use crate::active::ActiveSnippet;
37
38/// The live snippet sessions, keyed by the buffer each is expanding in
39/// (a buffer is absent from the map when no snippet is live there).
40#[derive(Default)]
41pub struct SnippetSession {
42    inner: Mutex<HashMap<BufferId, ActiveSnippet>>,
43}
44
45impl std::fmt::Debug for SnippetSession {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        // `try_lock` so a `{:?}` on the owning `Editor` can never
48        // deadlock against a concurrent navigation lock.
49        let active = self.inner.try_lock().map(|g| g.len());
50        f.debug_struct("SnippetSession")
51            .field("active_buffers", &active)
52            .finish()
53    }
54}
55
56/// Shared handle, registered in `ServiceRegistry` and held by the
57/// host. Per `feedback_servicesregistry_arc_typeid`: register and
58/// look up under this exact alias so the `TypeId` matches.
59pub type SnippetSessionHandle = Arc<SnippetSession>;
60
61impl SnippetSession {
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// `true` while a snippet is expanding **in `buffer`**. Backs the
67    /// host's many readers (keymap overlay sync, render state, …),
68    /// each of which names the buffer it cares about so a session in
69    /// one buffer never activates the mode in another.
70    pub fn is_active(&self, buffer: BufferId) -> bool {
71        self.lock().contains_key(&buffer)
72    }
73
74    /// Install a freshly-expanded session in `buffer`, replacing any
75    /// prior session for that same buffer.
76    pub fn set(&self, buffer: BufferId, active: ActiveSnippet) {
77        self.lock().insert(buffer, active);
78    }
79
80    /// End the session in `buffer` (reached `$0`, or the buffer/mode
81    /// tore down). A no-op if `buffer` had no live session.
82    pub fn clear(&self, buffer: BufferId) {
83        self.lock().remove(&buffer);
84    }
85
86    /// Mutate the live session for `buffer` in place. The closure sees
87    /// an `Option` so it can advance the focused tabstop *and* end the
88    /// session (set it to `None`) in one critical section — setting it
89    /// to `None` removes the buffer's entry from the map.
90    pub fn with_mut<R>(
91        &self,
92        buffer: BufferId,
93        f: impl FnOnce(&mut Option<ActiveSnippet>) -> R,
94    ) -> R {
95        let mut map = self.lock();
96        // Present the entry as an `Option` so the existing closure
97        // contract (`*s = None` ends the session) is preserved exactly.
98        let mut slot = map.remove(&buffer);
99        let r = f(&mut slot);
100        if let Some(active) = slot {
101            map.insert(buffer, active);
102        }
103        r
104    }
105
106    fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<BufferId, ActiveSnippet>> {
107        self.inner.lock().expect("SnippetSession mutex poisoned")
108    }
109}
110
111/// Predicate for the host's generic session-backed-minor
112/// reconciler: `active-snippet-mode` should be active on the
113/// active buffer exactly while a snippet session is live. The host
114/// pairs this with `SnippetActiveMode::mode_id()` at boot and
115/// reconciles it each overlay-sync cycle — so the host's generic
116/// sync carries no `snippet_session.is_active()` literal. Keeps the
117/// "when is my mode active?" policy in this crate per
118/// `feedback_mode_owns_its_surface`.
119pub fn snippet_active_predicate(
120    session: SnippetSessionHandle,
121) -> Arc<dyn Fn(BufferId) -> bool + Send + Sync> {
122    Arc::new(move |buffer| session.is_active(buffer))
123}
124
125#[cfg(test)]
126mod tests {
127    #![allow(clippy::unwrap_used)]
128    use super::*;
129
130    fn buf(n: u32) -> BufferId {
131        BufferId(n)
132    }
133
134    #[test]
135    fn empty_session_is_inactive() {
136        let s = SnippetSession::new();
137        assert!(!s.is_active(buf(0)));
138        s.clear(buf(0));
139        assert!(!s.is_active(buf(0)));
140    }
141
142    #[test]
143    fn unknown_buffer_is_inactive_never_panics() {
144        let s = SnippetSession::new();
145        // `is_active` / `clear` / `with_mut` on a buffer with no live
146        // session are graceful no-ops (SN.3e.2 graceful contract).
147        assert!(!s.is_active(buf(42)));
148        s.clear(buf(42));
149        let seen = s.with_mut(buf(42), |slot| slot.is_some());
150        assert!(!seen);
151        assert!(!s.is_active(buf(42)));
152    }
153}