Add activation script support for terminals/toolchains

This commit is contained in:
Lukas Wirth 2025-08-19 11:39:08 +02:00
parent 7b3d73d6fd
commit 8e11e6a03e
11 changed files with 98 additions and 32 deletions

View file

@ -10,14 +10,14 @@ use std::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use collections::HashMap; use collections::{FxHashMap, HashMap};
use gpui::{AsyncApp, SharedString}; use gpui::{AsyncApp, SharedString};
use settings::WorktreeId; use settings::WorktreeId;
use crate::{LanguageName, ManifestName}; use crate::{LanguageName, ManifestName};
/// Represents a single toolchain. /// Represents a single toolchain.
#[derive(Clone, Debug, Eq)] #[derive(Clone, Eq, Debug)]
pub struct Toolchain { pub struct Toolchain {
/// User-facing label /// User-facing label
pub name: SharedString, pub name: SharedString,
@ -25,24 +25,41 @@ pub struct Toolchain {
pub language_name: LanguageName, pub language_name: LanguageName,
/// Full toolchain data (including language-specific details) /// Full toolchain data (including language-specific details)
pub as_json: serde_json::Value, pub as_json: serde_json::Value,
/// shell -> script
pub startup_script: FxHashMap<String, String>,
} }
impl std::hash::Hash for Toolchain { impl std::hash::Hash for Toolchain {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state); let Self {
self.path.hash(state); name,
self.language_name.hash(state); path,
language_name,
as_json: _,
startup_script: _,
} = self;
name.hash(state);
path.hash(state);
language_name.hash(state);
} }
} }
impl PartialEq for Toolchain { impl PartialEq for Toolchain {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
let Self {
name,
path,
language_name,
as_json: _,
startup_script,
} = self;
// Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced. // Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced.
// Thus, there could be multiple entries that look the same in the UI. // Thus, there could be multiple entries that look the same in the UI.
(&self.name, &self.path, &self.language_name).eq(&( (name, path, language_name, startup_script).eq(&(
&other.name, &other.name,
&other.path, &other.path,
&other.language_name, &other.language_name,
&other.startup_script,
)) ))
} }
} }
@ -82,7 +99,7 @@ pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
) -> Option<Toolchain>; ) -> Option<Toolchain>;
} }
#[async_trait(?Send )] #[async_trait(?Send)]
impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T { impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
async fn active_toolchain( async fn active_toolchain(
self: Arc<Self>, self: Arc<Self>,

View file

@ -878,6 +878,8 @@ impl ToolchainLister for PythonToolchainProvider {
path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(), path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"), language_name: LanguageName::new("Python"),
as_json: serde_json::to_value(toolchain).ok()?, as_json: serde_json::to_value(toolchain).ok()?,
startup_script: std::iter::once(("fish".to_owned(), "test".to_owned()))
.collect(),
}) })
}) })
.collect(); .collect();

View file

@ -293,6 +293,7 @@ impl DapStore {
binary.cwd.as_deref(), binary.cwd.as_deref(),
binary.envs, binary.envs,
path_style, path_style,
None,
); );
Ok(DebugAdapterBinary { Ok(DebugAdapterBinary {

View file

@ -9204,6 +9204,7 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
path: venv_path.to_string_lossy().into_owned().into(), path: venv_path.to_string_lossy().into_owned().into(),
language_name: LanguageName(SharedString::new_static("Python")), language_name: LanguageName(SharedString::new_static("Python")),
as_json: serde_json::Value::Null, as_json: serde_json::Value::Null,
startup_script: Default::default(),
}) })
} }
} }

View file

