diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index e78e6c9f9964c..2a5e12b85a122 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -67,7 +67,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { const RANGE: RangeInclusive = 1..=1000; for i in RANGE { - let cmd = format!("%c{}:w", i); + let cmd = format!("%c{}:w!", i); command.push_str(&cmd); } diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 2c5043d68cf23..266c778af6040 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -322,6 +322,12 @@ impl AppBuilder { } } +pub async fn run_event_loop_to_idle(app: &mut Application) { + let (_, rx) = tokio::sync::mpsc::unbounded_channel(); + let mut rx_stream = UnboundedReceiverStream::new(rx); + app.event_loop_until_idle(&mut rx_stream).await; +} + pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> { file.flush()?; file.sync_all()?; diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index d0128edcaddf0..7eca93167f6c4 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -1,5 +1,5 @@ use std::{ - io::{Read, Write}, + io::{Read, Seek, SeekFrom, Write}, ops::RangeInclusive, }; @@ -37,6 +37,38 @@ async fn test_write() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_overwrite_protection() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; + + helpers::run_event_loop_to_idle(&mut app).await; + + file.as_file_mut() + .write_all(helpers::platform_line("extremely important content").as_bytes())?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + test_key_sequence(&mut app, Some(":x"), None, false).await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + file.seek(SeekFrom::Start(0))?; + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + + assert_eq!( + helpers::platform_line("extremely important content"), + file_content + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_write_quit() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; @@ -76,7 +108,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> { .build()?; for i in RANGE { - let cmd = format!("%c{}:w", i); + let cmd = format!("%c{}:w!", i); command.push_str(&cmd); } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0870852833918..a67c83d4dfb47 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -13,6 +13,7 @@ use std::future::Future; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; +use std::time::SystemTime; use helix_core::{ encoding, @@ -127,6 +128,10 @@ pub struct Document { pub savepoint: Option, + // Last time we wrote to the file. This will carry the time the file was last opened if there + // were no saves. + last_saved_time: SystemTime, + last_saved_revision: usize, version: i32, // should be usize? pub(crate) modified_since_accessed: bool, @@ -368,6 +373,7 @@ impl Document { version: 0, history: Cell::new(History::default()), savepoint: None, + last_saved_time: SystemTime::now(), last_saved_revision: 0, modified_since_accessed: false, language_server: None, @@ -557,9 +563,11 @@ impl Document { let encoding = self.encoding; + let last_saved_time = self.last_saved_time; + // We encode the file according to the `Document`'s encoding. let future = async move { - use tokio::fs::File; + use tokio::{fs, fs::File}; if let Some(parent) = path.parent() { // TODO: display a prompt asking the user if the directories should be created if !parent.exists() { @@ -571,6 +579,17 @@ impl Document { } } + // Protect against overwriting changes made externally + if !force { + 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 force"); + } + } + } + } + let mut file = File::create(&path).await?; to_writer(&mut file, encoding, &text).await?; @@ -644,6 +663,8 @@ impl Document { self.append_changes_to_history(view.id); self.reset_modified(); + self.last_saved_time = SystemTime::now(); + self.detect_indent_and_line_ending(); Ok(()) @@ -979,6 +1000,7 @@ impl Document { rev ); self.last_saved_revision = rev; + self.last_saved_time = SystemTime::now(); } /// Get the document's latest saved revision.