Skip to main content

lattice_mode/
media_source.rs

1//! The registry of inline-media producers (IM.6b).
2//!
3//! The media twin of [`decoration_source`](crate::decoration_source), and the
4//! same contract: an async, off-render-path producer the host drives on a
5//! trigger, whose result it caches. The renderer NEVER calls this.
6
7use std::future::Future;
8use std::path::PathBuf;
9use std::pin::Pin;
10use std::sync::Arc;
11
12use arc_swap::ArcSwap;
13
14/// One inline media block a producer wants drawn.
15///
16/// The native mirror of the WIT `media-block` (`wit/media.wit`). Note what is
17/// absent: any size. The producer names a file and a line; the HOST resolves
18/// the intrinsic dimensions and decides how many rows it reserves, so sizing
19/// policy lives in one place and a plugin cannot claim arbitrary vertical
20/// space in a buffer it does not own.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct MediaBlockRequest {
23    /// 0-based source line the block hangs below.
24    pub anchor_line: u32,
25    /// Path to the image, already resolved against the buffer's directory.
26    pub path: PathBuf,
27    /// What a renderer that cannot draw shows instead. `None` falls back to
28    /// the file name — never nothing.
29    pub alt: Option<String>,
30    /// How the image's intrinsic size maps into the block.
31    pub fit: lattice_cells::MediaFit,
32}
33
34/// The boxed future an [`AsyncMediaSource::produce`] returns.
35///
36/// `Ok(blocks)` replaces the buffer's cached blocks; `Err(reason)` means
37/// **keep the prior cached set**, never "clear". A transient failure mid-edit
38/// must not make every image in the document blink out.
39pub type MediaFuture<'a> =
40    Pin<Box<dyn Future<Output = Result<Vec<MediaBlockRequest>, String>> + Send + 'a>>;
41
42/// An async, off-render-path producer of a buffer's inline media blocks.
43pub trait AsyncMediaSource: Send + Sync + std::fmt::Debug {
44    /// Stable id of the producing plugin — the teardown key. Two producers
45    /// with the same id are the same plugin, so a reload replaces rather than
46    /// duplicates.
47    fn source_id(&self) -> u64;
48
49    /// Produce this buffer's media blocks off the render path.
50    /// `text` is the buffer's contents. Passed by value rather than through a
51    /// document handle because a media scan reads EVERY line: a handle would
52    /// cost one boundary crossing per line, where one copy costs one. The copy
53    /// is affordable because this runs on open / edit, not per frame.
54    fn produce(
55        &self,
56        buffer_id: u64,
57        path: Option<PathBuf>,
58        line_count: u32,
59        text: String,
60    ) -> MediaFuture<'_>;
61}
62
63/// Runtime-mutable registry of [`AsyncMediaSource`]s.
64#[derive(Default, Clone)]
65pub struct MediaSourceRegistry {
66    sources: Vec<Arc<dyn AsyncMediaSource>>,
67}
68
69impl std::fmt::Debug for MediaSourceRegistry {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("MediaSourceRegistry")
72            .field("sources", &self.sources.len())
73            .finish()
74    }
75}
76
77impl MediaSourceRegistry {
78    /// An empty registry.
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Register a producer. Idempotent per `source_id`: a re-register (reload)
84    /// replaces rather than accumulating a duplicate — otherwise every
85    /// `:plugin-reload` would double the images in a buffer.
86    pub fn register(&mut self, source: Arc<dyn AsyncMediaSource>) {
87        let id = source.source_id();
88        self.sources.retain(|s| s.source_id() != id);
89        self.sources.push(source);
90    }
91
92    /// Unregister every producer for `source_id`; returns the count removed.
93    /// No-op when absent, per the teardown contract.
94    pub fn unregister(&mut self, source_id: u64) -> usize {
95        let before = self.sources.len();
96        self.sources.retain(|s| s.source_id() != source_id);
97        before - self.sources.len()
98    }
99
100    /// A wait-free snapshot of the registered producers.
101    pub fn sources(&self) -> Vec<Arc<dyn AsyncMediaSource>> {
102        self.sources.clone()
103    }
104
105    /// True when no producer is registered.
106    pub fn is_empty(&self) -> bool {
107        self.sources.is_empty()
108    }
109
110    /// Number of registered producers.
111    pub fn len(&self) -> usize {
112        self.sources.len()
113    }
114}
115
116/// Boot-service handle. Register **and** look up with this exact alias (the
117/// `ServiceRegistry` TypeId rule).
118pub type MediaSourceRegistryHandle = Arc<ArcSwap<MediaSourceRegistry>>;
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[derive(Debug)]
125    struct Fake(u64);
126    impl AsyncMediaSource for Fake {
127        fn source_id(&self) -> u64 {
128            self.0
129        }
130        fn produce(&self, _b: u64, _p: Option<PathBuf>, _l: u32, _t: String) -> MediaFuture<'_> {
131            Box::pin(async { Ok(Vec::new()) })
132        }
133    }
134
135    /// A reload must REPLACE its producer, not add a second one — otherwise
136    /// every `:plugin-reload` doubles the images in the buffer.
137    #[test]
138    fn re_registering_the_same_source_id_replaces_rather_than_duplicates() {
139        let mut r = MediaSourceRegistry::new();
140        r.register(Arc::new(Fake(7)));
141        r.register(Arc::new(Fake(7)));
142        assert_eq!(r.len(), 1);
143        r.register(Arc::new(Fake(8)));
144        assert_eq!(r.len(), 2);
145    }
146
147    #[test]
148    fn unregister_reports_what_it_removed_and_is_idempotent() {
149        let mut r = MediaSourceRegistry::new();
150        r.register(Arc::new(Fake(7)));
151        assert_eq!(r.unregister(7), 1);
152        assert_eq!(r.unregister(7), 0, "idempotent, per the teardown contract");
153        assert!(r.is_empty());
154    }
155}