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 super::*;
151 use crate::services::plugins::hooks::HookRegistry;
152 use fresh_core::CursorId;
153
154 #[test]
155 fn test_insert_event_has_hooks() {
156 let event = Event::Insert {
157 position: 0,
158 text: "test".to_string(),
159 cursor_id: CursorId(0),
160 };
161
162 let buffer_id = BufferId(1);
163
164 assert!(event.before_hook(buffer_id).is_some());
166 assert!(event.after_hook(buffer_id).is_some());
167 }
168
169 #[test]
170 fn test_delete_event_has_hooks() {
171 let event = Event::Delete {
172 range: 0..5,
173 deleted_text: "test".to_string(),
174 cursor_id: CursorId(0),
175 };
176
177 let buffer_id = BufferId(1);
178
179 assert!(event.before_hook(buffer_id).is_some());
180 assert!(event.after_hook(buffer_id).is_some());
181 }
182
183 #[test]
184 fn test_overlay_event_no_hooks() {
185 let event = Event::AddOverlay {
186 range: 0..5,
187 face: crate::model::event::OverlayFace::Background { color: (255, 0, 0) },
188 priority: 10,
189 message: None,
190 extend_to_line_end: false,
191 namespace: None,
192 };
193
194 let buffer_id = BufferId(1);
195
196 assert!(event.before_hook(buffer_id).is_none());
198 assert!(event.after_hook(buffer_id).is_none());
199 }
200
201 #[test]
202 fn test_hooks_can_cancel() {
203 use crate::state::EditorState;
204 use std::sync::RwLock;
205
206 let mut state =
207 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
208 let hook_registry = RwLock::new(HookRegistry::new());
209
210 {
212 let mut registry = hook_registry.write().unwrap();
213 registry.add_hook("before_insert", Box::new(|_| false)); }
215
216 let event = Event::Insert {
217 position: 0,
218 text: "test".to_string(),
219 cursor_id: CursorId(0),
220 };
221
222 let buffer_id = BufferId(0);
223 let was_applied = apply_event_with_hooks(&mut state, &event, buffer_id, &hook_registry);
224
225 assert!(!was_applied);
227 assert_eq!(state.buffer.len(), 0); }
229
230 #[test]
231 fn test_hooks_allow_event() {
232 use crate::state::EditorState;
233 use std::sync::RwLock;
234
235 let mut state =
236 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
237 let hook_registry = RwLock::new(HookRegistry::new());
238
239 {
241 let mut registry = hook_registry.write().unwrap();
242 registry.add_hook("before_insert", Box::new(|_| true)); }
244
245 let event = Event::Insert {
246 position: 0,
247 text: "test".to_string(),
248 cursor_id: CursorId(0),
249 };
250
251 let buffer_id = BufferId(0);
252 let was_applied = apply_event_with_hooks(&mut state, &event, buffer_id, &hook_registry);
253
254 assert!(was_applied);
256 assert_eq!(state.buffer.to_string().unwrap(), "test");
257 }
258}