Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/document.rs')
-rw-r--r--helix-view/src/document.rs87
1 files changed, 60 insertions, 27 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index da68a67f..0c3a75f1 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -10,6 +10,7 @@ use helix_core::encoding::Encoding;
use helix_core::syntax::{Highlight, LanguageServerFeature};
use helix_core::text_annotations::{InlineAnnotation, Overlay};
use helix_lsp::util::lsp_pos_to_pos;
+use helix_stdx::faccess::{copy_metadata, readonly};
use helix_vcs::{DiffHandle, DiffProviderRegistry};
use ::parking_lot::Mutex;
@@ -870,7 +871,7 @@ impl Document {
// We encode the file according to the `Document`'s encoding.
let future = async move {
- use tokio::{fs, fs::File};
+ use tokio::fs;
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
@@ -893,8 +894,63 @@ impl Document {
}
}
- let mut file = File::create(&path).await?;
- to_writer(&mut file, encoding_with_bom_info, &text).await?;
+ if readonly(&path) {
+ bail!(std::io::Error::new(
+ std::io::ErrorKind::PermissionDenied,
+ "Path is read only"
+ ));
+ }
+ let backup = if path.exists() {
+ let path_ = path.clone();
+ // hacks: we use tempfile to handle the complex task of creating
+ // non clobbered temporary path for us we don't want
+ // the whole automatically delete path on drop thing
+ // since the path doesn't exist yet, we just want
+ // the path
+ tokio::task::spawn_blocking(move || -> Option<PathBuf> {
+ tempfile::Builder::new()
+ .prefix(path_.file_name()?)
+ .suffix(".bck")
+ .make_in(path_.parent()?, |backup| std::fs::rename(&path_, backup))
+ .ok()?
+ .into_temp_path()
+ .keep()
+ .ok()
+ })
+ .await
+ .ok()
+ .flatten()
+ } else {
+ None
+ };
+
+ let write_result: anyhow::Result<_> = async {
+ let mut dst = tokio::fs::File::create(&path).await?;
+ to_writer(&mut dst, encoding_with_bom_info, &text).await?;
+ Ok(())
+ }
+ .await;
+
+ if let Some(backup) = backup {
+ if write_result.is_err() {
+ // restore backup
+ let _ = tokio::fs::rename(&backup, &path)
+ .await
+ .map_err(|e| log::error!("Failed to restore backup on write failure: {e}"));
+ } else {
+ // copy metadata and delete backup
+ let path_ = path.clone();
+ let _ = tokio::task::spawn_blocking(move || {
+ let _ = copy_metadata(&backup, &path_)
+ .map_err(|e| log::error!("Failed to copy metadata on write: {e}"));
+ let _ = std::fs::remove_file(backup)
+ .map_err(|e| log::error!("Failed to remove backup file on write: {e}"));
+ })
+ .await;
+ }
+ }
+
+ write_result?;
let event = DocumentSavedEvent {
revision: current_rev,
@@ -955,35 +1011,12 @@ impl Document {
}
}
- #[cfg(unix)]
// Detect if the file is readonly and change the readonly field if necessary (unix only)
pub fn detect_readonly(&mut self) {
- use rustix::fs::{access, Access};
// Allows setting the flag for files the user cannot modify, like root files
self.readonly = match &self.path {
None => false,
- Some(p) => match access(p, Access::WRITE_OK) {
- Ok(_) => false,
- Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
- Err(_) => true,
- },
- };
- }
-
- #[cfg(not(unix))]
- // Detect if the file is readonly and change the readonly field if necessary (non-unix os)
- pub fn detect_readonly(&mut self) {
- // TODO Use the Windows' function `CreateFileW` to check if a file is readonly
- // Discussion: https://github.com/helix-editor/helix/pull/7740#issuecomment-1656806459
- // Vim implementation: https://github.com/vim/vim/blob/4c0089d696b8d1d5dc40568f25ea5738fa5bbffb/src/os_win32.c#L7665
- // Windows binding: https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Storage/FileSystem/fn.CreateFileW.html
- self.readonly = match &self.path {
- None => false,
- Some(p) => match std::fs::metadata(p) {
- Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
- Err(_) => false,
- Ok(metadata) => metadata.permissions().readonly(),
- },
+ Some(p) => readonly(p),
};
}