aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2021-09-20 22:25:55 +0200
committerMel <einebeere@gmail.com>2021-09-20 22:25:55 +0200
commitc25d22681f9bc6ddba512895182af0ce0252731b (patch)
treeeadcdec7d0b4d26d59e77169bd89b8db13e22fb6
parent8bcbf51cf4f7924600eae39db67ff653b48ad95d (diff)
downloadka-c25d22681f9bc6ddba512895182af0ce0252731b.tar.zst
ka-c25d22681f9bc6ddba512895182af0ce0252731b.zip
Split out common logic
-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);
+ }
+ }
+ }
+}