@ -11,7 +11,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use task::{Shell, ShellBuilder, SpawnInTerminal}; use task::{Shell, ShellBuilder, SpawnInTerminal, system_shell};
use terminal::{ use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
}; };
@ -173,6 +173,7 @@ impl Project {
path.as_deref(), path.as_deref(),
env, env,
path_style, path_style,
None,
); );
env = HashMap::default(); env = HashMap::default();
if let Some(envs) = envs { if let Some(envs) = envs {
@ -213,6 +214,7 @@ impl Project {
cx.entity_id().as_u64(), cx.entity_id().as_u64(),
Some(completion_tx), Some(completion_tx),
cx, cx,
None,
) )
.map(|builder| { .map(|builder| {
let terminal_handle = cx.new(|cx| builder.subscribe(cx)); let terminal_handle = cx.new(|cx| builder.subscribe(cx));
@ -276,21 +278,33 @@ impl Project {
let toolchain = let toolchain =
project_path_context.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); project_path_context.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
cx.spawn(async move |project, cx| { cx.spawn(async move |project, cx| {
let toolchain = maybe!(async { let shell = match &ssh_details {
let toolchain = toolchain?.await?; Some(ssh) => ssh.shell.clone(),
None => match &settings.shell {
Shell::Program(program) => program.clone(),
Shell::WithArguments {
program,
args: _,
title_override: _,
} => program.clone(),
Shell::System => system_shell(),
},
};
Some(()) let scripts = maybe!(async {
let toolchain = toolchain?.await?;
Some(toolchain.startup_script)
}) })
.await; .await;
let activation_script = scripts.as_ref().and_then(|it| it.get(&shell));
let (spawn_task, shell) = { let shell = {
match ssh_details { match ssh_details {
Some(SshDetails { Some(SshDetails {
host, host,
ssh_command, ssh_command,
envs, envs,
path_style, path_style,
shell, shell: _,
}) => { }) => {
log::debug!("Connecting to a remote server: {ssh_command:?}"); log::debug!("Connecting to a remote server: {ssh_command:?}");
@ -308,28 +322,37 @@ impl Project {
path.as_deref(), path.as_deref(),
env, env,
path_style, path_style,
activation_script.map(String::as_str),
); );
env = HashMap::default(); env = HashMap::default();
if let Some(envs) = envs { if let Some(envs) = envs {
env.extend(envs); env.extend(envs);
} }
( Shell::WithArguments {
Option::<TaskState>::None, program,
Shell::WithArguments { args,
program, title_override: Some(format!("{} — Terminal", host).into()),
args, }
title_override: Some(format!("{} — Terminal", host).into()),
},
)
} }
None => (None, settings.shell), None if activation_script.is_some() => Shell::WithArguments {
program: shell.clone(),
args: vec![
"-c".to_owned(),
format!(
"{}; exec {} -l",
activation_script.unwrap().to_string(),
shell
),
],
title_override: None,
},
None => settings.shell,
} }
}; };
project.update(cx, move |this, cx| { project.update(cx, move |this, cx| {
TerminalBuilder::new( TerminalBuilder::new(
local_path.map(|path| path.to_path_buf()), local_path.map(|path| path.to_path_buf()),
spawn_task, None,
shell, shell,
env, env,
settings.cursor_shape.unwrap_or_default(), settings.cursor_shape.unwrap_or_default(),
@ -339,6 +362,7 @@ impl Project {
cx.entity_id().as_u64(), cx.entity_id().as_u64(),
None, None,
cx, cx,
None,
) )
.map(|builder| { .map(|builder| {
let terminal_handle = cx.new(|cx| builder.subscribe(cx)); let terminal_handle = cx.new(|cx| builder.subscribe(cx));
@ -447,6 +471,7 @@ impl Project {
path.as_deref(), path.as_deref(),
env, env,
path_style, path_style,
None,
); );
let mut command = std::process::Command::new(command); let mut command = std::process::Command::new(command);
command.args(args); command.args(args);
@ -479,6 +504,7 @@ pub fn wrap_for_ssh(
path: Option<&Path>, path: Option<&Path>,
env: HashMap<String, String>, env: HashMap<String, String>,
path_style: PathStyle, path_style: PathStyle,
activation_script: Option<&str>,
) -> (String, Vec<String>) { ) -> (String, Vec<String>) {
let to_run = if let Some((command, args)) = command { let to_run = if let Some((command, args)) = command {
let command: Option<Cow<str>> = shlex::try_quote(command).ok(); let command: Option<Cow<str>> = shlex::try_quote(command).ok();
@ -495,6 +521,9 @@ pub fn wrap_for_ssh(
} }
} }
let activation_script = activation_script
.map(|s| format!(" {s};"))
.unwrap_or_default();
let commands = if let Some(path) = path { let commands = if let Some(path) = path {
let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string(); let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
// shlex will wrap the command in single quotes (''), disabling ~ expansion, // shlex will wrap the command in single quotes (''), disabling ~ expansion,
@ -506,12 +535,12 @@ pub fn wrap_for_ssh(
.trim_start_matches("~") .trim_start_matches("~")
.trim_start_matches("/"); .trim_start_matches("/");
format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}") format!("cd \"$HOME/{trimmed_path}\";{activation_script} {env_changes} {to_run}")
} else { } else {
format!("cd \"{path}\"; {env_changes} {to_run}") format!("cd \"{path}\";{activation_script} {env_changes} {to_run}")
} }
} else { } else {
format!("cd; {env_changes} {to_run}") format!("cd;{activation_script} {env_changes} {to_run}")
}; };
let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap()); let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap());

View file

@ -138,6 +138,7 @@ impl ToolchainStore {
// Do we need to convert path to native string? // Do we need to convert path to native string?
path: PathBuf::from(toolchain.path).to_proto().into(), path: PathBuf::from(toolchain.path).to_proto().into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json)?, as_json: serde_json::Value::from_str(&toolchain.raw_json)?,
startup_script: toolchain.activation_script.into_iter().collect(),
language_name, language_name,
}; };
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
@ -178,6 +179,7 @@ impl ToolchainStore {
name: toolchain.name.into(), name: toolchain.name.into(),
path: path.to_proto(), path: path.to_proto(),
raw_json: toolchain.as_json.to_string(), raw_json: toolchain.as_json.to_string(),
activation_script: toolchain.startup_script.into_iter().collect(),
} }
}), }),
}) })
@ -221,6 +223,7 @@ impl ToolchainStore {
name: toolchain.name.to_string(), name: toolchain.name.to_string(),
path: path.to_proto(), path: path.to_proto(),
raw_json: toolchain.as_json.to_string(), raw_json: toolchain.as_json.to_string(),
activation_script: toolchain.startup_script.into_iter().collect(),
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -449,6 +452,7 @@ impl RemoteToolchainStore {
name: toolchain.name.into(), name: toolchain.name.into(),
path: path.to_proto(), path: path.to_proto(),
raw_json: toolchain.as_json.to_string(), raw_json: toolchain.as_json.to_string(),
activation_script: toolchain.startup_script.into_iter().collect(),
}), }),
path: Some(project_path.path.to_string_lossy().into_owned()), path: Some(project_path.path.to_string_lossy().into_owned()),
}) })
@ -501,6 +505,7 @@ impl RemoteToolchainStore {
.to_string() .to_string()
.into(), .into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
startup_script: toolchain.activation_script.into_iter().collect(),
}) })
}) })
.collect(); .collect();
@ -557,6 +562,7 @@ impl RemoteToolchainStore {
.to_string() .to_string()
.into(), .into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
startup_script: toolchain.activation_script.into_iter().collect(),
}) })
}) })
}) })

