: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",
|
||||
"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"
|
||||
|
|
|
@ -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 }
|
||||
|
|
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 comment;
|
||||
pub mod diagnostic;
|
||||
pub mod diff;
|
||||
pub mod graphemes;
|
||||
pub mod history;
|
||||
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] = &[
|
||||
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(|| {
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Reference in a new issue