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}