1use crate::model::event::Event;
7use crate::services::plugins::hooks::{HookArgs, HookRegistry};
8use fresh_core::BufferId;
9use std::sync::RwLock;
10
11pub trait EventHooks {
13 fn before_hook(&self, buffer_id: BufferId) -> Option<HookArgs>;
15
16 fn after_hook(&self, buffer_id: BufferId) -> Option<HookArgs>;
18}
19
20impl EventHooks for Event {
21 fn before_hook(&self, buffer_id: BufferId) -> Option<HookArgs> {
22 match self {
23 Self::Insert {
24 position,
25 text,
26 cursor_id: _,
27 } => Some(HookArgs::BeforeInsert {
28 buffer_id,
29 position: *position,
30 text: text.clone(),
31 }),
32 Self::Delete { range, .. } => Some(HookArgs::BeforeDelete {
33 buffer_id,
34 start: range.start,
35 end: range.end,
36 }),
37 _ => None, }
39 }
40
41 fn after_hook(&self, buffer_id: BufferId) -> Option<HookArgs> {
42 match self {
43 Self::Insert {
44 position,
45 text,
46 cursor_id: _,
47 } => Some(HookArgs::AfterInsert {
48 buffer_id,
49 position: *position,
50 text: text.clone(),
51 affected_start: *position,
52 affected_end: *position + text.len(),
53 start_line: 0,
55 end_line: 0,
56 lines_added: 0,
57 }),
58 Self::Delete {
59 range,
60 deleted_text,
61 ..
62 } => Some(HookArgs::AfterDelete {
63 buffer_id,
64 start: range.start,
65 end: range.end,
66 deleted_text: deleted_text.clone(),
67 affected_start: range.start,
68 deleted_len: deleted_text.len(),
69 start_line: 0,
71 end_line: 0,
72 lines_removed: 0,
73 }),
74 Self::MoveCursor {
75 cursor_id,
76 old_position,
77 new_position,
78 ..
79 } => Some(HookArgs::CursorMoved {
80 buffer_id,
81 cursor_id: *cursor_id,
82 old_position: *old_position,
83 new_position: *new_position,
84 line: 0,
86 text_properties: Vec::new(),
87 }),
88 _ => None,
89 }
90 }
91}
92
93pub fn apply_event_with_hooks(
95 state: &mut crate::state::EditorState,
96 cursors: &mut crate::model::cursor::Cursors,
97 event: &Event,
98 buffer_id: BufferId,
99 hook_registry: &RwLock<HookRegistry>,
100) -> bool {
101 if let Some(before_args) = event.before_hook(buffer_id) {
103 let registry = hook_registry.read().unwrap();
104 let hook_name = match &before_args {
105 HookArgs::BeforeInsert { .. } => "before_insert",
106 HookArgs::BeforeDelete { .. } => "before_delete",
107 _ => "",
108 };
109
110 if !hook_name.is_empty() {
111 let should_continue = registry.run_hooks(hook_name, &before_args);
112 if !should_continue {
113 return false;
115 }
116 }
117 }
118
119 state.apply(cursors, event);
121
122 if let Some(mut after_args) = event.after_hook(buffer_id) {
124 if let HookArgs::CursorMoved {
126 new_position,
127 ref mut line,
128 ref mut text_properties,
129 ..
130 } = after_args
131 {
132 *line = state.buffer.get_line_number(new_position) + 1;
135 *text_properties = state
137 .text_properties
138 .get_at(new_position)
139 .into_iter()
140 .map(|tp| tp.properties.clone())
141 .collect();
142 }
143
144 let registry = hook_registry.read().unwrap();
145 let hook_name = match &after_args {
146 HookArgs::AfterInsert { .. } => "after_insert",
147 HookArgs::AfterDelete { .. } => "after_delete",
148 HookArgs::CursorMoved { .. } => "cursor_moved",
149 _ => "",
150 };
151
152 if !hook_name.is_empty() {
153 registry.run_hooks(hook_name, &after_args);
154 }
155 }
156
157 true }
159
160#[cfg(test)]
161mod tests {
162 use crate::model::filesystem::StdFileSystem;
163 use std::sync::Arc;
164
165 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
166 Arc::new(StdFileSystem)
167 }
168 use super::*;
169 use crate::services::plugins::hooks::HookRegistry;
170 use fresh_core::CursorId;
171
172 #[test]
173 fn test_insert_event_has_hooks() {
174 let event = Event::Insert {
175 position: 0,
176 text: "test".to_string(),
177 cursor_id: CursorId(0),
178 };
179
180 let buffer_id = BufferId(1);
181
182 assert!(event.before_hook(buffer_id).is_some());
184 assert!(event.after_hook(buffer_id).is_some());
185 }
186
187 #[test]
188 fn test_delete_event_has_hooks() {
189 let event = Event::Delete {
190 range: 0..5,
191 deleted_text: "test".to_string(),
192 cursor_id: CursorId(0),
193 };
194
195 let buffer_id = BufferId(1);
196
197 assert!(event.before_hook(buffer_id).is_some());
198 assert!(event.after_hook(buffer_id).is_some());
199 }
200
201 #[test]
202 fn test_overlay_event_no_hooks() {
203 let event = Event::AddOverlay {
204 range: 0..5,
205 face: crate::model::event::OverlayFace::Background { color: (255, 0, 0) },
206 priority: 10,
207 message: None,
208 extend_to_line_end: false,
209 namespace: None,
210 url: None,
211 };
212
213 let buffer_id = BufferId(1);
214
215 assert!(event.before_hook(buffer_id).is_none());
217 assert!(event.after_hook(buffer_id).is_none());
218 }
219
220 #[test]
221 fn test_hooks_can_cancel() {
222 use crate::state::EditorState;
223 use std::sync::RwLock;
224
225 let mut state = EditorState::new(
226 80,
227 24,
228 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
229 test_fs(),
230 );
231 let hook_registry = RwLock::new(HookRegistry::new());
232
233 {
235 let mut registry = hook_registry.write().unwrap();
236 registry.add_hook("before_insert", Box::new(|_| false)); }
238
239 let event = Event::Insert {
240 position: 0,
241 text: "test".to_string(),
242 cursor_id: CursorId(0),
243 };
244
245 let buffer_id = BufferId(0);
246 let mut cursors = crate::model::cursor::Cursors::new();
247 let was_applied =
248 apply_event_with_hooks(&mut state, &mut cursors, &event, buffer_id, &hook_registry);
249
250 assert!(!was_applied);
252 assert_eq!(state.buffer.len(), 0); }
254
255 #[test]
256 fn test_hooks_allow_event() {
257 use crate::state::EditorState;
258 use std::sync::RwLock;
259
260 let mut state = EditorState::new(
261 80,
262 24,
263 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
264 test_fs(),
265 );
266 let hook_registry = RwLock::new(HookRegistry::new());
267
268 {
270 let mut registry = hook_registry.write().unwrap();
271 registry.add_hook("before_insert", Box::new(|_| true)); }
273
274 let event = Event::Insert {
275 position: 0,
276 text: "test".to_string(),
277 cursor_id: CursorId(0),
278 };
279
280 let buffer_id = BufferId(0);
281 let mut cursors = crate::model::cursor::Cursors::new();
282 let was_applied =
283 apply_event_with_hooks(&mut state, &mut cursors, &event, buffer_id, &hook_registry);
284
285 assert!(was_applied);
287 assert_eq!(state.buffer.to_string().unwrap(), "test");
288 }
289}