View file

@ -12,6 +12,7 @@ message Toolchain {
string name = 1; string name = 1;
string path = 2; string path = 2;
string raw_json = 3; string raw_json = 3;
map<string, string> activation_script = 4;
} }
message ToolchainGroup { message ToolchainGroup {

View file

@ -178,7 +178,7 @@ impl ShellKind {
} }
} }
fn system_shell() -> String { pub fn system_shell() -> String {
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
// `alacritty_terminal` uses this as default on Windows. See: // `alacritty_terminal` uses this as default on Windows. See:
// https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130 // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130

View file

@ -22,7 +22,7 @@ pub use debug_format::{
AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest,
Request, TcpArgumentsTemplate, ZedDebugConfig, Request, TcpArgumentsTemplate, ZedDebugConfig,
}; };
pub use shell_builder::{ShellBuilder, ShellKind}; pub use shell_builder::{ShellBuilder, ShellKind, system_shell};
pub use task_template::{ pub use task_template::{
DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
substitute_variables_in_map, substitute_variables_in_str, substitute_variables_in_map, substitute_variables_in_str,

View file

@ -354,6 +354,7 @@ impl TerminalBuilder {
window_id: u64, window_id: u64,
completion_tx: Option<Sender<Option<ExitStatus>>>, completion_tx: Option<Sender<Option<ExitStatus>>>,
cx: &App, cx: &App,
startup_script: Option<String>,
) -> Result<TerminalBuilder> { ) -> Result<TerminalBuilder> {
// If the parent environment doesn't have a locale set // If the parent environment doesn't have a locale set
// (As is the case when launched from a .app on MacOS), // (As is the case when launched from a .app on MacOS),
@ -517,6 +518,7 @@ impl TerminalBuilder {
last_hyperlink_search_position: None, last_hyperlink_search_position: None,
#[cfg(windows)] #[cfg(windows)]
shell_program, shell_program,
startup_script,
template: CopyTemplate { template: CopyTemplate {
shell, shell,
env, env,
@ -710,6 +712,7 @@ pub struct Terminal {
#[cfg(windows)] #[cfg(windows)]
shell_program: Option<String>, shell_program: Option<String>,
template: CopyTemplate, template: CopyTemplate,
startup_script: Option<String>,
} }
struct CopyTemplate { struct CopyTemplate {
@ -1983,6 +1986,7 @@ impl Terminal {
self.template.window_id, self.template.window_id,
None, None,
cx, cx,
self.startup_script.clone(),
) )
} }
} }
@ -2214,6 +2218,7 @@ mod tests {
0, 0,
Some(completion_tx), Some(completion_tx),
cx, cx,
None,
) )
.unwrap() .unwrap()
.subscribe(cx) .subscribe(cx)

View file

@ -1403,7 +1403,9 @@ impl WorkspaceDb {
name: name.into(), name: name.into(),
path: path.into(), path: path.into(),
language_name, language_name,
as_json: serde_json::Value::from_str(&raw_json).ok()? as_json: serde_json::Value::from_str(&raw_json).ok()?,
// todo refresh?
startup_script: Default::default(),
}))) })))
}) })
.await .await
@ -1427,7 +1429,9 @@ impl WorkspaceDb {
name: name.into(), name: name.into(),
path: path.into(), path: path.into(),
language_name: LanguageName::new(&language_name), language_name: LanguageName::new(&language_name),
as_json: serde_json::Value::from_str(&raw_json).ok()? as_json: serde_json::Value::from_str(&raw_json).ok()?,
// todo refresh?
startup_script: Default::default(),
}, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect()) }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
}) })
.await .await