* 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
This commit is contained in:
Kirawi 2021-07-02 10:54:50 -04:00 committed by GitHub
parent e177b27baf
commit c5b2973739
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 196 additions and 8 deletions

35
Cargo.lock generated
View file

@ -317,10 +317,12 @@ dependencies = [
"etcetera", "etcetera",
"helix-syntax", "helix-syntax",
"once_cell", "once_cell",
"quickcheck",
"regex", "regex",
"ropey", "ropey",
"rust-embed", "rust-embed",
"serde", "serde",
"similar",
"smallvec", "smallvec",
"tendril", "tendril",
"toml", "toml",
@ -692,6 +694,15 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "quickcheck"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"rand",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.9"
@ -701,6 +712,24 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.9" version = "0.2.9"
@ -872,6 +901,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "similar"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.3" version = "0.4.3"

View file

@ -31,5 +31,10 @@ regex = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "0.5" toml = "0.5"
similar = "1.3"
etcetera = "0.3" etcetera = "0.3"
rust-embed = { version = "5.9.0", optional = true } rust-embed = { version = "5.9.0", optional = true }
[dev-dependencies]
quickcheck = { version = "1", default-features = false }

70
helix-core/src/diff.rs Normal file
View file

@ -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()
}
}
}

View file

@ -2,6 +2,7 @@ pub mod auto_pairs;
pub mod chars; pub mod chars;
pub mod comment; pub mod comment;
pub mod diagnostic; pub mod diagnostic;
pub mod diff;
pub mod graphemes; pub mod graphemes;
pub mod history; pub mod history;
pub mod indent; pub mod indent;

View file

@ -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] = &[ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand { TypableCommand {
name: "quit", name: "quit",
@ -1704,6 +1722,20 @@ mod cmd {
fun: show_current_directory, fun: show_current_directory,
completer: None, 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(|| { pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {

View file

@ -307,6 +307,19 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
Ok(()) 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 /// Like std::mem::replace() except it allows the replacement value to be mapped from the
/// original value. /// original value.
fn take_with<T, F>(mut_ref: &mut T, closure: F) 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 file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
let (mut rope, encoding) = from_reader(&mut file, encoding)?; let (mut rope, encoding) = from_reader(&mut file, encoding)?;
let line_ending = with_line_ending(&mut rope);
// 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 mut doc = Self::from(rope, Some(encoding)); 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) { fn detect_indent_style(&mut self) {
// Build a histogram of the indentation *increases* between // Build a histogram of the indentation *increases* between
// subsequent lines, ignoring lines that are all whitespace. // subsequent lines, ignoring lines that are all whitespace.