matrix_sdk_common/linked_chunk/as_vector.rs
1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16 collections::VecDeque,
17 iter::repeat_n,
18 ops::{ControlFlow, Not},
19 sync::{Arc, RwLock},
20};
21
22use eyeball_im::VectorDiff;
23
24use super::{
25 updates::{ReaderToken, Update, UpdatesInner},
26 ChunkContent, ChunkIdentifier, Iter, Position,
27};
28
29/// A type alias to represent a chunk's length. This is purely for commodity.
30type ChunkLength = usize;
31
32/// A type that transforms a `Vec<Update<Item, Gap>>` (given by
33/// [`ObservableUpdates::take`](super::ObservableUpdates::take)) into a
34/// `Vec<VectorDiff<Item>>` (this type). Basically, it helps to consume a
35/// [`LinkedChunk<CAP, Item, Gap>`](super::LinkedChunk) as if it was an
36/// [`eyeball_im::ObservableVector<Item>`].
37#[derive(Debug)]
38pub struct AsVector<Item, Gap> {
39 /// Strong reference to [`UpdatesInner`].
40 updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
41
42 /// The token to read the updates.
43 token: ReaderToken,
44
45 /// Mapper from `Update` to `VectorDiff`.
46 mapper: UpdateToVectorDiff,
47}
48
49impl<Item, Gap> AsVector<Item, Gap> {
50 /// Create a new [`AsVector`].
51 ///
52 /// `updates` is the inner value of
53 /// [`ObservableUpdates`][super::updates::ObservableUpdates].
54 /// It's required to read the new [`Update`]s. `token` is the
55 /// [`ReaderToken`] necessary for this type to read the [`Update`]s.
56 /// `chunk_iterator` is the iterator of all [`Chunk`](super::Chunk)s, used
57 /// to set up its internal state.
58 pub(super) fn new<const CAP: usize>(
59 updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
60 token: ReaderToken,
61 chunk_iterator: Iter<'_, CAP, Item, Gap>,
62 ) -> Self {
63 // Drain previous updates so that this type is synced with `Updates`.
64 {
65 let mut updates = updates.write().unwrap();
66 let _ = updates.take_with_token(token);
67 }
68
69 Self { updates, token, mapper: UpdateToVectorDiff::new(chunk_iterator) }
70 }
71
72 /// Take the new updates as [`VectorDiff`].
73 ///
74 /// It returns an empty `Vec` if there is no new `VectorDiff` for the
75 /// moment.
76 pub fn take(&mut self) -> Vec<VectorDiff<Item>>
77 where
78 Item: Clone,
79 {
80 let mut updates = self.updates.write().unwrap();
81
82 self.mapper.map(updates.take_with_token(self.token))
83 }
84}
85
86/// Internal type that converts [`Update`] into [`VectorDiff`].
87#[derive(Debug)]
88struct UpdateToVectorDiff {
89 /// Pairs of all known chunks and their respective length. This is the only
90 /// required data for this algorithm.
91 chunks: VecDeque<(ChunkIdentifier, ChunkLength)>,
92}
93
94impl UpdateToVectorDiff {
95 /// Construct [`UpdateToVectorDiff`], based on an iterator of
96 /// [`Chunk`](super::Chunk)s, used to set up its own internal state.
97 ///
98 /// See [`Self::map`] to learn more about the algorithm.
99 fn new<const CAP: usize, Item, Gap>(chunk_iterator: Iter<'_, CAP, Item, Gap>) -> Self {
100 let mut initial_chunk_lengths = VecDeque::new();
101
102 for chunk in chunk_iterator {
103 initial_chunk_lengths.push_back((
104 chunk.identifier(),
105 match chunk.content() {
106 ChunkContent::Gap(_) => 0,
107 ChunkContent::Items(items) => items.len(),
108 },
109 ))
110 }
111
112 Self { chunks: initial_chunk_lengths }
113 }
114
115 /// Map several [`Update`] into [`VectorDiff`].
116 ///
117 /// How does this type transform `Update` into `VectorDiff`? There is no
118 /// internal buffer of kind [`eyeball_im::ObservableVector<Item>`],
119 /// which could have been used to generate the `VectorDiff`s. They are
120 /// computed manually.
121 ///
122 /// The only buffered data is pairs of [`ChunkIdentifier`] and
123 /// [`ChunkLength`]. The following rules must be respected (they are defined
124 /// in [`Self::new`]):
125 ///
126 /// * A chunk of kind [`ChunkContent::Gap`] has a length of 0,
127 /// * A chunk of kind [`ChunkContent::Items`] has a length equals to its
128 /// number of items,
129 /// * The pairs must be ordered exactly like the chunks in [`LinkedChunk`],
130 /// i.e. the first pair must represent the first chunk, the last pair must
131 /// represent the last chunk.
132 ///
133 /// The only thing this algorithm does is maintaining the pairs:
134 ///
135 /// * [`Update::NewItemsChunk`] and [`Update::NewGapChunk`] are inserting a
136 /// new pair with a chunk length of 0 at the appropriate index,
137 /// * [`Update::RemoveChunk`] is removing a pair, and is potentially
138 /// emitting [`VectorDiff`],
139 /// * [`Update::PushItems`] is increasing the length of the appropriate pair
140 /// by the number of new items, and is potentially emitting
141 /// [`VectorDiff`],
142 /// * [`Update::DetachLastItems`] is decreasing the length of the
143 /// appropriate pair by the number of items to be detached; no
144 /// [`VectorDiff`] is emitted,
145 /// * [`Update::StartReattachItems`] and [`Update::EndReattachItems`] are
146 /// respectively muting or unmuting the emission of [`VectorDiff`] by
147 /// [`Update::PushItems`],
148 /// * [`Update::Clear`] reinitialises the state.
149 ///
150 /// The only `VectorDiff` that are emitted are [`VectorDiff::Insert`],
151 /// [`VectorDiff::Append`], [`VectorDiff::Remove`] and
152 /// [`VectorDiff::Clear`].
153 ///
154 /// `VectorDiff::Append` is an optimisation when numerous
155 /// `VectorDiff::Insert`s have to be emitted at the last position.
156 ///
157 /// `VectorDiff::Insert` needs an index. To compute this index, the
158 /// algorithm will iterate over all pairs to accumulate each chunk length
159 /// until it finds the appropriate pair (given by
160 /// [`Update::PushItems::at`]). This is _the offset_. To this offset, the
161 /// algorithm adds the position's index of the new items (still given by
162 /// [`Update::PushItems::at`]). This is _the index_. This logic works
163 /// for all cases as long as pairs are maintained according to the rules
164 /// hereinabove.
165 ///
166 /// That's a pretty memory compact and computation efficient way to map a
167 /// `Vec<Update<Item, Gap>>` into a `Vec<VectorDiff<Item>>`. The larger the
168 /// `LinkedChunk` capacity is, the fewer pairs the algorithm will have
169 /// to handle, e.g. for 1'000 items and a `LinkedChunk` capacity of 128,
170 /// it's only 8 pairs, that is 256 bytes.
171 ///
172 /// [`LinkedChunk`]: super::LinkedChunk
173 /// [`ChunkContent::Gap`]: super::ChunkContent::Gap
174 /// [`ChunkContent::Content`]: super::ChunkContent::Content
175 fn map<Item, Gap>(&mut self, updates: &[Update<Item, Gap>]) -> Vec<VectorDiff<Item>>
176 where
177 Item: Clone,
178 {
179 let mut diffs = Vec::with_capacity(updates.len());
180
181 // A flag specifying when updates are reattaching detached items.
182 //
183 // Why is it useful?
184 //
185 // Imagine a `LinkedChunk::<3, char, ()>` containing `['a', 'b', 'c'] ['d']`. If
186 // one wants to insert [`w`, x`, 'y', 'z'] at position
187 // `Position(ChunkIdentifier(0), 1)`, i.e. at the position of `b`, here is what
188 // happens:
189 //
190 // 1. `LinkedChunk` will split off `['a', 'b', 'c']` at index 1, the chunk
191 // becomes `['a']` and `b` and `c` are _detached_, thus we have:
192 //
193 // ['a'] ['d']
194 //
195 // 2. `LinkedChunk` will then insert `w`, `x`, `y` and `z` to get:
196 //
197 // ['a', 'w', 'x'] ['y', 'z'] ['d']
198 //
199 // 3. `LinkedChunk` will now reattach `b` and `c` after `z`, like so:
200 //
201 // ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d']
202 //
203 // This detaching/reattaching approach makes it reliable and safe. Good. Now,
204 // what updates are we going to receive for each step?
205 //
206 // Step 1, detaching last items:
207 //
208 // ```
209 // Update::DetachLastItems { at: Position(ChunkIdentifier(0), 1) }
210 // ```
211 //
212 // Step 2, inserting new items:
213 //
214 // ```
215 // Update::PushItems {
216 // at: Position(ChunkIdentifier(0), 1),
217 // items: vec!['w', 'x'],
218 // }
219 // Update::NewItemsChunk {
220 // previous: Some(ChunkIdentifier(0)),
221 // new: ChunkIdentifier(2),
222 // next: Some(ChunkIdentifier(1)),
223 // }
224 // Update::PushItems {
225 // at: Position(ChunkIdentifier(2), 0),
226 // items: vec!['y', 'z'],
227 // }
228 // ```
229 //
230 // Step 3, reattaching detached items:
231 //
232 // ```
233 // Update::StartReattachItems
234 // Update::PushItems {
235 // at: Position(ChunkIdentifier(2), 2),
236 // items: vec!['b']
237 // }
238 // Update::NewItemsChunk {
239 // previous: Some(ChunkIdentifier(2)),
240 // new: ChunkIdentifier(3),
241 // next: Some(ChunkIdentifier(1)),
242 // }
243 // Update::PushItems {
244 // at: Position(ChunkIdentifier(3), 0),
245 // items: vec!['c'],
246 // }
247 // Update::EndReattachItems
248 // ```
249 //
250 // To ensure an optimised behaviour of this algorithm:
251 //
252 // * `Update::DetachLastItems` must not emit `VectorDiff::Remove`,
253 //
254 // * `Update::PushItems` must not emit `VectorDiff::Insert`s or
255 // `VectorDiff::Append`s if it happens after `StartReattachItems` and before
256 // `EndReattachItems`. However, `Self::chunks` must always be updated.
257 //
258 // From the `VectorDiff` “point of view”, this optimisation aims at avoiding
259 // removing items to push them again later.
260 let mut reattaching = false;
261 let mut detaching = false;
262
263 for update in updates {
264 match update {
265 Update::NewItemsChunk { previous, new, next }
266 | Update::NewGapChunk { previous, new, next, .. } => {
267 match (previous, next) {
268 // New chunk at the end.
269 (Some(_previous), None) => {
270 // No need to check `previous`. It's possible that the linked chunk is
271 // lazily loaded, chunk by chunk. The `next` is always reliable, but the
272 // `previous` might not exist in-memory yet.
273
274 self.chunks.push_back((*new, 0));
275 }
276
277 // New chunk at the beginning.
278 (None, Some(next)) => {
279 debug_assert!(
280 matches!(self.chunks.front(), Some((n, _)) if n == next),
281 "Inserting new chunk at the end: The previous chunk is invalid"
282 );
283
284 self.chunks.push_front((*new, 0));
285 }
286
287 // New chunk is inserted between 2 chunks.
288 (Some(_previous), Some(next)) => {
289 let next_chunk_index = self
290 .chunks
291 .iter()
292 .position(|(chunk_identifier, _)| chunk_identifier == next)
293 // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not
294 // buggy, and assuming `Self::chunks` is correctly initialized, it
295 // is not possible to insert a chunk between two chunks where one
296 // does not exist. If this predicate fails, it means `LinkedChunk`
297 // or `ObservableUpdates` contain a bug.
298 .expect("Inserting new chunk: The chunk is not found");
299
300 // No need to check `previous`. It's possible that the linked chunk is
301 // lazily loaded, chunk by chunk. The `next` is always reliable, but the
302 // `previous` might not exist in-memory yet.
303
304 self.chunks.insert(next_chunk_index, (*new, 0));
305 }
306
307 // First chunk!
308 (None, None) if self.chunks.is_empty() => {
309 self.chunks.push_back((*new, 0));
310 }
311
312 // Impossible state.
313 (None, None) => {
314 unreachable!(
315 "Inserting new chunk with no previous nor next chunk identifiers \
316 is impossible"
317 );
318 }
319 }
320 }
321
322 Update::RemoveChunk(chunk_identifier) => {
323 let (offset, (chunk_index, _)) =
324 self.map_to_offset(&Position(*chunk_identifier, 0));
325
326 let (_, number_of_items) = self
327 .chunks
328 .remove(chunk_index)
329 .expect("Removing an index out of the bounds");
330
331 // Removing at the same index because each `Remove` shifts items to the left.
332 diffs.extend(repeat_n(VectorDiff::Remove { index: offset }, number_of_items));
333 }
334
335 Update::PushItems { at: position, items } => {
336 let number_of_chunks = self.chunks.len();
337 let (offset, (chunk_index, chunk_length)) = self.map_to_offset(position);
338
339 let is_pushing_back =
340 chunk_index + 1 == number_of_chunks && position.index() >= *chunk_length;
341
342 // Add the number of items to the chunk in `self.chunks`.
343 *chunk_length += items.len();
344
345 // See `reattaching` to learn more.
346 if reattaching {
347 continue;
348 }
349
350 // Optimisation: we can emit a `VectorDiff::Append` in this particular case.
351 if is_pushing_back && detaching.not() {
352 diffs.push(VectorDiff::Append { values: items.into() });
353 }
354 // No optimisation: let's emit `VectorDiff::Insert`.
355 else {
356 diffs.extend(items.iter().enumerate().map(|(nth, item)| {
357 VectorDiff::Insert { index: offset + nth, value: item.clone() }
358 }));
359 }
360 }
361
362 Update::ReplaceItem { at: position, item } => {
363 let (offset, (_chunk_index, _chunk_length)) = self.map_to_offset(position);
364
365 // The chunk length doesn't change.
366
367 diffs.push(VectorDiff::Set { index: offset, value: item.clone() });
368 }
369
370 Update::RemoveItem { at: position } => {
371 let (offset, (_chunk_index, chunk_length)) = self.map_to_offset(position);
372
373 // Remove one item to the chunk in `self.chunks`.
374 *chunk_length -= 1;
375
376 // See `reattaching` to learn more.
377 if reattaching {
378 continue;
379 }
380
381 // Let's emit a `VectorDiff::Remove`.
382 diffs.push(VectorDiff::Remove { index: offset });
383 }
384
385 Update::DetachLastItems { at: position } => {
386 let expected_chunk_identifier = position.chunk_identifier();
387 let new_length = position.index();
388
389 let chunk_length = self
390 .chunks
391 .iter_mut()
392 .find_map(|(chunk_identifier, chunk_length)| {
393 (*chunk_identifier == expected_chunk_identifier).then_some(chunk_length)
394 })
395 // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
396 // assuming `Self::chunks` is correctly initialized, it is not possible to
397 // detach items from a chunk that does not exist. If this predicate fails,
398 // it means `LinkedChunk` or `ObservableUpdates` contain a bug.
399 .expect("Detach last items: The chunk is not found");
400
401 *chunk_length = new_length;
402
403 // Entering the _detaching_ mode.
404 detaching = true;
405 }
406
407 Update::StartReattachItems => {
408 // Entering the _reattaching_ mode.
409 reattaching = true;
410 }
411
412 Update::EndReattachItems => {
413 // Exiting the _reattaching_ mode.
414 reattaching = false;
415
416 // Exiting the _detaching_ mode.
417 detaching = false;
418 }
419
420 Update::Clear => {
421 // Clean `self.chunks`.
422 self.chunks.clear();
423
424 // Let's straightforwardly emit a `VectorDiff::Clear`.
425 diffs.push(VectorDiff::Clear);
426 }
427 }
428 }
429
430 diffs
431 }
432
433 fn map_to_offset(&mut self, position: &Position) -> (usize, (usize, &mut usize)) {
434 let expected_chunk_identifier = position.chunk_identifier();
435
436 let (offset, (chunk_index, chunk_length)) = {
437 let control_flow = self.chunks.iter_mut().enumerate().try_fold(
438 position.index(),
439 |offset, (chunk_index, (chunk_identifier, chunk_length))| {
440 if chunk_identifier == &expected_chunk_identifier {
441 ControlFlow::Break((offset, (chunk_index, chunk_length)))
442 } else {
443 ControlFlow::Continue(offset + *chunk_length)
444 }
445 },
446 );
447
448 match control_flow {
449 // Chunk has been found, and all values have been calculated as
450 // expected.
451 ControlFlow::Break(values) => values,
452
453 // Chunk has not been found.
454 ControlFlow::Continue(..) => {
455 // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
456 // assuming `Self::chunks` is correctly initialized, it is not possible to work
457 // on a chunk that does not exist. If this predicate fails, it means
458 // `LinkedChunk` or `ObservableUpdates` contain a bug.
459 panic!("The chunk is not found");
460 }
461 }
462 };
463
464 (offset, (chunk_index, chunk_length))
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use std::fmt::Debug;
471
472 use assert_matches::assert_matches;
473 use imbl::{vector, Vector};
474
475 use super::{
476 super::{Chunk, ChunkIdentifierGenerator, LinkedChunk, Update},
477 VectorDiff,
478 };
479
480 fn apply_and_assert_eq<Item>(
481 accumulator: &mut Vector<Item>,
482 diffs: Vec<VectorDiff<Item>>,
483 expected_diffs: &[VectorDiff<Item>],
484 ) where
485 Item: PartialEq + Clone + Debug,
486 {
487 assert_eq!(diffs, expected_diffs);
488
489 for diff in diffs {
490 diff.apply(accumulator);
491 }
492 }
493
494 #[test]
495 fn test_as_vector() {
496 let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
497 let mut as_vector = linked_chunk.as_vector().unwrap();
498
499 let mut accumulator = Vector::new();
500
501 assert!(as_vector.take().is_empty());
502
503 linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
504 #[rustfmt::skip]
505 assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']);
506
507 // From an `ObservableVector` point of view, it would look like:
508 //
509 // 0 1 2 3 4
510 // +---+---+---+---+
511 // | a | b | c | d |
512 // +---+---+---+---+
513 // ^^^^^^^^^^^^^^^^
514 // |
515 // new
516 apply_and_assert_eq(
517 &mut accumulator,
518 as_vector.take(),
519 &[
520 VectorDiff::Append { values: vector!['a', 'b', 'c'] },
521 VectorDiff::Append { values: vector!['d'] },
522 ],
523 );
524
525 linked_chunk
526 .insert_items_at(
527 ['w', 'x', 'y', 'z'],
528 linked_chunk.item_position(|item| *item == 'b').unwrap(),
529 )
530 .unwrap();
531 assert_items_eq!(linked_chunk, ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d']);
532
533 // From an `ObservableVector` point of view, it would look like:
534 //
535 // 0 1 2 3 4 5 6 7 8
536 // +---+---+---+---+---+---+---+---+
537 // | a | w | x | y | z | b | c | d |
538 // +---+---+---+---+---+---+---+---+
539 // ^^^^^^^^^^^^^^^^
540 // |
541 // new
542 apply_and_assert_eq(
543 &mut accumulator,
544 as_vector.take(),
545 &[
546 VectorDiff::Insert { index: 1, value: 'w' },
547 VectorDiff::Insert { index: 2, value: 'x' },
548 VectorDiff::Insert { index: 3, value: 'y' },
549 VectorDiff::Insert { index: 4, value: 'z' },
550 ],
551 );
552
553 linked_chunk.push_gap_back(());
554 linked_chunk.push_items_back(['e', 'f', 'g', 'h']);
555 assert_items_eq!(
556 linked_chunk,
557 ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d'] [-] ['e', 'f', 'g'] ['h']
558 );
559
560 // From an `ObservableVector` point of view, it would look like:
561 //
562 // 0 1 2 3 4 5 6 7 8 9 10 11 12
563 // +---+---+---+---+---+---+---+---+---+---+---+---+
564 // | a | w | x | y | z | b | c | d | e | f | g | h |
565 // +---+---+---+---+---+---+---+---+---+---+---+---+
566 // ^^^^^^^^^^^^^^^^
567 // |
568 // new
569 apply_and_assert_eq(
570 &mut accumulator,
571 as_vector.take(),
572 &[
573 VectorDiff::Append { values: vector!['e', 'f', 'g'] },
574 VectorDiff::Append { values: vector!['h'] },
575 ],
576 );
577
578 linked_chunk
579 .replace_gap_at(
580 ['i', 'j', 'k', 'l'],
581 linked_chunk.chunk_identifier(|chunk| chunk.is_gap()).unwrap(),
582 )
583 .unwrap();
584 assert_items_eq!(
585 linked_chunk,
586 ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
587 );
588
589 // From an `ObservableVector` point of view, it would look like:
590 //
591 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
592 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
593 // | a | w | x | y | z | b | c | d | i | j | k | l | e | f | g | h |
594 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
595 // ^^^^^^^^^^^^^^^^
596 // |
597 // new
598 apply_and_assert_eq(
599 &mut accumulator,
600 as_vector.take(),
601 &[
602 VectorDiff::Insert { index: 8, value: 'i' },
603 VectorDiff::Insert { index: 9, value: 'j' },
604 VectorDiff::Insert { index: 10, value: 'k' },
605 VectorDiff::Insert { index: 11, value: 'l' },
606 ],
607 );
608
609 linked_chunk
610 .insert_items_at(['m'], linked_chunk.item_position(|item| *item == 'a').unwrap())
611 .unwrap();
612 assert_items_eq!(
613 linked_chunk,
614 ['m', 'a', 'w'] ['x'] ['y', 'z', 'b'] ['c'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
615 );
616
617 // From an `ObservableVector` point of view, it would look like:
618 //
619 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
620 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
621 // | m | a | w | x | y | z | b | c | d | i | j | k | l | e | f | g | h |
622 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
623 // ^^^^
624 // |
625 // new
626 apply_and_assert_eq(
627 &mut accumulator,
628 as_vector.take(),
629 &[VectorDiff::Insert { index: 0, value: 'm' }],
630 );
631
632 let removed_item = linked_chunk
633 .remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap())
634 .unwrap();
635 assert_eq!(removed_item, 'c');
636 assert_items_eq!(
637 linked_chunk,
638 ['m', 'a', 'w'] ['x'] ['y', 'z', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
639 );
640
641 // From an `ObservableVector` point of view, it would look like:
642 //
643 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
644 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
645 // | m | a | w | x | y | z | b | d | i | j | k | l | e | f | g | h |
646 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
647 // ^
648 // |
649 // `c` has been removed
650 apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]);
651
652 let removed_item = linked_chunk
653 .remove_item_at(linked_chunk.item_position(|item| *item == 'z').unwrap())
654 .unwrap();
655 assert_eq!(removed_item, 'z');
656 assert_items_eq!(
657 linked_chunk,
658 ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
659 );
660
661 // From an `ObservableVector` point of view, it would look like:
662 //
663 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
664 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
665 // | m | a | w | x | y | b | d | i | j | k | l | e | f | g | h |
666 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
667 // ^
668 // |
669 // `z` has been removed
670 apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 5 }]);
671
672 linked_chunk
673 .insert_items_at(['z'], linked_chunk.item_position(|item| *item == 'h').unwrap())
674 .unwrap();
675
676 assert_items_eq!(
677 linked_chunk,
678 ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h']
679 );
680
681 // From an `ObservableVector` point of view, it would look like:
682 //
683 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
684 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
685 // | m | a | w | x | y | b | d | i | j | k | l | e | f | g | z | h |
686 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
687 // ^^^^
688 // |
689 // new!
690 apply_and_assert_eq(
691 &mut accumulator,
692 as_vector.take(),
693 &[VectorDiff::Insert { index: 14, value: 'z' }],
694 );
695
696 // Ensure the “reconstitued” vector is the one expected.
697 assert_eq!(
698 accumulator,
699 vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
700 );
701
702 // Replace element 8 by an uppercase J.
703 linked_chunk
704 .replace_item_at(linked_chunk.item_position(|item| *item == 'j').unwrap(), 'J')
705 .unwrap();
706
707 assert_items_eq!(
708 linked_chunk,
709 ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'J', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h']
710 );
711
712 // From an `ObservableVector` point of view, it would look like:
713 //
714 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
715 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
716 // | m | a | w | x | y | b | d | i | J | k | l | e | f | g | z | h |
717 // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
718 // ^^^^
719 // |
720 // new!
721 apply_and_assert_eq(
722 &mut accumulator,
723 as_vector.take(),
724 &[VectorDiff::Set { index: 8, value: 'J' }],
725 );
726
727 // Let's try to clear the linked chunk now.
728 linked_chunk.clear();
729
730 apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Clear]);
731 assert!(accumulator.is_empty());
732
733 drop(linked_chunk);
734 assert!(as_vector.take().is_empty());
735 }
736
737 #[test]
738 fn test_as_vector_with_update_clear() {
739 let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
740 let mut as_vector = linked_chunk.as_vector().unwrap();
741
742 {
743 // 1 initial chunk in the `UpdateToVectorDiff` mapper.
744 let chunks = &as_vector.mapper.chunks;
745 assert_eq!(chunks.len(), 1);
746 assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
747 assert_eq!(chunks[0].1, 0);
748
749 assert!(as_vector.take().is_empty());
750 }
751
752 linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
753
754 {
755 let diffs = as_vector.take();
756 assert_eq!(diffs.len(), 2);
757 assert_matches!(&diffs[0], VectorDiff::Append { .. });
758 assert_matches!(&diffs[1], VectorDiff::Append { .. });
759
760 // 2 chunks in the `UpdateToVectorDiff` mapper.
761 assert_eq!(as_vector.mapper.chunks.len(), 2);
762 }
763
764 linked_chunk.clear();
765
766 {
767 let diffs = as_vector.take();
768 assert_eq!(diffs.len(), 1);
769 assert_matches!(&diffs[0], VectorDiff::Clear);
770
771 // 1 chunk in the `UpdateToVectorDiff` mapper.
772 let chunks = &as_vector.mapper.chunks;
773 assert_eq!(chunks.len(), 1);
774 assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
775 assert_eq!(chunks[0].1, 0);
776 }
777
778 // And we can push again.
779 linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
780
781 {
782 let diffs = as_vector.take();
783 assert_eq!(diffs.len(), 2);
784 assert_matches!(&diffs[0], VectorDiff::Append { .. });
785 assert_matches!(&diffs[1], VectorDiff::Append { .. });
786 }
787 }
788
789 #[test]
790 fn test_updates_are_drained_when_constructing_as_vector() {
791 let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
792
793 linked_chunk.push_items_back(['a']);
794
795 let mut as_vector = linked_chunk.as_vector().unwrap();
796 let diffs = as_vector.take();
797
798 // `diffs` are empty because `AsVector` is built _after_ `LinkedChunk`
799 // has been updated.
800 assert!(diffs.is_empty());
801
802 linked_chunk.push_items_back(['b']);
803
804 let diffs = as_vector.take();
805
806 // `diffs` is not empty because new updates are coming.
807 assert_eq!(diffs.len(), 1);
808 }
809
810 #[test]
811 fn test_as_vector_with_initial_content() {
812 // Fill the linked chunk with some initial items.
813 let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
814 linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
815
816 #[rustfmt::skip]
817 assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']);
818
819 // Empty updates first.
820 let _ = linked_chunk.updates().unwrap().take();
821
822 // Start observing future updates.
823 let mut as_vector = linked_chunk.as_vector().unwrap();
824
825 assert!(as_vector.take().is_empty());
826
827 // It's important to cause a change that will create new chunks, like pushing
828 // enough items.
829 linked_chunk.push_items_back(['e', 'f', 'g']);
830 #[rustfmt::skip]
831 assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g']);
832
833 // And the vector diffs can be computed without crashing.
834 let diffs = as_vector.take();
835 assert_eq!(diffs.len(), 2);
836 assert_matches!(&diffs[0], VectorDiff::Append { values } => {
837 assert_eq!(*values, ['e', 'f'].into());
838 });
839 assert_matches!(&diffs[1], VectorDiff::Append { values } => {
840 assert_eq!(*values, ['g'].into());
841 });
842 }
843
844 #[test]
845 fn test_as_vector_remove_chunk() {
846 let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
847 let mut as_vector = linked_chunk.as_vector().unwrap();
848
849 let mut accumulator = Vector::new();
850
851 assert!(as_vector.take().is_empty());
852
853 linked_chunk.push_items_back(['a', 'b']);
854 linked_chunk.push_gap_back(());
855 linked_chunk.push_items_back(['c']);
856 linked_chunk.push_gap_back(());
857 linked_chunk.push_items_back(['d', 'e', 'f', 'g']);
858
859 assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['c'] [-] ['d', 'e', 'f'] ['g']);
860
861 // From an `ObservableVector` point of view, it would look like:
862 //
863 // 0 1 2 3 4 5 6 7
864 // +---+---+---+---+---+---+---+
865 // | a | b | c | d | e | f | g |
866 // +---+---+---+---+---+---+---+
867 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
868 // |
869 // new
870 apply_and_assert_eq(
871 &mut accumulator,
872 as_vector.take(),
873 &[
874 VectorDiff::Append { values: vector!['a', 'b'] },
875 VectorDiff::Append { values: vector!['c'] },
876 VectorDiff::Append { values: vector!['d', 'e', 'f'] },
877 VectorDiff::Append { values: vector!['g'] },
878 ],
879 );
880
881 // Empty a chunk, and remove it once it is empty.
882 linked_chunk
883 .remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap())
884 .unwrap();
885
886 assert_items_eq!(linked_chunk, ['a', 'b'] [-] [-] ['d', 'e', 'f'] ['g']);
887
888 // From an `ObservableVector` point of view, it would look like:
889 //
890 // 0 1 2 3 4 5 6
891 // +---+---+---+---+---+---+
892 // | a | b | d | e | f | g |
893 // +---+---+---+---+---+---+
894 // ^
895 // |
896 // `c` has been removed
897 apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 2 }]);
898
899 // Remove a gap.
900 linked_chunk
901 .remove_empty_chunk_at(linked_chunk.chunk_identifier(Chunk::is_gap).unwrap())
902 .unwrap();
903
904 assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d', 'e', 'f'] ['g']);
905
906 // From an `ObservableVector` point of view, nothing changes.
907 apply_and_assert_eq(&mut accumulator, as_vector.take(), &[]);
908
909 // Remove a non-empty chunk. This is not possible with the public
910 // `LinkedChunk` API yet, but let's try.
911 let d_e_and_f = linked_chunk.item_position(|item| *item == 'f').unwrap().chunk_identifier();
912 let updates = linked_chunk.updates().unwrap();
913 updates.push(Update::RemoveChunk(d_e_and_f));
914 // Note that `linked_chunk` is getting out of sync with `AsVector`
915 // but it's just a test. Better, it's the end of the test.
916
917 // From an `ObservableVector` point of view, it would look like:
918 //
919 // 0 1 2 3
920 // +---+---+---+
921 // | a | b | g |
922 // +---+---+---+
923 // ^
924 // |
925 // `d`, `e` and `f` have been removed
926 apply_and_assert_eq(
927 &mut accumulator,
928 as_vector.take(),
929 &[
930 VectorDiff::Remove { index: 2 },
931 VectorDiff::Remove { index: 2 },
932 VectorDiff::Remove { index: 2 },
933 ],
934 );
935 }
936
937 #[cfg(not(target_arch = "wasm32"))]
938 mod proptests {
939 use proptest::prelude::*;
940
941 use super::*;
942
943 #[derive(Debug, Clone)]
944 enum AsVectorOperation {
945 PushItems { items: Vec<char> },
946 PushGap,
947 ReplaceLastGap { items: Vec<char> },
948 RemoveItem { item: char },
949 }
950
951 fn as_vector_operation_strategy() -> impl Strategy<Value = AsVectorOperation> {
952 prop_oneof![
953 3 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25)
954 .prop_map(|items| AsVectorOperation::PushItems { items }),
955
956 2 => Just(AsVectorOperation::PushGap),
957
958 1 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25)
959 .prop_map(|items| AsVectorOperation::ReplaceLastGap { items }),
960
961 1 => prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into())
962 .prop_map(|item| AsVectorOperation::RemoveItem { item }),
963 ]
964 }
965
966 proptest! {
967 #[test]
968 fn test_as_vector_is_correct(
969 operations in prop::collection::vec(as_vector_operation_strategy(), 50..=200)
970 ) {
971 let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
972 let mut as_vector = linked_chunk.as_vector().unwrap();
973
974 for operation in operations {
975 match operation {
976 AsVectorOperation::PushItems { items } => {
977 linked_chunk.push_items_back(items);
978 }
979
980 AsVectorOperation::PushGap => {
981 linked_chunk.push_gap_back(());
982 }
983
984 AsVectorOperation::ReplaceLastGap { items } => {
985 let Some(gap_identifier) = linked_chunk
986 .rchunks()
987 .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
988 else {
989 continue;
990 };
991
992 linked_chunk.replace_gap_at(items, gap_identifier).expect("Failed to replace a gap");
993 }
994
995 AsVectorOperation::RemoveItem { item: expected_item } => {
996 let Some(position) = linked_chunk
997 .items().find_map(|(position, item)| (*item == expected_item).then_some(position))
998 else {
999 continue;
1000 };
1001
1002 linked_chunk.remove_item_at(position).expect("Failed to remove an item");
1003 }
1004 }
1005 }
1006
1007 let mut vector_from_diffs = Vec::new();
1008
1009 // Read all updates as `VectorDiff` and rebuild a `Vec<char>`.
1010 for diff in as_vector.take() {
1011 match diff {
1012 VectorDiff::Insert { index, value } => vector_from_diffs.insert(index, value),
1013 VectorDiff::Append { values } => {
1014 let mut values = values.iter().copied().collect();
1015
1016 vector_from_diffs.append(&mut values);
1017 }
1018 VectorDiff::Remove { index } => {
1019 vector_from_diffs.remove(index);
1020 }
1021 _ => unreachable!(),
1022 }
1023 }
1024
1025 // Iterate over all chunks and collect items as `Vec<char>`.
1026 let vector_from_chunks = linked_chunk.items().map(|(_, item)| *item).collect::<Vec<_>>();
1027
1028 // Compare both `Vec`s.
1029 assert_eq!(vector_from_diffs, vector_from_chunks);
1030 }
1031 }
1032 }
1033}