Skip to content

Commit

Permalink
feat(sdk): Add LinkedChunk::remove_item_at.
Browse files Browse the repository at this point in the history
This patch adds the `LinkedChunk::remove_item_at` method, along with
`Update::RemoveItem` variant.
  • Loading branch information
Hywan committed Oct 22, 2024
1 parent 97b88ed commit 5b1949c
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 0 deletions.
4 changes: 4 additions & 0 deletions crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ impl UpdateToVectorDiff {
}
}

Update::RemoveItem { at } => {
todo!()
}

Update::DetachLastItems { at } => {
let expected_chunk_identifier = at.chunk_identifier();
let new_length = at.index();
Expand Down
273 changes: 273 additions & 0 deletions crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,79 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
Ok(())
}

/// Remove item at a specified position in the [`LinkedChunk`].
///
/// Because the `position` can be invalid, this method returns a
/// `Result`.
pub fn remove_item_at(&mut self, position: Position) -> Result<Item, Error> {
let chunk_identifier = position.chunk_identifier();
let item_index = position.index();

let mut chunk_ptr = None;
let removed_item;

{
let chunk = self
.links
.chunk_mut(chunk_identifier)
.ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?;

let can_unlink_chunk = match &mut chunk.content {
ChunkContent::Gap(..) => {
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
}

ChunkContent::Items(current_items) => {
let current_items_length = current_items.len();

if item_index > current_items_length {
return Err(Error::InvalidItemIndex { index: item_index });
}

removed_item = current_items.remove(item_index);

if let Some(updates) = self.updates.as_mut() {
updates
.push(Update::RemoveItem { at: Position(chunk_identifier, item_index) })
}

current_items.is_empty()
}
};

// If the `chunk` can be unlinked, and if the `chunk` is not the first one, we
// can remove it.
if can_unlink_chunk && chunk.is_first_chunk().not() {
// Unlink `chunk`.
chunk.unlink(&mut self.updates);

chunk_ptr = Some(chunk.as_ptr());

// We need to update `self.last` if and only if `chunk` _is_ the last chunk. The
// new last chunk is the chunk before `chunk`.
if chunk.is_last_chunk() {
self.links.last = chunk.previous;
}
}

self.length -= 1;

// Stop borrowing `chunk`.
}

if let Some(chunk_ptr) = chunk_ptr {
// `chunk` has been unlinked.

// Re-box the chunk, and let Rust does its job.
//
// SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't
// use it anymore, it's a leak. It is time to re-`Box` it and drop it.
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
}

Ok(removed_item)
}

/// Insert a gap at a specified position in the [`LinkedChunk`].
///
/// Because the `position` can be invalid, this method returns a
Expand Down Expand Up @@ -1851,6 +1924,206 @@ mod tests {
Ok(())
}

#[test]
fn test_remove_item_at() -> Result<(), Error> {
use super::Update::*;

let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']);
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 11);

// Ignore previous updates.
let _ = linked_chunk.updates().unwrap().take();

// Remove the last item of the middle chunk, 3 times. The chunk is empty after
// that. The chunk is removed.
{
let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_f)?;

assert_eq!(removed_item, 'f');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 10);

let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_e)?;

assert_eq!(removed_item, 'e');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 9);

let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_d)?;

assert_eq!(removed_item, 'd');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 8);

assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(1), 2) },
RemoveItem { at: Position(ChunkIdentifier(1), 1) },
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
RemoveChunk(ChunkIdentifier(1)),
]
);
}

// Remove the first item of the first chunk, 3 times. The chunk is empty after
// that. The chunk is NOT removed because it's the first chunk.
{
let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
let removed_item = linked_chunk.remove_item_at(first_position)?;

assert_eq!(removed_item, 'a');
assert_items_eq!(linked_chunk, ['b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 7);

let removed_item = linked_chunk.remove_item_at(first_position)?;

assert_eq!(removed_item, 'b');
assert_items_eq!(linked_chunk, ['c'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 6);

let removed_item = linked_chunk.remove_item_at(first_position)?;

assert_eq!(removed_item, 'c');
assert_items_eq!(linked_chunk, [] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 5);

assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
]
);
}

// Remove the first item of the middle chunk, 3 times. The chunk is empty after
// that. The chunk is removed.
{
let first_position = linked_chunk.item_position(|item| *item == 'g').unwrap();
let removed_item = linked_chunk.remove_item_at(first_position)?;

assert_eq!(removed_item, 'g');
assert_items_eq!(linked_chunk, [] ['h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 4);

let removed_item = linked_chunk.remove_item_at(first_position)?;

assert_eq!(removed_item, 'h');
assert_items_eq!(linked_chunk, [] ['i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 3);

let removed_item = linked_chunk.remove_item_at(first_position)?;

assert_eq!(removed_item, 'i');
assert_items_eq!(linked_chunk, [] ['j', 'k']);
assert_eq!(linked_chunk.len(), 2);

assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
RemoveChunk(ChunkIdentifier(2)),
]
);
}

// Remove the last item of the last chunk, twice. The chunk is empty after that.
// The chunk is removed.
{
let position_of_k = linked_chunk.item_position(|item| *item == 'k').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_k)?;

assert_eq!(removed_item, 'k');
#[rustfmt::skip]
assert_items_eq!(linked_chunk, [] ['j']);
assert_eq!(linked_chunk.len(), 1);

let position_of_j = linked_chunk.item_position(|item| *item == 'j').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_j)?;

assert_eq!(removed_item, 'j');
assert_items_eq!(linked_chunk, []);
assert_eq!(linked_chunk.len(), 0);

assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(3), 1) },
RemoveItem { at: Position(ChunkIdentifier(3), 0) },
RemoveChunk(ChunkIdentifier(3)),
]
);
}

// Add a couple more items, delete one, add a gap, and delete more items.
{
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);

#[rustfmt::skip]
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']);
assert_eq!(linked_chunk.len(), 4);

let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
linked_chunk.insert_gap_at((), position_of_c)?;

assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['c'] ['d']);
assert_eq!(linked_chunk.len(), 4);

// Ignore updates.
let _ = linked_chunk.updates().unwrap().take();

let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_c)?;

assert_eq!(removed_item, 'c');
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d']);
assert_eq!(linked_chunk.len(), 3);

let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_d)?;

assert_eq!(removed_item, 'd');
assert_items_eq!(linked_chunk, ['a', 'b'] [-]);
assert_eq!(linked_chunk.len(), 2);

let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
let removed_item = linked_chunk.remove_item_at(first_position)?;

assert_eq!(removed_item, 'a');
assert_items_eq!(linked_chunk, ['b'] [-]);
assert_eq!(linked_chunk.len(), 1);

let removed_item = linked_chunk.remove_item_at(first_position)?;

assert_eq!(removed_item, 'b');
assert_items_eq!(linked_chunk, [] [-]);
assert_eq!(linked_chunk.len(), 0);

assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(6), 0) },
RemoveChunk(ChunkIdentifier(6)),
RemoveItem { at: Position(ChunkIdentifier(4), 0) },
RemoveChunk(ChunkIdentifier(4)),
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
]
);
}

Ok(())
}

#[test]
fn test_insert_gap_at() -> Result<(), Error> {
use super::Update::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ pub enum Update<Item, Gap> {
items: Vec<Item>,
},

/// An item has been removed inside a chunk of kind Items.
RemoveItem {
/// The [`Position`] of the item.
at: Position,
},

/// The last items of a chunk have been detached, i.e. the chunk has been
/// truncated.
DetachLastItems {
Expand Down

0 comments on commit 5b1949c

Please sign in to comment.