on master help sign in
Check for external file modifications when writing (#5805)
f386ff7Clément Delafargue
helix-term/tests/test/commands.rs+1−1
@@ -67,7 +67,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
67 const RANGE: RangeInclusive<i32> = 1..=1000;
67 const RANGE: RangeInclusive<i32> = 1..=1000;
68
68
69 for i in RANGE {
69 for i in RANGE {
70− let cmd = format!("%c{}<esc>:w<ret>", i);
70+ let cmd = format!("%c{}<esc>:w!<ret>", i);
71 command.push_str(&cmd);
71 command.push_str(&cmd);
72 }
72 }
73
73
helix-term/tests/test/helpers.rs+6
@@ -319,6 +319,12 @@ impl AppBuilder {
319 }
319 }
320}
320}
321
321
322+pub async fn run_event_loop_until_idle(app: &mut Application) {
323+ let (_, rx) = tokio::sync::mpsc::unbounded_channel();
324+ let mut rx_stream = UnboundedReceiverStream::new(rx);
325+ app.event_loop_until_idle(&mut rx_stream).await;
326+}
327+
322pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {
328pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {
323 file.flush()?;
329 file.flush()?;
324 file.sync_all()?;
330 file.sync_all()?;
helix-term/tests/test/write.rs+34−2
@@ -1,5 +1,5 @@
1use std::{
1use std::{
2− io::{Read, Write},
2+ io::{Read, Seek, SeekFrom, Write},
3 ops::RangeInclusive,
3 ops::RangeInclusive,
4};
4};
5
5
@@ -37,6 +37,38 @@ async fn test_write() -> anyhow::Result<()> {
37 Ok(())
37 Ok(())
38}
38}
39
39
40+#[tokio::test(flavor = "multi_thread")]
41+async fn test_overwrite_protection() -> anyhow::Result<()> {
42+ let mut file = tempfile::NamedTempFile::new()?;
43+ let mut app = helpers::AppBuilder::new()
44+ .with_file(file.path(), None)
45+ .build()?;
46+
47+ helpers::run_event_loop_until_idle(&mut app).await;
48+
49+ file.as_file_mut()
50+ .write_all(helpers::platform_line("extremely important content").as_bytes())?;
51+
52+ file.as_file_mut().flush()?;
53+ file.as_file_mut().sync_all()?;
54+
55+ test_key_sequence(&mut app, Some(":x<ret>"), None, false).await?;
56+
57+ file.as_file_mut().flush()?;
58+ file.as_file_mut().sync_all()?;
59+
60+ file.seek(SeekFrom::Start(0))?;
61+ let mut file_content = String::new();
62+ file.as_file_mut().read_to_string(&mut file_content)?;
63+
64+ assert_eq!(
65+ helpers::platform_line("extremely important content"),
66+ file_content
67+ );
68+
69+ Ok(())
70+}
71+
40#[tokio::test(flavor = "multi_thread")]
72#[tokio::test(flavor = "multi_thread")]
41async fn test_write_quit() -> anyhow::Result<()> {
73async fn test_write_quit() -> anyhow::Result<()> {
42 let mut file = tempfile::NamedTempFile::new()?;
74 let mut file = tempfile::NamedTempFile::new()?;
@@ -76,7 +108,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
76 .build()?;
108 .build()?;
77
109
78 for i in RANGE {
110 for i in RANGE {
79− let cmd = format!("%c{}<esc>:w<ret>", i);
111+ let cmd = format!("%c{}<esc>:w!<ret>", i);
80 command.push_str(&cmd);
112 command.push_str(&cmd);
81 }
113 }
82
114
helix-view/src/document.rs+24−1
@@ -19,6 +19,7 @@ use std::future::Future;
19use std::path::{Path, PathBuf};
19use std::path::{Path, PathBuf};
20use std::str::FromStr;
20use std::str::FromStr;
21use std::sync::Arc;
21use std::sync::Arc;
22+use std::time::SystemTime;
22
23
23use helix_core::{
24use helix_core::{
24 encoding,
25 encoding,
@@ -135,6 +136,10 @@ pub struct Document {
135
136
136 pub savepoint: Option<Transaction>,
137 pub savepoint: Option<Transaction>,
137
138
139+ // Last time we wrote to the file. This will carry the time the file was last opened if there
140+ // were no saves.
141+ last_saved_time: SystemTime,
142+
138 last_saved_revision: usize,
143 last_saved_revision: usize,
139 version: i32, // should be usize?
144 version: i32, // should be usize?
140 pub(crate) modified_since_accessed: bool,
145 pub(crate) modified_since_accessed: bool,
@@ -160,6 +165,7 @@ impl fmt::Debug for Document {
160 .field("changes", &self.changes)
165 .field("changes", &self.changes)
161 .field("old_state", &self.old_state)
166 .field("old_state", &self.old_state)
162 // .field("history", &self.history)
167 // .field("history", &self.history)
168+ .field("last_saved_time", &self.last_saved_time)
163 .field("last_saved_revision", &self.last_saved_revision)
169 .field("last_saved_revision", &self.last_saved_revision)
164 .field("version", &self.version)
170 .field("version", &self.version)
165 .field("modified_since_accessed", &self.modified_since_accessed)
171 .field("modified_since_accessed", &self.modified_since_accessed)
@@ -382,6 +388,7 @@ impl Document {
382 version: 0,
388 version: 0,
383 history: Cell::new(History::default()),
389 history: Cell::new(History::default()),
384 savepoint: None,
390 savepoint: None,
391+ last_saved_time: SystemTime::now(),
385 last_saved_revision: 0,
392 last_saved_revision: 0,
386 modified_since_accessed: false,
393 modified_since_accessed: false,
387 language_server: None,
394 language_server: None,
@@ -577,9 +584,11 @@ impl Document {
577
584
578 let encoding = self.encoding;
585 let encoding = self.encoding;
579
586
587+ let last_saved_time = self.last_saved_time;
588+
580 // We encode the file according to the `Document`'s encoding.
589 // We encode the file according to the `Document`'s encoding.
581 let future = async move {
590 let future = async move {
582− use tokio::fs::File;
591+ use tokio::{fs, fs::File};
583 if let Some(parent) = path.parent() {
592 if let Some(parent) = path.parent() {
584 // TODO: display a prompt asking the user if the directories should be created
593 // TODO: display a prompt asking the user if the directories should be created
585 if !parent.exists() {
594 if !parent.exists() {
@@ -591,6 +600,17 @@ impl Document {
591 }
600 }
592 }
601 }
593
602
603+ // Protect against overwriting changes made externally
604+ if !force {
605+ if let Ok(metadata) = fs::metadata(&path).await {
606+ if let Ok(mtime) = metadata.modified() {
607+ if last_saved_time < mtime {
608+ bail!("file modified by an external process, use :w! to overwrite");
609+ }
610+ }
611+ }
612+ }
613+
594 let mut file = File::create(&path).await?;
614 let mut file = File::create(&path).await?;
595 to_writer(&mut file, encoding, &text).await?;
615 to_writer(&mut file, encoding, &text).await?;
596
616
@@ -668,6 +688,8 @@ impl Document {
668 self.append_changes_to_history(view);
688 self.append_changes_to_history(view);
669 self.reset_modified();
689 self.reset_modified();
670
690
691+ self.last_saved_time = SystemTime::now();
692+
671 self.detect_indent_and_line_ending();
693 self.detect_indent_and_line_ending();
672
694
673 match provider_registry.get_diff_base(&path) {
695 match provider_registry.get_diff_base(&path) {
@@ -1016,6 +1038,7 @@ impl Document {
1016 rev
1038 rev
1017 );
1039 );
1018 self.last_saved_revision = rev;
1040 self.last_saved_revision = rev;
1041+ self.last_saved_time = SystemTime::now();
1019 }
1042 }
1020
1043
1021 /// Get the document's latest saved revision.
1044 /// Get the document's latest saved revision.