aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock35
-rw-r--r--helix-core/Cargo.toml5
-rw-r--r--helix-core/src/diff.rs70
-rw-r--r--helix-core/src/lib.rs1
-rw-r--r--helix-term/src/commands.rs32
-rw-r--r--helix-view/src/document.rs61
6 files changed, 196 insertions, 8 deletions
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",
@@ -693,6 +695,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -702,6 +713,24 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -873,6 +902,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<Change> = 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<HashMap<&'static str, &'static TypableCommand>> = 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<T, F>(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.