Skip to content

Commit ce8be5e

Browse files
committed
Add undo manager
1 parent 17eadd4 commit ce8be5e

File tree

9 files changed

+198
-3
lines changed

9 files changed

+198
-3
lines changed

python/pycrdt/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
from ._text import TextEvent as TextEvent
1010
from ._transaction import ReadTransaction as ReadTransaction
1111
from ._transaction import Transaction as Transaction
12+
from ._undo import UndoManager as UndoManager

python/pycrdt/_pycrdt.pyi

+18
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,21 @@ class Map:
172172
def unobserve(self, subscription: Subscription) -> None:
173173
"""Unsubscribes previously subscribed event callback identified by given
174174
`subscription`."""
175+
176+
class UndoManager:
177+
"""Undo manager."""
178+
179+
def can_undo(self) -> bool:
180+
"""Whether there is any change to undo."""
181+
182+
def undo(self) -> bool:
183+
"""Undo last action tracked by current undo manager."""
184+
185+
def can_redo(self) -> bool:
186+
"""Whether there is any change to redo."""
187+
188+
def redo(self) -> bool:
189+
"""Redo last action previously undone by current undo manager."""
190+
191+
def clear(self) -> None:
192+
"""Clear all items stored within current undo manager."""

python/pycrdt/_undo.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from ._base import BaseType
4+
from ._pycrdt import UndoManager as _UndoManager
5+
6+
7+
class UndoManager:
8+
def __init__(self, scope: BaseType, capture_timeout_millis: int = 500) -> None:
9+
undo_manager = _UndoManager()
10+
method = getattr(undo_manager, f"from_{scope.type_name}")
11+
self._undo_manager = method(scope.doc._doc, scope._integrated, capture_timeout_millis)
12+
13+
def can_undo(self) -> bool:
14+
return self._undo_manager.can_undo()
15+
16+
def undo(self) -> bool:
17+
return self._undo_manager.undo()
18+
19+
def can_redo(self) -> bool:
20+
return self._undo_manager.can_redo()
21+
22+
def redo(self) -> bool:
23+
return self._undo_manager.redo()
24+
25+
def clear(self) -> None:
26+
self._undo_manager.clear()

src/array.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::doc::Doc;
2424

2525
#[pyclass(unsendable)]
2626
pub struct Array {
27-
array: ArrayRef,
27+
pub array: ArrayRef,
2828
}
2929

