linux: fix invalid cross-device link error (#8437)

Aryan Sjet created

This PR fix the "invalid cross-device link" error occurred in linux when
trying to write the settings file atomically, like when click the
"Enable vim mode" checkbox at first start.

```plain
[2024-02-26T22:59:25+08:00 ERROR util] .../zed/crates/settings/src/settings_file.rs:135: Failed to write settings to file "/home/$USER/.config/zed/settings.json"

Caused by:
0: failed to persist temporary file: Invalid cross-device link (os error 18)
1: Invalid cross-device link (os error 18)
```

Currently the `fs::RealFs::atomic_write()` method write to a temp file
created with `NamedTempFile::new()` and then call `persist()` method to
write to the config file path, which actually do a `rename` syscall
under the hood. As the
[issue](https://github.com/Stebalien/tempfile/issues/245) said

> `NamedTempFile::new()` will create a temporary file in your system's
temporary file directory. You need `NamedTempFile::new_in()`.

The temporary file directory in linux is in `/tmp`, which is mounted to
`tmpfs` filesystem, and in most case(all case I guess)
`$HOME/.config/zed` is mounted to a different filesystem. And the
`rename` syscall between different filesystems will return a `EXDEV`
errno, as described in the man page
[rename(2)](https://man7.org/linux/man-pages/man2/renameat2.2.html):

```plain
       EXDEV  oldpath and newpath are not on the same mounted
              filesystem.  (Linux permits a filesystem to be mounted at
              multiple points, but rename() does not work across
              different mount points, even if the same filesystem is
              mounted on both.)
```

And as the issue above said, use a different temp dir with
`NamedTempFile::new_in()` for linux platform might be a solution, since
the `rename` syscall provides atomicity.

Release Notes:
- Fix `settings.json` save failed with invalid cross-device link error
in linux

Change summary

crates/fs/src/fs.rs      | 11 +++++++++--
crates/util/src/paths.rs |  1 +
crates/zed/src/main.rs   |  2 ++
3 files changed, 12 insertions(+), 2 deletions(-)

Detailed changes

crates/fs/src/fs.rs 🔗

@@ -30,7 +30,7 @@ use std::{
 };
 use tempfile::{NamedTempFile, TempDir};
 use text::LineEnding;
-use util::ResultExt;
+use util::{paths, ResultExt};
 
 #[cfg(any(test, feature = "test-support"))]
 use collections::{btree_map, BTreeMap};
@@ -189,7 +189,14 @@ impl Fs for RealFs {
 
     async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
         smol::unblock(move || {
-            let mut tmp_file = NamedTempFile::new()?;
+            let mut tmp_file = if cfg!(target_os = "linux") {
+                // Use the directory of the destination as temp dir to avoid
+                // invalid cross-device link error, and XDG_CACHE_DIR for fallback.
+                // See https://github.com/zed-industries/zed/pull/8437 for more details.
+                NamedTempFile::new_in(path.parent().unwrap_or(&paths::TEMP_DIR))
+            } else {
+                NamedTempFile::new()
+            }?;
             tmp_file.write_all(data.as_bytes())?;
             tmp_file.persist(path)?;
             Ok::<(), anyhow::Error>(())

crates/util/src/paths.rs 🔗

@@ -44,6 +44,7 @@ lazy_static::lazy_static! {
     pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
     pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
     pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json");
+    pub static ref TEMP_DIR: PathBuf = HOME.join(".cache").join("zed");
 }
 
 pub trait PathExt {

crates/zed/src/main.rs 🔗

@@ -493,6 +493,8 @@ fn init_paths() {
     std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path");
     std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path");
     std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path");
+    #[cfg(target_os = "linux")]
+    std::fs::create_dir_all(&*util::paths::TEMP_DIR).expect("could not create tmp path");
 }
 
 fn init_logger() {