Skip to main content

fresh/model/
control_event.rs

1//! Control Events - Observable notifications about editor state changes
2//!
3//! Simple, elegant event system:
4//! - Events are just (name, data) pairs
5//! - Names are namespaced strings: "editor:file_saved", "lsp:status_changed", "plugin:git:branch_changed"
6//! - Data is arbitrary JSON
7//! - Plugins emit events, editor emits events, anyone can listen
8//!
9//! IMPORTANT: All core events must be registered in EventRegistry for documentation and validation.
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::VecDeque;
14use std::sync::{Arc, Mutex};
15
16/// Authoritative registry of all core events.
17/// This is the single source of truth for event names and their data schemas.
18/// Plugin events (plugin:*) are exempt from this registry.
19pub mod events {
20    use serde_json::{json, Value};
21
22    /// Event definition with name and data schema
23    pub struct EventDef {
24        pub name: &'static str,
25        pub description: &'static str,
26        /// Function that returns the data schema (can't use json! in const)
27        pub data_schema_fn: fn() -> Value,
28    }
29
30    // ===== Editor Events =====
31
32    pub const FILE_OPENED: EventDef = EventDef {
33        name: "editor:file_opened",
34        description: "File opened in editor",
35        data_schema_fn: || json!({"path": "string", "buffer_id": "number"}),
36    };
37
38    pub const FILE_SAVED: EventDef = EventDef {
39        name: "editor:file_saved",
40        description: "File saved to disk",
41        data_schema_fn: || json!({"path": "string"}),
42    };
43
44    // ===== LSP Events =====
45
46    pub const LSP_STATUS_CHANGED: EventDef = EventDef {
47        name: "lsp:status_changed",
48        description: "LSP server status changed",
49        data_schema_fn: || json!({"language": "string", "old_status": "string", "status": "string"}),
50    };
51
52    /// Get all registered events (for schema generation)
53    pub fn all_events() -> Vec<&'static EventDef> {
54        vec![&FILE_OPENED, &FILE_SAVED, &LSP_STATUS_CHANGED]
55    }
56
57    /// Get schema for all events as JSON
58    pub fn schema() -> Value {
59        let mut events = serde_json::Map::new();
60        for event in all_events() {
61            events.insert(
62                event.name.to_string(),
63                json!({
64                    "description": event.description,
65                    "data": (event.data_schema_fn)()
66                }),
67            );
68        }
69        Value::Object(events)
70    }
71}
72
73/// A single control event - just a name and some data
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
75pub struct ControlEvent {
76    /// Event name (namespaced): "editor:file_saved", "lsp:ready", "plugin:git:status"
77    pub name: String,
78    /// Arbitrary JSON data
79    pub data: Value,
80}
81
82impl ControlEvent {
83    /// Create a new event
84    pub fn new(name: impl Into<String>, data: Value) -> Self {
85        Self {
86            name: name.into(),
87            data,
88        }
89    }
90
91    /// Create an event with no data
92    pub fn simple(name: impl Into<String>) -> Self {
93        Self {
94            name: name.into(),
95            data: Value::Null,
96        }
97    }
98
99    /// Check if event name matches a pattern
100    /// Patterns can use "*" as wildcard: "lsp:*", "plugin:git:*", "*:error"
101    pub fn matches(&self, pattern: &str) -> bool {
102        if pattern == "*" {
103            return true;
104        }
105
106        if !pattern.contains('*') {
107            return self.name == pattern;
108        }
109
110        // Simple glob matching
111        let parts: Vec<&str> = pattern.split('*').collect();
112        if parts.len() == 2 {
113            let (prefix, suffix) = (parts[0], parts[1]);
114            self.name.starts_with(prefix) && self.name.ends_with(suffix)
115        } else {
116            // More complex patterns - just do exact match for now
117            self.name == pattern
118        }
119    }
120
121    /// Check if data contains expected values (shallow match)
122    pub fn data_matches(&self, expected: &Value) -> bool {
123        match (expected, &self.data) {
124            (Value::Null, _) => true, // Null pattern matches anything
125            (Value::Object(exp_map), Value::Object(data_map)) => {
126                // All expected keys must match
127                exp_map.iter().all(|(k, v)| {
128                    data_map.get(k).is_some_and(|data_v| {
129                        if v.is_null() {
130                            true // Null in pattern means "key exists, any value"
131                        } else {
132                            v == data_v
133                        }
134                    })
135                })
136            }
137            _ => expected == &self.data,
138        }
139    }
140}
141
142/// Broadcasts events to subscribers
143#[derive(Clone)]
144pub struct EventBroadcaster {
145    events: Arc<Mutex<VecDeque<ControlEvent>>>,
146    max_history: usize,
147}
148
149impl EventBroadcaster {
150    pub fn new(max_history: usize) -> Self {
151        Self {
152            events: Arc::new(Mutex::new(VecDeque::with_capacity(max_history))),
153            max_history,
154        }
155    }
156
157    /// Emit an event
158    pub fn emit(&self, event: ControlEvent) {
159        let mut events = self.events.lock().unwrap();
160        if events.len() >= self.max_history {
161            events.pop_front();
162        }
163        events.push_back(event);
164    }
165
166    /// Convenience: emit with name and data
167    pub fn emit_named(&self, name: impl Into<String>, data: Value) {
168        self.emit(ControlEvent::new(name, data));
169    }
170
171    /// Convenience: emit simple event (no data)
172    pub fn emit_simple(&self, name: impl Into<String>) {
173        self.emit(ControlEvent::simple(name));
174    }
175
176    /// Check if any event matches pattern
177    pub fn has_match(&self, name_pattern: &str, data_pattern: &Value) -> bool {
178        let events = self.events.lock().unwrap();
179        events
180            .iter()
181            .any(|e| e.matches(name_pattern) && e.data_matches(data_pattern))
182    }
183
184    /// Take first event matching pattern (removes it and all events before it)
185    pub fn take_match(&self, name_pattern: &str, data_pattern: &Value) -> Option<ControlEvent> {
186        let mut events = self.events.lock().unwrap();
187        let pos = events
188            .iter()
189            .position(|e| e.matches(name_pattern) && e.data_matches(data_pattern));
190
191        if let Some(idx) = pos {
192            let event = events.get(idx).cloned();
193            events.drain(..=idx);
194            event
195        } else {
196            None
197        }
198    }
199
200    /// Drain all events
201    pub fn drain(&self) -> Vec<ControlEvent> {
202        let mut events = self.events.lock().unwrap();
203        events.drain(..).collect()
204    }
205
206    /// Peek at all events
207    pub fn peek(&self) -> Vec<ControlEvent> {
208        let events = self.events.lock().unwrap();
209        events.iter().cloned().collect()
210    }
211
212    /// Clear all events
213    pub fn clear(&self) {
214        let mut events = self.events.lock().unwrap();
215        events.clear();
216    }
217
218    /// Number of pending events
219    pub fn len(&self) -> usize {
220        let events = self.events.lock().unwrap();
221        events.len()
222    }
223
224    /// Is empty?
225    pub fn is_empty(&self) -> bool {
226        self.len() == 0
227    }
228}
229
230impl Default for EventBroadcaster {
231    fn default() -> Self {
232        Self::new(1000)
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use serde_json::json;
240
241    #[test]
242    fn test_event_matching() {
243        let event = ControlEvent::new(
244            "lsp:status_changed",
245            json!({"language": "rust", "status": "running"}),
246        );
247
248        assert!(event.matches("lsp:status_changed"));
249        assert!(event.matches("lsp:*"));
250        assert!(event.matches("*:status_changed"));
251        assert!(event.matches("*"));
252        assert!(!event.matches("lsp:error"));
253        assert!(!event.matches("editor:*"));
254    }
255
256    #[test]
257    fn test_data_matching() {
258        let event = ControlEvent::new("test", json!({"a": 1, "b": "hello", "c": true}));
259
260        // Exact match
261        assert!(event.data_matches(&json!({"a": 1})));
262        assert!(event.data_matches(&json!({"b": "hello"})));
263        assert!(event.data_matches(&json!({"a": 1, "b": "hello"})));
264
265        // Null means "any"
266        assert!(event.data_matches(&json!({"a": null})));
267        assert!(event.data_matches(&Value::Null));
268
269        // Mismatch
270        assert!(!event.data_matches(&json!({"a": 2})));
271        assert!(!event.data_matches(&json!({"d": 1})));
272    }
273
274    #[test]
275    fn test_broadcaster() {
276        let bc = EventBroadcaster::new(10);
277
278        bc.emit_simple("editor:init");
279        bc.emit_named(
280            "lsp:status_changed",
281            json!({"language": "rust", "status": "starting"}),
282        );
283        bc.emit_named(
284            "lsp:status_changed",
285            json!({"language": "rust", "status": "running"}),
286        );
287
288        assert_eq!(bc.len(), 3);
289
290        // Find LSP running event
291        assert!(bc.has_match("lsp:status_changed", &json!({"status": "running"})));
292
293        // Take it
294        let event = bc.take_match("lsp:status_changed", &json!({"status": "running"}));
295        assert!(event.is_some());
296        assert_eq!(event.unwrap().data["status"], "running");
297
298        // Only one event left (after the match)
299        assert_eq!(bc.len(), 0);
300    }
301
302    #[test]
303    fn test_max_history() {
304        let bc = EventBroadcaster::new(2);
305
306        bc.emit_simple("a");
307        bc.emit_simple("b");
308        bc.emit_simple("c");
309
310        let events = bc.drain();
311        assert_eq!(events.len(), 2);
312        assert_eq!(events[0].name, "b");
313        assert_eq!(events[1].name, "c");
314    }
315}