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 range: range.clone(),
35 }),
36 _ => None, }
38 }
39
40 fn after_hook(&self, buffer_id: BufferId) -> Option<HookArgs> {
41 match self {
42 Self::Insert {
43 position,
44 text,
45 cursor_id: _,
46 } => Some(HookArgs::AfterInsert {
47 buffer_id,
48 position: *position,
49 text: text.clone(),
50 affected_start: *position,
51 affected_end: *position + text.len(),
52 start_line: 0,
54 end_line: 0,
55 lines_added: 0,
56 }),
57 Self::Delete {
58 range,
59 deleted_text,
60 ..
61 } => Some(HookArgs::AfterDelete {
62 buffer_id,
63 range: range.clone(),
64 deleted_text: deleted_text.clone(),
65 affected_start: range.start,
66 deleted_len: deleted_text.len(),
67 start_line: 0,
69 end_line: 0,
70 lines_removed: 0,
71 }),
72 Self::MoveCursor {
73 cursor_id,
74 old_position,
75 new_position,
76 ..
77 } => Some(HookArgs::CursorMoved {
78 buffer_id,
79 cursor_id: *cursor_id,
80 old_position: *old_position,
81 new_position: *new_position,
82 line: 0,
84 }),
85 _ => None,
86 }
87 }
88}
89
90pub fn apply_event_with_hooks(
92 state: &mut crate::state::EditorState,
93 event: &Event,
94 buffer_id: BufferId,
95 hook_registry: &RwLock<HookRegistry>,
96) -> bool {
97 if let Some(before_args) = event.before_hook(buffer_id) {
99 let registry = hook_registry.read().unwrap();
100 let hook_name = match &before_args {
101 HookArgs::BeforeInsert { .. } => "before_insert",
102 HookArgs::BeforeDelete { .. } => "before_delete",
103 _ => "",
104 };
105
106 if !hook_name.is_empty() {
107 let should_continue = registry.run_hooks(hook_name, &before_args);
108 if !should_continue {
109 return false;
111 }
112 }
113 }
114
115 state.apply(event);
117
118 if let Some(mut after_args) = event.after_hook(buffer_id) {
120 if let HookArgs::CursorMoved {
122 new_position,
123 ref mut line,
124 ..
125 } = after_args
126 {
127 *line = state.buffer.get_line_number(new_position) + 1;
130 }
131
132 let registry = hook_registry.read().unwrap();
133 let hook_name = match &after_args {
134 HookArgs::AfterInsert { .. } => "after_insert",
135 HookArgs::AfterDelete { .. } => "after_delete",
136 HookArgs::CursorMoved { .. } => "cursor_moved",
137 _ => "",
138 };
139
140 if !hook_name.is_empty() {
141 registry.run_hooks(hook_name, &after_args);
142 }
143 }
144
145 true }
147
148#[cfg(test)]
149mod tests {
150 use crate::model::filesystem::StdFileSystem;
151 use std::sync::Arc;
152
153 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
154 Arc::new(StdFileSystem)
155 }
156 use super::*;
157 use crate::services::plugins::hooks::HookRegistry;
158 use fresh_core::CursorId;
159
160 #[test]
161 fn test_insert_event_has_hooks() {
162 let event = Event::Insert {
163 position: 0,
164 text: "test".to_string(),
165 cursor_id: CursorId(0),
166 };
167
168 let buffer_id = BufferId(1);
169
170 assert!(event.before_hook(buffer_id).is_some());
172 assert!(event.after_hook(buffer_id).is_some());
173 }
174
175 #[test]
176 fn test_delete_event_has_hooks() {
177 let event = Event::Delete {
178 range: 0..5,
179 deleted_text: "test".to_string(),
180 cursor_id: CursorId(0),
181 };
182
183 let buffer_id = BufferId(1);
184
185 assert!(event.before_hook(buffer_id).is_some());
186 assert!(event.after_hook(buffer_id).is_some());
187 }
188
189 #[test]
190 fn test_overlay_event_no_hooks() {
191 let event = Event::AddOverlay {
192 range: 0..5,
193 face: crate::model::event::OverlayFace::Background { color: (255, 0, 0) },
194 priority: 10,
195 message: None,
196 extend_to_line_end: false,
197 namespace: None,
198 };
199
200 let buffer_id = BufferId(1);
201
202 assert!(event.before_hook(buffer_id).is_none());
204 assert!(event.after_hook(buffer_id).is_none());
205 }
206
207 #[test]
208 fn test_hooks_can_cancel() {
209 use crate::state::EditorState;
210 use std::sync::RwLock;
211
212 let mut state = EditorState::new(
213 80,
214 24,
215 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
216 test_fs(),
217 );
218 let hook_registry = RwLock::new(HookRegistry::new());
219
220 {
222 let mut registry = hook_registry.write().unwrap();
223 registry.add_hook("before_insert", Box::new(|_| false)); }
225
226 let event = Event::Insert {
227 position: 0,
228 text: "test".to_string(),
229 cursor_id: CursorId(0),
230 };
231
232 let buffer_id = BufferId(0);
233 let was_applied = apply_event_with_hooks(&mut state, &event, buffer_id, &hook_registry);
234
235 assert!(!was_applied);
237 assert_eq!(state.buffer.len(), 0); }
239
240 #[test]
241 fn test_hooks_allow_event() {
242 use crate::state::EditorState;
243 use std::sync::RwLock;
244
245 let mut state = EditorState::new(
246 80,
247 24,
248 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
249 test_fs(),
250 );
251 let hook_registry = RwLock::new(HookRegistry::new());
252
253 {
255 let mut registry = hook_registry.write().unwrap();
256 registry.add_hook("before_insert", Box::new(|_| true)); }
258
259 let event = Event::Insert {
260 position: 0,
261 text: "test".to_string(),
262 cursor_id: CursorId(0),
263 };
264
265 let buffer_id = BufferId(0);
266 let was_applied = apply_event_with_hooks(&mut state, &event, buffer_id, &hook_registry);
267
268 assert!(was_applied);
270 assert_eq!(state.buffer.to_string().unwrap(), "test");
271 }
272}