on master help sign in
`: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

commitc5b2973739901f8cd4bc26f3cfc8232249eb7968 authorKirawi <67773714+kirawi@users.noreply.github.com> dateFri 2 Jul 2021, 10:54 (5 years ago) parente177b27
6 files changed+196−8
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+53−8
Cargo.lock+35
@@ -317,10 +317,12 @@ dependencies = [
317317 "etcetera",
318318 "helix-syntax",
319319 "once_cell",
320+ "quickcheck",
320321 "regex",
321322 "ropey",
322323 "rust-embed",
323324 "serde",
325+ "similar",
324326 "smallvec",
325327 "tendril",
326328 "toml",
@@ -692,6 +694,15 @@ dependencies = [
692694 "unicase",
693695]
694696
697+[[package]]
698+name = "quickcheck"
699+version = "1.0.3"
700+source = "registry+https://github.com/rust-lang/crates.io-index"
701+checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
702+dependencies = [
703+ "rand",
704+]
705+
695706[[package]]
696707name = "quote"
697708version = "1.0.9"
@@ -701,6 +712,24 @@ dependencies = [
701712 "proc-macro2",
702713]
703714
715+[[package]]
716+name = "rand"
717+version = "0.8.4"
718+source = "registry+https://github.com/rust-lang/crates.io-index"
719+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
720+dependencies = [
721+ "rand_core",
722+]
723+
724+[[package]]
725+name = "rand_core"
726+version = "0.6.3"
727+source = "registry+https://github.com/rust-lang/crates.io-index"
728+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
729+dependencies = [
730+ "getrandom",
731+]
732+
704733[[package]]
705734name = "redox_syscall"
706735version = "0.2.9"
@@ -872,6 +901,12 @@ dependencies = [
872901 "libc",
873902]
874903
904+[[package]]
905+name = "similar"
906+version = "1.3.0"
907+source = "registry+https://github.com/rust-lang/crates.io-index"
908+checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
909+
875910[[package]]
876911name = "slab"
877912version = "0.4.3"
helix-core/Cargo.toml+5
@@ -31,5 +31,10 @@ regex = "1"
3131serde = { version = "1.0", features = ["derive"] }
3232toml = "0.5"
3333
34+similar = "1.3"
35+
3436etcetera = "0.3"
3537rust-embed = { version = "5.9.0", optional = true }
38+
39+[dev-dependencies]
40+quickcheck = { version = "1", default-features = false }
helix-core/src/diff.rsnew file+70
@@ -0,0 +1,70 @@
1+use ropey::Rope;
2+
3+use crate::{Change, Transaction};
4+
5+/// Compares `old` and `new` to generate a [`Transaction`] describing
6+/// the steps required to get from `old` to `new`.
7+pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction {
8+ // `similar` only works on contiguous data, so a `Rope` has
9+ // to be temporarily converted into a `String`.
10+ let old_converted = old.to_string();
11+ let new_converted = new.to_string();
12+
13+ // A timeout is set so after 1 seconds, the algorithm will start
14+ // approximating. This is especially important for big `Rope`s or
15+ // `Rope`s that are extremely dissimilar to each other.
16+ //
17+ // Note: Ignore the clippy warning, as the trait bounds of
18+ // `Transaction::change()` require an iterator implementing
19+ // `ExactIterator`.
20+ let mut config = similar::TextDiff::configure();
21+ config.timeout(std::time::Duration::from_secs(1));
22+
23+ let diff = config.diff_chars(&old_converted, &new_converted);
24+
25+ // The current position of the change needs to be tracked to
26+ // construct the `Change`s.
27+ let mut pos = 0;
28+ let changes: Vec<Change> = diff
29+ .ops()
30+ .iter()
31+ .map(|op| op.as_tag_tuple())
32+ .filter_map(|(tag, old_range, new_range)| {
33+ // `old_pos..pos` is equivalent to `start..end` for where
34+ // the change should be applied.
35+ let old_pos = pos;
36+ pos += old_range.end - old_range.start;
37+
38+ match tag {
39+ // Semantically, inserts and replacements are the same thing.
40+ similar::DiffTag::Insert | similar::DiffTag::Replace => {
41+ // This is the text from the `new` rope that should be
42+ // inserted into `old`.
43+ let text: &str = {
44+ let start = new.char_to_byte(new_range.start);
45+ let end = new.char_to_byte(new_range.end);
46+ &new_converted[start..end]
47+ };
48+ Some((old_pos, pos, Some(text.into())))
49+ }
50+ similar::DiffTag::Delete => Some((old_pos, pos, None)),
51+ similar::DiffTag::Equal => None,
52+ }
53+ })
54+ .collect();
55+ Transaction::change(old, changes.into_iter())
56+}
57+
58+#[cfg(test)]
59+mod tests {
60+ use super::*;
61+
62+ quickcheck::quickcheck! {
63+ fn test_compare_ropes(a: String, b: String) -> bool {
64+ let mut old = Rope::from(a);
65+ let new = Rope::from(b);
66+ compare_ropes(&old, &new).apply(&mut old);
67+ old.to_string() == new.to_string()
68+ }
69+ }
70+}
helix-core/src/lib.rs+1
@@ -2,6 +2,7 @@ pub mod auto_pairs;
22pub mod chars;
33pub mod comment;
44pub mod diagnostic;
5+pub mod diff;
56pub mod graphemes;
67pub mod history;
78pub mod indent;
helix-term/src/commands.rs+32
@@ -1521,6 +1521,24 @@ mod cmd {
15211521 }
15221522 }
15231523
1524+ /// Sets the [`Document`]'s encoding..
1525+ fn set_encoding(cx: &mut compositor::Context, args: &[&str], _: PromptEvent) {
1526+ let (_, doc) = current!(cx.editor);
1527+ if let Some(label) = args.first() {
1528+ doc.set_encoding(label)
1529+ .unwrap_or_else(|e| cx.editor.set_error(e.to_string()));
1530+ } else {
1531+ let encoding = doc.encoding().name().to_string();
1532+ cx.editor.set_status(encoding)
1533+ }
1534+ }
1535+
1536+ /// Reload the [`Document`] from its source file.
1537+ fn reload(cx: &mut compositor::Context, _args: &[&str], _: PromptEvent) {
1538+ let (view, doc) = current!(cx.editor);
1539+ doc.reload(view.id).unwrap();
1540+ }
1541+
15241542 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
15251543 TypableCommand {
15261544 name: "quit",
@@ -1704,6 +1722,20 @@ mod cmd {
17041722 fun: show_current_directory,
17051723 completer: None,
17061724 },
1725+ TypableCommand {
1726+ name: "encoding",
1727+ alias: None,
1728+ doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
1729+ fun: set_encoding,
1730+ completer: None,
1731+ },
1732+ TypableCommand {
1733+ name: "reload",
1734+ alias: None,
1735+ doc: "Discard changes and reload from the source file.",
1736+ fun: reload,
1737+ completer: None,
1738+ }
17071739 ];
17081740
17091741 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
helix-view/src/document.rs+53−8
@@ -307,6 +307,19 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
307307 Ok(())
308308}
309309
310+/// 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)
311+pub fn with_line_ending(rope: &mut Rope) -> LineEnding {
312+ // search for line endings
313+ let line_ending = auto_detect_line_ending(rope).unwrap_or(DEFAULT_LINE_ENDING);
314+
315+ // add missing newline at the end of file
316+ if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
317+ rope.insert(rope.len_chars(), line_ending.as_str());
318+ }
319+
320+ line_ending
321+}
322+
310323/// Like std::mem::replace() except it allows the replacement value to be mapped from the
311324/// original value.
312325fn take_with<T, F>(mut_ref: &mut T, closure: F)
@@ -449,14 +462,7 @@ impl Document {
449462
450463 let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
451464 let (mut rope, encoding) = from_reader(&mut file, encoding)?;
452−
453− // search for line endings
454− let line_ending = auto_detect_line_ending(&rope).unwrap_or(DEFAULT_LINE_ENDING);
455−
456− // add missing newline at the end of file
457− if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
458− rope.insert(rope.len_chars(), line_ending.as_str());
459− }
465+ let line_ending = with_line_ending(&mut rope);
460466
461467 let mut doc = Self::from(rope, Some(encoding));
462468
@@ -586,6 +592,45 @@ impl Document {
586592 }
587593 }
588594
595+ /// Reload the document from its path.
596+ pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> {
597+ let encoding = &self.encoding;
598+ let path = self.path().filter(|path| path.exists());
599+
600+ // If there is no path or the path no longer exists.
601+ if path.is_none() {
602+ return Err(anyhow!("can't find file to reload from"));
603+ }
604+
605+ let mut file = std::fs::File::open(path.unwrap())?;
606+ let (mut rope, ..) = from_reader(&mut file, Some(encoding))?;
607+ let line_ending = with_line_ending(&mut rope);
608+
609+ let transaction = helix_core::diff::compare_ropes(self.text(), &rope);
610+ self.apply(&transaction, view_id);
611+ self.append_changes_to_history(view_id);
612+
613+ // Detect indentation style and set line ending.
614+ self.detect_indent_style();
615+ self.line_ending = line_ending;
616+
617+ Ok(())
618+ }
619+
620+ /// Sets the [`Document`]'s encoding with the encoding correspondent to `label`.
621+ pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> {
622+ match encoding_rs::Encoding::for_label(label.as_bytes()) {
623+ Some(encoding) => self.encoding = encoding,
624+ None => return Err(anyhow::anyhow!("unknown encoding")),
625+ }
626+ Ok(())
627+ }
628+
629+ /// Returns the [`Document`]'s current encoding.
630+ pub fn encoding(&self) -> &'static encoding_rs::Encoding {
631+ self.encoding
632+ }
633+
589634 fn detect_indent_style(&mut self) {
590635 // Build a histogram of the indentation *increases* between
591636 // subsequent lines, ignoring lines that are all whitespace.