Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persistent Undo #5608

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
60c6baa
implement workspace locking per instance
kirawi Jan 18, 2023
ce14c55
implement serializing/deserializing undo history
kirawi Jan 21, 2023
6d73215
implement workspace commands
kirawi Jan 29, 2023
95a6b7b
inline workspace commands
kirawi Jan 30, 2023
eb7c7eb
run `cargo xtask docgen`
kirawi Jan 30, 2023
859f73e
close buffer after checking if the saved state
kirawi Jan 30, 2023
1b0f1ad
rebase on top of master
kirawi Feb 1, 2023
103eb68
arc
kirawi Feb 7, 2023
8f34005
change test
kirawi Feb 11, 2023
7a0179c
remove workspace mod
kirawi Feb 11, 2023
2854dae
remove typed commands
kirawi Feb 11, 2023
9a3aa57
remove command refs
kirawi Feb 11, 2023
860c3e9
clippy
kirawi Feb 11, 2023
e61256f
cleanup
kirawi Feb 11, 2023
8e1f411
remove flock
kirawi Feb 12, 2023
1243eeb
improve unit test
kirawi Feb 12, 2023
5259da2
create undo directory if it doesn't exist
kirawi Feb 13, 2023
9dd7597
don't reload history
kirawi Feb 13, 2023
9f0f7a2
add to book
kirawi Feb 13, 2023
c4aa3b8
improve quickcheck test
kirawi Feb 13, 2023
2c343a7
figure out last child in memory
kirawi Feb 16, 2023
d8be1aa
append history if possible
kirawi Feb 18, 2023
b49fa5f
update test
kirawi Feb 18, 2023
b83fb40
update integration
kirawi Feb 18, 2023
251bc4c
reload history with reload command
kirawi Feb 18, 2023
7140e6a
remove libc dependency
kirawi Feb 19, 2023
ca3948d
delete file
kirawi Feb 19, 2023
bbf41d2
write reload unit test
kirawi Feb 19, 2023
8a60b79
add merge test
kirawi Feb 20, 2023
cf81ab7
reload histories if persistent_undo is dynamically enabled
kirawi Feb 20, 2023
cea1fe5
address clippy
kirawi Feb 20, 2023
5dd2ad0
report error if reload fails
kirawi Feb 20, 2023
e5ea16b
wip
kirawi Feb 26, 2023
c2e54de
wip
kirawi Feb 26, 2023
0d36a78
wip
kirawi Mar 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions helix-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ bitflags = "1.3"
ahash = "0.8.3"
hashbrown = { version = "0.13.2", features = ["raw"] }

sha1_smol = "1.0"

log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand All @@ -47,5 +49,9 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
etcetera = "0.4"
textwrap = "0.16.0"

[target.'cfg(unix)'.dependencies]
kirawi marked this conversation as resolved.
Show resolved Hide resolved
libc = "0.2"

[dev-dependencies]
quickcheck = { version = "1", default-features = false }
tempfile = "3.3.0"
163 changes: 149 additions & 14 deletions helix-core/src/history.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use crate::parse::*;
use crate::{Assoc, ChangeSet, Range, Rope, Selection, Transaction};
use once_cell::sync::Lazy;
use regex::Regex;
use std::io::{Read, Write};
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -47,7 +51,7 @@ pub struct State {
/// delete, we also store an inversion of the transaction.
///
/// Using time to navigate the history: <https://github.com/helix-editor/helix/pull/194>
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct History {
revisions: Vec<Revision>,
current: usize,
Expand All @@ -58,10 +62,10 @@ pub struct History {
struct Revision {
parent: usize,
last_child: Option<NonZeroUsize>,
transaction: Transaction,
transaction: Arc<Transaction>,
// We need an inversion for undos because delete transactions don't store
// the deleted text.
inversion: Transaction,
inversion: Arc<Transaction>,
timestamp: Instant,
}

Expand All @@ -72,16 +76,116 @@ impl Default for History {
revisions: vec![Revision {
parent: 0,
last_child: None,
transaction: Transaction::from(ChangeSet::new(&Rope::new())),
inversion: Transaction::from(ChangeSet::new(&Rope::new())),
transaction: Arc::new(Transaction::from(ChangeSet::new(&Rope::new()))),
inversion: Arc::new(Transaction::from(ChangeSet::new(&Rope::new()))),
timestamp: Instant::now(),
}],
current: 0,
}
}
}

impl Revision {
fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
write_usize(writer, self.parent)?;
write_usize(writer, self.last_child.map(|n| n.get()).unwrap_or(0))?;
self.transaction.serialize(writer)?;
self.inversion.serialize(writer)?;

Ok(())
}

fn deserialize<R: Read>(reader: &mut R, timestamp: Instant) -> std::io::Result<Self> {
let parent = read_usize(reader)?;
let last_child = match read_usize(reader)? {
0 => None,
n => Some(unsafe { NonZeroUsize::new_unchecked(n) }),
};
let transaction = Arc::new(Transaction::deserialize(reader)?);
let inversion = Arc::new(Transaction::deserialize(reader)?);
Ok(Revision {
parent,
last_child,
transaction,
inversion,
timestamp,
})
}
}

