about summary refs log tree commit diff
path: root/src/diff.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/diff.rs')
-rw-r--r--src/diff.rs127
1 files changed, 127 insertions, 0 deletions
diff --git a/src/diff.rs b/src/diff.rs
new file mode 100644
index 0000000..4e3531c
--- /dev/null
+++ b/src/diff.rs
@@ -0,0 +1,127 @@
+use std::time::{Duration, Instant};
+
+use serde::{Deserialize, Serialize};
+use similar::{Algorithm, DiffOp};
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub enum ContentChange {
+    Inserted { at: usize, new_content: Vec<u8> },
+    Deleted { at: usize, upto: usize },
+}
+
+impl ContentChange {
+    pub fn diff(old: &[u8], new: &[u8]) -> Vec<Self> {
+        let deadline = Instant::now() + Duration::from_millis(100);
+        let change_set =
+            similar::capture_diff_slices_deadline(Algorithm::Myers, old, new, Some(deadline));
+
+        let mut at = 0;
+        let mut changes = Vec::new();
+
+        for diff in change_set {
+            match diff {
+                DiffOp::Delete { old_len, .. } => {
+                    changes.push(ContentChange::Deleted {
+                        at,
+                        upto: at + old_len,
+                    });
+                }
+                DiffOp::Insert {
+                    new_index, new_len, ..
+                } => {
+                    let new_content = &new[new_index..new_index + new_len];
+                    let change = ContentChange::Inserted {
+                        at,
+                        new_content: new_content.to_vec(),
+                    };
+                    at += new_len;
+                    changes.push(change);
+                }
+                DiffOp::Replace {
+                    old_len,
+                    new_index,
+                    new_len,
+                    ..
+                } => {
+                    let new_content = &new[new_index..new_index + new_len];
+
+                    let removed_change = ContentChange::Deleted {
+                        at,
+                        upto: at + old_len,
+                    };
+                    let added_change = ContentChange::Inserted {
+                        at,
+                        new_content: new_content.to_vec(),
+                    };
+
+                    changes.push(removed_change);
+                    changes.push(added_change);
+
+                    at += new_len;
+                }
+                DiffOp::Equal { len, .. } => {
+                    at += len;
+                }
+            }
+        }
+
+        changes
+    }
+
+    pub fn apply(&self, buffer: &mut Vec<u8>) {
+        match self {
+            ContentChange::Deleted { at, upto } => {
+                buffer.drain(at..upto);
+            }
+            ContentChange::Inserted { at, new_content } => {
+                buffer.splice(at..at, new_content.clone());
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{ContentChange::*, *};
+
+    #[test]
+    fn test_diff() {
+        let old = "This is an old string...";
+        let new = "This is a new string...!";
+
+        let changes = ContentChange::diff(old.as_bytes(), new.as_bytes());
+        assert_eq!(
+            changes.as_slice(),
+            [
+                Inserted {
+                    at: 9,
+                    new_content: " ".into()
+                },
+                Deleted { at: 11, upto: 15 },
+                Inserted {
+                    at: 11,
+                    new_content: "ew".into()
+                },
+                Inserted {
+                    at: 23,
+                    new_content: "!".into()
+                }
+            ],
+        );
+    }
+
+    #[test]
+    fn test_apply() {
+        let old = "This is an old string...";
+        let new = "This is a new text...!";
+
+        let changes = ContentChange::diff(old.as_bytes(), new.as_bytes());
+
+        let mut buffer = old.as_bytes().to_vec();
+        for change in changes {
+            change.apply(&mut buffer);
+        }
+
+        assert_eq!(&buffer, new.as_bytes());
+    }
+}