Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hash check if external process changed the content of the file #10662

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions helix-view/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ toml = "0.8"
log = "~0.4"

parking_lot = "0.12.2"
blake3 = { version = "1.5.1", features = ["mmap"] }


[target.'cfg(windows)'.dependencies]
Expand Down
59 changes: 41 additions & 18 deletions helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ pub struct Document {
// when document was used for most-recent-used buffer picker
pub focused_at: std::time::Instant,

// Some(hash): the hash value of the document after the first read
// None: If the document doesn't exist in the filesystem
hash: Option<blake3::Hash>,

pub readonly: bool,
}

Expand Down Expand Up @@ -630,6 +634,7 @@ use url::Url;
impl Document {
pub fn from(
text: Rope,
doc_hash: Option<blake3::Hash>,
encoding_with_bom_info: Option<(&'static Encoding, bool)>,
config: Arc<dyn DynAccess<Config>>,
) -> Self {
Expand All @@ -640,6 +645,7 @@ impl Document {

Self {
id: DocumentId::default(),
hash: doc_hash,
path: None,
encoding,
has_bom,
Expand Down Expand Up @@ -674,7 +680,7 @@ impl Document {
pub fn default(config: Arc<dyn DynAccess<Config>>) -> Self {
let line_ending: LineEnding = config.load().default_line_ending.into();
let text = Rope::from(line_ending.as_str());
Self::from(text, None, config)
Self::from(text, None, None, config)
}

// TODO: async fn?
Expand All @@ -687,17 +693,23 @@ impl Document {
config: Arc<dyn DynAccess<Config>>,
) -> Result<Self, Error> {
// Open the file if it exists, otherwise assume it is a new file (and thus empty).
let (rope, encoding, has_bom) = if path.exists() {
let (rope, encoding, has_bom, doc_hash) = if path.exists() {
let mut file =
std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
from_reader(&mut file, encoding)?

let mut hasher = blake3::Hasher::new();
hasher.update_mmap(path)?;

let (rope, encoding, has_bom) = from_reader(&mut file, encoding)?;

(rope, encoding, has_bom, Some(hasher.finalize()))
} else {
let line_ending: LineEnding = config.load().default_line_ending.into();
let encoding = encoding.unwrap_or(encoding::UTF_8);
(Rope::from(line_ending.as_str()), encoding, false)
(Rope::from(line_ending.as_str()), encoding, false, None)
};

let mut doc = Self::from(rope, Some((encoding, has_bom)), config);
let mut doc = Self::from(rope, doc_hash, Some((encoding, has_bom)), config);

// set the path and try detecting the language
doc.set_path(Some(path));
Expand Down Expand Up @@ -868,6 +880,7 @@ impl Document {

let encoding_with_bom_info = (self.encoding, self.has_bom);
let last_saved_time = self.last_saved_time;
let doc_hash = self.hash;

// We encode the file according to the `Document`'s encoding.
let future = async move {
Expand All @@ -888,7 +901,15 @@ impl Document {
if let Ok(metadata) = fs::metadata(&path).await {
if let Ok(mtime) = metadata.modified() {
if last_saved_time < mtime {
bail!("file modified by an external process, use :w! to overwrite");
if let Some(doc_hash) = doc_hash {
let mut hasher = blake3::Hasher::new();
hasher.update_mmap(&path)?;
let new_doc_hash = hasher.finalize();

if new_doc_hash != doc_hash {
bail!("file modified by an external process, use :w! to overwrite");
}
}
}
}
}
Expand Down Expand Up @@ -2033,15 +2054,22 @@ mod test {

use super::*;

#[test]
fn changeset_to_changes_ignore_line_endings() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let text = Rope::from("hello\r\nworld");
let mut doc = Document::from(
fn create_test_doc(content: &str) -> Document {
let text = Rope::from(content);
let hash = blake3::hash(content.as_bytes());

Document::from(
text,
Some(hash),
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
)
}

#[test]
fn changeset_to_changes_ignore_line_endings() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let mut doc = create_test_doc("hello\r\nworld");
let view = ViewId::default();
doc.set_selection(view, Selection::single(0, 0));

Expand Down Expand Up @@ -2074,12 +2102,7 @@ mod test {
#[test]
fn changeset_to_changes() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let text = Rope::from("hello");
let mut doc = Document::from(
text,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let mut doc = create_test_doc("hello");
let view = ViewId::default();
doc.set_selection(view, Selection::single(5, 5));

Expand Down
1 change: 1 addition & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,7 @@ impl Editor {
let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?;
let doc = Document::from(
helix_core::Rope::default(),
None,
Some((encoding, has_bom)),
self.config.clone(),
);
Expand Down
48 changes: 16 additions & 32 deletions helix-view/src/gutter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,17 +344,22 @@ mod tests {
use arc_swap::ArcSwap;
use helix_core::Rope;

fn create_test_doc(content: &str) -> Document {
let rope = Rope::from_str(content);
let hash = blake3::hash(content.as_bytes());
Document::from(
rope,
Some(hash),
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
)
}

#[test]
fn test_default_gutter_widths() {
let mut view = View::new(DocumentId::default(), GutterConfig::default());
view.area = Rect::new(40, 40, 40, 40);

let rope = Rope::from_str("abc\n\tdef");
let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let doc = create_test_doc("abc\n\tdef");

assert_eq!(view.gutters.layout.len(), 5);
assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
Expand All @@ -374,12 +379,7 @@ mod tests {
let mut view = View::new(DocumentId::default(), gutters);
view.area = Rect::new(40, 40, 40, 40);

let rope = Rope::from_str("abc\n\tdef");
let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let doc = create_test_doc("abc\n\tdef");

assert_eq!(view.gutters.layout.len(), 1);
assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
Expand All @@ -392,12 +392,7 @@ mod tests {
let mut view = View::new(DocumentId::default(), gutters);
view.area = Rect::new(40, 40, 40, 40);

let rope = Rope::from_str("abc\n\tdef");
let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let doc = create_test_doc("abc\n\tdef");

assert_eq!(view.gutters.layout.len(), 2);
assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
Expand All @@ -414,19 +409,8 @@ mod tests {
let mut view = View::new(DocumentId::default(), gutters);
view.area = Rect::new(40, 40, 40, 40);

let rope = Rope::from_str("a\nb");
let doc_short = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);

let rope = Rope::from_str("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np");
let doc_long = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let doc_short = create_test_doc("a\nb");
let doc_long = create_test_doc("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np");

assert_eq!(view.gutters.layout.len(), 2);
assert_eq!(view.gutters.layout[1].width(&view, &doc_short), 1);
Expand Down
47 changes: 16 additions & 31 deletions helix-view/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,17 +645,22 @@ mod tests {
use crate::document::Document;
use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig, GutterType};

#[test]
fn test_text_pos_at_screen_coords() {
let mut view = View::new(DocumentId::default(), GutterConfig::default());
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
let doc = Document::from(
fn create_test_doc(content: &str) -> Document {
let rope = Rope::from_str(content);
let hash = blake3::hash(content.as_bytes());
Document::from(
rope,
Some(hash),
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
)
}

#[test]
fn test_text_pos_at_screen_coords() {
let mut view = View::new(DocumentId::default(), GutterConfig::default());
view.area = Rect::new(40, 40, 40, 40);
let doc = create_test_doc("abc\n\tdef");
assert_eq!(
view.text_pos_at_screen_coords(
&doc,
Expand Down Expand Up @@ -823,12 +828,7 @@ mod tests {
},
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let doc = create_test_doc("abc\n\tdef");
assert_eq!(
view.text_pos_at_screen_coords(
&doc,
Expand All @@ -852,12 +852,7 @@ mod tests {
},
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let doc = create_test_doc("abc\n\tdef");
assert_eq!(
view.text_pos_at_screen_coords(
&doc,
Expand All @@ -875,12 +870,7 @@ mod tests {
fn test_text_pos_at_screen_coords_cjk() {
let mut view = View::new(DocumentId::default(), GutterConfig::default());
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hi! こんにちは皆さん");
let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let doc = create_test_doc("Hi! こんにちは皆さん");

assert_eq!(
view.text_pos_at_screen_coords(
Expand Down Expand Up @@ -958,12 +948,7 @@ mod tests {
fn test_text_pos_at_screen_coords_graphemes() {
let mut view = View::new(DocumentId::default(), GutterConfig::default());
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hèl̀l̀ò world!");
let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
);
let doc = create_test_doc("Hèl̀l̀ò world!");

assert_eq!(
view.text_pos_at_screen_coords(
Expand Down