1#![expect(clippy::doc_markdown)] use crate::IndigoBuildHasher;
11use std::{collections::HashMap, ops::Range};
12
13pub struct Document {
14 id: DocumentId,
15 content: Vec<Item>,
16 version: Version,
17}
18
19impl Document {
21 #[must_use]
23 pub fn new(id: usize) -> Self {
24 Self {
25 id: DocumentId(id),
26 content: Vec::new(),
27 version: Version::default(),
28 }
29 }
30
31 #[must_use]
32 pub fn text(&self) -> String {
33 self.content
34 .iter()
35 .filter(|item| !item.deleted)
36 .map(|item| item.content)
37 .collect()
38 }
39
40 pub fn insert(&mut self, byte_index: usize, text: &str) -> anyhow::Result<()> {
41 let index = self.insertion_index(byte_index)?;
43 for (index, char) in (index..).zip(text.chars()) {
44 self.insert_char_at(index, char);
45 }
46 Ok(())
47 }
48
49 pub fn delete(&mut self, byte_range: Range<usize>) -> anyhow::Result<()> {
50 let text = self.text();
51 if byte_range.start > byte_range.end
52 || byte_range.end > text.len()
53 || !text.is_char_boundary(byte_range.start)
54 || !text.is_char_boundary(byte_range.end)
55 {
56 anyhow::bail!("invalid byte range {byte_range:?}");
57 }
58 let mut byte_index = 0;
59 for item in &mut self.content {
60 if item.deleted {
61 continue;
62 }
63 if byte_range.contains(&byte_index) {
64 item.deleted = true;
65 }
66 byte_index += item.content.len_utf8();
67 }
68 Ok(())
69 }
70
71 pub fn merge(&mut self, other: &Self) {
72 let mut pending = other
74 .content
75 .iter()
76 .filter(|item| !self.version.contains(item.id))
77 .cloned()
78 .collect::<Vec<Item>>();
79 while !pending.is_empty() {
80 let (ready, waiting): (Vec<Item>, Vec<Item>) = pending
81 .into_iter()
82 .partition(|item| self.can_integrate(item));
83 assert!(
84 !ready.is_empty(),
85 "documents are causally complete, so every pass integrates something",
86 );
87 for item in ready {
88 self.integrate(item);
89 }
90 pending = waiting;
91 }
92
93 let mut content = self.content.iter_mut();
95 for item in other.content.iter().filter(|item| item.deleted) {
96 let ours = content
97 .by_ref()
98 .find(|ours| ours.id == item.id)
99 .expect("item exists");
100 ours.deleted = true;
101 }
102 }
103}
104
105impl Document {
107 fn insert_char_at(&mut self, index: usize, char: char) {
110 let item = Item {
111 id: ItemId {
112 document_id: self.id,
113 sequence_number: self.version.next_sequence_number(self.id),
114 },
115 content: char,
116 origin_left: index.checked_sub(1).map(|left| self.content[left].id),
117 origin_right: self.content.get(index).map(|right| right.id),
118 deleted: false,
119 };
120 self.integrate_between(item, index.checked_sub(1), index);
121 }
122
123 fn insertion_index(&self, byte_index: usize) -> anyhow::Result<usize> {
127 let mut remaining = byte_index;
128 for (index, item) in self.content.iter().enumerate() {
129 if remaining == 0 {
130 return Ok(index);
131 }
132 if item.deleted {
133 continue;
134 }
135 let Some(rest) = remaining.checked_sub(item.content.len_utf8()) else {
136 anyhow::bail!("byte index {byte_index} is not a char boundary");
137 };
138 remaining = rest;
139 }
140 if remaining == 0 {
141 Ok(self.content.len())
142 } else {
143 anyhow::bail!("byte index {byte_index} is past the end of the document");
144 }
145 }
146
147 fn index_of(&self, id: ItemId) -> usize {
148 self.content
149 .iter()
150 .position(|item| item.id == id)
151 .expect("item exists")
152 }
153
154 fn can_integrate(&self, item: &Item) -> bool {
157 let previous = item
158 .id
159 .sequence_number
160 .checked_sub(1)
161 .map(|sequence_number| ItemId {
162 document_id: item.id.document_id,
163 sequence_number,
164 });
165 !self.version.contains(item.id)
166 && previous.is_none_or(|id| self.version.contains(id))
167 && item.origin_left.is_none_or(|id| self.version.contains(id))
168 && item.origin_right.is_none_or(|id| self.version.contains(id))
169 }
170
171 fn integrate(&mut self, item: Item) {
172 let left = item.origin_left.map(|id| self.index_of(id));
173 let right = item
174 .origin_right
175 .map_or(self.content.len(), |id| self.index_of(id));
176 self.integrate_between(item, left, right);
177 }
178
179 fn integrate_between(&mut self, item: Item, left: Option<usize>, right: usize) {
181 self.version.record(item.id);
182
183 let mut destination = left.map_or(0, |index| index + 1);
184 let mut scanning = false;
185 for index in destination.. {
186 if !scanning {
187 destination = index;
188 }
189 if index == self.content.len() || index == right {
190 break;
191 }
192 let other = &self.content[index];
193 let other_left = other.origin_left.map(|id| self.index_of(id));
194 let other_right = other
195 .origin_right
196 .map_or(self.content.len(), |id| self.index_of(id));
197 if other_left < left
198 || (other_left == left
199 && other_right == right
200 && item.id.document_id < other.id.document_id)
201 {
202 break;
203 }
204 if other_left == left {
205 scanning = other_right < right;
206 }
207 }
208 self.content.insert(destination, item);
209 }
210}
211
212#[cfg(test)]
213impl Document {
214 pub(crate) fn item_summaries(&self) -> Vec<(usize, usize, bool)> {
217 self.content
218 .iter()
219 .map(|item| (item.id.document_id.0, item.id.sequence_number, item.deleted))
220 .collect()
221 }
222}
223
224#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
225struct DocumentId(usize);
226
227#[derive(Clone, Copy, PartialEq)]
228struct ItemId {
229 document_id: DocumentId,
230 sequence_number: usize,
231}
232
233#[derive(Clone)]
234struct Item {
235 id: ItemId,
236 content: char,
237 origin_left: Option<ItemId>,
238 origin_right: Option<ItemId>,
239 deleted: bool,
240}
241
242#[derive(Default)]
243struct Version(HashMap<DocumentId, usize, IndigoBuildHasher>);
244
245impl Version {
246 fn contains(&self, id: ItemId) -> bool {
247 self.0
248 .get(&id.document_id)
249 .is_some_and(|highest| *highest >= id.sequence_number)
250 }
251
252 fn record(&mut self, id: ItemId) {
254 assert_eq!(
255 id.sequence_number,
256 self.next_sequence_number(id.document_id),
257 "items must be recorded in order",
258 );
259 self.0.insert(id.document_id, id.sequence_number);
260 }
261
262 fn next_sequence_number(&self, document_id: DocumentId) -> usize {
263 self.0.get(&document_id).map_or(0, |highest| highest + 1)
264 }
265}
266
267#[cfg(test)]
314mod tests {
315 use super::*;
316 use hegel::generators as gs;
317
318 #[test]
319 fn ties_broken_by_smaller_document_id() -> anyhow::Result<()> {
320 let mut doc1 = Document::new(1);
321 let mut doc2 = Document::new(2);
322
323 doc1.insert(0, "A")?;
324 doc2.insert(0, "B")?;
325
326 doc1.merge(&doc2);
327 doc2.merge(&doc1);
328
329 assert_eq!(doc1.text(), "AB");
330 assert_eq!(doc2.text(), "AB");
331 Ok(())
332 }
333
334 fn draw_boundary(tc: &hegel::TestCase, text: &str) -> usize {
336 let mut index = tc.draw(gs::integers::<usize>().max_value(text.len()));
337 while !text.is_char_boundary(index) {
338 index += 1;
339 }
340 index
341 }
342
343 fn edit_both(tc: &hegel::TestCase, doc: &mut Document, model: &mut String) {
346 if model.is_empty() || tc.draw(gs::booleans()) {
347 let index = draw_boundary(tc, model);
348 let text: String = tc.draw(gs::text().max_size(8));
349 doc.insert(index, &text).unwrap();
350 model.insert_str(index, &text);
351 } else {
352 let one = draw_boundary(tc, model);
353 let two = draw_boundary(tc, model);
354 let range = one.min(two)..one.max(two);
355 doc.delete(range.clone()).unwrap();
356 model.replace_range(range, "");
357 }
358 }
359
360 fn edit_randomly(tc: &hegel::TestCase, doc: &mut Document) {
362 let edits = tc.draw(gs::integers::<usize>().max_value(3));
363 for _ in 0..edits {
364 let mut model = doc.text();
365 edit_both(tc, doc, &mut model);
366 }
367 }
368
369 fn items(doc: &Document) -> Vec<(usize, usize, char, bool)> {
374 doc.content
375 .iter()
376 .map(|item| {
377 (
378 item.id.document_id.0,
379 item.id.sequence_number,
380 item.content,
381 item.deleted,
382 )
383 })
384 .collect()
385 }
386
387 fn type_at(tc: &hegel::TestCase, doc: &mut Document, byte_index: usize, text: &str) {
391 if tc.draw(gs::booleans()) {
392 let mut byte_index = byte_index;
393 for char in text.chars() {
394 doc.insert(byte_index, &char.to_string()).unwrap();
395 byte_index += char.len_utf8();
396 }
397 } else {
398 for char in text.chars().rev() {
399 doc.insert(byte_index, &char.to_string()).unwrap();
400 }
401 }
402 }
403
404 fn merge_pair(docs: &mut [Document], into: usize, from: usize) {
407 if into < from {
408 let (left, right) = docs.split_at_mut(from);
409 left[into].merge(&right[0]);
410 } else {
411 let (left, right) = docs.split_at_mut(into);
412 right[0].merge(&left[from]);
413 }
414 }
415
416 #[hegel::test(test_cases = 5_000)]
417 fn local_edits_match_string(tc: hegel::TestCase) {
418 let mut doc = Document::new(1);
419 let mut model = String::new();
420 let edits = tc.draw(gs::integers::<usize>().max_value(20));
421 for _ in 0..edits {
422 edit_both(&tc, &mut doc, &mut model);
423 assert_eq!(doc.text(), model);
424 }
425 }
426
427 #[hegel::test(test_cases = 1_500)]
428 fn concurrent_edits_converge(tc: hegel::TestCase) {
429 let count = tc.draw(gs::integers::<usize>().min_value(2).max_value(4));
430 let mut docs = (1..=count).map(Document::new).collect::<Vec<Document>>();
431
432 let rounds = tc.draw(gs::integers::<usize>().min_value(1).max_value(3));
433 for _ in 0..rounds {
434 for doc in &mut docs {
436 edit_randomly(&tc, doc);
437 }
438 let into = tc.draw(gs::integers::<usize>().max_value(count - 1));
440 let from = tc.draw(gs::integers::<usize>().max_value(count - 1));
441 if into != from {
442 merge_pair(&mut docs, into, from);
443 }
444 }
445
446 for into in 0..count {
448 for from in 0..count {
449 if into != from {
450 merge_pair(&mut docs, into, from);
451 }
452 }
453 }
454 for pair in docs.windows(2) {
455 assert_eq!(items(&pair[0]), items(&pair[1]));
456 }
457 }
458
459 #[hegel::test(test_cases = 2_000)]
463 fn concurrent_typing_does_not_interleave(tc: hegel::TestCase) {
464 let base: String = tc.draw(gs::text().alphabet("xyz"));
465 let mut doc1 = Document::new(1);
466 doc1.insert(0, &base).unwrap();
467 let mut doc2 = Document::new(2);
468 doc2.merge(&doc1);
469
470 let byte_index = draw_boundary(&tc, &base);
471 let text1: String = tc.draw(gs::text().alphabet("ABC").min_size(1));
472 let text2: String = tc.draw(gs::text().alphabet("123").min_size(1));
473 type_at(&tc, &mut doc1, byte_index, &text1);
474 type_at(&tc, &mut doc2, byte_index, &text2);
475
476 doc1.merge(&doc2);
477 doc2.merge(&doc1);
478 assert_eq!(items(&doc1), items(&doc2));
479 assert!(doc1.text().contains(&text1));
480 assert!(doc1.text().contains(&text2));
481 }
482
483 #[hegel::test(test_cases = 2_000)]
484 fn merge_is_idempotent(tc: hegel::TestCase) {
485 let mut doc1 = Document::new(1);
486 let mut doc2 = Document::new(2);
487 edit_randomly(&tc, &mut doc1);
488 edit_randomly(&tc, &mut doc2);
489
490 doc1.merge(&doc2);
493 doc2.merge(&doc1);
494 edit_randomly(&tc, &mut doc1);
495 edit_randomly(&tc, &mut doc2);
496
497 doc1.merge(&doc2);
498 let merged = items(&doc1);
499 doc1.merge(&doc2);
500 assert_eq!(items(&doc1), merged);
501 }
502
503 #[hegel::test(test_cases = 5_000)]
504 fn invalid_edits_error_and_change_nothing(tc: hegel::TestCase) {
505 let mut doc = Document::new(1);
506 let initial: String = tc.draw(gs::text());
507 doc.insert(0, &initial).unwrap();
508 let mut model = doc.text();
509
510 let index = tc.draw(hegel::one_of!(
512 gs::integers::<usize>().max_value(model.len() + 1),
513 gs::integers::<usize>(),
514 ));
515 let text: String = tc.draw(gs::text());
516 if model.is_char_boundary(index) {
517 doc.insert(index, &text).unwrap();
518 model.insert_str(index, &text);
519 } else {
520 assert!(doc.insert(index, &text).is_err());
521 }
522 assert_eq!(doc.text(), model);
523
524 let start = tc.draw(hegel::one_of!(
526 gs::integers::<usize>().max_value(model.len() + 1),
527 gs::integers::<usize>(),
528 ));
529 let end = tc.draw(hegel::one_of!(
530 gs::integers::<usize>().max_value(model.len() + 1),
531 gs::integers::<usize>(),
532 ));
533 if start <= end && model.is_char_boundary(start) && model.is_char_boundary(end) {
534 doc.delete(start..end).unwrap();
535 model.replace_range(start..end, "");
536 } else {
537 assert!(doc.delete(start..end).is_err());
538 }
539 assert_eq!(doc.text(), model);
540 }
541}