project: Use user configured shells for project env fetching (#41288)

Closes https://github.com/zed-industries/zed/issues/40464

Release Notes:

- Fix shell environment sourcing not respecting users remote shells
This commit is contained in:
Lukas Wirth
2025-11-03 16:29:07 +00:00
committed by GitHub
parent a6b177d806
commit 38e1e3f498
12 changed files with 177 additions and 153 deletions
+94 -34
View File
@@ -5,11 +5,12 @@ use remote::RemoteClient;
use rpc::proto::{self, REMOTE_SERVER_PROJECT_ID};
use std::{collections::VecDeque, path::Path, sync::Arc};
use task::{Shell, shell_to_proto};
use util::ResultExt;
use terminal::terminal_settings::TerminalSettings;
use util::{ResultExt, rel_path::RelPath};
use worktree::Worktree;
use collections::HashMap;
use gpui::{AppContext as _, Context, Entity, EventEmitter, Task};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task, WeakEntity};
use settings::Settings as _;
use crate::{
@@ -23,6 +24,8 @@ pub struct ProjectEnvironment {
remote_environments: HashMap<(Shell, Arc<Path>), Shared<Task<Option<HashMap<String, String>>>>>,
environment_error_messages: VecDeque<String>,
environment_error_messages_tx: mpsc::UnboundedSender<String>,
worktree_store: WeakEntity<WorktreeStore>,
remote_client: Option<WeakEntity<RemoteClient>>,
_tasks: Vec<Task<()>>,
}
@@ -33,7 +36,12 @@ pub enum ProjectEnvironmentEvent {
impl EventEmitter<ProjectEnvironmentEvent> for ProjectEnvironment {}
impl ProjectEnvironment {
pub fn new(cli_environment: Option<HashMap<String, String>>, cx: &mut Context<Self>) -> Self {
pub fn new(
cli_environment: Option<HashMap<String, String>>,
worktree_store: WeakEntity<WorktreeStore>,
remote_client: Option<WeakEntity<RemoteClient>>,
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 {
@@ -50,12 +58,17 @@ impl ProjectEnvironment {
remote_environments: Default::default(),
environment_error_messages: Default::default(),
environment_error_messages_tx: tx,
worktree_store,
remote_client,
_tasks: vec![task],
}
}
/// Returns the inherited CLI environment, if this project was opened from the Zed CLI.
pub(crate) fn get_cli_environment(&self) -> Option<HashMap<String, String>> {
if cfg!(any(test, feature = "test-support")) {
return Some(HashMap::default());
}
if let Some(mut env) = self.cli_environment.clone() {
set_origin_marker(&mut env, EnvironmentOrigin::Cli);
Some(env)
@@ -64,16 +77,12 @@ impl ProjectEnvironment {
}
}
pub(crate) fn get_buffer_environment(
pub fn buffer_environment(
&mut self,
buffer: &Entity<Buffer>,
worktree_store: &Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {
return Task::ready(Some(HashMap::default())).shared();
}
if let Some(cli_environment) = self.get_cli_environment() {
log::debug!("using project environment variables from CLI");
return Task::ready(Some(cli_environment)).shared();
@@ -87,54 +96,105 @@ impl ProjectEnvironment {
else {
return Task::ready(None).shared();
};
self.get_worktree_environment(worktree, cx)
self.worktree_environment(worktree, cx)
}
pub fn get_worktree_environment(
pub fn worktree_environment(
&mut self,
worktree: Entity<Worktree>,
cx: &mut Context<Self>,
cx: &mut App,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {
return Task::ready(Some(HashMap::default())).shared();
}
if let Some(cli_environment) = self.get_cli_environment() {
log::debug!("using project environment variables from CLI");
return Task::ready(Some(cli_environment)).shared();
}
let mut abs_path = worktree.read(cx).abs_path();
if !worktree.read(cx).is_local() {
log::error!(
"attempted to get project environment for a non-local worktree at {abs_path:?}"
);
return Task::ready(None).shared();
} else if worktree.read(cx).is_single_file() {
let worktree = worktree.read(cx);
let mut abs_path = worktree.abs_path();
if worktree.is_single_file() {
let Some(parent) = abs_path.parent() else {
return Task::ready(None).shared();
};
abs_path = parent.into();
}
self.get_local_directory_environment(&Shell::System, abs_path, cx)
let remote_client = self.remote_client.as_ref().and_then(|it| it.upgrade());
match remote_client {
Some(remote_client) => remote_client.clone().read(cx).shell().map(|shell| {
self.remote_directory_environment(
&Shell::Program(shell),
abs_path,
remote_client,
cx,
)
}),
None => Some({
let shell = TerminalSettings::get(
Some(settings::SettingsLocation {
worktree_id: worktree.id(),
path: RelPath::empty(),
}),
cx,
)
.shell
.clone();
self.local_directory_environment(&shell, abs_path, cx)
}),
}
.unwrap_or_else(|| Task::ready(None).shared())
}
pub fn directory_environment(
&mut self,
abs_path: Arc<Path>,
cx: &mut App,
) -> Shared<Task<Option<HashMap<String, String>>>> {
let remote_client = self.remote_client.as_ref().and_then(|it| it.upgrade());
match remote_client {
Some(remote_client) => remote_client.clone().read(cx).shell().map(|shell| {
self.remote_directory_environment(
&Shell::Program(shell),
abs_path,
remote_client,
cx,
)
}),
None => self
.worktree_store
.read_with(cx, |worktree_store, cx| {
worktree_store.find_worktree(&abs_path, cx)
})
.ok()
.map(|worktree| {
let shell = terminal::terminal_settings::TerminalSettings::get(
worktree
.as_ref()
.map(|(worktree, path)| settings::SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: &path,
}),
cx,
)
.shell
.clone();
self.local_directory_environment(&shell, abs_path, cx)
}),
}
.unwrap_or_else(|| Task::ready(None).shared())
}
/// Returns the project environment, if possible.
/// If the project was opened from the CLI, then the inherited CLI environment is returned.
/// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in
/// that directory, to get environment variables as if the user has `cd`'d there.
pub fn get_local_directory_environment(
pub fn local_directory_environment(
&mut self,
shell: &Shell,
abs_path: Arc<Path>,
cx: &mut Context<Self>,
cx: &mut App,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {
return Task::ready(Some(HashMap::default())).shared();
}
if let Some(cli_environment) = self.get_cli_environment() {
log::debug!("using project environment variables from CLI");
return Task::ready(Some(cli_environment)).shared();
@@ -146,7 +206,7 @@ impl ProjectEnvironment {
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| {
cx.spawn(async move |cx| {
let mut shell_env = cx
.background_spawn(load_directory_shell_environment(
shell,
@@ -178,12 +238,12 @@ impl ProjectEnvironment {
.clone()
}
pub fn get_remote_directory_environment(
pub fn remote_directory_environment(
&mut self,
shell: &Shell,
abs_path: Arc<Path>,
remote_client: Entity<RemoteClient>,
cx: &mut Context<Self>,
cx: &mut App,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {
return Task::ready(Some(HashMap::default())).shared();
@@ -201,7 +261,7 @@ impl ProjectEnvironment {
shell: Some(shell_to_proto(shell.clone())),
directory: abs_path.to_string_lossy().to_string(),
});
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let environment = response.await.log_err()?;
Some(environment.environment.into_iter().collect())
})