From cf81ab797fa33f7ed7629325f753cde463ba250e Mon Sep 17 00:00:00 2001 From: Shafkath Shuhan Date: Mon, 20 Feb 2023 10:45:32 -0500 Subject: [PATCH] reload histories if persistent_undo is dynamically enabled --- helix-core/src/history.rs | 25 +++++++++++++++++++++++-- helix-term/src/application.rs | 26 ++++++++++++++++++++++++++ helix-view/src/document.rs | 7 +++---- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index c8418a7f51da..93e96ff4ddd9 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -96,6 +96,7 @@ impl Default for History { impl Revision { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + // `timestamp` is ignored since `Instant`s can't be serialized. write_usize(writer, self.parent)?; self.transaction.serialize(writer)?; self.inversion.serialize(writer)?; @@ -117,8 +118,7 @@ impl Revision { } } -// Temporarily 3 for review. -const HEADER_TAG: &str = "Helix Undofile 4\n"; +const HEADER_TAG: &str = "Helix Undofile 1\n"; fn get_hash(reader: &mut R) -> std::io::Result<[u8; 20]> { const BUF_SIZE: usize = 8192; @@ -144,6 +144,7 @@ impl History { revision: usize, last_saved_revision: usize, ) -> std::io::Result<()> { + // Header let mtime = std::fs::metadata(path)? .modified()? .duration_since(std::time::UNIX_EPOCH) @@ -164,9 +165,14 @@ impl History { Ok(()) } + /// Returns the deserialized [`History`] and the last_saved_revision. pub fn deserialize(reader: &mut R, path: &Path) -> std::io::Result<(usize, Self)> { let (current, last_saved_revision) = Self::read_header(reader, path)?; + + // Since `timestamp` can't be serialized, a new timestamp is created. let timestamp = Instant::now(); + + // Read the revisions and construct the tree. let len = read_usize(reader)?; let mut revisions: Vec = Vec::with_capacity(len); for _ in 0..len { @@ -184,6 +190,16 @@ impl History { Ok((last_saved_revision, history)) } + /// If two histories originate from: `A -> B (B is head)` but have deviated since then such that + /// the first history is: `A -> B -> C -> D (D is head)` and the second one is: + /// `A -> B -> E -> F (F is head)`. + /// Then they are merged into + /// ``` + /// A -> B -> C -> D + /// \ + /// \ -> E -> F + /// ``` + /// and retain their revision heads. pub fn merge(&mut self, mut other: History, offset: usize) -> std::io::Result<()> { let revisions = self.revisions.split_off(offset); let len = other.revisions.len(); @@ -823,6 +839,7 @@ mod test { quickcheck!( fn serde_history(original: String, changes_a: Vec, changes_b: Vec) -> bool { + // Constructs a set of transactions and applies them to the history. fn create_changes(history: &mut History, doc: &mut Rope, changes: Vec) { for c in changes.into_iter().map(Rope::from) { let transaction = crate::diff::compare_ropes(doc, &c); @@ -843,6 +860,8 @@ mod test { let file = tempfile::NamedTempFile::new().unwrap(); history.serialize(&mut cursor, file.path(), 0, 0).unwrap(); cursor.set_position(0); + + // Check if the original and deserialized history match. let (_, res) = History::deserialize(&mut cursor, file.path()).unwrap(); assert_eq!(history, res); @@ -854,6 +873,8 @@ mod test { .serialize(&mut cursor, file.path(), 0, last_saved_revision) .unwrap(); cursor.set_position(0); + + // Check if they are the same after appending new changes. let (_, res) = History::deserialize(&mut cursor, file.path()).unwrap(); history == res } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1c88cba3cd95..05fc91abc3e0 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -385,6 +385,21 @@ impl Application { // the Application can apply it. ConfigEvent::Update(editor_config) => { let mut app_config = (*self.config.load().clone()).clone(); + if self.config.load().editor.persistent_undo == false + && editor_config.persistent_undo == true + { + for doc in self.editor.documents_mut() { + // HAXX: Do this so all revisions in this doc are treated as new. + doc.set_last_saved_revision(0); + if let Err(e) = doc.load_history() { + log::error!( + "failed to reload history for {}: {e}", + doc.path().unwrap().to_string_lossy() + ); + return; + } + } + } app_config.editor = *editor_config; self.config.store(Arc::new(app_config)); } @@ -439,6 +454,17 @@ impl Application { let mut refresh_config = || -> Result<(), Error> { let default_config = Config::load_default() .map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?; + + // Merge histories of existing docs if persistent undo was enabled. + if self.config.load().editor.persistent_undo == false + && default_config.editor.persistent_undo == true + { + for doc in self.editor.documents_mut() { + // HAXX: Do this so all revisions in this doc are treated as new. + doc.set_last_saved_revision(0); + doc.load_history()?; + } + } self.refresh_language_config()?; self.refresh_theme(&default_config)?; // Store new config diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 2c6f64c6c41a..db67103c7e77 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1387,10 +1387,8 @@ impl Display for FormatterError { #[cfg(test)] mod test { - use arc_swap::{access::Map, ArcSwap}; - use quickcheck::Gen; - use super::*; + use arc_swap::ArcSwap; #[test] fn changeset_to_changes_ignore_line_endings() { @@ -1561,6 +1559,7 @@ mod test { #[tokio::test(flavor = "multi_thread")] async fn reload_history() { let test_fn: fn(Vec) -> bool = |changes| -> bool { + // Divide the vec into 3 sets of changes. let len = changes.len() / 3; let mut original = Rope::new(); let mut iter = changes.into_iter(); @@ -1623,7 +1622,6 @@ mod test { for c in changes_b { doc_1.apply(&c, view_id); } - for c in changes_c { doc_2.apply(&c, view_id); } @@ -1632,6 +1630,7 @@ mod test { doc_1.load_history().unwrap(); doc_3.load_history().unwrap(); + // doc_3 had no diverging edits, so they should be the same. assert_eq!(doc_2.history.get_mut(), doc_3.history.get_mut()); helix_lsp::block_on(doc_1.save::(None, true).unwrap()).unwrap();