1use super::list::ListType;
2use crate::{impl_type, Content, JwstCodecResult};
3
4impl_type!(Text);
5
6impl ListType for Text {}
7
8impl Text {
9    #[inline]
10    pub fn len(&self) -> u64 {
11        self.content_len()
12    }
13
14    #[inline]
15    pub fn is_empty(&self) -> bool {
16        self.len() == 0
17    }
18
19    #[inline]
20    pub fn insert<T: ToString>(&mut self, char_index: u64, str: T) -> JwstCodecResult {
21        self.insert_at(char_index, Content::String(str.to_string()))
22    }
23
24    #[inline]
25    pub fn remove(&mut self, char_index: u64, len: u64) -> JwstCodecResult {
26        self.remove_at(char_index, len)
27    }
28}
29
30impl ToString for Text {
31    fn to_string(&self) -> String {
32        let mut ret = String::with_capacity(self.len() as usize);
33
34        self.iter_item().fold(&mut ret, |ret, item| {
35            if let Content::String(str) = item.get().unwrap().content.as_ref() {
36                ret.push_str(str);
37            }
38
39            ret
40        });
41
42        ret
43    }
44}
45
46impl serde::Serialize for Text {
47    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
48    where
49        S: serde::Serializer,
50    {
51        serializer.serialize_str(&self.to_string())
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use rand::{Rng, SeedableRng};
58    use rand_chacha::ChaCha20Rng;
59    use yrs::{Options, Text, Transact};
60
61    use crate::{
62        loom_model,
63        sync::{thread, Arc, AtomicUsize, Ordering},
64        Doc, DocOptions,
65    };
66
67    #[test]
68    fn test_manipulate_text() {
69        let options = DocOptions {
70            client: Some(rand::random()),
71            guid: Some(nanoid::nanoid!()),
72        };
73
74        loom_model!({
75            let doc = Doc::with_options(options.clone());
76            let mut text = doc.create_text().unwrap();
77
78            text.insert(0, "llo").unwrap();
79            text.insert(0, "he").unwrap();
80            text.insert(5, " world").unwrap();
81            text.insert(6, "great ").unwrap();
82            text.insert(17, '!').unwrap();
83
84            assert_eq!(text.to_string(), "hello great world!");
85            assert_eq!(text.len(), 18);
86
87            text.remove(4, 4).unwrap();
88            assert_eq!(text.to_string(), "helleat world!");
89            assert_eq!(text.len(), 14);
90        });
91    }
92
93    #[test]
94    #[cfg(not(loom))]
95    fn test_parallel_insert_text() {
96        let seed = rand::thread_rng().gen();
97        let rand = ChaCha20Rng::seed_from_u64(seed);
98        let mut handles = Vec::new();
99
100        let doc = Doc::with_client(1);
101        let mut text = doc.get_or_create_text("test").unwrap();
102        text.insert(0, "This is a string with length 32.").unwrap();
103
104        let added_len = Arc::new(AtomicUsize::new(32));
105
106        {
108            for i in 0..2 {
109                let mut text = text.clone();
110                let mut rand = rand.clone();
111                let len = added_len.clone();
112
113                handles.push(thread::spawn(move || {
114                    for j in 0..10 {
115                        let pos = rand.gen_range(0..text.len());
116                        let string = format!("hello {}", i * j);
117
118                        text.insert(pos, &string).unwrap();
119
120                        len.fetch_add(string.len(), Ordering::SeqCst);
121                    }
122                }));
123            }
124        }
125
126        {
128            for i in 0..2 {
129                let doc = doc.clone();
130                let mut rand = rand.clone();
131                let len = added_len.clone();
132
133                handles.push(thread::spawn(move || {
134                    let mut text = doc.get_or_create_text("test").unwrap();
135                    for j in 0..10 {
136                        let pos = rand.gen_range(0..text.len());
137                        let string = format!("hello doc{}", i * j);
138
139                        text.insert(pos, &string).unwrap();
140
141                        len.fetch_add(string.len(), Ordering::SeqCst);
142                    }
143                }));
144            }
145        }
146
147        for handle in handles {
148            handle.join().unwrap();
149        }
150
151        assert_eq!(text.to_string().len(), added_len.load(Ordering::SeqCst));
152        assert_eq!(text.len(), added_len.load(Ordering::SeqCst) as u64);
153    }
154
155    fn parallel_ins_del_text(seed: u64, thread: i32, iteration: i32) {
156        let doc = Doc::with_client(1);
157        let rand = ChaCha20Rng::seed_from_u64(seed);
158        let mut text = doc.get_or_create_text("test").unwrap();
159        text.insert(0, "This is a string with length 32.").unwrap();
160
161        let mut handles = Vec::new();
162        let len = Arc::new(AtomicUsize::new(32));
163
164        for i in 0..thread {
165            let len = len.clone();
166            let mut rand = rand.clone();
167            let text = text.clone();
168            handles.push(thread::spawn(move || {
169                for j in 0..iteration {
170                    let len = len.clone();
171                    let mut text = text.clone();
172                    let ins = i % 2 == 0;
173                    let pos = rand.gen_range(0..16);
174
175                    if ins {
176                        let str = format!("hello {}", i * j);
177                        text.insert(pos, &str).unwrap();
178
179                        len.fetch_add(str.len(), Ordering::SeqCst);
180                    } else {
181                        text.remove(pos, 6).unwrap();
182
183                        len.fetch_sub(6, Ordering::SeqCst);
184                    }
185                }
186            }));
187        }
188
189        for handle in handles {
190            handle.join().unwrap();
191        }
192
193        assert_eq!(text.to_string().len(), len.load(Ordering::SeqCst));
194        assert_eq!(text.len(), len.load(Ordering::SeqCst) as u64);
195    }
196
197    #[test]
198    #[cfg(not(loom))]
199    fn test_parallel_ins_del_text() {
200        parallel_ins_del_text(973078538, 2, 2);
203        parallel_ins_del_text(18414938500869652479, 2, 2);
204    }
205
206    #[test]
207    fn loom_parallel_ins_del_text() {
208        let options = DocOptions {
209            client: Some(rand::random()),
210            guid: Some(nanoid::nanoid!()),
211        };
212
213        let seed = rand::thread_rng().gen();
214        let mut rand = ChaCha20Rng::seed_from_u64(seed);
215        let ranges = (0..20).map(|_| rand.gen_range(0..16)).collect::<Vec<_>>();
216
217        loom_model!({
218            let doc = Doc::with_options(options.clone());
219            let mut text = doc.get_or_create_text("test").unwrap();
220            text.insert(0, "This is a string with length 32.").unwrap();
221
222            let handles = (0..2)
224                .map(|i| {
225                    let text = text.clone();
226                    let ranges = ranges.clone();
227                    thread::spawn(move || {
228                        let mut text = text.clone();
229                        let ins = i % 2 == 0;
230                        let pos = ranges[i];
231
232                        if ins {
233                            let str = format!("hello {}", i);
234                            text.insert(pos, &str).unwrap();
235                        } else {
236                            text.remove(pos, 6).unwrap();
237                        }
238                    })
239                })
240                .collect::<Vec<_>>();
241
242            for handle in handles {
243                handle.join().unwrap();
244            }
245        });
246    }
247
248    #[test]
249    #[cfg_attr(miri, ignore)]
250    fn test_recover_from_yjs_encoder() {
251        let yrs_options = Options {
252            client_id: rand::random(),
253            guid: nanoid::nanoid!().into(),
254            ..Default::default()
255        };
256        let binary = {
257            let doc = yrs::Doc::with_options(yrs_options.clone());
258            let text = doc.get_or_insert_text("greating");
259            let mut trx = doc.transact_mut();
260            text.insert(&mut trx, 0, "hello").unwrap();
261            text.insert(&mut trx, 5, " world!").unwrap();
262            text.remove_range(&mut trx, 11, 1).unwrap();
263
264            trx.encode_update_v1().unwrap()
265        };
266
267        let options = DocOptions {
268            client: Some(rand::random()),
269            guid: Some(nanoid::nanoid!()),
270        };
271
272        loom_model!({
273            let binary = binary.clone();
274            let doc = Doc::new_from_binary_with_options(binary, options.clone()).unwrap();
275            let mut text = doc.get_or_create_text("greating").unwrap();
276
277            assert_eq!(text.to_string(), "hello world");
278
279            text.insert(6, "great ").unwrap();
280            text.insert(17, '!').unwrap();
281            assert_eq!(text.to_string(), "hello great world!");
282        });
283    }
284
285    #[test]
286    fn test_recover_from_octobase_encoder() {
287        let options = DocOptions {
288            client: Some(rand::random()),
289            guid: Some(nanoid::nanoid!()),
290        };
291
292        loom_model!({
293            let binary = {
294                let doc = Doc::with_options(options.clone());
295                let mut text = doc.get_or_create_text("greating").unwrap();
296                text.insert(0, "hello").unwrap();
297                text.insert(5, " world!").unwrap();
298                text.remove(11, 1).unwrap();
299
300                doc.encode_update_v1().unwrap()
301            };
302
303            let binary = binary.clone();
304            let doc = Doc::new_from_binary_with_options(binary, options.clone()).unwrap();
305            let mut text = doc.get_or_create_text("greating").unwrap();
306
307            assert_eq!(text.to_string(), "hello world");
308
309            text.insert(6, "great ").unwrap();
310            text.insert(17, '!').unwrap();
311            assert_eq!(text.to_string(), "hello great world!");
312        });
313    }
314}