oxigdal_sync/ot/
text_operation.rs1use crate::ot::Transform;
4use crate::{SyncError, SyncResult};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum Operation {
10 Retain(usize),
12 Insert(String),
14 Delete(usize),
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct TextOperation {
21 ops: Vec<Operation>,
23 base_length: usize,
25 target_length: usize,
27}
28
29impl TextOperation {
30 pub fn new() -> Self {
32 Self {
33 ops: Vec::new(),
34 base_length: 0,
35 target_length: 0,
36 }
37 }
38
39 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 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 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 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 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 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 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 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 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 pub fn base_length(&self) -> usize {
180 self.base_length
181 }
182
183 pub fn target_length(&self) -> usize {
185 self.target_length
186 }
187
188 pub fn operations(&self) -> &[Operation] {
190 &self.ops
191 }
192
193 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 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 let mut op = TextOperation::new();
494 op.retain(5); op.insert(", world".to_string());
496 op.retain(1); 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}