@@ -11,8 +11,7 @@ use language::{
     LanguageServerStatusUpdate, ServerHealth,
 };
 use project::{
-    EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
-    ProjectEnvironmentEvent,
+    LanguageServerProgress, LspStoreEvent, Project, ProjectEnvironmentEvent,
     git_store::{GitStoreEvent, Repository},
 };
 use smallvec::SmallVec;
@@ -327,20 +326,20 @@ impl ActivityIndicator {
             .flatten()
     }
 
-    fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a EnvironmentErrorMessage> {
+    fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> {
         self.project.read(cx).peek_environment_error(cx)
     }
 
     fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
         // Show if any direnv calls failed
-        if let Some(error) = self.pending_environment_error(cx) {
+        if let Some(message) = self.pending_environment_error(cx) {
             return Some(Content {
                 icon: Some(
                     Icon::new(IconName::Warning)
                         .size(IconSize::Small)
                         .into_any_element(),
                 ),
-                message: error.0.clone(),
+                message: message.clone(),
                 on_click: Some(Arc::new(move |this, window, cx| {
                     this.project.update(cx, |project, cx| {
                         project.pop_environment_error(cx);
  
  
  
    
    @@ -1,82 +0,0 @@
-use crate::environment::EnvironmentErrorMessage;
-use std::process::ExitStatus;
-
-use {collections::HashMap, std::path::Path, util::ResultExt};
-
-#[derive(Clone)]
-pub enum DirenvError {
-    NotFound,
-    FailedRun,
-    NonZeroExit(ExitStatus, Vec<u8>),
-    InvalidJson,
-}
-
-impl From<DirenvError> for Option<EnvironmentErrorMessage> {
-    fn from(value: DirenvError) -> Self {
-        match value {
-            DirenvError::NotFound => None,
-            DirenvError::FailedRun | DirenvError::NonZeroExit(_, _) => {
-                Some(EnvironmentErrorMessage(String::from(
-                    "Failed to run direnv. See logs for more info",
-                )))
-            }
-            DirenvError::InvalidJson => Some(EnvironmentErrorMessage(String::from(
-                "Direnv returned invalid json. See logs for more info",
-            ))),
-        }
-    }
-}
-
-pub async fn load_direnv_environment(
-    env: &HashMap<String, String>,
-    dir: &Path,
-) -> Result<HashMap<String, Option<String>>, DirenvError> {
-    let Ok(direnv_path) = which::which("direnv") else {
-        return Err(DirenvError::NotFound);
-    };
-
-    let args = &["export", "json"];
-    let Some(direnv_output) = smol::process::Command::new(&direnv_path)
-        .args(args)
-        .envs(env)
-        .env("TERM", "dumb")
-        .current_dir(dir)
-        .output()
-        .await
-        .log_err()
-    else {
-        return Err(DirenvError::FailedRun);
-    };
-
-    if !direnv_output.status.success() {
-        log::error!(
-            "Loading direnv environment failed ({}), stderr: {}",
-            direnv_output.status,
-            String::from_utf8_lossy(&direnv_output.stderr)
-        );
-        return Err(DirenvError::NonZeroExit(
-            direnv_output.status,
-            direnv_output.stderr,
-        ));
-    }
-
-    let output = String::from_utf8_lossy(&direnv_output.stdout);
-    if output.is_empty() {
-        // direnv outputs nothing when it has no changes to apply to environment variables
-        return Ok(HashMap::default());
-    }
-
-    match serde_json::from_str(&output) {
-        Ok(env) => Ok(env),
-        Err(err) => {
-            log::error!(
-                "json parse error {}, while parsing output of `{} {}`:\n{}",
-                err,
-                direnv_path.display(),
-                args.join(" "),
-                output
-            );
-            Err(DirenvError::InvalidJson)
-        }
-    }
-}
  
  
  
    
    @@ -1,4 +1,5 @@
-use futures::{FutureExt, future::Shared};
+use anyhow::{Context as _, bail};
+use futures::{FutureExt, StreamExt as _, channel::mpsc, future::Shared};
 use language::Buffer;
 use remote::RemoteClient;
 use rpc::proto::{self, REMOTE_SERVER_PROJECT_ID};
@@ -20,7 +21,9 @@ pub struct ProjectEnvironment {
     cli_environment: Option<HashMap<String, String>>,
     local_environments: HashMap<(Shell, Arc<Path>), Shared<Task<Option<HashMap<String, String>>>>>,
     remote_environments: HashMap<(Shell, Arc<Path>), Shared<Task<Option<HashMap<String, String>>>>>,
-    environment_error_messages: VecDeque<EnvironmentErrorMessage>,
+    environment_error_messages: VecDeque<String>,
+    environment_error_messages_tx: mpsc::UnboundedSender<String>,
+    _tasks: Vec<Task<()>>,
 }
 
 pub enum ProjectEnvironmentEvent {
@@ -30,12 +33,24 @@ pub enum ProjectEnvironmentEvent {
 impl EventEmitter<ProjectEnvironmentEvent> for ProjectEnvironment {}
 
 impl ProjectEnvironment {
-    pub fn new(cli_environment: Option<HashMap<String, String>>) -> Self {
+    pub fn new(cli_environment: Option<HashMap<String, String>>, cx: &mut Context<Self>) -> Self {
+        let (tx, mut rx) = mpsc::unbounded();
+        let task = cx.spawn(async move |this, cx| {
+            while let Some(message) = rx.next().await {
+                this.update(cx, |this, cx| {
+                    this.environment_error_messages.push_back(message);
+                    cx.emit(ProjectEnvironmentEvent::ErrorsUpdated);
+                })
+                .ok();
+            }
+        });
         Self {
             cli_environment,
             local_environments: Default::default(),
             remote_environments: Default::default(),
             environment_error_messages: Default::default(),
+            environment_error_messages_tx: tx,
+            _tasks: vec![task],
         }
     }
 
@@ -128,7 +143,37 @@ impl ProjectEnvironment {
         self.local_environments
             .entry((shell.clone(), abs_path.clone()))
             .or_insert_with(|| {
-                get_local_directory_environment_impl(shell, abs_path.clone(), cx).shared()
+                let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
+                let shell = shell.clone();
+                let tx = self.environment_error_messages_tx.clone();
+                cx.spawn(async move |_, cx| {
+                    let mut shell_env = cx
+                        .background_spawn(load_directory_shell_environment(
+                            shell,
+                            abs_path.clone(),
+                            load_direnv,
+                            tx,
+                        ))
+                        .await
+                        .log_err();
+
+                    if let Some(shell_env) = shell_env.as_mut() {
+                        let path = shell_env
+                            .get("PATH")
+                            .map(|path| path.as_str())
+                            .unwrap_or_default();
+                        log::info!(
+                            "using project environment variables shell launched in {:?}. PATH={:?}",
+                            abs_path,
+                            path
+                        );
+
+                        set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
+                    }
+
+                    shell_env
+                })
+                .shared()
             })
             .clone()
     }
@@ -165,11 +210,11 @@ impl ProjectEnvironment {
             .clone()
     }
 
-    pub fn peek_environment_error(&self) -> Option<&EnvironmentErrorMessage> {
+    pub fn peek_environment_error(&self) -> Option<&String> {
         self.environment_error_messages.front()
     }
 
-    pub fn pop_environment_error(&mut self) -> Option<EnvironmentErrorMessage> {
+    pub fn pop_environment_error(&mut self) -> Option<String> {
         self.environment_error_messages.pop_front()
     }
 }
@@ -194,125 +239,72 @@ impl From<EnvironmentOrigin> for String {
     }
 }
 
-#[derive(Debug)]
-pub struct EnvironmentErrorMessage(pub String);
-
-impl std::fmt::Display for EnvironmentErrorMessage {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
-
-impl EnvironmentErrorMessage {
-    #[allow(dead_code)]
-    fn from_str(s: &str) -> Self {
-        Self(String::from(s))
-    }
-}
-
 async fn load_directory_shell_environment(
-    shell: &Shell,
-    abs_path: &Path,
-    load_direnv: &DirenvSettings,
-) -> (
-    Option<HashMap<String, String>>,
-    Option<EnvironmentErrorMessage>,
-) {
-    match smol::fs::metadata(abs_path).await {
-        Ok(meta) => {
-            let dir = if meta.is_dir() {
-                abs_path
-            } else if let Some(parent) = abs_path.parent() {
-                parent
-            } else {
-                return (
-                    None,
-                    Some(EnvironmentErrorMessage(format!(
-                        "Failed to load shell environment in {}: not a directory",
-                        abs_path.display()
-                    ))),
-                );
-            };
-
-            load_shell_environment(shell, dir, load_direnv).await
-        }
-        Err(err) => (
-            None,
-            Some(EnvironmentErrorMessage(format!(
-                "Failed to load shell environment in {}: {}",
-                abs_path.display(),
-                err
-            ))),
-        ),
-    }
-}
-
-async fn load_shell_environment(
-    shell: &Shell,
-    dir: &Path,
-    load_direnv: &DirenvSettings,
-) -> (
-    Option<HashMap<String, String>>,
-    Option<EnvironmentErrorMessage>,
-) {
-    use crate::direnv::load_direnv_environment;
-    use util::shell_env;
-
-    if cfg!(any(test, feature = "test-support")) {
-        let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
-            .into_iter()
-            .collect();
-        (Some(fake_env), None)
-    } else if cfg!(target_os = "windows") {
+    shell: Shell,
+    abs_path: Arc<Path>,
+    load_direnv: DirenvSettings,
+    tx: mpsc::UnboundedSender<String>,
+) -> anyhow::Result<HashMap<String, String>> {
+    let meta = smol::fs::metadata(&abs_path).await.with_context(|| {
+        tx.unbounded_send(format!("Failed to open {}", abs_path.display()))
+            .ok();
+        format!("stat {abs_path:?}")
+    })?;
+
+    let dir = if meta.is_dir() {
+        abs_path.clone()
+    } else {
+        abs_path
+            .parent()
+            .with_context(|| {
+                tx.unbounded_send(format!("Failed to open {}", abs_path.display()))
+                    .ok();
+                format!("getting parent of {abs_path:?}")
+            })?
+            .into()
+    };
+
+    if cfg!(target_os = "windows") {
+        // Note: direnv is not available on Windows, so we skip direnv processing
+        // and just return the shell environment
         let (shell, args) = shell.program_and_args();
-        let mut envs = match shell_env::capture(shell, args, dir).await {
-            Ok(envs) => envs,
-            Err(err) => {
-                util::log_err(&err);
-                return (
-                    None,
-                    Some(EnvironmentErrorMessage(format!(
-                        "Failed to load environment variables: {}",
-                        err
-                    ))),
-                );
-            }
-        };
+        let mut envs = util::shell_env::capture(shell.clone(), args, abs_path)
+            .await
+            .with_context(|| {
+                tx.unbounded_send("Failed to load environment variables".into())
+                    .ok();
+                format!("capturing shell environment with {shell:?}")
+            })?;
         if let Some(path) = envs.remove("Path") {
             // windows env vars are case-insensitive, so normalize the path var
             // so we can just assume `PATH` in other places
             envs.insert("PATH".into(), path);
         }
-
-        // Note: direnv is not available on Windows, so we skip direnv processing
-        // and just return the shell environment
-        (Some(envs), None)
+        Ok(envs)
     } else {
-        let dir_ = dir.to_owned();
         let (shell, args) = shell.program_and_args();
-        let mut envs = match shell_env::capture(shell, args, &dir_).await {
-            Ok(envs) => envs,
-            Err(err) => {
-                util::log_err(&err);
-                return (
-                    None,
-                    Some(EnvironmentErrorMessage::from_str(
-                        "Failed to load environment variables. See log for details",
-                    )),
-                );
-            }
-        };
+        let mut envs = util::shell_env::capture(shell.clone(), args, abs_path)
+            .await
+            .with_context(|| {
+                tx.unbounded_send("Failed to load environment variables".into())
+                    .ok();
+                format!("capturing shell environment with {shell:?}")
+            })?;
 
         // If the user selects `Direct` for direnv, it would set an environment
         // variable that later uses to know that it should not run the hook.
         // We would include in `.envs` call so it is okay to run the hook
         // even if direnv direct mode is enabled.
-        let (direnv_environment, direnv_error) = match load_direnv {
-            DirenvSettings::ShellHook => (None, None),
-            DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await {
-                Ok(env) => (Some(env), None),
-                Err(err) => (None, err.into()),
-            },
+        let direnv_environment = match load_direnv {
+            DirenvSettings::ShellHook => None,
+            DirenvSettings::Direct => load_direnv_environment(&envs, &dir)
+                .await
+                .with_context(|| {
+                    tx.unbounded_send("Failed to load direnv environment".into())
+                        .ok();
+                    "load direnv environment"
+                })
+                .log_err(),
         };
         if let Some(direnv_environment) = direnv_environment {
             for (key, value) in direnv_environment {
@@ -324,51 +316,39 @@ async fn load_shell_environment(
             }
         }
 
-        (Some(envs), direnv_error)
+        Ok(envs)
     }
 }
 
-fn get_local_directory_environment_impl(
-    shell: &Shell,
-    abs_path: Arc<Path>,
-    cx: &Context<ProjectEnvironment>,
-) -> Task<Option<HashMap<String, String>>> {
-    let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
-
-    let shell = shell.clone();
-    cx.spawn(async move |this, cx| {
-        let (mut shell_env, error_message) = cx
-            .background_spawn({
-                let abs_path = abs_path.clone();
-                async move {
-                    load_directory_shell_environment(&shell, &abs_path, &load_direnv).await
-                }
-            })
-            .await;
-
-        if let Some(shell_env) = shell_env.as_mut() {
-            let path = shell_env
-                .get("PATH")
-                .map(|path| path.as_str())
-                .unwrap_or_default();
-            log::info!(
-                "using project environment variables shell launched in {:?}. PATH={:?}",
-                abs_path,
-                path
-            );
-
-            set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
-        }
+async fn load_direnv_environment(
+    env: &HashMap<String, String>,
+    dir: &Path,
+) -> anyhow::Result<HashMap<String, Option<String>>> {
+    let direnv_path = which::which("direnv").context("finding direnv binary")?;
+
+    let args = &["export", "json"];
+    let direnv_output = smol::process::Command::new(&direnv_path)
+        .args(args)
+        .envs(env)
+        .env("TERM", "dumb")
+        .current_dir(dir)
+        .output()
+        .await
+        .context("running direnv")?;
+
+    if !direnv_output.status.success() {
+        bail!(
+            "Loading direnv environment failed ({}), stderr: {}",
+            direnv_output.status,
+            String::from_utf8_lossy(&direnv_output.stderr)
+        );
+    }
 
-        if let Some(error) = error_message {
-            this.update(cx, |this, cx| {
-                log::error!("{error}");
-                this.environment_error_messages.push_back(error);
-                cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
-            })
-            .log_err();
-        }
+    let output = String::from_utf8_lossy(&direnv_output.stdout);
+    if output.is_empty() {
+        // direnv outputs nothing when it has no changes to apply to environment variables
+        return Ok(HashMap::default());
+    }
 
-        shell_env
-    })
+    serde_json::from_str(&output).context("parsing direnv json")
 }
  
  
  
    
    @@ -23,11 +23,10 @@ pub mod worktree_store;
 #[cfg(test)]
 mod project_tests;
 
-mod direnv;
 mod environment;
 use buffer_diff::BufferDiff;
 use context_server_store::ContextServerStore;
-pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
+pub use environment::ProjectEnvironmentEvent;
 use git::repository::get_git_committer;
 use git_store::{Repository, RepositoryId};
 pub mod search_history;
@@ -1071,7 +1070,7 @@ impl Project {
             let context_server_store =
                 cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
 
-            let environment = cx.new(|_| ProjectEnvironment::new(env));
+            let environment = cx.new(|cx| ProjectEnvironment::new(env, cx));
             let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
             let toolchain_store = cx.new(|cx| {
                 ToolchainStore::local(
@@ -1306,7 +1305,7 @@ impl Project {
             cx.subscribe(&settings_observer, Self::on_settings_observer_event)
                 .detach();
 
-            let environment = cx.new(|_| ProjectEnvironment::new(None));
+            let environment = cx.new(|cx| ProjectEnvironment::new(None, cx));
 
             let lsp_store = cx.new(|cx| {
                 LspStore::new_remote(
@@ -1519,7 +1518,7 @@ impl Project {
             ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
         })?;
 
-        let environment = cx.new(|_| ProjectEnvironment::new(None))?;
+        let environment = cx.new(|cx| ProjectEnvironment::new(None, cx))?;
 
         let breakpoint_store =
             cx.new(|_| BreakpointStore::remote(remote_id, client.clone().into()))?;
@@ -1951,10 +1950,7 @@ impl Project {
     }
 
     #[inline]
-    pub fn peek_environment_error<'a>(
-        &'a self,
-        cx: &'a App,
-    ) -> Option<&'a EnvironmentErrorMessage> {
+    pub fn peek_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> {
         self.environment.read(cx).peek_environment_error()
     }