const HEADER_TAG: &str = "Helix Undofile 1\n";

fn get_hash<R: Read>(reader: &mut R) -> std::io::Result<[u8; 20]> {
const BUF_SIZE: usize = 8192;

let mut buf = [0u8; BUF_SIZE];
let mut hash = sha1_smol::Sha1::new();
loop {
let total_read = reader.read(&mut buf)?;
if total_read == 0 {
break;
}

hash.update(&buf[0..total_read]);
}
Ok(hash.digest().bytes())
}

impl History {
pub fn serialize<W: Write>(
&self,
writer: &mut W,
path: &Path,
last_saved_revision: usize,
) -> std::io::Result<()> {
write_string(writer, HEADER_TAG)?;
write_usize(writer, self.current)?;
write_usize(writer, last_saved_revision)?;

let last_mtime = std::fs::metadata(path)?
.modified()?
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
write_u64(writer, last_mtime)?;
writer.write_all(&get_hash(&mut std::fs::File::open(path)?)?)?;
write_vec(writer, &self.revisions, |writer, rev| rev.serialize(writer))?;
Ok(())
}

pub fn deserialize<R: Read>(reader: &mut R, path: &Path) -> std::io::Result<(usize, Self)> {
let header = read_string(reader)?;
if HEADER_TAG != header {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"missing undofile header",
))
} else {
let timestamp = Instant::now();
let current = read_usize(reader)?;
let last_saved_revision = read_usize(reader)?;
let mtime = read_u64(reader)?;
let last_mtime = std::fs::metadata(path)?
.modified()?
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let mut hash = [0u8; 20];
reader.read_exact(&mut hash)?;

if mtime != last_mtime && hash != get_hash(&mut std::fs::File::open(path)?)? {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"outdated undo file",
));
}

let revisions = read_vec(reader, |reader| Revision::deserialize(reader, timestamp))?;
let history = History { current, revisions };
Ok((last_saved_revision, history))
}
}

pub fn commit_revision(&mut self, transaction: &Transaction, original: &State) {
self.commit_revision_at_timestamp(transaction, original, Instant::now());
}
Expand All @@ -92,17 +196,19 @@ impl History {
original: &State,
timestamp: Instant,
) {
let inversion = transaction
.invert(&original.doc)
// Store the current cursor position
.with_selection(original.selection.clone());
let inversion = Arc::new(
transaction
.invert(&original.doc)
// Store the current cursor position
.with_selection(original.selection.clone()),
);

let new_current = self.revisions.len();
self.revisions[self.current].last_child = NonZeroUsize::new(new_current);
self.revisions.push(Revision {
parent: self.current,
last_child: None,
transaction: transaction.clone(),
transaction: Arc::new(transaction.clone()),
inversion,
timestamp,
});
Expand All @@ -128,8 +234,10 @@ impl History {
let up_txns = up
.iter()
.rev()
.map(|&n| self.revisions[n].inversion.clone());
let down_txns = down.iter().map(|&n| self.revisions[n].transaction.clone());
.map(|&n| self.revisions[n].inversion.as_ref().clone());
let down_txns = down
.iter()
.map(|&n| self.revisions[n].transaction.as_ref().clone());

down_txns.chain(up_txns).reduce(|acc, tx| tx.compose(acc))
}
Expand Down Expand Up @@ -215,11 +323,13 @@ impl History {
let up = self.path_up(self.current, lca);
let down = self.path_up(to, lca);
self.current = to;
let up_txns = up.iter().map(|&n| self.revisions[n].inversion.clone());
let up_txns = up
.iter()
.map(|&n| self.revisions[n].inversion.as_ref().clone());
let down_txns = down
.iter()
.rev()
.map(|&n| self.revisions[n].transaction.clone());
.map(|&n| self.revisions[n].transaction.as_ref().clone());
up_txns.chain(down_txns).collect()
}

Expand Down Expand Up @@ -386,6 +496,8 @@ impl std::str::FromStr for UndoKind {

#[cfg(test)]
mod test {
use quickcheck::quickcheck;

use super::*;
use crate::Selection;

Expand Down Expand Up @@ -630,4 +742,27 @@ mod test {
Err("duration too large".to_string())
);
}

quickcheck!(
fn serde_history(original: String, changes: Vec<String>) -> bool {
let mut history = History::default();
let mut original = Rope::from(original);

for c in changes.into_iter().map(Rope::from) {
let transaction = crate::diff::compare_ropes(&original, &c);
let state = State {
doc: original,
selection: Selection::point(0),
};
history.commit_revision(&transaction, &state);
original = c;
}

let mut buf = Vec::new();
let file = tempfile::NamedTempFile::new().unwrap();
history.serialize(&mut buf, file.path(), 0).unwrap();
History::deserialize(&mut buf.as_slice(), file.path()).unwrap();
true
}
);
}
1 change: 1 addition & 0 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod macros;
pub mod match_brackets;
pub mod movement;
pub mod object;
pub mod parse;
pub mod path;
mod position;
pub mod register;
Expand Down
Loading