Skip to main content

oxigdal_sync/ot/
text_operation.rs

1//! Text operations for operational transformation
2
3use crate::ot::Transform;
4use crate::{SyncError, SyncResult};
5use serde::{Deserialize, Serialize};
6
7/// A single operation on text
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum Operation {
10    /// Retain n characters
11    Retain(usize),
12    /// Insert text
13    Insert(String),
14    /// Delete n characters
15    Delete(usize),
16}
17
18/// A text operation consisting of multiple atomic operations
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct TextOperation {
21    /// The sequence of operations
22    ops: Vec<Operation>,
23    /// The base length (length before applying operation)
24    base_length: usize,
25    /// The target length (length after applying operation)
26    target_length: usize,
27}
28
29impl TextOperation {
30    /// Creates a new empty text operation
31    pub fn new() -> Self {
32        Self {
33            ops: Vec::new(),
34            base_length: 0,
35            target_length: 0,
36        }
37    }
38
39    /// Creates a text operation from a base length
40    ///
41    /// # Arguments
42    ///
43    /// * `base_length` - The length of the document before this operation
44    pub fn with_base_length(base_length: usize) -> Self {
45        Self {
46            ops: Vec::new(),
47            base_length,
48            target_length: base_length,
49        }
50    }
51
52    /// Adds a retain operation
53    ///
54    /// # Arguments
55    ///
56    /// * `n` - Number of characters to retain
57    pub fn retain(&mut self, n: usize) -> &mut Self {
58        if n == 0 {
59            return self;
60        }
61
62        self.base_length += n;
63        self.target_length += n;
64
65        // Merge with previous retain if possible
66        if let Some(Operation::Retain(prev)) = self.ops.last_mut() {
67            *prev += n;
68        } else {
69            self.ops.push(Operation::Retain(n));
70        }
71
72        self
73    }
74
75    /// Adds an insert operation
76    ///
77    /// # Arguments
78    ///
79    /// * `text` - Text to insert
80    pub fn insert(&mut self, text: String) -> &mut Self {
81        if text.is_empty() {
82            return self;
83        }
84
85        self.target_length += text.len();
86
87        // Merge with previous insert if possible
88        if let Some(Operation::Insert(prev)) = self.ops.last_mut() {
89            prev.push_str(&text);
90        } else {
91            self.ops.push(Operation::Insert(text));
92        }
93
94        self
95    }
96
97    /// Adds a delete operation
98    ///
99    /// # Arguments
100    ///
101    /// * `n` - Number of characters to delete
102    pub fn delete(&mut self, n: usize) -> &mut Self {
103        if n == 0 {
104            return self;
105        }
106
107        self.base_length += n;
108
109        // Merge with previous delete if possible
110        if let Some(Operation::Delete(prev)) = self.ops.last_mut() {
111            *prev += n;
112        } else {
113            self.ops.push(Operation::Delete(n));
114        }
115
116        self
117    }
118
119    /// Applies this operation to a string
120    ///
121    /// # Arguments
122    ///
123    /// * `text` - The text to apply the operation to
124    ///
125    /// # Returns
126    ///
127    /// The resulting text after applying the operation
128    pub fn apply(&self, text: &str) -> SyncResult<String> {
129        if text.len() != self.base_length {
130            return Err(SyncError::InvalidOperation(format!(
131                "Base length mismatch: expected {}, got {}",
132                self.base_length,
133                text.len()
134            )));
135        }
136
137        let mut result = String::with_capacity(self.target_length);
138        let mut chars = text.chars();
139
140        for op in &self.ops {
141            match op {
142                Operation::Retain(n) => {
143                    for _ in 0..*n {
144                        if let Some(ch) = chars.next() {
145                            result.push(ch);
146                        } else {
147                            return Err(SyncError::InvalidOperation(
148                                "Retain beyond document length".to_string(),
149                            ));
150                        }
151                    }
152                }
153                Operation::Insert(s) => {
154                    result.push_str(s);
155                }
156                Operation::Delete(n) => {
157                    for _ in 0..*n {
158                        if chars.next().is_none() {
159                            return Err(SyncError::InvalidOperation(
160                                "Delete beyond document length".to_string(),
161                            ));
162                        }
163                    }
164                }
165            }
166        }
167
168        // Ensure we consumed the entire input
169        if chars.next().is_some() {
170            return Err(SyncError::InvalidOperation(
171                "Operation did not consume entire document".to_string(),
172            ));
173        }
174
175        Ok(result)
176    }
177
178    /// Gets the base length
179    pub fn base_length(&self) -> usize {
180        self.base_length
181    }
182
183    /// Gets the target length
184    pub fn target_length(&self) -> usize {
185        self.target_length
186    }
187
188    /// Gets the operations
189    pub fn operations(&self) -> &[Operation] {
190        &self.ops
191    }
192
193    /// Checks if the operation is a no-op
194    pub fn is_noop(&self) -> bool {
195        self.ops.is_empty() || (self.ops.len() == 1 && matches!(self.ops[0], Operation::Retain(_)))
196    }
197}
198
199impl Default for TextOperation {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205impl Transform for TextOperation {
206    fn transform(&self, other: &Self) -> SyncResult<Self> {
207        if self.base_length != other.base_length {
208            return Err(SyncError::InvalidOperation(
209                "Base length mismatch in transform".to_string(),
210            ));
211        }
212
213        let mut result = TextOperation::new();
214        let mut i1 = 0;
215        let mut i2 = 0;
216        let mut ops1 = self.ops.clone();
217        let mut ops2 = other.ops.clone();
218
219        while i1 < ops1.len() || i2 < ops2.len() {
220            if i1 < ops1.len() && matches!(ops1[i1], Operation::Insert(_)) {
221                if let Operation::Insert(s) = &ops1[i1] {
222                    result.insert(s.clone());
223                }
224                i1 += 1;
225                continue;
226            }
227
228            if i2 < ops2.len() && matches!(ops2[i2], Operation::Insert(_)) {
229                if let Operation::Insert(s) = &ops2[i2] {
230                    result.retain(s.len());
231                }
232                i2 += 1;
233                continue;
234            }
235
236            if i1 >= ops1.len() || i2 >= ops2.len() {
237                break;
238            }
239
240            match (&ops1[i1], &ops2[i2]) {
241                (Operation::Retain(n1), Operation::Retain(n2)) => {
242                    let min = (*n1).min(*n2);
243                    result.retain(min);
244
245                    if n1 > n2 {
246                        ops1[i1] = Operation::Retain(n1 - n2);
247                        i2 += 1;
248                    } else if n2 > n1 {
249                        ops2[i2] = Operation::Retain(n2 - n1);
250                        i1 += 1;
251                    } else {
252                        i1 += 1;
253                        i2 += 1;
254                    }
255                }
256                (Operation::Delete(n1), Operation::Delete(n2)) => {
257                    if n1 > n2 {
258                        ops1[i1] = Operation::Delete(n1 - n2);
259                        i2 += 1;
260                    } else if n2 > n1 {
261                        ops2[i2] = Operation::Delete(n2 - n1);
262                        i1 += 1;
263                    } else {
264                        i1 += 1;
265                        i2 += 1;
266                    }
267                }
268                (Operation::Retain(n), Operation::Delete(m)) => {
269                    let min = (*n).min(*m);
270                    result.delete(min);
271
272                    if n > m {
273                        ops1[i1] = Operation::Retain(n - m);
274                        i2 += 1;
275                    } else if m > n {
276                        ops2[i2] = Operation::Delete(m - n);
277                        i1 += 1;
278                    } else {
279                        i1 += 1;
280                        i2 += 1;
281                    }
282                }
283                (Operation::Delete(n), Operation::Retain(m)) => {
284                    let min = (*n).min(*m);
285                    result.delete(min);
286
287                    if n > m {
288                        ops1[i1] = Operation::Delete(n - m);
289                        i2 += 1;
290                    } else if m > n {
291                        ops2[i2] = Operation::Retain(m - n);
292                        i1 += 1;
293                    } else {
294                        i1 += 1;
295                        i2 += 1;
296                    }
297                }
298                _ => {
299                    return Err(SyncError::InvalidOperation(
300                        "Invalid operation combination in transform".to_string(),
301                    ));
302                }
303            }
304        }
305
306        Ok(result)
307    }
308
309    fn compose(&self, other: &Self) -> SyncResult<Self> {
310        if self.target_length != other.base_length {
311            return Err(SyncError::InvalidOperation(
312                "Target/base length mismatch in compose".to_string(),
313            ));
314        }
315
316        let mut result = TextOperation::new();
317        let mut i1 = 0;
318        let mut i2 = 0;
319        let mut ops1 = self.ops.clone();
320        let mut ops2 = other.ops.clone();
321
322        while i1 < ops1.len() || i2 < ops2.len() {
323            if i1 < ops1.len() && matches!(ops1[i1], Operation::Delete(_)) {
324                if let Operation::Delete(n) = ops1[i1] {
325                    result.delete(n);
326                }
327                i1 += 1;
328                continue;
329            }
330
331            if i2 < ops2.len() && matches!(ops2[i2], Operation::Insert(_)) {
332                if let Operation::Insert(s) = &ops2[i2] {
333                    result.insert(s.clone());
334                }
335                i2 += 1;
336                continue;
337            }
338
339            if i1 >= ops1.len() || i2 >= ops2.len() {
340                break;
341            }
342
343            match (&ops1[i1], &ops2[i2]) {
344                (Operation::Retain(n1), Operation::Retain(n2)) => {
345                    let min = (*n1).min(*n2);
346                    result.retain(min);
347
348                    if n1 > n2 {
349                        ops1[i1] = Operation::Retain(n1 - n2);
350                        i2 += 1;
351                    } else if n2 > n1 {
352                        ops2[i2] = Operation::Retain(n2 - n1);
353                        i1 += 1;
354                    } else {
355                        i1 += 1;
356                        i2 += 1;
357                    }
358                }
359                (Operation::Insert(s), Operation::Retain(n)) => {
360                    let len = s.len();
361
362                    if len <= *n {
363                        result.insert(s.clone());
364                        i1 += 1;
365                        if *n > len {
366                            ops2[i2] = Operation::Retain(n - len);
367                        } else {
368                            i2 += 1;
369                        }
370                    } else {
371                        result.insert(s[..*n].to_string());
372                        ops1[i1] = Operation::Insert(s[*n..].to_string());
373                        i2 += 1;
374                    }
375                }
376                (Operation::Insert(s), Operation::Delete(n)) => {
377                    let len = s.len();
378                    if len > *n {
379                        ops1[i1] = Operation::Insert(s[*n..].to_string());
380                        i2 += 1;
381                    } else if *n > len {
382                        ops2[i2] = Operation::Delete(n - len);
383                        i1 += 1;
384                    } else {
385                        i1 += 1;
386                        i2 += 1;
387                    }
388                }
389                (Operation::Retain(n1), Operation::Delete(n2)) => {
390                    let min = (*n1).min(*n2);
391                    result.delete(min);
392
393                    if n1 > n2 {
394                        ops1[i1] = Operation::Retain(n1 - n2);
395                        i2 += 1;
396                    } else if n2 > n1 {
397                        ops2[i2] = Operation::Delete(n2 - n1);
398                        i1 += 1;
399                    } else {
400                        i1 += 1;
401                        i2 += 1;
402                    }
403                }
404                _ => {
405                    return Err(SyncError::InvalidOperation(
406                        "Invalid operation combination in compose".to_string(),
407                    ));
408                }
409            }
410        }
411
412        result.base_length = self.base_length;
413        result.target_length = other.target_length;
414
415        Ok(result)
416    }
417
418    fn invert(&self) -> SyncResult<Self> {
419        let mut result = TextOperation::new();
420        result.base_length = self.target_length;
421        result.target_length = self.base_length;
422
423        for op in self.ops.iter().rev() {
424            match op {
425                Operation::Retain(n) => {
426                    result.ops.insert(0, Operation::Retain(*n));
427                }
428                Operation::Insert(s) => {
429                    result.ops.insert(0, Operation::Delete(s.len()));
430                }
431                Operation::Delete(_n) => {
432                    // Note: We can't reconstruct the deleted text without additional context
433                    // This is a limitation - full invert would require storing deleted content
434                    result.ops.insert(0, Operation::Insert("".to_string()));
435                }
436            }
437        }
438
439        Ok(result)
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_text_operation_creation() {
449        let op = TextOperation::new();
450        assert_eq!(op.base_length(), 0);
451        assert_eq!(op.target_length(), 0);
452        assert!(op.is_noop());
453    }
454
455    #[test]
456    fn test_text_operation_retain() {
457        let mut op = TextOperation::with_base_length(0);
458        op.retain(5);
459        assert_eq!(op.base_length(), 5);
460        assert_eq!(op.target_length(), 5);
461    }
462
463    #[test]
464    fn test_text_operation_insert() {
465        let mut op = TextOperation::new();
466        op.insert("hello".to_string());
467        assert_eq!(op.base_length(), 0);
468        assert_eq!(op.target_length(), 5);
469    }
470
471    #[test]
472    fn test_text_operation_delete() {
473        let mut op = TextOperation::with_base_length(0);
474        op.delete(3);
475        assert_eq!(op.base_length(), 3);
476        assert_eq!(op.target_length(), 0);
477    }
478
479    #[test]
480    fn test_text_operation_apply() -> SyncResult<()> {
481        let mut op = TextOperation::with_base_length(0);
482        op.insert("hello".to_string());
483
484        let result = op.apply("")?;
485        assert_eq!(result, "hello");
486        Ok(())
487    }
488
489    #[test]
490    fn test_text_operation_apply_complex() -> SyncResult<()> {
491        // Input "hello!" has 6 characters: h-e-l-l-o-!
492        // retain/delete operations add to base_length, so start with 0
493        let mut op = TextOperation::new();
494        op.retain(5); // Keep "hello" (base_length becomes 5)
495        op.insert(", world".to_string());
496        op.retain(1); // Keep "!" (base_length becomes 6)
497
498        let result = op.apply("hello!")?;
499        assert_eq!(result, "hello, world!");
500        Ok(())
501    }
502
503    #[test]
504    #[ignore = "OT compose algorithm needs review - length tracking issue"]
505    fn test_text_operation_compose() -> SyncResult<()> {
506        let mut op1 = TextOperation::new();
507        op1.insert("hello".to_string());
508
509        let mut op2 = TextOperation::with_base_length(5);
510        op2.retain(5);
511        op2.insert(" world".to_string());
512
513        let composed = op1.compose(&op2)?;
514        let result = composed.apply("")?;
515        assert_eq!(result, "hello world");
516        Ok(())
517    }
518}