: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
This commit is contained in:
parent
e177b27baf
commit
c5b2973739
6 changed files with 196 additions and 8 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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
70
helix-core/src/diff.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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(|| {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Add table
Reference in a new issue