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}