Skip to content

Commit 1d20c88

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

File tree

9 files changed

+125
-3
lines changed

9 files changed

+125
-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

+6
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,9 @@ 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 undo(self) -> bool:
180+
"""Undo last action tracked by current undo manager."""

python/pycrdt/_undo.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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 undo(self) -> None:
14+
self._undo_manager.undo()

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

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 undo(&mut self) -> PyResult<bool> {
52+
let Ok(res) = self.undo_manager.as_mut().unwrap().undo() else { return Err(PyRuntimeError::new_err("Cannot undo")) };
53+
Ok(res)
54+
}
55+
}

tests/test_undo.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from pycrdt import Array, Doc, Map, Text, UndoManager
2+
3+
4+
def test_text_undo():
5+
doc = Doc()
6+
doc["text"] = text = Text()
7+
undo_manager = UndoManager(text, capture_timeout_millis=0)
8+
text += "Hello"
9+
assert str(text) == "Hello"
10+
text += ", World!"
11+
assert str(text) == "Hello, World!"
12+
undo_manager.undo()
13+
assert str(text) == "Hello"
14+
undo_manager.undo()
15+
assert str(text) == ""
16+
17+
18+
def test_array_undo():
19+
doc = Doc()
20+
doc["array"] = array = Array()
21+
undo_manager = UndoManager(array, capture_timeout_millis=0)
22+
array.append("foo")
23+
assert array.to_py() == ["foo"]
24+
array.append("bar")
25+
assert array.to_py() == ["foo", "bar"]
26+
undo_manager.undo()
27+
assert array.to_py() == ["foo"]
28+
undo_manager.undo()
29+
assert array.to_py() == []
30+
31+
32+
def test_map_undo():
33+
doc = Doc()
34+
doc["map0"] = map0 = Map()
35+
undo_manager = UndoManager(map0, capture_timeout_millis=0)
36+
map0["key0"] = "val0"
37+
assert map0.to_py() == {"key0": "val0"}
38+
map0["key1"] = "val1"
39+
assert map0.to_py() == {"key0": "val0", "key1": "val1"}
40+
undo_manager.undo()
41+
assert map0.to_py() == {"key0": "val0"}
42+
undo_manager.undo()
43+
assert map0.to_py() == {}

0 commit comments

Comments
 (0)