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}