diff --git a/Cargo.lock b/Cargo.lock index 038cbdf9..f1a2f561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1213,6 +1213,7 @@ name = "loro-ffi" version = "1.1.3" dependencies = [ "loro 1.1.0", + "loro-internal 1.1.0", "serde_json", ] diff --git a/crates/loro-ffi/Cargo.toml b/crates/loro-ffi/Cargo.toml index 938b3d8c..83f593e5 100644 --- a/crates/loro-ffi/Cargo.toml +++ b/crates/loro-ffi/Cargo.toml @@ -9,4 +9,5 @@ publish = false [dependencies] loro = { path = "../loro", features = ["counter", "jsonpath"] } +loro-internal = { path = "../loro-internal", features = ["counter", "jsonpath"] } serde_json = { workspace = true } diff --git a/crates/loro-ffi/src/undo.rs b/crates/loro-ffi/src/undo.rs index 41bdbcc9..0673e346 100644 --- a/crates/loro-ffi/src/undo.rs +++ b/crates/loro-ffi/src/undo.rs @@ -56,11 +56,16 @@ impl UndoManager { /// Set the listener for push events. /// The listener will be called when a new undo/redo item is pushed into the stack. pub fn set_on_push(&self, on_push: Option>) { - let on_push = on_push.map(|x| { - Box::new(move |u, c| loro::undo::UndoItemMeta::from(x.on_push(u, c))) - as loro::undo::OnPush - }); - self.0.write().unwrap().set_on_push(on_push) + if let Some(on_push) = on_push { + self.0 + .write() + .unwrap() + .set_on_push(Some(Box::new(move |u, c, e| { + loro::undo::UndoItemMeta::from(on_push.on_push(u, c, e)) + }))); + } else { + self.0.write().unwrap().set_on_push(None); + } } /// Set the listener for pop events. @@ -78,7 +83,8 @@ pub trait OnPush: Send + Sync { fn on_push( &self, undo_or_redo: loro::undo::UndoOrRedo, - couter_span: loro::CounterSpan, + counter_span: loro::CounterSpan, + diff_event: Option, ) -> UndoItemMeta; } @@ -86,7 +92,7 @@ pub trait OnPop: Send + Sync { fn on_pop( &self, undo_or_redo: loro::undo::UndoOrRedo, - couter_span: loro::CounterSpan, + counter_span: loro::CounterSpan, undo_meta: UndoItemMeta, ); } diff --git a/crates/loro-internal/src/undo.rs b/crates/loro-internal/src/undo.rs index 3b418c7e..9e5e4ba4 100644 --- a/crates/loro-internal/src/undo.rs +++ b/crates/loro-internal/src/undo.rs @@ -16,7 +16,7 @@ use crate::{ delta::TreeExternalDiff, event::{Diff, EventTriggerKind}, version::Frontiers, - ContainerDiff, DocDiff, LoroDoc, Subscription, + ContainerDiff, DiffEvent, DocDiff, LoroDoc, Subscription, }; #[derive(Debug, Clone, Default)] @@ -155,7 +155,9 @@ impl UndoOrRedo { /// When a undo/redo item is pushed, the undo manager will call the on_push callback to get the meta data of the undo item. /// The returned cursors will be recorded for a new pushed undo item. -pub type OnPush = Box UndoItemMeta + Send + Sync>; +pub type OnPush = Box< + dyn for<'a> Fn(UndoOrRedo, CounterSpan, Option>) -> UndoItemMeta + Send + Sync, +>; pub type OnPop = Box; struct UndoManagerInner { @@ -386,7 +388,7 @@ impl UndoManagerInner { } } - fn record_checkpoint(&mut self, latest_counter: Counter) { + fn record_checkpoint(&mut self, latest_counter: Counter, event: Option) { if Some(latest_counter) == self.next_counter { return; } @@ -402,7 +404,7 @@ impl UndoManagerInner { let meta = self .on_push .as_ref() - .map(|x| x(UndoOrRedo::Undo, span)) + .map(|x| x(UndoOrRedo::Undo, span, event)) .unwrap_or_default(); if !self.undo_stack.is_empty() && now - self.last_undo_time < self.merge_interval { @@ -471,7 +473,7 @@ impl UndoManager { inner.redo_stack.compose_remote_event(event.events); inner.next_counter = Some(id.counter + 1); } else { - inner.record_checkpoint(id.counter + 1); + inner.record_checkpoint(id.counter + 1, Some(event)); } } } @@ -553,7 +555,10 @@ impl UndoManager { doc.commit_then_renew(); let counter = get_counter_end(doc, self.peer()); - self.inner.try_lock().unwrap().record_checkpoint(counter); + self.inner + .try_lock() + .unwrap() + .record_checkpoint(counter, None); Ok(()) } @@ -695,7 +700,13 @@ impl UndoManager { let mut meta = inner .on_push .as_ref() - .map(|x| x(kind.opposite(), CounterSpan::new(end_counter, new_counter))) + .map(|x| { + x( + kind.opposite(), + CounterSpan::new(end_counter, new_counter), + None, + ) + }) .unwrap_or_default(); if matches!(kind, UndoOrRedo::Undo) && get_opposite(&mut inner).is_empty() { // If it's the first undo, we use the cursors from the users diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 6e7b16a8..2f463895 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -4462,53 +4462,65 @@ impl UndoManager { /// Every time an undo step or redo step is pushed, the on push event listener will be called. #[wasm_bindgen(skip_typescript)] pub fn setOnPush(&mut self, on_push: JsValue) { + let doc = Arc::downgrade(&self.doc); let on_push = on_push.dyn_into::().ok(); if let Some(on_push) = on_push { let on_push = observer::Observer::new(on_push); - self.undo.set_on_push(Some(Box::new(move |kind, span| { - let is_undo = JsValue::from_bool(matches!(kind, UndoOrRedo::Undo)); - let counter_range = js_sys::Object::new(); - js_sys::Reflect::set( - &counter_range, - &JsValue::from_str("start"), - &JsValue::from_f64(span.start as f64), - ) - .unwrap(); - js_sys::Reflect::set( - &counter_range, - &JsValue::from_str("end"), - &JsValue::from_f64(span.end as f64), - ) - .unwrap(); + self.undo + .set_on_push(Some(Box::new(move |kind, span, event| { + let is_undo = JsValue::from_bool(matches!(kind, UndoOrRedo::Undo)); + let counter_range = js_sys::Object::new(); + js_sys::Reflect::set( + &counter_range, + &JsValue::from_str("start"), + &JsValue::from_f64(span.start as f64), + ) + .unwrap(); + js_sys::Reflect::set( + &counter_range, + &JsValue::from_str("end"), + &JsValue::from_f64(span.end as f64), + ) + .unwrap(); - let mut undo_item_meta = UndoItemMeta::new(); - match on_push.call2(&is_undo, &counter_range) { - Ok(v) => { - if let Ok(obj) = v.dyn_into::() { - if let Ok(value) = - js_sys::Reflect::get(&obj, &JsValue::from_str("value")) - { - let value: LoroValue = value.into(); - undo_item_meta.value = value; - } - if let Ok(cursors) = - js_sys::Reflect::get(&obj, &JsValue::from_str("cursors")) - { - let cursors: js_sys::Array = cursors.into(); - for cursor in cursors.iter() { - let cursor = js_to_cursor(cursor).unwrap_throw(); - undo_item_meta.add_cursor(&cursor.pos); + let mut undo_item_meta = UndoItemMeta::new(); + let r = if let Some(e) = event { + if let Some(doc_ref) = doc.upgrade() { + let diff = diff_event_to_js_value(e, &doc_ref); + on_push.call3(&is_undo, &counter_range, &diff) + } else { + on_push.call2(&is_undo, &counter_range) + } + } else { + on_push.call2(&is_undo, &counter_range) + }; + match r { + Ok(v) => { + if let Ok(obj) = v.dyn_into::() { + if let Ok(value) = + js_sys::Reflect::get(&obj, &JsValue::from_str("value")) + { + let value: LoroValue = value.into(); + undo_item_meta.value = value; + } + if let Ok(cursors) = + js_sys::Reflect::get(&obj, &JsValue::from_str("cursors")) + { + let cursors: js_sys::Array = cursors.into(); + for cursor in cursors.iter() { + let cursor = js_to_cursor(cursor).unwrap_throw(); + undo_item_meta.add_cursor(&cursor.pos); + } } } } + Err(e) => { + throw_error_after_micro_task(e); + } } - Err(e) => { - throw_error_after_micro_task(e); - } - } - undo_item_meta - }))); + undo_item_meta + }))); } else { self.undo.set_on_push(None); } @@ -5067,7 +5079,7 @@ export type UndoConfig = { mergeInterval?: number, maxUndoSteps?: number, excludeOriginPrefixes?: string[], - onPush?: (isUndo: boolean, counterRange: { start: number, end: number }) => { value: Value, cursors: Cursor[] }, + onPush?: (isUndo: boolean, counterRange: { start: number, end: number }, event?: LoroEventBatch) => { value: Value, cursors: Cursor[] }, onPop?: (isUndo: boolean, value: { value: Value, cursors: Cursor[] }, counterRange: { start: number, end: number }) => void }; export type Container = LoroList | LoroMap | LoroText | LoroTree | LoroMovableList; diff --git a/crates/loro-wasm/tests/undo.test.ts b/crates/loro-wasm/tests/undo.test.ts index 4fbb4d21..a61df317 100644 --- a/crates/loro-wasm/tests/undo.test.ts +++ b/crates/loro-wasm/tests/undo.test.ts @@ -229,4 +229,24 @@ describe("undo", () => { expect(doc.getCursorPos(poppedCursors[0]).offset).toBe(0); expect(doc.getCursorPos(poppedCursors[1]).offset).toBe(5); }); + + test("it can retrieve event in onPush event", async () => { + const doc = new LoroDoc(); + let ran = false; + const undo = new UndoManager(doc, { + mergeInterval: 0, + onPush: (isUndo, counterRange, event) => { + expect(event).toBeDefined(); + expect(event?.by).toBe("local"); + expect(event?.origin).toBe("test"); + ran = true; + return { value: null, cursors: [] }; + } + }); + + doc.getText("text").insert(0, "hello"); + doc.commit({ origin: "test" }); + await new Promise((r) => setTimeout(r, 1)); + expect(ran).toBeTruthy(); + }) }); diff --git a/crates/loro/tests/integration_test/undo_test.rs b/crates/loro/tests/integration_test/undo_test.rs index f11f60b6..a164f52b 100644 --- a/crates/loro/tests/integration_test/undo_test.rs +++ b/crates/loro/tests/integration_test/undo_test.rs @@ -1536,7 +1536,7 @@ fn undo_manager_events() -> anyhow::Result<()> { let pop_count_clone = pop_count.clone(); let popped_value = Arc::new(Mutex::new(LoroValue::Null)); let popped_value_clone = popped_value.clone(); - undo.set_on_push(Some(Box::new(move |_source, span| { + undo.set_on_push(Some(Box::new(move |_source, span, _| { push_count_clone.fetch_add(1, atomic::Ordering::SeqCst); UndoItemMeta { value: LoroValue::I64(span.start as i64), @@ -1583,7 +1583,7 @@ fn undo_transform_cursor_position() -> anyhow::Result<()> { let mut undo = UndoManager::new(&doc); let cursors: Arc>> = Arc::new(Mutex::new(Vec::new())); let cursors_clone = cursors.clone(); - undo.set_on_push(Some(Box::new(move |_, _| { + undo.set_on_push(Some(Box::new(move |_, _, _| { let mut ans = UndoItemMeta::new(); let cursors = cursors_clone.try_lock().unwrap(); for c in cursors.iter() {