diff --git a/src/y_array.rs b/src/y_array.rs index d794417..a8931c0 100644 --- a/src/y_array.rs +++ b/src/y_array.rs @@ -177,6 +177,86 @@ impl YArray { } } + /// Moves the element from the index source to target. + pub fn move_to(&mut self, txn: &mut YTransaction, source: u32, target: u32) -> PyResult<()> { + match &mut self.0 { + SharedType::Integrated(v) => { + v.move_to(txn, source, target); + Ok(()) + }, + SharedType::Prelim(_) if source < 0 as u32 || target < 0 as u32 => Err(PyIndexError::default_message()), + SharedType::Prelim(v) if source < v.len() as u32 && target < v.len() as u32 => { + if source < target { + let el = v.remove(source as usize); + v.insert((target-1) as usize, el); + } else if source > target { + let el = v.remove(source as usize); + v.insert(target as usize, el); + } + Ok(()) + } + _ => Err(PyIndexError::default_message()), + } + } + + /// Moves all elements found within `start`..`end` indexes range (both side inclusive) into + /// new position pointed by `target` index. All elements inserted concurrently by other peers + /// inside of moved range will be moved as well after synchronization (although it make take + /// more than one sync roundtrip to achieve convergence). + /// + /// `assoc_start`/`assoc_end` flags are used to mark if ranges should include elements that + /// might have been inserted concurrently at the edges of the range definition. + /// + /// Example: + /// ``` + /// use yrs::Doc; + /// let doc = Doc::new(); + /// let array = doc.transact().get_array("array"); + /// array.insert_range(&mut doc.transact(), 0, [1,2,3,4]); + /// // move elements 2 and 3 after the 4 + /// array.move_range_to(&mut doc.transact(), 1, 2, 4); + /// ``` + pub fn move_range_to( + &mut self, + txn: &mut YTransaction, + start: u32, + end: u32, + target: u32, + ) -> PyResult<()> { + match &mut self.0 { + SharedType::Integrated(v) => { + v.move_range_to(txn, start, true, end, false, target); + Ok(()) + }, + + // y-rs does nothing if end < start + // SharedType::Prelim(_) if end < start => Err(PyIndexError::default_message()), + SharedType::Prelim(_) if start < 0 as u32 || end < 0 as u32 || target < 0 as u32 => Err(PyIndexError::default_message()), + SharedType::Prelim(v) if start > v.len() as u32 || end > v.len() as u32 || target > v.len() as u32 => Err(PyIndexError::default_message()), + + // It doesn't make sense to move a range into the same range (it's basically a no-op). + SharedType::Prelim(_) if target >= start && target <= end => Ok(()), + + SharedType::Prelim(v) => { + let mut i: usize = 0; + let mut n: usize = (end - start + 1) as usize; + let backwards = target > end; + + while n > 0 { + let item = v.remove(start as usize + i); + if backwards { + v.insert(target as usize - 1, item); + } else { + v.insert(target as usize + i, item); + i += 1; + } + n -= 1; + } + Ok(()) + } + } + } + pub fn __getitem__(&self, index: Index) -> PyResult { // Apply index to the Array type match index { diff --git a/tests/test_y_array.py b/tests/test_y_array.py index 6eb97bc..5e58e7f 100644 --- a/tests/test_y_array.py +++ b/tests/test_y_array.py @@ -242,3 +242,137 @@ def callback(e: list): container[0].append(txn, 4) assert events == None + +def test_move_to(): + """ + Ensure that move_to works. + """ + doc = YDoc() + arr = doc.get_array('test') + + # Move 0 to 10 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_to(t, 0, 10)) + assert arr.to_json() == [1,2,3,4,5,6,7,8,9,0] + + # Move 9 to 0 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_to(t, 9, 0)) + assert arr.to_json() == [9,0,1,2,3,4,5,6,7,8] + + # Move 6 to 5 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_to(t, 6, 5)) + assert arr.to_json() == [0,1,2,3,4,6,5,7,8,9] + + # Move -1 to 5 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + with pytest.raises(Exception): + doc.transact(lambda t: arr.move_to(t, -1, 5)) + + # Move 0 to -5 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + with pytest.raises(Exception): + doc.transact(lambda t: arr.move_to(t, 0, -5)) + +def test_move_range_to(): + """ + Ensure that move_range_to works. + """ + doc = YDoc() + arr = doc.get_array('test') + + # Move 1-2 to 4 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3]) + doc.transact(lambda t: arr.move_range_to(t, 1, 2, 4)) + assert arr.to_json() == [0,3,1,2] + + # Move 0-0 to 10 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 0, 0, 10)) + assert arr.to_json() == [1,2,3,4,5,6,7,8,9,0] + + # Move 0-1 to 10 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 0, 1, 10)) + assert arr.to_json() == [2,3,4,5,6,7,8,9,0,1] + + + # Move 3-5 to 7 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 3, 5, 7)) + assert arr.to_json() == [0,1,2,6,3,4,5,7,8,9] + + # Move 1-0 to 10 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 1, 0, 10)) + assert arr.to_json() == [0,1,2,3,4,5,6,7,8,9] + + # Move 3-5 to 5 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 3, 5, 5)) + assert arr.to_json() == [0,1,2,3,4,5,6,7,8,9] + + # Move 9-9 to 0 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 9, 9, 0)) + assert arr.to_json() == [9,0,1,2,3,4,5,6,7,8] + + # Move 8-9 to 0 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 8, 9, 0)) + assert arr.to_json() == [8,9,0,1,2,3,4,5,6,7] + + # Move 4-6 to 3 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 4, 6, 3)) + assert arr.to_json() == [0,1,2,4,5,6,3,7,8,9] + + # Move 3-5 to 3 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + doc.transact(lambda t: arr.move_range_to(t, 3, 5, 3)) + assert arr.to_json() == [0,1,2,3,4,5,6,7,8,9] + + # Move -1-2 to 5 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + with pytest.raises(Exception): + doc.transact(lambda t: arr.move_range_to(t, -1, 2, 5)) + + # Move 0--1 to 3 + with doc.begin_transaction() as t: + arr.delete_range(t, 0, len(arr)) + arr.extend(t, [0,1,2,3,4,5,6,7,8,9]) + with pytest.raises(Exception): + doc.transact(lambda t: arr.move_range_to(t, 0, -1, 3)) diff --git a/y_py.pyi b/y_py.pyi index d843bb9..17371d4 100644 --- a/y_py.pyi +++ b/y_py.pyi @@ -585,6 +585,42 @@ class YArray: Deletes a range of items of given `length` from current `YArray` instance, starting from given `index`. """ + def move_to(self, txn: YTransaction, source: int, target: int): + """ + Moves a single item found at `source` index into `target` index position. + + Args: + txn: The transaction where the array is being modified. + source: The index of the element to be moved. + target: The new position of the element. + """ + def move_range_to(self, txn: YTransaction, start: int, end: int, target: int): + """ + Moves all elements found within `start`..`end` indexes range (both side inclusive) into + new position pointed by `target` index. All elements inserted concurrently by other peers + inside of moved range will be moved as well after synchronization (although it make take + more than one sync roundtrip to achieve convergence). + + Args: + txn: The transaction where the array is being modified. + start: The index of the first element of the range (inclusive). + end: The index of the last element of the range (inclusive). + target: The new position of the element. + + Example: + ``` + import y_py as Y + doc = Y.Doc(); + array = doc.get_array('array') + + with doc.begin_transaction() as t: + array.insert_range(t, 0, [1,2,3,4]); + + // move elements 2 and 3 after the 4 + with doc.begin_transaction() as t: + array.move_range_to(t, 1, 2, 4); + ``` + """ def __getitem__(self, index: Union[int, slice]) -> Any: """ Returns: