From c5b2973739901f8cd4bc26f3cfc8232249eb7968 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Fri, 2 Jul 2021 10:54:50 -0400 Subject: [PATCH] `:reload` (#374) * reloading functionality * fn with_newline_eof() * fmt * wip * wip * wip * wip * moved to core, added simd feature for encoding_rs * wip * rm * .gitignore * wip * local wip * wip * wip * no features * wip * nit * remove simd * doc * clippy * clippy * address comments * add indentation & line ending change --- Cargo.lock | 35 +++++++++++++++++++ helix-core/Cargo.toml | 5 +++ helix-core/src/diff.rs | 70 ++++++++++++++++++++++++++++++++++++++ helix-core/src/lib.rs | 1 + helix-term/src/commands.rs | 32 +++++++++++++++++ helix-view/src/document.rs | 61 ++++++++++++++++++++++++++++----- 6 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 helix-core/src/diff.rs diff --git a/Cargo.lock b/Cargo.lock index 473ae8c8..a377e2f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,10 +317,12 @@ dependencies = [ "etcetera", "helix-syntax", "once_cell", + "quickcheck", "regex", "ropey", "rust-embed", "serde", + "similar", "smallvec", "tendril", "toml", @@ -692,6 +694,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "rand", +] + [[package]] name = "quote" version = "1.0.9" @@ -701,6 +712,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.9" @@ -872,6 +901,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + [[package]] name = "slab" version = "0.4.3" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 726e90cc..80d559a9 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -31,5 +31,10 @@ regex = "1" serde = { version = "1.0", features = ["derive"] } toml = "0.5" +similar = "1.3" + etcetera = "0.3" rust-embed = { version = "5.9.0", optional = true } + +[dev-dependencies] +quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/diff.rs b/helix-core/src/diff.rs new file mode 100644 index 00000000..9c1fc999 --- /dev/null +++ b/helix-core/src/diff.rs @@ -0,0 +1,70 @@ +use ropey::Rope; + +use crate::{Change, Transaction}; + +/// Compares `old` and `new` to generate a [`Transaction`] describing +/// the steps required to get from `old` to `new`. +pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction { + // `similar` only works on contiguous data, so a `Rope` has + // to be temporarily converted into a `String`. + let old_converted = old.to_string(); + let new_converted = new.to_string(); + + // A timeout is set so after 1 seconds, the algorithm will start + // approximating. This is especially important for big `Rope`s or + // `Rope`s that are extremely dissimilar to each other. + // + // Note: Ignore the clippy warning, as the trait bounds of + // `Transaction::change()` require an iterator implementing + // `ExactIterator`. + let mut config = similar::TextDiff::configure(); + config.timeout(std::time::Duration::from_secs(1)); + + let diff = config.diff_chars(&old_converted, &new_converted); + + // The current position of the change needs to be tracked to + // construct the `Change`s. + let mut pos = 0; + let changes: Vec = diff + .ops() + .iter() + .map(|op| op.as_tag_tuple()) + .filter_map(|(tag, old_range, new_range)| { + // `old_pos..pos` is equivalent to `start..end` for where + // the change should be applied. + let old_pos = pos; + pos += old_range.end - old_range.start; + + match tag { + // Semantically, inserts and replacements are the same thing. + similar::DiffTag::Insert | similar::DiffTag::Replace => { + // This is the text from the `new` rope that should be + // inserted into `old`. + let text: &str = { + let start = new.char_to_byte(new_range.start); + let end = new.char_to_byte(new_range.end); + &new_converted[start..end] + }; + Some((old_pos, pos, Some(text.into()))) + } + similar::DiffTag::Delete => Some((old_pos, pos, None)), + similar::DiffTag::Equal => None, + } + }) + .collect(); + Transaction::change(old, changes.into_iter()) +} + +#[cfg(test)] +mod tests { + use super::*; + + quickcheck::quickcheck! { + fn test_compare_ropes(a: String, b: String) -> bool { + let mut old = Rope::from(a); + let new = Rope::from(b); + compare_ropes(&old, &new).apply(&mut old); + old.to_string() == new.to_string() + } + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index dfbbd748..c2bb8c55 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -2,6 +2,7 @@ pub mod auto_pairs; pub mod chars; pub mod comment; pub mod diagnostic; +pub mod diff; pub mod graphemes; pub mod history; pub mod indent; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a3799e7e..860d8e22 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1521,6 +1521,24 @@ mod cmd { } } + /// Sets the [`Document`]'s encoding.. + fn set_encoding(cx: &mut compositor::Context, args: &[&str], _: PromptEvent) { + let (_, doc) = current!(cx.editor); + if let Some(label) = args.first() { + doc.set_encoding(label) + .unwrap_or_else(|e| cx.editor.set_error(e.to_string())); + } else { + let encoding = doc.encoding().name().to_string(); + cx.editor.set_status(encoding) + } + } + + /// Reload the [`Document`] from its source file. + fn reload(cx: &mut compositor::Context, _args: &[&str], _: PromptEvent) { + let (view, doc) = current!(cx.editor); + doc.reload(view.id).unwrap(); + } + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -1704,6 +1722,20 @@ mod cmd { fun: show_current_directory, completer: None, }, + TypableCommand { + name: "encoding", + alias: None, + doc: "Set encoding based on `https://encoding.spec.whatwg.org`", + fun: set_encoding, + completer: None, + }, + TypableCommand { + name: "reload", + alias: None, + doc: "Discard changes and reload from the source file.", + fun: reload, + completer: None, + } ]; pub static COMMANDS: Lazy> = Lazy::new(|| { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0f1f3a8f..f85ded11 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -307,6 +307,19 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>( Ok(()) } +/// Inserts the final line ending into `rope` if it's missing. [Why?](https://stackoverflow.com/questions/729692/why-should-text-files-end-with-a-newline) +pub fn with_line_ending(rope: &mut Rope) -> LineEnding { + // search for line endings + let line_ending = auto_detect_line_ending(rope).unwrap_or(DEFAULT_LINE_ENDING); + + // add missing newline at the end of file + if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) { + rope.insert(rope.len_chars(), line_ending.as_str()); + } + + line_ending +} + /// Like std::mem::replace() except it allows the replacement value to be mapped from the /// original value. fn take_with(mut_ref: &mut T, closure: F) @@ -449,14 +462,7 @@ impl Document { let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?; let (mut rope, encoding) = from_reader(&mut file, encoding)?; - - // search for line endings - let line_ending = auto_detect_line_ending(&rope).unwrap_or(DEFAULT_LINE_ENDING); - - // add missing newline at the end of file - if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) { - rope.insert(rope.len_chars(), line_ending.as_str()); - } + let line_ending = with_line_ending(&mut rope); let mut doc = Self::from(rope, Some(encoding)); @@ -586,6 +592,45 @@ impl Document { } } + /// Reload the document from its path. + pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> { + let encoding = &self.encoding; + let path = self.path().filter(|path| path.exists()); + + // If there is no path or the path no longer exists. + if path.is_none() { + return Err(anyhow!("can't find file to reload from")); + } + + let mut file = std::fs::File::open(path.unwrap())?; + let (mut rope, ..) = from_reader(&mut file, Some(encoding))?; + let line_ending = with_line_ending(&mut rope); + + let transaction = helix_core::diff::compare_ropes(self.text(), &rope); + self.apply(&transaction, view_id); + self.append_changes_to_history(view_id); + + // Detect indentation style and set line ending. + self.detect_indent_style(); + self.line_ending = line_ending; + + Ok(()) + } + + /// Sets the [`Document`]'s encoding with the encoding correspondent to `label`. + pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> { + match encoding_rs::Encoding::for_label(label.as_bytes()) { + Some(encoding) => self.encoding = encoding, + None => return Err(anyhow::anyhow!("unknown encoding")), + } + Ok(()) + } + + /// Returns the [`Document`]'s current encoding. + pub fn encoding(&self) -> &'static encoding_rs::Encoding { + self.encoding + } + fn detect_indent_style(&mut self) { // Build a histogram of the indentation *increases* between // subsequent lines, ignoring lines that are all whitespace.