diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 73c142c8ca..0f49e2d12b 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -10,14 +10,14 @@ use std::{ }; use async_trait::async_trait; -use collections::HashMap; +use collections::{FxHashMap, HashMap}; use gpui::{AsyncApp, SharedString}; use settings::WorktreeId; use crate::{LanguageName, ManifestName}; /// Represents a single toolchain. -#[derive(Clone, Debug, Eq)] +#[derive(Clone, Eq, Debug)] pub struct Toolchain { /// User-facing label pub name: SharedString, @@ -25,24 +25,41 @@ pub struct Toolchain { pub language_name: LanguageName, /// Full toolchain data (including language-specific details) pub as_json: serde_json::Value, + /// shell -> script + pub startup_script: FxHashMap, } impl std::hash::Hash for Toolchain { fn hash(&self, state: &mut H) { - self.name.hash(state); - self.path.hash(state); - self.language_name.hash(state); + let Self { + name, + path, + language_name, + as_json: _, + startup_script: _, + } = self; + name.hash(state); + path.hash(state); + language_name.hash(state); } } impl PartialEq for Toolchain { 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. // 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.path, &other.language_name, + &other.startup_script, )) } } @@ -82,7 +99,7 @@ pub trait LocalLanguageToolchainStore: Send + Sync + 'static { ) -> Option; } -#[async_trait(?Send )] +#[async_trait(?Send)] impl LanguageToolchainStore for T { async fn active_toolchain( self: Arc, diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index d21b5dabd3..a87049be22 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -878,6 +878,8 @@ impl ToolchainLister for PythonToolchainProvider { path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(), language_name: LanguageName::new("Python"), as_json: serde_json::to_value(toolchain).ok()?, + startup_script: std::iter::once(("fish".to_owned(), "test".to_owned())) + .collect(), }) }) .collect(); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 77edd1e8a6..12be1d17c6 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -293,6 +293,7 @@ impl DapStore { binary.cwd.as_deref(), binary.envs, path_style, + None, ); Ok(DebugAdapterBinary { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 6dcd07482e..a46b723bdf 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9204,6 +9204,7 @@ fn python_lang(fs: Arc) -> Arc { path: venv_path.to_string_lossy().into_owned().into(), language_name: LanguageName(SharedString::new_static("Python")), as_json: serde_json::Value::Null, + startup_script: Default::default(), }) } } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 5543033415..61f62f217a 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -11,7 +11,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{Shell, ShellBuilder, SpawnInTerminal}; +use task::{Shell, ShellBuilder, SpawnInTerminal, system_shell}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; @@ -173,6 +173,7 @@ impl Project { path.as_deref(), env, path_style, + None, ); env = HashMap::default(); if let Some(envs) = envs { @@ -213,6 +214,7 @@ impl Project { cx.entity_id().as_u64(), Some(completion_tx), cx, + None, ) .map(|builder| { let terminal_handle = cx.new(|cx| builder.subscribe(cx)); @@ -276,21 +278,33 @@ impl Project { let toolchain = project_path_context.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); cx.spawn(async move |project, cx| { - let toolchain = maybe!(async { - let toolchain = toolchain?.await?; + let shell = match &ssh_details { + 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; - - let (spawn_task, shell) = { + let activation_script = scripts.as_ref().and_then(|it| it.get(&shell)); + let shell = { match ssh_details { Some(SshDetails { host, ssh_command, envs, path_style, - shell, + shell: _, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); @@ -308,28 +322,37 @@ impl Project { path.as_deref(), env, path_style, + activation_script.map(String::as_str), ); env = HashMap::default(); if let Some(envs) = envs { env.extend(envs); } - ( - Option::::None, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) + Shell::WithArguments { + program, + 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| { TerminalBuilder::new( local_path.map(|path| path.to_path_buf()), - spawn_task, + None, shell, env, settings.cursor_shape.unwrap_or_default(), @@ -339,6 +362,7 @@ impl Project { cx.entity_id().as_u64(), None, cx, + None, ) .map(|builder| { let terminal_handle = cx.new(|cx| builder.subscribe(cx)); @@ -447,6 +471,7 @@ impl Project { path.as_deref(), env, path_style, + None, ); let mut command = std::process::Command::new(command); command.args(args); @@ -479,6 +504,7 @@ pub fn wrap_for_ssh( path: Option<&Path>, env: HashMap, path_style: PathStyle, + activation_script: Option<&str>, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { let command: Option> = 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 path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string(); // 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("/"); - format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}") + format!("cd \"$HOME/{trimmed_path}\";{activation_script} {env_changes} {to_run}") } else { - format!("cd \"{path}\"; {env_changes} {to_run}") + format!("cd \"{path}\";{activation_script} {env_changes} {to_run}") } } 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()); diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index ac87e64248..54610ad9a6 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -138,6 +138,7 @@ impl ToolchainStore { // Do we need to convert path to native string? path: PathBuf::from(toolchain.path).to_proto().into(), as_json: serde_json::Value::from_str(&toolchain.raw_json)?, + startup_script: toolchain.activation_script.into_iter().collect(), language_name, }; let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); @@ -178,6 +179,7 @@ impl ToolchainStore { name: toolchain.name.into(), path: path.to_proto(), 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(), path: path.to_proto(), raw_json: toolchain.as_json.to_string(), + activation_script: toolchain.startup_script.into_iter().collect(), } }) .collect::>(); @@ -449,6 +452,7 @@ impl RemoteToolchainStore { name: toolchain.name.into(), path: path.to_proto(), 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()), }) @@ -501,6 +505,7 @@ impl RemoteToolchainStore { .to_string() .into(), as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, + startup_script: toolchain.activation_script.into_iter().collect(), }) }) .collect(); @@ -557,6 +562,7 @@ impl RemoteToolchainStore { .to_string() .into(), as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, + startup_script: toolchain.activation_script.into_iter().collect(), }) }) }) diff --git a/crates/proto/proto/toolchain.proto b/crates/proto/proto/toolchain.proto index 08844a307a..004afc7823 100644 --- a/crates/proto/proto/toolchain.proto +++ b/crates/proto/proto/toolchain.proto @@ -12,6 +12,7 @@ message Toolchain { string name = 1; string path = 2; string raw_json = 3; + map activation_script = 4; } message ToolchainGroup { diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index de4ddc00f4..0ebba1b3c8 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -178,7 +178,7 @@ impl ShellKind { } } -fn system_shell() -> String { +pub fn system_shell() -> String { if cfg!(target_os = "windows") { // `alacritty_terminal` uses this as default on Windows. See: // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130 diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index eb9e59f087..2dd771b9e5 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -22,7 +22,7 @@ pub use debug_format::{ AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, Request, TcpArgumentsTemplate, ZedDebugConfig, }; -pub use shell_builder::{ShellBuilder, ShellKind}; +pub use shell_builder::{ShellBuilder, ShellKind, system_shell}; pub use task_template::{ DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 292b9f8728..1936eaa2d1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -354,6 +354,7 @@ impl TerminalBuilder { window_id: u64, completion_tx: Option>>, cx: &App, + startup_script: Option, ) -> Result { // If the parent environment doesn't have a locale set // (As is the case when launched from a .app on MacOS), @@ -517,6 +518,7 @@ impl TerminalBuilder { last_hyperlink_search_position: None, #[cfg(windows)] shell_program, + startup_script, template: CopyTemplate { shell, env, @@ -710,6 +712,7 @@ pub struct Terminal { #[cfg(windows)] shell_program: Option, template: CopyTemplate, + startup_script: Option, } struct CopyTemplate { @@ -1983,6 +1986,7 @@ impl Terminal { self.template.window_id, None, cx, + self.startup_script.clone(), ) } } @@ -2214,6 +2218,7 @@ mod tests { 0, Some(completion_tx), cx, + None, ) .unwrap() .subscribe(cx) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 39a1e08c93..dc3a502ef8 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1403,7 +1403,9 @@ impl WorkspaceDb { name: name.into(), path: path.into(), 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 @@ -1427,7 +1429,9 @@ impl WorkspaceDb { name: name.into(), path: path.into(), 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()) }) .await