From 8afc0282f28e73cf78d1bd7b11d78fd853ae2036 Mon Sep 17 00:00:00 2001
From: Yomain <40139584+yo-main@users.noreply.github.com>
Date: Tue, 11 Jul 2023 19:51:04 +0200
Subject: [PATCH] Fix crash when cwd is deleted (#7185)

---
 Cargo.lock                       |  1 +
 helix-core/src/path.rs           |  8 +++---
 helix-loader/Cargo.toml          |  1 +
 helix-loader/src/lib.rs          | 44 ++++++++++++++++++++++++++++++--
 helix-lsp/src/lib.rs             |  2 +-
 helix-term/src/application.rs    |  2 +-
 helix-term/src/commands.rs       | 23 ++++++++++++++---
 helix-term/src/commands/dap.rs   |  2 +-
 helix-term/src/commands/lsp.rs   |  2 +-
 helix-term/src/commands/typed.rs | 18 +++++++------
 helix-term/src/ui/mod.rs         |  2 +-
 11 files changed, 81 insertions(+), 24 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 1a18a4dd..68d5727f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1260,6 +1260,7 @@ version = "0.6.0"
 dependencies = [
  "anyhow",
  "cc",
+ "dunce",
  "etcetera",
  "libloading",
  "log",
diff --git a/helix-core/src/path.rs b/helix-core/src/path.rs
index efa46c46..85c60255 100644
--- a/helix-core/src/path.rs
+++ b/helix-core/src/path.rs
@@ -88,7 +88,7 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
 pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
     let path = expand_tilde(path);
     let path = if path.is_relative() {
-        std::env::current_dir().map(|current_dir| current_dir.join(path))?
+        helix_loader::current_working_dir().join(path)
     } else {
         path
     };
@@ -99,9 +99,7 @@ pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
 pub fn get_relative_path(path: &Path) -> PathBuf {
     let path = PathBuf::from(path);
     let path = if path.is_absolute() {
-        let cwdir = std::env::current_dir()
-            .map(|path| get_normalized_path(&path))
-            .expect("couldn't determine current directory");
+        let cwdir = get_normalized_path(&helix_loader::current_working_dir());
         get_normalized_path(&path)
             .strip_prefix(cwdir)
             .map(PathBuf::from)
@@ -142,7 +140,7 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
 /// ```
 ///
 pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
-    let cwd = std::env::current_dir().unwrap_or_default();
+    let cwd = helix_loader::current_working_dir();
     let path = path
         .as_ref()
         .strip_prefix(cwd)
diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml
index 3e7fc2e7..903d36c0 100644
--- a/helix-loader/Cargo.toml
+++ b/helix-loader/Cargo.toml
@@ -29,6 +29,7 @@ which = "4.4"
 cc = { version = "1" }
 threadpool = { version = "1.0" }
 tempfile = "3.6.0"
+dunce = "1.0.4"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 libloading = "0.8"
diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs
index c51c9c7d..7ff4cada 100644
--- a/helix-loader/src/lib.rs
+++ b/helix-loader/src/lib.rs
@@ -3,9 +3,12 @@ pub mod grammar;
 
 use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
 use std::path::{Path, PathBuf};
+use std::sync::RwLock;
 
 pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
 
+static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
+
 static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
     once_cell::sync::Lazy::new(prioritize_runtime_dirs);
 
@@ -13,6 +16,31 @@ static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCe
 
 static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
 
+// Get the current working directory.
+// This information is managed internally as the call to std::env::current_dir
+// might fail if the cwd has been deleted.
+pub fn current_working_dir() -> PathBuf {
+    if let Some(path) = &*CWD.read().unwrap() {
+        return path.clone();
+    }
+
+    let path = std::env::current_dir()
+        .and_then(dunce::canonicalize)
+        .expect("Couldn't determine current working directory");
+    let mut cwd = CWD.write().unwrap();
+    *cwd = Some(path.clone());
+
+    path
+}
+
+pub fn set_current_working_dir(path: PathBuf) -> std::io::Result<()> {
+    let path = dunce::canonicalize(path)?;
+    std::env::set_current_dir(path.clone())?;
+    let mut cwd = CWD.write().unwrap();
+    *cwd = Some(path);
+    Ok(())
+}
+
 pub fn initialize_config_file(specified_file: Option<PathBuf>) {
     let config_file = specified_file.unwrap_or_else(default_config_file);
     ensure_parent_dir(&config_file);
@@ -217,7 +245,7 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
 /// If no workspace was found returns (CWD, true).
 /// Otherwise (workspace, false) is returned
 pub fn find_workspace() -> (PathBuf, bool) {
-    let current_dir = std::env::current_dir().expect("unable to determine current directory");
+    let current_dir = current_working_dir();
     for ancestor in current_dir.ancestors() {
         if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
             return (ancestor.to_owned(), false);
@@ -243,9 +271,21 @@ fn ensure_parent_dir(path: &Path) {
 mod merge_toml_tests {
     use std::str;
 
-    use super::merge_toml_values;
+    use super::{current_working_dir, merge_toml_values, set_current_working_dir};
     use toml::Value;
 
+    #[test]
+    fn current_dir_is_set() {
+        let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
+        let cwd = current_working_dir();
+        assert_ne!(cwd, new_path);
+
+        set_current_working_dir(new_path.clone()).expect("Couldn't set new path");
+
+        let cwd = current_working_dir();
+        assert_eq!(cwd, new_path);
+    }
+
     #[test]
     fn language_toml_map_merges() {
         const USER: &str = r#"
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 277a4c28..95c61086 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -931,7 +931,7 @@ pub fn find_lsp_workspace(
     let mut file = if file.is_absolute() {
         file.to_path_buf()
     } else {
-        let current_dir = std::env::current_dir().expect("unable to determine current directory");
+        let current_dir = helix_loader::current_working_dir();
         current_dir.join(file)
     };
     file = path::get_normalized_path(&file);
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 4c86a19f..546a57a9 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -163,7 +163,7 @@ impl Application {
         } else if !args.files.is_empty() {
             let first = &args.files[0].0; // we know it's not empty
             if first.is_dir() {
-                std::env::set_current_dir(first).context("set current dir")?;
+                helix_loader::set_current_working_dir(first.clone())?;
                 editor.new_file(Action::VerticalSplit);
                 let picker = ui::file_picker(".".into(), &config.load().editor);
                 compositor.push(Box::new(overlaid(picker)));
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index ae715887..47b1a175 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -2080,8 +2080,12 @@ fn global_search(cx: &mut Context) {
                     .binary_detection(BinaryDetection::quit(b'\x00'))
                     .build();
 
-                let search_root = std::env::current_dir()
-                    .expect("Global search error: Failed to get current dir");
+                let search_root = helix_loader::current_working_dir();
+                if !search_root.exists() {
+                    editor.set_error("Current working directory does not exist");
+                    return;
+                }
+
                 let dedup_symlinks = file_picker_config.deduplicate_links;
                 let absolute_root = search_root
                     .canonicalize()
@@ -2173,7 +2177,9 @@ fn global_search(cx: &mut Context) {
         let call: job::Callback = Callback::EditorCompositor(Box::new(
             move |editor: &mut Editor, compositor: &mut Compositor| {
                 if all_matches.is_empty() {
-                    editor.set_status("No matches found");
+                    if !editor.is_err() {
+                        editor.set_status("No matches found");
+                    }
                     return;
                 }
 
@@ -2518,6 +2524,10 @@ fn append_mode(cx: &mut Context) {
 
 fn file_picker(cx: &mut Context) {
     let root = find_workspace().0;
+    if !root.exists() {
+        cx.editor.set_error("Workspace directory does not exist");
+        return;
+    }
     let picker = ui::file_picker(root, &cx.editor.config());
     cx.push_layer(Box::new(overlaid(picker)));
 }
@@ -2539,7 +2549,12 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
     cx.push_layer(Box::new(overlaid(picker)));
 }
 fn file_picker_in_current_directory(cx: &mut Context) {
-    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./"));
+    let cwd = helix_loader::current_working_dir();
+    if !cwd.exists() {
+        cx.editor
+            .set_error("Current working directory does not exist");
+        return;
+    }
     let picker = ui::file_picker(cwd, &cx.editor.config());
     cx.push_layer(Box::new(overlaid(picker)));
 }
diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs
index 70a5ec21..e26dc08d 100644
--- a/helix-term/src/commands/dap.rs
+++ b/helix-term/src/commands/dap.rs
@@ -217,7 +217,7 @@ pub fn dap_start_impl(
         }
     }
 
-    args.insert("cwd", to_value(std::env::current_dir().unwrap())?);
+    args.insert("cwd", to_value(helix_loader::current_working_dir())?);
 
     let args = to_value(args).unwrap();
 
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index 55153648..145bddd0 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -1033,7 +1033,7 @@ fn goto_impl(
     locations: Vec<lsp::Location>,
     offset_encoding: OffsetEncoding,
 ) {
-    let cwdir = std::env::current_dir().unwrap_or_default();
+    let cwdir = helix_loader::current_working_dir();
 
     match locations.as_slice() {
         [location] => {
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 94cc33f0..dfc71dfd 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -1093,14 +1093,11 @@ fn change_current_directory(
             .as_ref(),
     );
 
-    if let Err(e) = std::env::set_current_dir(dir) {
-        bail!("Couldn't change the current working directory: {}", e);
-    }
+    helix_loader::set_current_working_dir(dir)?;
 
-    let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
     cx.editor.set_status(format!(
         "Current working directory is now {}",
-        cwd.display()
+        helix_loader::current_working_dir().display()
     ));
     Ok(())
 }
@@ -1114,9 +1111,14 @@ fn show_current_directory(
         return Ok(());
     }
 
-    let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
-    cx.editor
-        .set_status(format!("Current working directory is {}", cwd.display()));
+    let cwd = helix_loader::current_working_dir();
+    let message = format!("Current working directory is {}", cwd.display());
+
+    if cwd.exists() {
+        cx.editor.set_status(message);
+    } else {
+        cx.editor.set_error(format!("{} (deleted)", message));
+    }
     Ok(())
 }
 
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 155f2435..3359155d 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -472,7 +472,7 @@ pub mod completers {
                 match path.parent() {
                     Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
                     // Path::new("h")'s parent is Some("")...
-                    _ => std::env::current_dir().expect("couldn't determine current directory"),
+                    _ => helix_loader::current_working_dir(),
                 }
             };