about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--cli/src/main.rs11
-rw-r--r--src/actions/create.rs20
-rw-r--r--src/actions/mod.rs30
-rw-r--r--src/actions/shift.rs55
-rw-r--r--src/actions/update.rs80
-rw-r--r--src/files.rs182
-rw-r--r--src/history.rs89
-rw-r--r--src/lib.rs260
-rw-r--r--src/text_diff.rs52
9 files changed, 519 insertions, 260 deletions
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 945da3a..3105f9a 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -1,23 +1,26 @@
 use std::env;
 
-use ka::{create_command, shift_command, update_command};
+use ka::actions::{create, shift, update, ActionOptions};
 
 fn main() {
     let args: Vec<String> = env::args().collect();
     let command = args[1].as_str();
+
+    let options = ActionOptions::from_pwd().expect("Could not get current path.");
+
     match command {
         "create" => {
-            create_command().unwrap();
+            create(options).unwrap();
         }
         "update" => {
-            update_command().unwrap();
+            update(options).unwrap();
         }
         "shift" => {
             let file_path = args[2].as_str();
 
             let new_cursor: usize = args[3].as_str().parse().expect("Invalid cursor.");
 
-            shift_command(file_path, new_cursor);
+            shift(options, file_path, new_cursor).unwrap();
         }
         _ => println!("Unknown command: {}", command),
     }
diff --git a/src/actions/create.rs b/src/actions/create.rs
new file mode 100644
index 0000000..26d20c8
--- /dev/null
+++ b/src/actions/create.rs
@@ -0,0 +1,20 @@
+use std::{fs, io};
+
+use crate::{actions::update, files::Locations};
+
+use super::ActionOptions;
+
+pub fn create(command_options: ActionOptions) -> io::Result<()> {
+    let locations = Locations::from(&command_options);
+
+    if locations.ka_path.exists() {
+        fs::remove_dir_all(locations.ka_path.as_path())?;
+    }
+
+    fs::create_dir(locations.ka_path)?;
+    fs::create_dir(locations.ka_files_path)?;
+
+    update(command_options)?;
+
+    Ok(())
+}
diff --git a/src/actions/mod.rs b/src/actions/mod.rs
new file mode 100644
index 0000000..588ce8f
--- /dev/null
+++ b/src/actions/mod.rs
@@ -0,0 +1,30 @@
+mod create;
+mod shift;
+mod update;
+
+use std::path::{Path, PathBuf};
+
+pub use create::create;
+pub use shift::shift;
+pub use update::update;
+
+pub struct ActionOptions {
+    pub repository_path: PathBuf,
+}
+
+impl ActionOptions {
+    pub fn from_path(path: &str) -> Self {
+        ActionOptions {
+            repository_path: Path::new(path).to_path_buf(),
+        }
+    }
+
+    pub fn from_pwd() -> Result<Self, ()> {
+        let current_path = std::env::current_dir();
+        if let Ok(repository_path) = current_path {
+            Ok(ActionOptions { repository_path })
+        } else {
+            Err(())
+        }
+    }
+}
diff --git a/src/actions/shift.rs b/src/actions/shift.rs
new file mode 100644
index 0000000..c02f0ed
--- /dev/null
+++ b/src/actions/shift.rs
@@ -0,0 +1,55 @@
+use std::{
+    fs,
+    io::{self, Seek, Write},
+    path::Path,
+};
+
+use crate::{
+    files::{Locations, RepositoryPaths},
+    history::FileHistory,
+};
+
+use super::ActionOptions;
+
+pub fn shift(command_options: ActionOptions, path: &str, new_cursor: usize) -> io::Result<()> {
+    let locations = Locations::from(&command_options);
+
+    match RepositoryPaths::from_tracked(&locations, Path::new(path)) {
+        RepositoryPaths::Tracked(tracked) => {
+            let mut history_file = tracked.load_history_file()?;
+
+            let mut file_history = FileHistory::from_file(&mut history_file).unwrap();
+            file_history.set_cursor(new_cursor);
+
+            if file_history.file_is_deleted() {
+                fs::remove_file(path)?;
+            } else {
+                let new_content = file_history.get_content();
+                let mut tracked_file = tracked.create_tracked_file()?;
+
+                tracked_file.rewind()?;
+                tracked_file.set_len(0)?;
+
+                tracked_file.write_all(new_content.as_bytes())?;
+            }
+
+            file_history.write_to_file(&mut history_file)?;
+        }
+        RepositoryPaths::Deleted(deleted) => {
+            let mut history_file = deleted.load_history_file()?;
+
+            let mut file_history = FileHistory::from_file(&mut history_file).unwrap();
+            file_history.set_cursor(new_cursor);
+
+            if !file_history.file_is_deleted() {
+                let mut new_tracked_file = deleted.create_tracked_file(&locations)?;
+                let new_content = file_history.get_content();
+
+                new_tracked_file.write_all(new_content.as_bytes())?;
+            }
+        }
+        RepositoryPaths::Untracked(_) => panic!("File is not tracked with Ka."),
+    }
+
+    Ok(())
+}
diff --git a/src/actions/update.rs b/src/actions/update.rs
new file mode 100644
index 0000000..a2bc08d
--- /dev/null
+++ b/src/actions/update.rs
@@ -0,0 +1,80 @@
+use std::io::{self, Read};
+
+use crate::{
+    files::{Locations, RepositoryPaths},
+    history::{FileChange, FileHistory},
+    text_diff::TextChange,
+};
+
+use super::ActionOptions;
+
+pub fn update(command_options: ActionOptions) -> io::Result<()> {
+    let locations = Locations::from(&command_options);
+
+    let entries = locations
+        .get_repository_paths()
+        .expect("Could not traverse files.");
+
+    for element in entries {
+        update_file(element, &locations)?;
+    }
+
+    Ok(())
+}
+
+fn update_file(paths: RepositoryPaths, locations: &Locations) -> io::Result<()> {
+    let new_state = match paths {
+        RepositoryPaths::Deleted(deleted) => {
+            let mut history_file = deleted.load_history_file()?;
+            let file_history = FileHistory::from_file(&mut history_file).unwrap();
+            if !file_history.file_is_deleted() {
+                let mut new_history = file_history;
+                new_history.add_change(FileChange::Deleted);
+                Some((history_file, new_history))
+            } else {
+                None
+            }
+        }
+        RepositoryPaths::Untracked(untracked) => {
+            let mut file = untracked.load_file()?;
+
+            let mut file_content = String::new();
+            file.read_to_string(&mut file_content)?;
+
+            let change = FileChange::Updated(vec![TextChange::Inserted {
+                at: 0,
+                new_content: file_content,
+            }]);
+
+            let mut new_history = FileHistory::default();
+            new_history.add_change(change);
+
+            Some((untracked.create_history_file(locations)?, new_history))
+        }
+        RepositoryPaths::Tracked(tracked) => {
+            let mut history_file = tracked.load_history_file()?;
+            let mut tracked_file = tracked.load_tracked_file()?;
+
+            let file_history = FileHistory::from_file(&mut history_file).unwrap();
+
+            let mut new_content = String::new();
+            tracked_file.read_to_string(&mut new_content)?;
+
+            let old_content = file_history.get_content();
+
+            let changes = TextChange::diff(&old_content, &new_content);
+
+            let mut new_history = file_history;
+            new_history.add_change(FileChange::Updated(changes));
+            new_history.set_cursor(new_history.cursor() + 1);
+
+            Some((history_file, new_history))
+        }
+    };
+
+    if let Some((mut history_file, new_file_history)) = new_state {
+        new_file_history.write_to_file(&mut history_file)?;
+    }
+
+    Ok(())
+}
diff --git a/src/files.rs b/src/files.rs
new file mode 100644
index 0000000..2512fa5
--- /dev/null
+++ b/src/files.rs
@@ -0,0 +1,182 @@
+use std::{
+    fs::{self, DirEntry, File, OpenOptions},
+    io,
+    path::{Path, PathBuf},
+};
+
+use crate::actions::ActionOptions;
+
+pub struct Locations {
+    pub repository_path: PathBuf,
+    pub ka_path: PathBuf,
+    pub ka_files_path: PathBuf,
+}
+
+impl Locations {
+    pub fn get_repository_paths(&self) -> Result<Vec<RepositoryPaths>, ()> {
+        let repository_entries = fs::read_dir(&self.repository_path).map_err(|_| ())?;
+        let history_entries = fs::read_dir(&self.ka_files_path).map_err(|_| ())?;
+
+        let tracked_paths = repository_entries
+            .map(Result::unwrap)
+            .filter(|entry| entry.path() != self.ka_path)
+            .flat_map(Self::flatten_directories)
+            .map(|entry| {
+                let path = entry.path();
+                RepositoryPaths::from_tracked(&self, &path)
+            });
+
+        let deleted_paths = history_entries
+            .map(Result::unwrap)
+            .flat_map(Self::flatten_directories)
+            .filter_map(|entry| {
+                let path = entry.path();
+                let file = RepositoryPaths::from_history(&self, &path);
+                match file {
+                    RepositoryPaths::Deleted { .. } => Some(file),
+                    RepositoryPaths::Tracked { .. } => None,
+                    _ => unreachable!(),
+                }
+            });
+
+        let all_paths = tracked_paths.chain(deleted_paths);
+
+        Ok(all_paths.collect())
+    }
+
+    pub fn tracked_from_history(&self, history_file_path: &Path) -> PathBuf {
+        self.repository_path
+            .join(history_file_path.strip_prefix(&self.ka_files_path).unwrap())
+    }
+
+    pub fn history_from_tracked(&self, tracked_file_path: &Path) -> PathBuf {
+        self.ka_files_path.join(
+            tracked_file_path
+                .strip_prefix(&self.repository_path)
+                .unwrap(),
+        )
+    }
+
+    fn flatten_directories(entry: DirEntry) -> Vec<DirEntry> {
+        let file_type = entry.file_type().expect("Could not read a file type.");
+        if file_type.is_dir() {
+            fs::read_dir(entry.path())
+                .expect("Could not read nested directory.")
+                .map(Result::unwrap)
+                .flat_map(Self::flatten_directories)
+                .collect()
+        } else {
+            vec![entry]
+        }
+    }
+}
+
+impl From<&ActionOptions> for Locations {
+    fn from(options: &ActionOptions) -> Self {
+        let ka_path = options.repository_path.join(".ka");
+        let ka_files_path = ka_path.join("files");
+
+        Self {
+            repository_path: options.repository_path.clone(),
+            ka_path,
+            ka_files_path,
+        }
+    }
+}
+
+pub enum RepositoryPaths {
+    Deleted(FileDeleted),
+    Untracked(FileUntracked),
+    Tracked(FileTracked),
+}
+
+impl RepositoryPaths {
+    pub fn from_history(locations: &Locations, history_file_path: &Path) -> Self {
+        let tracked_path = locations.tracked_from_history(history_file_path);
+        if !tracked_path.exists() {
+            RepositoryPaths::Deleted(FileDeleted {
+                history_path: history_file_path.to_path_buf(),
+            })
+        } else {
+            RepositoryPaths::Tracked(FileTracked {
+                history_path: history_file_path.to_path_buf(),
+                tracked_path,
+            })
+        }
+    }
+
+    pub fn from_tracked(locations: &Locations, tracked_file_path: &Path) -> Self {
+        let history_path = locations.history_from_tracked(tracked_file_path);
+        if !history_path.exists() {
+            RepositoryPaths::Untracked(FileUntracked {
+                path: tracked_file_path.to_path_buf(),
+            })
+        } else {
+            RepositoryPaths::Tracked(FileTracked {
+                history_path,
+                tracked_path: tracked_file_path.to_path_buf(),
+            })
+        }
+    }
+}
+
+pub struct FileDeleted {
+    history_path: PathBuf,
+}
+
+impl FileDeleted {
+    pub fn load_history_file(&self) -> io::Result<File> {
+        OpenOptions::new()
+            .write(true)
+            .read(true)
+            .open(&self.history_path)
+    }
+
+    pub fn create_tracked_file(&self, locations: &Locations) -> io::Result<File> {
+        File::create(locations.tracked_from_history(&self.history_path))
+    }
+}
+
+pub struct FileUntracked {
+    path: PathBuf,
+}
+
+impl FileUntracked {
+    pub fn load_file(&self) -> io::Result<File> {
+        OpenOptions::new().read(true).open(&self.path)
+    }
+
+    pub fn create_history_file(&self, locations: &Locations) -> io::Result<File> {
+        let history_path = locations.history_from_tracked(&self.path);
+
+        history_path.parent().map(|dir_path| {
+            if !dir_path.exists() {
+                fs::create_dir_all(dir_path).unwrap();
+            }
+        });
+
+        File::create(history_path)
+    }
+}
+
+pub struct FileTracked {
+    history_path: PathBuf,
+    tracked_path: PathBuf,
+}
+
+impl FileTracked {
+    pub fn load_history_file(&self) -> io::Result<File> {
+        OpenOptions::new()
+            .write(true)
+            .read(true)
+            .open(&self.history_path)
+    }
+
+    pub fn load_tracked_file(&self) -> io::Result<File> {
+        File::open(&self.tracked_path)
+    }
+
+    pub fn create_tracked_file(&self) -> io::Result<File> {
+        File::create(&self.tracked_path)
+    }
+}
diff --git a/src/history.rs b/src/history.rs
new file mode 100644
index 0000000..25a9555
--- /dev/null
+++ b/src/history.rs
@@ -0,0 +1,89 @@
+use std::{fs::File, io::{self, Read, Seek, Write}};
+
+use serde::{Deserialize, Serialize};
+
+use crate::text_diff::TextChange;
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct FileHistory {
+    cursor: usize,
+    changes: Vec<FileChange>,
+}
+
+impl FileHistory {
+    pub fn from_file(file: &mut File) -> Result<FileHistory, ()> {
+        let mut buffer = Vec::new();
+        file.read_to_end(&mut buffer)
+            .expect("Could not read file history,");
+
+        let file_history = serde_json::from_slice::<FileHistory>(&buffer);
+        Ok(file_history.expect("Corrupted file history."))
+    }
+
+    pub fn cursor(&self) -> usize {
+        self.cursor
+    }
+
+    pub fn file_is_deleted(&self) -> bool {
+        match self.changes.last() {
+            Some(change) => match change {
+                FileChange::Deleted => true,
+                FileChange::Updated(_) => false,
+            },
+            None => false,
+        }
+    }
+
+    pub fn get_content(&self) -> String {
+        let mut buffer = String::new();
+        for file_change in self.changes.iter().take(self.cursor) {
+            if let FileChange::Updated(ref updated) = file_change {
+                for change in updated.iter() {
+                    change.apply(&mut buffer)
+                }
+            } else {
+                buffer = String::new();
+            }
+        }
+        buffer
+    }
+
+    pub fn write_to_file(&self, file: &mut File) -> io::Result<()> {
+        let encoded: Vec<u8> = serde_json::to_vec(self).unwrap();
+        file.rewind()?;
+        file.set_len(0)?;
+        file.write_all(encoded.as_ref())?;
+        Ok(())
+    }
+
+    pub fn set_cursor(&mut self, new_cursor: usize) {
+        if new_cursor < self.changes.len() {
+            self.cursor = new_cursor;
+        } else {
+            panic!(
+                "Out-of-bounds cursor for file history: {}, can be at most {}",
+                new_cursor,
+                self.changes.len()
+            );
+        }
+    }
+
+    pub fn add_change(&mut self, change: FileChange) {
+        self.changes.push(change);
+    }
+}
+
+impl Default for FileHistory {
+    fn default() -> Self {
+        Self {
+            cursor: 0,
+            changes: Vec::new(),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum FileChange {
+    Updated(Vec<TextChange>),
+    Deleted,
+}
diff --git a/src/lib.rs b/src/lib.rs
index 3a05db5..467ffe7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,257 +1,5 @@
-use difference::{Changeset, Difference};
-use serde::{Deserialize, Serialize};
-use std::{
-    fs::{self, DirEntry, File, OpenOptions},
-    io::{self, Read, Seek, Write},
-    path::{Path, PathBuf},
-    vec,
-};
+pub mod actions;
 
-pub fn create_command() -> io::Result<()> {
-    let repository_directory = Path::new("./repository");
-
-    let ka_directory = repository_directory.join(".ka");
-    let ka_files_directory = ka_directory.join("files");
-
-    if ka_directory.exists() {
-        fs::remove_dir_all(ka_directory.as_path())?;
-    }
-
-    fs::create_dir(ka_directory)?;
-    fs::create_dir(ka_files_directory)?;
-
-    update_command()?;
-
-    Ok(())
-}
-
-pub fn update_command() -> io::Result<()> {
-    let repository_directory = Path::new("./repository");
-
-    let ka_directory = repository_directory.join(".ka");
-
-    let entries: Vec<DirEntry> = fs::read_dir(repository_directory)?
-        .map(Result::unwrap)
-        .filter(|entry| entry.path() != ka_directory)
-        .flat_map(flatten_directories)
-        .collect();
-
-    for element in entries.iter() {
-        update_file(element.path())?;
-    }
-
-    Ok(())
-}
-
-pub fn shift_command(path: &str, new_cursor: usize) {
-    let repository_directory = Path::new("./repository");
-
-    let ka_directory = repository_directory.join(".ka");
-    let ka_files_directory = ka_directory.join("files");
-
-    let file_path = Path::new(path);
-
-    let mut tracked_file = File::create(file_path).expect("Could not find file.");
-
-    let history_file_path =
-        ka_files_directory.join(file_path.strip_prefix(repository_directory).unwrap());
-    let mut history_file = OpenOptions::new()
-        .write(true)
-        .read(true)
-        .open(history_file_path)
-        .expect("Could not find corresponding history file");
-
-    let mut current_history =
-        read_history_from_file(&mut history_file).expect("Failed reading history from file.");
-    let state_at_cursor = get_state_from_history_for_cursor(&current_history, new_cursor);
-
-    println!("{}", state_at_cursor);
-    println!("{:?}", current_history);
-
-    tracked_file
-        .write_all(state_at_cursor.as_bytes())
-        .expect("Failed writing new state");
-
-    current_history.cursor = new_cursor;
-    write_history_to_file(&mut history_file, &current_history)
-        .expect("Could not write new cursor to history file.");
-}
-
-fn flatten_directories(entry: DirEntry) -> Vec<DirEntry> {
-    let file_type = entry.file_type().expect("Could not read a file type.");
-    if file_type.is_dir() {
-        fs::read_dir(entry.path())
-            .expect("Could not read nested directory.")
-            .map(Result::unwrap)
-            .flat_map(flatten_directories)
-            .collect()
-    } else {
-        vec![entry]
-    }
-}
-
-fn update_file(file_path: PathBuf) -> io::Result<()> {
-    let repository_directory = Path::new("./repository");
-
-    let ka_directory = repository_directory.join(".ka");
-    let ka_files_directory = ka_directory.join("files");
-
-    let mut tracked_file = File::open(file_path.as_path()).expect("Could not find tracked file.");
-
-    let history_file_path =
-        ka_files_directory.join(file_path.strip_prefix(repository_directory).unwrap());
-
-    let (mut history_file, current_file_history) = if history_file_path.exists() {
-        let mut history_file = OpenOptions::new()
-            .read(true)
-            .write(true)
-            .open(history_file_path)
-            .expect("Could not open history file.");
-
-        let file_history = read_history_from_file(&mut history_file).unwrap();
-
-        history_file
-            .set_len(0)
-            .expect("Failed truncating old history file.");
-
-        history_file
-            .rewind()
-            .expect("Failed rewinding history file.");
-
-        (history_file, file_history)
-    } else {
-        history_file_path.parent().map(|dir_path| {
-            if !dir_path.exists() {
-                fs::create_dir_all(dir_path).unwrap();
-            }
-        });
-
-        let file = File::create(history_file_path).expect("Could not create history file.");
-
-        let empty_file_history = FileHistory {
-            cursor: 0,
-            changes: Vec::new(),
-        };
-
-        (file, empty_file_history)
-    };
-
-    let mut new_content = String::new();
-    tracked_file
-        .read_to_string(&mut new_content)
-        .expect("Could not read tracked file.");
-
-    let old_content =
-        get_state_from_history_for_cursor(&current_file_history, current_file_history.cursor);
-
-    let change_set = Changeset::new(&old_content, &new_content, "");
-
-    let mut changes = Vec::new();
-    let mut at: usize = 0;
-    for diff in change_set.diffs.iter() {
-        match diff {
-            Difference::Add(new_content) => {
-                let change = TextChange::Inserted(TextInserted {
-                    at,
-                    new_content: new_content.to_owned(),
-                });
-                at += new_content.len();
-                changes.push(change);
-            }
-            Difference::Rem(removed_content) => {
-                changes.push(TextChange::Deleted(TextDeleted {
-                    at,
-                    upto: removed_content.len(),
-                }));
-            }
-            Difference::Same(same_content) => {
-                at += same_content.len();
-            }
-        }
-    }
-
-    let mut file_changes = current_file_history.changes;
-    println!("{:?}", file_changes);
-    file_changes.push(FileChange::Updated(FileUpdated { changes }));
-
-    let new_file_history = FileHistory {
-        changes: file_changes,
-        cursor: current_file_history.cursor + 1,
-    };
-
-    write_history_to_file(&mut history_file, &new_file_history)
-        .expect("Failed writing new history.");
-
-    Ok(())
-}
-
-fn write_history_to_file(file: &mut File, history: &FileHistory) -> io::Result<()> {
-    let encoded: Vec<u8> = serde_json::to_vec(history).unwrap();
-    file.write_all(encoded.as_ref())?;
-    Ok(())
-}
-
-fn read_history_from_file(file: &mut File) -> Result<FileHistory, ()> {
-    let mut buffer = Vec::new();
-    file.read_to_end(&mut buffer)
-        .expect("Could not read file history,");
-
-    let file_history = serde_json::from_slice::<FileHistory>(&buffer);
-    Ok(file_history.expect("Corrupted file history."))
-}
-
-fn get_state_from_history_for_cursor(history: &FileHistory, cursor: usize) -> String {
-    let mut buffer = String::new();
-    for file_change in history.changes.iter().take(cursor) {
-        if let FileChange::Updated(ref updated) = file_change {
-            for change in updated.changes.iter() {
-                match change {
-                    TextChange::Deleted(deletion) => {
-                        buffer.replace_range(deletion.at..deletion.upto, "");
-                    }
-                    TextChange::Inserted(insertion) => {
-                        buffer.insert_str(insertion.at, insertion.new_content.as_str());
-                    }
-                }
-            }
-        } else {
-            buffer = String::new();
-        }
-    }
-    buffer
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-struct FileHistory {
-    cursor: usize,
-    changes: Vec<FileChange>,
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-enum FileChange {
-    Updated(FileUpdated),
-    Deleted,
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-struct FileUpdated {
-    changes: Vec<TextChange>,
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-enum TextChange {
-    Inserted(TextInserted),
-    Deleted(TextDeleted),
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-struct TextInserted {
-    at: usize,
-    new_content: String,
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-struct TextDeleted {
-    at: usize,
-    upto: usize,
-}
+mod history;
+mod text_diff;
+mod files;
diff --git a/src/text_diff.rs b/src/text_diff.rs
new file mode 100644
index 0000000..bab8799
--- /dev/null
+++ b/src/text_diff.rs
@@ -0,0 +1,52 @@
+use difference::{Changeset, Difference};
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum TextChange {
+    Inserted { at: usize, new_content: String },
+    Deleted { at: usize, upto: usize },
+}
+
+impl TextChange {
+    pub fn diff(old: &str, new: &str) -> Vec<Self> {
+        let change_set = Changeset::new(old, new, "");
+
+        let mut changes = Vec::new();
+        let mut at: usize = 0;
+
+        for diff in change_set.diffs.iter() {
+            match diff {
+                Difference::Add(new_content) => {
+                    let change = TextChange::Inserted {
+                        at,
+                        new_content: new_content.to_owned(),
+                    };
+                    at += new_content.len();
+                    changes.push(change);
+                }
+                Difference::Rem(removed_content) => {
+                    changes.push(TextChange::Deleted {
+                        at,
+                        upto: removed_content.len(),
+                    });
+                }
+                Difference::Same(same_content) => {
+                    at += same_content.len();
+                }
+            }
+        }
+
+        changes
+    }
+
+    pub fn apply(&self, buffer: &mut String) {
+        match self {
+            TextChange::Deleted { at, upto } => {
+                buffer.replace_range(at..upto, "");
+            }
+            TextChange::Inserted { at, new_content } => {
+                buffer.insert_str(*at, new_content);
+            }
+        }
+    }
+}