3030
impl Array {

src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod map;
66
mod transaction;
77
mod subscription;
88
mod type_conversions;
9+
mod undo;
910
use crate::doc::Doc;
1011
use crate::doc::TransactionEvent;
1112
use crate::doc::SubdocsEvent;
@@ -14,6 +15,7 @@ use crate::array::{Array, ArrayEvent};
1415
use crate::map::{Map, MapEvent};
1516
use crate::transaction::Transaction;
1617
use crate::subscription::Subscription;
18+
use crate::undo::UndoManager;
1719

1820
#[pymodule]
1921
fn _pycrdt(_py: Python, m: &PyModule) -> PyResult<()> {
@@ -28,5 +30,6 @@ fn _pycrdt(_py: Python, m: &PyModule) -> PyResult<()> {
2830
m.add_class::<MapEvent>()?;
2931
m.add_class::<Transaction>()?;
3032
m.add_class::<Subscription>()?;
33+
m.add_class::<UndoManager>()?;
3134
Ok(())
3235
}

src/map.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::doc::Doc;
2424

2525
#[pyclass(unsendable)]
2626
pub struct Map {
27-
map: MapRef,
27+
pub map: MapRef,
2828
}
2929

3030
impl Map {

src/text.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::type_conversions::ToPython;
1515

1616
#[pyclass(unsendable)]
1717
pub struct Text {
18-
text: TextRef,
18+
pub text: TextRef,
1919
}
2020

2121
impl Text {

src/undo.rs

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use pyo3::prelude::*;
2+
use pyo3::exceptions::PyRuntimeError;
3+
use yrs::{
4+
UndoManager as _UndoManager,
5+
};
6+
use yrs::undo::Options;
7+
use crate::doc::Doc;
8+
use crate::text::Text;
9+
use crate::array::Array;
10+
use crate::map::Map;
11+
12+
13+
#[pyclass(unsendable)]
14+
pub struct UndoManager {
15+
undo_manager: Option<_UndoManager>,
16+
}
17+
18+
impl UndoManager {
19+
fn get_options(&self, capture_timeout_millis: &u64) -> Options {
20+
let mut options = Options::default();
21+
options.capture_timeout_millis = *capture_timeout_millis;
22+
options
23+
}
24+
}
25+
26+
#[pymethods]
27+
impl UndoManager {
28+
#[new]
29+
fn new() -> Self {
30+
UndoManager { undo_manager: None }
31+
}
32+
33+
pub fn from_text(&self, doc: &Doc, scope: &Text, capture_timeout_millis: u64) -> Self {
34+
let options = self.get_options(&capture_timeout_millis);
35+
let undo_manager = _UndoManager::with_options(&doc.doc, &scope.text, options);
36+
UndoManager { undo_manager: Some(undo_manager) }
37+
}
38+
39+
pub fn from_array(&self, doc: &Doc, scope: &Array, capture_timeout_millis: u64) -> Self {
40+
let options = self.get_options(&capture_timeout_millis);
41+
let undo_manager = _UndoManager::with_options(&doc.doc, &scope.array, options);
42+
UndoManager { undo_manager: Some(undo_manager) }
43+
}
44+
45+
pub fn from_map(&self, doc: &Doc, scope: &Map, capture_timeout_millis: u64) -> Self {
46+
let options = self.get_options(&capture_timeout_millis);
47+
let undo_manager = _UndoManager::with_options(&doc.doc, &scope.map, options);
48+
UndoManager { undo_manager: Some(undo_manager) }
49+
}
50+
51+
pub fn can_undo(&mut self) -> bool {
52+
self.undo_manager.as_ref().unwrap().can_undo()
53+
}
54+
55+
pub fn undo(&mut self) -> PyResult<bool> {
56+
let Ok(res) = self.undo_manager.as_mut().unwrap().undo() else { return Err(PyRuntimeError::new_err("Cannot undo")) };
57+
Ok(res)
58+
}
59+
60+
pub fn can_redo(&mut self) -> bool {
61+
self.undo_manager.as_ref().unwrap().can_redo()
62+
}
63+
64+
pub fn redo(&mut self) -> PyResult<bool> {
65+
let Ok(res) = self.undo_manager.as_mut().unwrap().redo() else { return Err(PyRuntimeError::new_err("Cannot redo")) };
66+
Ok(res)
67+
}
68+
69+
pub fn clear(&mut self) -> PyResult<()> {
70+
let Ok(res) = self.undo_manager.as_mut().unwrap().clear() else { return Err(PyRuntimeError::new_err("Cannot clear")) };
71+
Ok(res)
72+
}
73+
}

tests/test_undo.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from pycrdt import Array, Doc, Map, Text, UndoManager
2+
3+
4+
def undo_redo(data, undo_manager, val0, val1, val3):
5+
assert undo_manager.can_undo()
6+
undone = undo_manager.undo()
7+
assert undone
8+
assert data.to_py() == val1
9+
assert undo_manager.can_undo()
10+
undone = undo_manager.undo()
11+
assert undone
12+
assert data.to_py() == val0
13+
assert not undo_manager.can_undo()
14+
undone = undo_manager.undo()
15+
assert not undone
16+
assert undo_manager.can_redo()
17+
redone = undo_manager.redo()
18+
assert redone
19+
assert data.to_py() == val1
20+
assert undo_manager.can_redo()
21+
redone = undo_manager.redo()
22+
assert redone
23+
assert data.to_py() == val3
24+
assert not undo_manager.can_redo()
25+
redone = undo_manager.redo()
26+
assert not redone
27+
assert undo_manager.can_undo()
28+
undo_manager.clear()
29+
assert not undo_manager.can_undo()
30+
31+
32+
def test_text_undo():
33+
doc = Doc()
34+
doc["data"] = data = Text()
35+
undo_manager = UndoManager(data, capture_timeout_millis=0)
36+
val0 = ""
37+
val1 = "Hello"
38+
val2 = ", World!"
39+
val3 = val1 + val2
40+
data += val1
41+
assert data.to_py() == val1
42+
data += val2
43+
assert data.to_py() == val3
44+
undo_redo(data, undo_manager, val0, val1, val3)
45+
46+
47+
def test_array_undo():
48+
doc = Doc()
49+
doc["data"] = data = Array()
50+
undo_manager = UndoManager(data, capture_timeout_millis=0)
51+
val0 = []
52+
val1 = ["foo"]
53+
val2 = ["bar"]
54+
val3 = val1 + val2
55+
data += val1
56+
assert data.to_py() == val1
57+
data += val2
58+
assert data.to_py() == val3
59+
undo_redo(data, undo_manager, val0, val1, val3)
60+
61+
62+
def test_map_undo():
63+
doc = Doc()
64+
doc["data"] = data = Map()
65+
undo_manager = UndoManager(data, capture_timeout_millis=0)
66+
val0 = {}
67+
val1 = {"key0": "val0"}
68+
val2 = {"key1": "val1"}
69+
val3 = dict(**val1, **val2)
70+
data.update(val1)
71+
assert data.to_py() == val1
72+
data.update(val2)
73+
assert data.to_py() == val3
74+
undo_redo(data, undo_manager, val0, val1, val3)

0 commit comments

Comments
 (0)