Skip to main content

taino_edit_core/
plugin.rs

1//! Stateful editor plugins.
2//!
3//! A [`Plugin`] is a small unit of editor state that updates on every
4//! [`Transaction`](crate::Transaction). Compared to an
5//! [`Extension`](taino_edit_extensions::Extension), which only contributes
6//! schema and keymap bindings, a `Plugin` carries its own typed state that
7//! the editor folds forward as the document changes — think word counters,
8//! spell-check state, autosave queues, future CRDT bridges.
9//!
10//! v0.2 ships the trait, the typed-erased registry baked into
11//! [`EditorState`], and the [`PluginKey`] lookup.
12//!
13//! ## Observers, not drivers
14//!
15//! This trait is for **observer** plugins: [`Plugin::apply`] folds the
16//! plugin's own state forward from each transaction
17//! (`apply(tx, prev, state) -> state`) and deliberately *cannot* change
18//! the document. That keeps the abstraction small and predictable.
19//!
20//! Components that need to *drive* the document — replace it wholesale,
21//! like undo/redo — are a different category and intentionally do **not**
22//! use this trait. The built-in `History` is the canonical example: it
23//! rewrites the doc through a dedicated `HistoryIntent` short-circuit in
24//! [`EditorState::apply`] and stays a first-class `EditorState` field. A
25//! "HistoryPlugin" was evaluated and rejected (see `ROADMAP.md`,
26//! v0.2.x backlog) — it would have bloated this trait with history-only
27//! hooks for no gain.
28//!
29//! ```
30//! use std::sync::Arc;
31//! use taino_edit_core::{
32//!     EditorState, NodeSpec, Plugin, PluginKey, PluginSet, SchemaBuilder,
33//!     Transaction,
34//! };
35//!
36//! /// Counts every doc-changing transaction.
37//! struct WordCount;
38//!
39//! impl Plugin for WordCount {
40//!     const NAME: &'static str = "word_count";
41//!     type State = usize;
42//!     fn init(&self, _state: &EditorState) -> usize { 0 }
43//!     fn apply(&self, tx: &Transaction, _prev: &EditorState, n: usize) -> usize {
44//!         if tx.doc_changed() { n + 1 } else { n }
45//!     }
46//! }
47//!
48//! const WC_KEY: PluginKey<WordCount> = PluginKey::new();
49//!
50//! let schema = SchemaBuilder::new()
51//!     .node("doc",  NodeSpec { content: Some("text*".into()), ..Default::default() })
52//!     .node("text", NodeSpec::default())
53//!     .top_node("doc")
54//!     .build()
55//!     .unwrap();
56//! let doc = schema.node("doc", Default::default(), vec![], vec![]).unwrap();
57//! let plugins = PluginSet::new().with(WordCount);
58//! let state = EditorState::with_plugins(doc, schema, plugins);
59//! assert_eq!(state.plugin(WC_KEY), Some(&0));
60//! ```
61
62use std::any::Any;
63use std::marker::PhantomData;
64use std::sync::Arc;
65
66use crate::state::{EditorState, Transaction};
67
68/// A stateful editor plugin. Implementations carry no instance data beyond
69/// configuration — the *state* the plugin manages lives in `EditorState`
70/// and is fed back into [`Plugin::apply`] on each transaction.
71pub trait Plugin: Send + Sync + 'static {
72    /// A static identifier, unique within a `PluginSet`. The registry uses
73    /// it as the storage key.
74    const NAME: &'static str;
75
76    /// The plugin's per-state value type. Cloned on each state update so
77    /// `EditorState` stays inexpensive to fork.
78    type State: Clone + Send + Sync + 'static;
79
80    /// Compute the plugin's initial state, given the freshly-constructed
81    /// editor state (doc + selection are already populated; other plugins
82    /// may or may not be initialised yet, so don't peek at them here).
83    fn init(&self, state: &EditorState) -> Self::State;
84
85    /// Fold a transaction into the plugin's state. The default returns
86    /// the previous state unchanged — handy for plugins that only read
87    /// the doc.
88    fn apply(
89        &self,
90        _tx: &Transaction,
91        _prev_state: &EditorState,
92        state: Self::State,
93    ) -> Self::State {
94        state
95    }
96}
97
98/// A typed lookup handle for a plugin's state. Build one as
99/// `const FOO_KEY: PluginKey<Foo> = PluginKey::new();` and pass it to
100/// [`EditorState::plugin`].
101pub struct PluginKey<P: Plugin>(PhantomData<fn() -> P>);
102
103impl<P: Plugin> PluginKey<P> {
104    /// A new key for plugin type `P`. The key is zero-sized; clone/copy
105    /// freely.
106    pub const fn new() -> Self {
107        PluginKey(PhantomData)
108    }
109
110    /// The plugin's static name. Convenience accessor; you rarely need it.
111    pub const fn name(&self) -> &'static str {
112        P::NAME
113    }
114}
115
116impl<P: Plugin> Default for PluginKey<P> {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl<P: Plugin> Clone for PluginKey<P> {
123    fn clone(&self) -> Self {
124        *self
125    }
126}
127impl<P: Plugin> Copy for PluginKey<P> {}
128
129/// Object-safe shim so heterogeneous plugins can live in one collection.
130pub(crate) trait StoredPlugin: Send + Sync {
131    fn name(&self) -> &'static str;
132    fn init_erased(&self, state: &EditorState) -> Box<dyn Any + Send + Sync>;
133    fn apply_erased(
134        &self,
135        tx: &Transaction,
136        prev_state: &EditorState,
137        state: &(dyn Any + Send + Sync),
138    ) -> Box<dyn Any + Send + Sync>;
139}
140
141struct PluginAdapter<P: Plugin>(P);
142
143impl<P: Plugin> StoredPlugin for PluginAdapter<P> {
144    fn name(&self) -> &'static str {
145        P::NAME
146    }
147    fn init_erased(&self, state: &EditorState) -> Box<dyn Any + Send + Sync> {
148        Box::new(self.0.init(state))
149    }
150    fn apply_erased(
151        &self,
152        tx: &Transaction,
153        prev_state: &EditorState,
154        state: &(dyn Any + Send + Sync),
155    ) -> Box<dyn Any + Send + Sync> {
156        let typed: &P::State = state
157            .downcast_ref::<P::State>()
158            .expect("plugin state type mismatch — registry must be consistent");
159        Box::new(self.0.apply(tx, prev_state, typed.clone()))
160    }
161}
162
163/// Builder + container for the plugins an [`EditorState`] runs.
164#[derive(Clone, Default)]
165pub struct PluginSet {
166    plugins: Vec<Arc<dyn StoredPlugin>>,
167}
168
169impl PluginSet {
170    /// An empty registry.
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Append `plugin` to the set. Plugins run in registration order on
176    /// every transaction.
177    pub fn with<P: Plugin>(mut self, plugin: P) -> Self {
178        self.plugins.push(Arc::new(PluginAdapter(plugin)));
179        self
180    }
181
182    /// Number of registered plugins.
183    pub fn len(&self) -> usize {
184        self.plugins.len()
185    }
186
187    /// Whether the set has no plugins.
188    pub fn is_empty(&self) -> bool {
189        self.plugins.is_empty()
190    }
191
192    pub(crate) fn iter(&self) -> impl Iterator<Item = &Arc<dyn StoredPlugin>> {
193        self.plugins.iter()
194    }
195}
196
197impl std::fmt::Debug for PluginSet {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        f.debug_struct("PluginSet")
200            .field(
201                "plugins",
202                &self.plugins.iter().map(|p| p.name()).collect::<Vec<_>>(),
203            )
204            .finish()
205    }
206}
207
208/// The per-state map of plugin states. Stored inside `EditorState`.
209#[derive(Clone, Default)]
210pub(crate) struct PluginStates {
211    states: Vec<(&'static str, Arc<dyn Any + Send + Sync>)>,
212    set: PluginSet,
213}
214
215impl PluginStates {
216    pub(crate) fn from_set(set: PluginSet, state: &EditorState) -> Self {
217        let states = set
218            .iter()
219            .map(|p| (p.name(), Arc::from(p.init_erased(state))))
220            .collect();
221        PluginStates { states, set }
222    }
223
224    pub(crate) fn apply(&self, tx: &Transaction, prev_state: &EditorState) -> Self {
225        let new_states: Vec<(&'static str, Arc<dyn Any + Send + Sync>)> = self
226            .set
227            .iter()
228            .zip(self.states.iter())
229            .map(|(plugin, (name, state))| {
230                let next = plugin.apply_erased(tx, prev_state, state.as_ref());
231                (*name, Arc::from(next))
232            })
233            .collect();
234        PluginStates {
235            states: new_states,
236            set: self.set.clone(),
237        }
238    }
239
240    pub(crate) fn get<P: Plugin>(&self) -> Option<&P::State> {
241        self.states
242            .iter()
243            .find(|(n, _)| *n == P::NAME)
244            .and_then(|(_, s)| s.downcast_ref::<P::State>())
245    }
246}
247
248impl std::fmt::Debug for PluginStates {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        f.debug_struct("PluginStates")
251            .field(
252                "plugins",
253                &self.states.iter().map(|(n, _)| *n).collect::<Vec<_>>(),
254            )
255            .finish()
256    }
257}