loro_ffi/container/
text.rs

1use std::{fmt::Display, sync::Arc};
2
3use loro::TextDelta as InternalTextDelta;
4use loro::{
5    cursor::{PosType, Side},
6    ContainerTrait, LoroResult, PeerID, UpdateOptions, UpdateTimeoutError,
7};
8
9use crate::{
10    ContainerID, DiffEvent, LoroDoc, LoroValue, LoroValueLike, Subscriber, Subscription, TextDelta,
11};
12
13use super::Cursor;
14
15#[derive(Debug, Clone)]
16pub struct LoroText {
17    pub(crate) inner: loro::LoroText,
18}
19
20impl LoroText {
21    /// Create a new container that is detached from the document.
22    ///
23    /// The edits on a detached container will not be persisted.
24    /// To attach the container to the document, please insert it into an attached container.
25    pub fn new() -> Self {
26        Self {
27            inner: loro::LoroText::new(),
28        }
29    }
30
31    /// Whether the container is attached to a document
32    ///
33    /// The edits on a detached container will not be persisted.
34    /// To attach the container to the document, please insert it into an attached container.
35    pub fn is_attached(&self) -> bool {
36        self.inner.is_attached()
37    }
38
39    /// If a detached container is attached, this method will return its corresponding attached handler.
40    pub fn get_attached(&self) -> Option<Arc<LoroText>> {
41        self.inner
42            .get_attached()
43            .map(|x| Arc::new(LoroText { inner: x }))
44    }
45
46    /// Get the [ContainerID]  of the text container.
47    pub fn id(&self) -> ContainerID {
48        self.inner.id().into()
49    }
50
51    /// Iterate each span(internal storage unit) of the text.
52    ///
53    /// The callback function will be called for each character in the text.
54    /// If the callback returns `false`, the iteration will stop.
55    // TODO:
56    pub fn iter(&self, callback: impl FnMut(&str) -> bool) {
57        self.inner.iter(callback);
58    }
59
60    /// Insert a string at the given unicode position.
61    pub fn insert(&self, pos: u32, s: &str) -> LoroResult<()> {
62        self.inner.insert(pos as usize, s)
63    }
64
65    /// Insert a string at the given utf-8 position.
66    pub fn insert_utf8(&self, pos: u32, s: &str) -> LoroResult<()> {
67        self.inner.insert_utf8(pos as usize, s)
68    }
69
70    /// Insert a string at the given utf-16 position.
71    pub fn insert_utf16(&self, pos: u32, s: &str) -> LoroResult<()> {
72        self.inner.insert_utf16(pos as usize, s)
73    }
74
75    /// Delete a range of text at the given unicode position with unicode length.
76    pub fn delete(&self, pos: u32, len: u32) -> LoroResult<()> {
77        self.inner.delete(pos as usize, len as usize)
78    }
79
80    /// Delete a range of text at the given utf-8 position with utf-8 length.
81    pub fn delete_utf8(&self, pos: u32, len: u32) -> LoroResult<()> {
82        self.inner.delete_utf8(pos as usize, len as usize)
83    }
84
85    /// Delete a range of text at the given utf-16 position with utf-16 length.
86    pub fn delete_utf16(&self, pos: u32, len: u32) -> LoroResult<()> {
87        self.inner.delete_utf16(pos as usize, len as usize)
88    }
89
90    /// Get a string slice at the given Unicode range
91    pub fn slice(&self, start_index: u32, end_index: u32) -> LoroResult<String> {
92        self.inner.slice(start_index as usize, end_index as usize)
93    }
94
95    /// Get a string slice at the given UTF-16 range
96    pub fn slice_utf16(&self, start_index: u32, end_index: u32) -> LoroResult<String> {
97        self.inner
98            .slice_utf16(start_index as usize, end_index as usize)
99    }
100
101    pub fn slice_delta(
102        &self,
103        start_index: u32,
104        end_index: u32,
105        pos_type: PosType,
106    ) -> LoroResult<Vec<TextDelta>> {
107        self.inner
108            .slice_delta(start_index as usize, end_index as usize, pos_type)
109            .map(|d| d.into_iter().map(|x| x.into()).collect())
110    }
111
112    /// Get the characters at given unicode position.
113    pub fn char_at(&self, pos: u32) -> LoroResult<String> {
114        self.inner.char_at(pos as usize).map(|c| c.to_string())
115    }
116
117    /// Delete specified character and insert string at the same position at given unicode position.
118    pub fn splice(&self, pos: u32, len: u32, s: &str) -> LoroResult<String> {
119        self.inner.splice(pos as usize, len as usize, s)
120    }
121
122    /// Delete specified range and insert a string at the same UTF-16 position.
123    pub fn splice_utf16(&self, pos: u32, len: u32, s: &str) -> LoroResult<()> {
124        self.inner.splice_utf16(pos as usize, len as usize, s)
125    }
126
127    /// Whether the text container is empty.
128    pub fn is_empty(&self) -> bool {
129        self.inner.is_empty()
130    }
131
132    /// Get the length of the text container in UTF-8.
133    pub fn len_utf8(&self) -> u32 {
134        self.inner.len_utf8() as u32
135    }
136
137    /// Get the length of the text container in Unicode.
138    pub fn len_unicode(&self) -> u32 {
139        self.inner.len_unicode() as u32
140    }
141
142    /// Get the length of the text container in UTF-16.
143    pub fn len_utf16(&self) -> u32 {
144        self.inner.len_utf16() as u32
145    }
146
147    /// Update the current text based on the provided text.
148    pub fn update(&self, text: &str, options: UpdateOptions) -> Result<(), UpdateTimeoutError> {
149        self.inner.update(text, options)
150    }
151
152    /// Apply a [delta](https://quilljs.com/docs/delta/) to the text container.
153    pub fn apply_delta(&self, delta: Vec<TextDelta>) -> LoroResult<()> {
154        let internal_delta: Vec<InternalTextDelta> = delta.into_iter().map(|d| d.into()).collect();
155        self.inner.apply_delta(&internal_delta)
156    }
157
158    /// Mark a range of text with a key-value pair.
159    ///
160    /// You can use it to create a highlight, make a range of text bold, or add a link to a range of text.
161    ///
162    /// You can specify the `expand` option to set the behavior when inserting text at the boundary of the range.
163    ///
164    /// - `after`(default): when inserting text right after the given range, the mark will be expanded to include the inserted text
165    /// - `before`: when inserting text right before the given range, the mark will be expanded to include the inserted text
166    /// - `none`: the mark will not be expanded to include the inserted text at the boundaries
167    /// - `both`: when inserting text either right before or right after the given range, the mark will be expanded to include the inserted text
168    ///
169    /// *You should make sure that a key is always associated with the same expand type.*
170    ///
171    /// Note: this is not suitable for unmergeable annotations like comments.
172    pub fn mark(
173        &self,
174        from: u32,
175        to: u32,
176        key: &str,
177        value: Arc<dyn LoroValueLike>,
178    ) -> LoroResult<()> {
179        self.inner
180            .mark(from as usize..to as usize, key, value.as_loro_value())
181    }
182
183    pub fn mark_utf8(
184        &self,
185        from: u32,
186        to: u32,
187        key: &str,
188        value: Arc<dyn LoroValueLike>,
189    ) -> LoroResult<()> {
190        self.inner
191            .mark_utf8(from as usize..to as usize, key, value.as_loro_value())
192    }
193
194    pub fn mark_utf16(
195        &self,
196        from: u32,
197        to: u32,
198        key: &str,
199        value: Arc<dyn LoroValueLike>,
200    ) -> LoroResult<()> {
201        self.inner
202            .mark_utf16(from as usize..to as usize, key, value.as_loro_value())
203    }
204
205    /// Unmark a range of text with a key and a value.
206    ///
207    /// You can use it to remove highlights, bolds or links
208    ///
209    /// You can specify the `expand` option to set the behavior when inserting text at the boundary of the range.
210    ///
211    /// **Note: You should specify the same expand type as when you mark the text.**
212    ///
213    /// - `after`(default): when inserting text right after the given range, the mark will be expanded to include the inserted text
214    /// - `before`: when inserting text right before the given range, the mark will be expanded to include the inserted text
215    /// - `none`: the mark will not be expanded to include the inserted text at the boundaries
216    /// - `both`: when inserting text either right before or right after the given range, the mark will be expanded to include the inserted text
217    ///
218    /// *You should make sure that a key is always associated with the same expand type.*
219    ///
220    /// Note: you cannot delete unmergeable annotations like comments by this method.
221    pub fn unmark(&self, from: u32, to: u32, key: &str) -> LoroResult<()> {
222        self.inner.unmark(from as usize..to as usize, key)
223    }
224
225    pub fn unmark_utf16(&self, from: u32, to: u32, key: &str) -> LoroResult<()> {
226        self.inner.unmark_utf16(from as usize..to as usize, key)
227    }
228
229    /// Get the text in [Delta](https://quilljs.com/docs/delta/) format.
230    ///
231    /// # Example
232    /// ```
233    /// use loro::{LoroDoc, ToJson, ExpandType, TextDelta};
234    /// use serde_json::json;
235    /// use std::collections::HashMap;
236    ///
237    /// let doc = LoroDoc::new();
238    /// let text = doc.get_text("text");
239    /// text.insert(0, "Hello world!").unwrap();
240    /// text.mark(0..5, "bold", true).unwrap();
241    /// assert_eq!(
242    ///     text.to_delta(),
243    ///     vec![
244    ///         TextDelta::Insert {
245    ///             insert: "Hello".to_string(),
246    ///             attributes: Some(HashMap::from_iter([("bold".to_string(), true.into())])),
247    ///         },
248    ///         TextDelta::Insert {
249    ///             insert: " world!".to_string(),
250    ///             attributes: None,
251    ///         },
252    ///     ]
253    /// );
254    /// text.unmark(3..5, "bold").unwrap();
255    /// assert_eq!(
256    ///     text.to_delta(),
257    ///     vec![
258    ///         TextDelta::Insert {
259    ///             insert: "Hel".to_string(),
260    ///             attributes: Some(HashMap::from_iter([("bold".to_string(), true.into())])),
261    ///         },
262    ///         TextDelta::Insert {
263    ///             insert: "lo world!".to_string(),
264    ///             attributes: None,
265    ///         },
266    ///     ]
267    /// );
268    /// ```
269    pub fn to_delta(&self) -> Vec<TextDelta> {
270        self.inner
271            .to_delta()
272            .into_iter()
273            .map(|d| d.into())
274            .collect()
275    }
276
277    /// Get the text in [Delta](https://quilljs.com/docs/delta/) format.
278    ///
279    /// # Example
280    /// ```
281    /// # use loro::{LoroDoc, ToJson, ExpandType};
282    /// # use serde_json::json;
283    ///
284    /// let doc = LoroDoc::new();
285    /// let text = doc.get_text("text");
286    /// text.insert(0, "Hello world!").unwrap();
287    /// text.mark(0..5, "bold", true).unwrap();
288    /// assert_eq!(
289    ///     text.get_richtext_value().to_json_value(),
290    ///     json!([
291    ///         { "insert": "Hello", "attributes": {"bold": true} },
292    ///         { "insert": " world!" },
293    ///     ])
294    /// );
295    /// text.unmark(3..5, "bold").unwrap();
296    /// assert_eq!(
297    ///     text.get_richtext_value().to_json_value(),
298    ///     json!([
299    ///         { "insert": "Hel", "attributes": {"bold": true} },
300    ///         { "insert": "lo world!" },
301    ///    ])
302    /// );
303    /// ```
304    pub fn get_richtext_value(&self) -> LoroValue {
305        self.inner.get_richtext_value().into()
306    }
307
308    /// Get the cursor at the given position.
309    ///
310    /// Using "index" to denote cursor positions can be unstable, as positions may
311    /// shift with document edits. To reliably represent a position or range within
312    /// a document, it is more effective to leverage the unique ID of each item/character
313    /// in a List CRDT or Text CRDT.
314    ///
315    /// Loro optimizes State metadata by not storing the IDs of deleted elements. This
316    /// approach complicates tracking cursors since they rely on these IDs. The solution
317    /// recalculates position by replaying relevant history to update stable positions
318    /// accurately. To minimize the performance impact of history replay, the system
319    /// updates cursor info to reference only the IDs of currently present elements,
320    /// thereby reducing the need for replay.
321    ///
322    /// # Example
323    ///
324    /// ```
325    /// # use loro::{LoroDoc, ToJson};
326    /// let doc = LoroDoc::new();
327    /// let text = &doc.get_text("text");
328    /// text.insert(0, "01234").unwrap();
329    /// let pos = text.get_cursor(5, Default::default()).unwrap();
330    /// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 5);
331    /// text.insert(0, "01234").unwrap();
332    /// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 10);
333    /// text.delete(0, 10).unwrap();
334    /// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 0);
335    /// text.insert(0, "01234").unwrap();
336    /// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 5);
337    /// ```
338    pub fn get_cursor(&self, pos: u32, side: Side) -> Option<Arc<Cursor>> {
339        self.inner
340            .get_cursor(pos as usize, side)
341            .map(|v| Arc::new(v.into()))
342    }
343
344    pub fn update_by_line(
345        &self,
346        text: &str,
347        options: UpdateOptions,
348    ) -> Result<(), UpdateTimeoutError> {
349        self.inner.update_by_line(text, options)
350    }
351
352    pub fn is_deleted(&self) -> bool {
353        self.inner.is_deleted()
354    }
355
356    pub fn push_str(&self, s: &str) -> LoroResult<()> {
357        self.inner.push_str(s)
358    }
359
360    pub fn get_editor_at_unicode_pos(&self, pos: u32) -> Option<PeerID> {
361        self.inner.get_editor_at_unicode_pos(pos as usize)
362    }
363
364    pub fn doc(&self) -> Option<Arc<LoroDoc>> {
365        self.inner.doc().map(|x| Arc::new(LoroDoc { doc: x }))
366    }
367
368    pub fn convert_pos(&self, index: u32, from: PosType, to: PosType) -> Option<u32> {
369        self.inner
370            .convert_pos(index as usize, from, to)
371            .map(|v| v as u32)
372    }
373
374    pub fn subscribe(&self, subscriber: Arc<dyn Subscriber>) -> Option<Arc<Subscription>> {
375        self.inner
376            .subscribe(Arc::new(move |e| {
377                subscriber.on_diff(DiffEvent::from(e));
378            }))
379            .map(|x| Arc::new(x.into()))
380    }
381}
382
383impl Display for LoroText {
384    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385        write!(f, "{}", self.inner.to_string())
386    }
387}
388
389impl Default for LoroText {
390    fn default() -> Self {
391        Self::new()
392    }
393}