Skip to main content

lattice_mode/
pending_inlays.rs

1//! Mode-published inline virtual text (DL.3b).
2//!
3//! The inlay peer of [`crate::pending_synthetic_highlights`], and it
4//! exists for the same reason: buffer-locals are written host-side, so
5//! a mode living in its own crate cannot reach one directly. It pushes
6//! here instead, and the Editor drains the map into the buffer's
7//! `ExtraInlays` local on the next tick.
8//!
9//! Deliberately generic. Nothing here knows about listings — a mode
10//! that wants leading icons, a plugin that wants trailing annotations,
11//! and any future producer of inline virtual text use the same channel.
12//! The LSP hint path is unchanged and merges beside these.
13//!
14//! `store_and_wake` fires the waker, so a producer that lands off the
15//! actor thread reaches the screen **without a keypress** — the failure
16//! mode `feedback_async_needs_wake` records, designed out at the seam
17//! rather than left to each caller to remember.
18
19use std::collections::HashMap;
20use std::sync::{Arc, Mutex};
21
22use lattice_cells::Style;
23use lattice_core::BufferId;
24
25/// One piece of inline virtual text: where it anchors, what it says,
26/// and how to paint it.
27///
28/// Mirrors the host's `InlayHintRow` without depending on it —
29/// `lattice-mode` sits below `lattice-host`, so the host converts on
30/// drain. `style` is normally [`Style::Element`] naming an element the
31/// producing mode registered.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct InlayRow {
34    /// 0-based buffer line.
35    pub line: u32,
36    /// 0-based utf-8 byte offset within that line. `0` anchors the
37    /// text at the start of the row (a leading icon).
38    pub byte: u32,
39    /// The virtual text itself.
40    pub text: String,
41    /// How to paint it.
42    pub style: Style,
43}
44
45/// Shared state between mode-side producers and the Editor's tick
46/// drain. A buffer's entry is a **full replacement** — a producer that
47/// recomputes a listing publishes the whole set, so a shorter listing
48/// leaves nothing of the old behind.
49pub struct PendingInlays {
50    /// Undrained rows by buffer; the host's tick drain moves each entry
51    /// into the buffer's inlay local.
52    pub map: Arc<Mutex<HashMap<BufferId, Vec<InlayRow>>>>,
53    /// The editor's `async_landed` notify ([`set_waker`](Self::set_waker)
54    /// at boot). `None` means stores land but nothing wakes.
55    pub waker: Arc<Mutex<Option<Arc<tokio::sync::Notify>>>>,
56}
57
58/// Shared-handle alias for a producer to keep.
59///
60/// The host registers the bare [`PendingInlays`] (so
61/// `ServiceRegistry::get::<PendingInlays>()` is the lookup, and it returns
62/// this `Arc`). Per the `ServiceRegistry` `TypeId` rule, looking up
63/// `PendingInlaysHandle` against that registration returns `None`.
64pub type PendingInlaysHandle = Arc<PendingInlays>;
65
66impl Default for PendingInlays {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl PendingInlays {
73    /// Empty map, no waker installed.
74    pub fn new() -> Self {
75        Self {
76            map: Arc::new(Mutex::new(HashMap::new())),
77            waker: Arc::new(Mutex::new(None)),
78        }
79    }
80
81    /// Publish `rows` for `buffer_id`, replacing any previous set, and
82    /// wake the Editor so they reach the screen without waiting for a
83    /// keystroke.
84    pub fn store_and_wake(&self, buffer_id: BufferId, rows: Vec<InlayRow>) {
85        if let Ok(mut map) = self.map.lock() {
86            map.insert(buffer_id, rows);
87        }
88        self.fire_waker();
89    }
90
91    /// Drop a buffer's virtual text (buffer closed, or the producing
92    /// mode deactivated). Wakes, so the removal is painted promptly.
93    pub fn clear_and_wake(&self, buffer_id: BufferId) {
94        if let Ok(mut map) = self.map.lock() {
95            map.remove(&buffer_id);
96        }
97        self.fire_waker();
98    }
99
100    /// Install the notify every `*_and_wake` fires. Called once by the host
101    /// with the editor's `async_landed`; replaces any previous waker.
102    pub fn set_waker(&self, waker: Arc<tokio::sync::Notify>) {
103        if let Ok(mut w) = self.waker.lock() {
104            *w = Some(waker);
105        }
106    }
107
108    fn fire_waker(&self) {
109        if let Ok(w) = self.waker.lock()
110            && let Some(n) = w.as_ref()
111        {
112            n.notify_one();
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    #![allow(clippy::unwrap_used)]
120    use super::*;
121
122    fn row(line: u32, text: &str) -> InlayRow {
123        InlayRow {
124            line,
125            byte: 0,
126            text: text.to_string(),
127            style: Style::InlayHint,
128        }
129    }
130
131    #[test]
132    fn store_replaces_rather_than_appends() {
133        let p = PendingInlays::new();
134        let b = BufferId(1);
135        p.store_and_wake(b, vec![row(0, "a"), row(1, "b")]);
136        p.store_and_wake(b, vec![row(0, "c")]);
137        let map = p.map.lock().unwrap();
138        assert_eq!(
139            map.get(&b).unwrap().len(),
140            1,
141            "a recomputed listing publishes the whole set; a shorter one \
142             must not leave the old rows behind"
143        );
144    }
145
146    #[test]
147    fn clear_removes_the_buffers_rows() {
148        let p = PendingInlays::new();
149        let b = BufferId(1);
150        p.store_and_wake(b, vec![row(0, "a")]);
151        p.clear_and_wake(b);
152        assert!(p.map.lock().unwrap().get(&b).is_none());
153    }
154
155    #[tokio::test]
156    async fn store_wakes_without_a_keystroke() {
157        let p = PendingInlays::new();
158        let notify = Arc::new(tokio::sync::Notify::new());
159        p.set_waker(notify.clone());
160
161        let waited = notify.notified();
162        p.store_and_wake(BufferId(1), vec![row(0, "a")]);
163        // `notify_one` before `.await` is remembered, so this resolves.
164        tokio::time::timeout(std::time::Duration::from_secs(5), waited)
165            .await
166            .expect(
167                "store_and_wake must fire the waker — without it the rows sit \
168                     until the user happens to press a key",
169            );
170    }
171}