feat: UndoManager's onPush now can access the change event (#588)

Include `origin` and `commitMessage` in the UndoManager's onPush and onPop #584
This commit is contained in:
Zixuan Chen 2024-12-23 11:49:35 +08:00 committed by GitHub
parent 42949c0e24
commit 479c2268f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 106 additions and 55 deletions

1
Cargo.lock generated
View file

@ -1213,6 +1213,7 @@ name = "loro-ffi"
version = "1.1.3"
dependencies = [
"loro 1.1.0",
"loro-internal 1.1.0",
"serde_json",
]

View file

@ -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 }

View file

@ -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<Arc<dyn OnPush>>) {
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<loro_internal::event::DiffEvent>,
) -> 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,
);
}

View file

@ -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<dyn Fn(UndoOrRedo, CounterSpan) -> UndoItemMeta + Send + Sync>;
pub type OnPush = Box<
dyn for<'a> Fn(UndoOrRedo, CounterSpan, Option<DiffEvent<'a>>) -> UndoItemMeta + Send + Sync,
>;
pub type OnPop = Box<dyn Fn(UndoOrRedo, CounterSpan, UndoItemMeta) + Send + Sync>;
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<DiffEvent>) {
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

View file

@ -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::<js_sys::Function>().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::<js_sys::Object>() {
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::<js_sys::Object>() {
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;

View file

@ -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();
})
});

View file

@ -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<Mutex<Vec<Cursor>>> = 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() {