From 835e5ba662e5bade1a4bd91a45e64faa7f64aff4 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 28 Aug 2025 16:40:43 +0200 Subject: [PATCH] Inject venv environment via the toolchain (#36576) Instead of manually constructing the venv we now ask the python toolchain for the relevant information, unifying the approach of vent inspection Fixes https://github.com/zed-industries/zed/issues/27350 Release Notes: - Improved the detection of python virtual environments for terminals and tasks in remote projects. --- crates/agent2/src/tools/terminal_tool.rs | 8 +- crates/assistant_tools/src/terminal_tool.rs | 9 +- crates/debugger_ui/src/session/running.rs | 15 +- crates/language/src/toolchain.rs | 30 +- crates/languages/src/python.rs | 44 +- crates/project/src/debugger/dap_store.rs | 1 + crates/project/src/project_tests.rs | 3 + crates/project/src/terminals.rs | 789 ++++++++---------- crates/proto/build.rs | 1 + crates/remote/src/remote_client.rs | 12 +- crates/remote/src/transport/ssh.rs | 13 +- crates/task/src/shell_builder.rs | 39 +- crates/terminal/src/terminal.rs | 63 +- crates/terminal_view/src/persistence.rs | 12 +- crates/terminal_view/src/terminal_panel.rs | 253 ++++-- .../src/terminal_path_like_target.rs | 6 +- crates/terminal_view/src/terminal_view.rs | 29 +- crates/util/src/util.rs | 12 + crates/workspace/src/persistence.rs | 16 +- 19 files changed, 704 insertions(+), 651 deletions(-) diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index f41b909d0b286b80bb3c9e8e8c18d0d03f3e05c7..2270a7c32f076bee774c7c8177c4276985adc0b6 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -2,7 +2,7 @@ use agent_client_protocol as acp; use anyhow::Result; use futures::{FutureExt as _, future::Shared}; use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::{Project, terminals::TerminalKind}; +use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ @@ -144,14 +144,14 @@ impl AgentTool for TerminalTool { let terminal = self .project .update(cx, |project, cx| { - project.create_terminal( - TerminalKind::Task(task::SpawnInTerminal { + project.create_terminal_task( + task::SpawnInTerminal { command: Some(program), args, cwd: working_dir.clone(), env, ..Default::default() - }), + }, cx, ) })? diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index b28e55e78aef40554a8ebe60108bd81da3f9d95a..774f32426540e077e5bde72081db789329f86262 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -15,7 +15,7 @@ use language::LineEnding; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use portable_pty::{CommandBuilder, PtySize, native_pty_system}; -use project::{Project, terminals::TerminalKind}; +use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -213,17 +213,16 @@ impl Tool for TerminalTool { async move |cx| { let program = program.await; let env = env.await; - project .update(cx, |project, cx| { - project.create_terminal( - TerminalKind::Task(task::SpawnInTerminal { + project.create_terminal_task( + task::SpawnInTerminal { command: Some(program), args, cwd, env, ..Default::default() - }), + }, cx, ) })? diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 25146dc7e42bd2359ccd4a16644ba9e41565a0a8..46e5f35aecb0ad13e55ceb8d2dd12e7ae791a2c5 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -36,7 +36,6 @@ use module_list::ModuleList; use project::{ DebugScenarioContext, Project, WorktreeId, debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus}, - terminals::TerminalKind, }; use rpc::proto::ViewId; use serde_json::Value; @@ -1040,12 +1039,11 @@ impl RunningState { }; let terminal = project .update(cx, |project, cx| { - project.create_terminal( - TerminalKind::Task(task_with_shell.clone()), + project.create_terminal_task( + task_with_shell.clone(), cx, ) - })? - .await?; + })?.await?; let terminal_view = cx.new_window_entity(|window, cx| { TerminalView::new( @@ -1189,7 +1187,7 @@ impl RunningState { .filter(|title| !title.is_empty()) .or_else(|| command.clone()) .unwrap_or_else(|| "Debug terminal".to_string()); - let kind = TerminalKind::Task(task::SpawnInTerminal { + let kind = task::SpawnInTerminal { id: task::TaskId("debug".to_string()), full_label: title.clone(), label: title.clone(), @@ -1207,12 +1205,13 @@ impl RunningState { show_summary: false, show_command: false, show_rerun: false, - }); + }; let workspace = self.workspace.clone(); let weak_project = project.downgrade(); - let terminal_task = project.update(cx, |project, cx| project.create_terminal(kind, cx)); + let terminal_task = + project.update(cx, |project, cx| project.create_terminal_task(kind, cx)); let terminal_task = cx.spawn_in(window, async move |_, cx| { let terminal = terminal_task.await?; diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 66879e56da0a4a7cdac5b64acbce9e31313bf373..2a8dfd58418812b94c625845dce9724e145c7388 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -11,13 +11,14 @@ use std::{ use async_trait::async_trait; use collections::HashMap; +use fs::Fs; 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, @@ -29,21 +30,29 @@ pub struct Toolchain { 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: _, + } = 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: _, + } = 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(&( - &other.name, - &other.path, - &other.language_name, - )) + (name, path, language_name).eq(&(&other.name, &other.path, &other.language_name)) } } @@ -59,6 +68,7 @@ pub trait ToolchainLister: Send + Sync { fn term(&self) -> SharedString; /// Returns the name of the manifest file for this toolchain. fn manifest_name(&self) -> ManifestName; + async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option; } #[async_trait(?Send)] @@ -82,7 +92,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 0f78d5c5dfaaf3f24a337cab32ed9656354ac911..37d38de9dab6bb5968b446e7009a42c5f2e86e86 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -2,6 +2,7 @@ use anyhow::{Context as _, ensure}; use anyhow::{Result, anyhow}; use async_trait::async_trait; use collections::HashMap; +use futures::AsyncBufReadExt; use gpui::{App, Task}; use gpui::{AsyncApp, SharedString}; use language::Toolchain; @@ -30,8 +31,6 @@ use std::{ borrow::Cow, ffi::OsString, fmt::Write, - fs, - io::{self, BufRead}, path::{Path, PathBuf}, sync::Arc, }; @@ -741,14 +740,16 @@ fn env_priority(kind: Option) -> usize { /// Return the name of environment declared in Option { - fs::File::open(worktree_root.join(".venv")) - .and_then(|file| { - let mut venv_name = String::new(); - io::BufReader::new(file).read_line(&mut venv_name)?; - Ok(venv_name.trim().to_string()) - }) - .ok() +async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option { + let file = async_fs::File::open(worktree_root.join(".venv")) + .await + .ok()?; + let mut venv_name = String::new(); + smol::io::BufReader::new(file) + .read_line(&mut venv_name) + .await + .ok()?; + Some(venv_name.trim().to_string()) } #[async_trait] @@ -793,7 +794,7 @@ impl ToolchainLister for PythonToolchainProvider { .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard)); let wr = worktree_root; - let wr_venv = get_worktree_venv_declaration(&wr); + let wr_venv = get_worktree_venv_declaration(&wr).await; // Sort detected environments by: // environment name matching activation file (/.venv) // environment project dir matching worktree_root @@ -858,7 +859,7 @@ impl ToolchainLister for PythonToolchainProvider { .into_iter() .filter_map(|toolchain| { let mut name = String::from("Python"); - if let Some(ref version) = toolchain.version { + if let Some(version) = &toolchain.version { _ = write!(name, " {version}"); } @@ -879,7 +880,7 @@ impl ToolchainLister for PythonToolchainProvider { name: name.into(), path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(), language_name: LanguageName::new("Python"), - as_json: serde_json::to_value(toolchain).ok()?, + as_json: serde_json::to_value(toolchain.clone()).ok()?, }) }) .collect(); @@ -893,6 +894,23 @@ impl ToolchainLister for PythonToolchainProvider { fn term(&self) -> SharedString { self.term.clone() } + async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option { + let toolchain = serde_json::from_value::( + toolchain.as_json.clone(), + ) + .ok()?; + let mut activation_script = None; + if let Some(prefix) = &toolchain.prefix { + #[cfg(not(target_os = "windows"))] + let path = prefix.join(BINARY_DIR).join("activate"); + #[cfg(target_os = "windows")] + let path = prefix.join(BINARY_DIR).join("activate.ps1"); + if fs.is_file(&path).await { + activation_script = Some(format!(". {}", path.display())); + } + } + activation_script + } } pub struct EnvironmentApi<'a> { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index d8c6d3acc1116e9a97b2f6ca3fc54ec098029cbe..859574c82a5b4470d477df555b314498cbfcd0e0 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -276,6 +276,7 @@ impl DapStore { &binary.arguments, &binary.envs, binary.cwd.map(|path| path.display().to_string()), + None, port_forwarding, ) })??; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a8f911883d619214a35f3ef5d80f83d6dc1b3894..c814d6207e92608c13502a4da3a0781836acce0e 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9222,6 +9222,9 @@ fn python_lang(fs: Arc) -> Arc { fn manifest_name(&self) -> ManifestName { SharedString::new_static("pyproject.toml").into() } + async fn activation_script(&self, _: &Toolchain, _: &dyn Fs) -> Option { + None + } } Arc::new( Language::new( diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 7e1be67e21380e8b1ae751600b3553a527067e46..aad5ce941125c2c747df3a76473a9dbffba0b80e 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,44 +1,28 @@ -use crate::{Project, ProjectPath}; -use anyhow::{Context as _, Result}; +use anyhow::Result; use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; +use itertools::Itertools; use language::LanguageName; use remote::RemoteClient; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ - env::{self}, + borrow::Cow, path::{Path, PathBuf}, sync::Arc, }; use task::{Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ - TaskState, TaskStatus, Terminal, TerminalBuilder, - terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, + TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; -use util::{ResultExt, paths::RemotePathBuf}; +use util::{get_default_system_shell, get_system_shell, maybe}; -/// The directory inside a Python virtual environment that contains executables -const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") { - "Scripts" -} else { - "bin" -}; +use crate::{Project, ProjectPath}; pub struct Terminals { pub(crate) local_handles: Vec>, } -/// Terminals are opened either for the users shell, or to run a task. - -#[derive(Debug)] -pub enum TerminalKind { - /// Run a shell at the given path (or $HOME if None) - Shell(Option), - /// Run a task. - Task(SpawnInTerminal), -} - impl Project { pub fn active_project_directory(&self, cx: &App) -> Option> { self.active_entry() @@ -58,54 +42,33 @@ impl Project { } } - pub fn create_terminal( + pub fn create_terminal_task( &mut self, - kind: TerminalKind, + spawn_task: SpawnInTerminal, cx: &mut Context, ) -> Task>> { - let path: Option> = match &kind { - TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), - TerminalKind::Task(spawn_task) => { - if let Some(cwd) = &spawn_task.cwd { - Some(Arc::from(cwd.as_ref())) - } else { - self.active_project_directory(cx) - } - } - }; - - let mut settings_location = None; - if let Some(path) = path.as_ref() - && let Some((worktree, _)) = self.find_worktree(path, cx) - { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, + let is_via_remote = self.remote_client.is_some(); + let project_path_context = self + .active_entry() + .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx)) + .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id())) + .map(|worktree_id| ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), }); - } - let venv = TerminalSettings::get(settings_location, cx) - .detect_venv - .clone(); - cx.spawn(async move |project, cx| { - let python_venv_directory = if let Some(path) = path { - project - .update(cx, |this, cx| this.python_venv_directory(path, venv, cx))? - .await + let path: Option> = if let Some(cwd) = &spawn_task.cwd { + if is_via_remote { + Some(Arc::from(cwd.as_ref())) } else { - None - }; - project.update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, cx) - })? - }) - } + let cwd = cwd.to_string_lossy(); + let tilde_substituted = shellexpand::tilde(&cwd); + Some(Arc::from(Path::new(tilde_substituted.as_ref()))) + } + } else { + self.active_project_directory(cx) + }; - pub fn terminal_settings<'a>( - &'a self, - path: &'a Option, - cx: &'a App, - ) -> &'a TerminalSettings { let mut settings_location = None; if let Some(path) = path.as_ref() && let Some((worktree, _)) = self.find_worktree(path, cx) @@ -115,74 +78,176 @@ impl Project { path, }); } - TerminalSettings::get(settings_location, cx) - } + let settings = TerminalSettings::get(settings_location, cx).clone(); + let detect_venv = settings.detect_venv.as_option().is_some(); - pub fn exec_in_shell(&self, command: String, cx: &App) -> Result { - let path = self.first_project_directory(cx); - let remote_client = self.remote_client.as_ref(); - let settings = self.terminal_settings(&path, cx).clone(); - let remote_shell = remote_client - .as_ref() - .and_then(|remote_client| remote_client.read(cx).shell()); - let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive(); - let (command, args) = builder.build(Some(command), &Vec::new()); + let (completion_tx, completion_rx) = bounded(1); + // Start with the environment that we might have inherited from the Zed CLI. let mut env = self .environment .read(cx) .get_cli_environment() .unwrap_or_default(); + // Then extend it with the explicit env variables from the settings, so they take + // precedence. env.extend(settings.env); - match remote_client { - Some(remote_client) => { - let command_template = - remote_client - .read(cx) - .build_command(Some(command), &args, &env, None, None)?; - let mut command = std::process::Command::new(command_template.program); - command.args(command_template.args); - command.envs(command_template.env); - Ok(command) - } - None => { - let mut command = std::process::Command::new(command); - command.args(args); - command.envs(env); - if let Some(path) = path { - command.current_dir(path); - } - Ok(command) - } - } + let local_path = if is_via_remote { None } else { path.clone() }; + let task_state = Some(TaskState { + id: spawn_task.id, + full_label: spawn_task.full_label, + label: spawn_task.label, + command_label: spawn_task.command_label, + hide: spawn_task.hide, + status: TaskStatus::Running, + show_summary: spawn_task.show_summary, + show_command: spawn_task.show_command, + show_rerun: spawn_task.show_rerun, + completion_rx, + }); + let remote_client = self.remote_client.clone(); + let shell = match &remote_client { + Some(remote_client) => remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell), + None => match &settings.shell { + Shell::Program(program) => program.clone(), + Shell::WithArguments { + program, + args: _, + title_override: _, + } => program.clone(), + Shell::System => get_system_shell(), + }, + }; + + let toolchain = project_path_context + .filter(|_| detect_venv) + .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); + let lang_registry = self.languages.clone(); + let fs = self.fs.clone(); + cx.spawn(async move |project, cx| { + let activation_script = maybe!(async { + let toolchain = toolchain?.await?; + lang_registry + .language_for_name(&toolchain.language_name.0) + .await + .ok()? + .toolchain_lister()? + .activation_script(&toolchain, fs.as_ref()) + .await + }) + .await; + + project.update(cx, move |this, cx| { + let shell = { + env.extend(spawn_task.env); + match remote_client { + Some(remote_client) => create_remote_shell( + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), + &mut env, + path, + remote_client, + activation_script.clone(), + cx, + )?, + None => match activation_script.clone() { + Some(activation_script) => { + let to_run = if let Some(command) = spawn_task.command { + let command: Option> = shlex::try_quote(&command).ok(); + let args = spawn_task + .args + .iter() + .filter_map(|arg| shlex::try_quote(arg).ok()); + command.into_iter().chain(args).join(" ") + } else { + format!("exec {shell} -l") + }; + Shell::WithArguments { + program: get_default_system_shell(), + args: vec![ + "-c".to_owned(), + format!("{activation_script}; {to_run}",), + ], + title_override: None, + } + } + None => { + if let Some(program) = spawn_task.command { + Shell::WithArguments { + program, + args: spawn_task.args, + title_override: None, + } + } else { + Shell::System + } + } + }, + } + }; + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + task_state, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_via_remote, + cx.entity_id().as_u64(), + Some(completion_tx), + cx, + activation_script, + ) + .map(|builder| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; + + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } + }) + .detach(); + + terminal_handle + }) + })? + }) } - pub fn create_terminal_with_venv( + pub fn create_terminal_shell( &mut self, - kind: TerminalKind, - python_venv_directory: Option, + cwd: Option, cx: &mut Context, - ) -> Result> { + ) -> Task>> { + let project_path_context = self + .active_entry() + .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx)) + .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id())) + .map(|worktree_id| ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }); + let path = cwd.map(|p| Arc::from(&*p)); let is_via_remote = self.remote_client.is_some(); - let path: Option> = match &kind { - TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), - TerminalKind::Task(spawn_task) => { - if let Some(cwd) = &spawn_task.cwd { - if is_via_remote { - Some(Arc::from(cwd.as_ref())) - } else { - let cwd = cwd.to_string_lossy(); - let tilde_substituted = shellexpand::tilde(&cwd); - Some(Arc::from(Path::new(tilde_substituted.as_ref()))) - } - } else { - self.active_project_directory(cx) - } - } - }; - let mut settings_location = None; if let Some(path) = path.as_ref() && let Some((worktree, _)) = self.find_worktree(path, cx) @@ -193,8 +258,7 @@ impl Project { }); } let settings = TerminalSettings::get(settings_location, cx).clone(); - - let (completion_tx, completion_rx) = bounded(1); + let detect_venv = settings.detect_venv.as_option().is_some(); // Start with the environment that we might have inherited from the Zed CLI. let mut env = self @@ -208,107 +272,111 @@ impl Project { let local_path = if is_via_remote { None } else { path.clone() }; - let mut python_venv_activate_command = Task::ready(None); - + let toolchain = project_path_context + .filter(|_| detect_venv) + .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); let remote_client = self.remote_client.clone(); - let spawn_task; - let shell; - match kind { - TerminalKind::Shell(_) => { - if let Some(python_venv_directory) = &python_venv_directory { - python_venv_activate_command = self.python_activate_command( - python_venv_directory, - &settings.detect_venv, - &settings.shell, - cx, - ); - } - - spawn_task = None; - shell = match remote_client { - Some(remote_client) => { - create_remote_shell(None, &mut env, path, remote_client, cx)? - } - None => settings.shell, - }; - } - TerminalKind::Task(task) => { - env.extend(task.env); - - if let Some(venv_path) = &python_venv_directory { - env.insert( - "VIRTUAL_ENV".to_string(), - venv_path.to_string_lossy().to_string(), - ); - } - - spawn_task = Some(TaskState { - id: task.id, - full_label: task.full_label, - label: task.label, - command_label: task.command_label, - hide: task.hide, - status: TaskStatus::Running, - show_summary: task.show_summary, - show_command: task.show_command, - show_rerun: task.show_rerun, - completion_rx, - }); - shell = match remote_client { - Some(remote_client) => { - let path_style = remote_client.read(cx).path_style(); - if let Some(venv_directory) = &python_venv_directory - && let Ok(str) = - shlex::try_quote(venv_directory.to_string_lossy().as_ref()) - { - let path = - RemotePathBuf::new(PathBuf::from(str.to_string()), path_style) - .to_string(); - env.insert("PATH".into(), format!("{}:$PATH ", path)); - } + let shell = match &remote_client { + Some(remote_client) => remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell), + None => match &settings.shell { + Shell::Program(program) => program.clone(), + Shell::WithArguments { + program, + args: _, + title_override: _, + } => program.clone(), + Shell::System => get_system_shell(), + }, + }; - create_remote_shell( - task.command.as_ref().map(|command| (command, &task.args)), + let lang_registry = self.languages.clone(); + let fs = self.fs.clone(); + cx.spawn(async move |project, cx| { + let activation_script = maybe!(async { + let toolchain = toolchain?.await?; + let language = lang_registry + .language_for_name(&toolchain.language_name.0) + .await + .ok(); + let lister = language?.toolchain_lister(); + lister?.activation_script(&toolchain, fs.as_ref()).await + }) + .await; + project.update(cx, move |this, cx| { + let shell = { + match remote_client { + Some(remote_client) => create_remote_shell( + None, &mut env, path, remote_client, + activation_script.clone(), cx, - )? + )?, + None => match activation_script.clone() { + Some(activation_script) => Shell::WithArguments { + program: get_default_system_shell(), + args: vec![ + "-c".to_owned(), + format!("{activation_script}; exec {shell} -l",), + ], + title_override: Some(shell.into()), + }, + None => settings.shell, + }, } - None => { - if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join(PYTHON_VENV_BIN_DIR)) - .log_err(); + }; + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + None, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_via_remote, + cx.entity_id().as_u64(), + None, + cx, + activation_script, + ) + .map(|builder| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; + + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); } + }) + .detach(); - if let Some(program) = task.command { - Shell::WithArguments { - program, - args: task.args, - title_override: None, - } - } else { - Shell::System - } - } - }; - } - }; - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - python_venv_directory, - spawn_task, - shell, - env, - settings.cursor_shape.unwrap_or_default(), - settings.alternate_scroll, - settings.max_scroll_history_lines, - is_via_remote, - cx.entity_id().as_u64(), - completion_tx, - cx, - ) - .map(|builder| { + terminal_handle + }) + })? + }) + } + + pub fn clone_terminal( + &mut self, + terminal: &Entity, + cx: &mut Context<'_, Project>, + cwd: impl FnOnce() -> Option, + ) -> Result> { + terminal.read(cx).clone_builder(cx, cwd).map(|builder| { let terminal_handle = cx.new(|cx| builder.subscribe(cx)); self.terminals @@ -329,211 +397,72 @@ impl Project { }) .detach(); - self.activate_python_virtual_environment( - python_venv_activate_command, - &terminal_handle, - cx, - ); - terminal_handle }) } - fn python_venv_directory( - &self, - abs_path: Arc, - venv_settings: VenvSettings, - cx: &Context, - ) -> Task> { - cx.spawn(async move |this, cx| { - if let Some((worktree, relative_path)) = this - .update(cx, |this, cx| this.find_worktree(&abs_path, cx)) - .ok()? - { - let toolchain = this - .update(cx, |this, cx| { - this.active_toolchain( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path.into(), - }, - LanguageName::new("Python"), - cx, - ) - }) - .ok()? - .await; - - if let Some(toolchain) = toolchain { - let toolchain_path = Path::new(toolchain.path.as_ref()); - return Some(toolchain_path.parent()?.parent()?.to_path_buf()); - } - } - let venv_settings = venv_settings.as_option()?; - this.update(cx, move |this, cx| { - if let Some(path) = this.find_venv_in_worktree(&abs_path, &venv_settings, cx) { - return Some(path); - } - this.find_venv_on_filesystem(&abs_path, &venv_settings, cx) - }) - .ok() - .flatten() - }) - } - - fn find_venv_in_worktree( - &self, - abs_path: &Path, - venv_settings: &terminal_settings::VenvSettingsContent, - cx: &App, - ) -> Option { - venv_settings - .directories - .iter() - .map(|name| abs_path.join(name)) - .find(|venv_path| { - let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR); - self.find_worktree(&bin_path, cx) - .and_then(|(worktree, relative_path)| { - worktree.read(cx).entry_for_path(&relative_path) - }) - .is_some_and(|entry| entry.is_dir()) - }) - } - - fn find_venv_on_filesystem( - &self, - abs_path: &Path, - venv_settings: &terminal_settings::VenvSettingsContent, - cx: &App, - ) -> Option { - let (worktree, _) = self.find_worktree(abs_path, cx)?; - let fs = worktree.read(cx).as_local()?.fs(); - venv_settings - .directories - .iter() - .map(|name| abs_path.join(name)) - .find(|venv_path| { - let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR); - // One-time synchronous check is acceptable for terminal/task initialization - smol::block_on(fs.metadata(&bin_path)) - .ok() - .flatten() - .is_some_and(|meta| meta.is_dir) - }) - } - - fn activate_script_kind(shell: Option<&str>) -> ActivateScript { - let shell_env = std::env::var("SHELL").ok(); - let shell_path = shell.or_else(|| shell_env.as_deref()); - let shell = std::path::Path::new(shell_path.unwrap_or("")) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(""); - match shell { - "fish" => ActivateScript::Fish, - "tcsh" => ActivateScript::Csh, - "nu" => ActivateScript::Nushell, - "powershell" | "pwsh" => ActivateScript::PowerShell, - _ => ActivateScript::Default, + pub fn terminal_settings<'a>( + &'a self, + path: &'a Option, + cx: &'a App, + ) -> &'a TerminalSettings { + let mut settings_location = None; + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = self.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); } + TerminalSettings::get(settings_location, cx) } - fn python_activate_command( - &self, - venv_base_directory: &Path, - venv_settings: &VenvSettings, - shell: &Shell, - cx: &mut App, - ) -> Task> { - let Some(venv_settings) = venv_settings.as_option() else { - return Task::ready(None); - }; - let activate_keyword = match venv_settings.activate_script { - terminal_settings::ActivateScript::Default => match std::env::consts::OS { - "windows" => ".", - _ => ".", - }, - terminal_settings::ActivateScript::Nushell => "overlay use", - terminal_settings::ActivateScript::PowerShell => ".", - terminal_settings::ActivateScript::Pyenv => "pyenv", - _ => "source", - }; - let script_kind = - if venv_settings.activate_script == terminal_settings::ActivateScript::Default { - match shell { - Shell::Program(program) => Self::activate_script_kind(Some(program)), - Shell::WithArguments { - program, - args: _, - title_override: _, - } => Self::activate_script_kind(Some(program)), - Shell::System => Self::activate_script_kind(None), - } - } else { - venv_settings.activate_script - }; - - let activate_script_name = match script_kind { - terminal_settings::ActivateScript::Default - | terminal_settings::ActivateScript::Pyenv => "activate", - terminal_settings::ActivateScript::Csh => "activate.csh", - terminal_settings::ActivateScript::Fish => "activate.fish", - terminal_settings::ActivateScript::Nushell => "activate.nu", - terminal_settings::ActivateScript::PowerShell => "activate.ps1", - }; + pub fn exec_in_shell(&self, command: String, cx: &App) -> Result { + let path = self.first_project_directory(cx); + let remote_client = self.remote_client.as_ref(); + let settings = self.terminal_settings(&path, cx).clone(); + let remote_shell = remote_client + .as_ref() + .and_then(|remote_client| remote_client.read(cx).shell()); + let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive(); + let (command, args) = builder.build(Some(command), &Vec::new()); - let line_ending = match std::env::consts::OS { - "windows" => "\r", - _ => "\n", - }; + let mut env = self + .environment + .read(cx) + .get_cli_environment() + .unwrap_or_default(); + env.extend(settings.env); - if venv_settings.venv_name.is_empty() { - let path = venv_base_directory - .join(PYTHON_VENV_BIN_DIR) - .join(activate_script_name) - .to_string_lossy() - .to_string(); - - let is_valid_path = self.resolve_abs_path(path.as_ref(), cx); - cx.background_spawn(async move { - let quoted = shlex::try_quote(&path).ok()?; - if is_valid_path.await.is_some_and(|meta| meta.is_file()) { - Some(format!( - "{} {} ; clear{}", - activate_keyword, quoted, line_ending - )) - } else { - None + match remote_client { + Some(remote_client) => { + let command_template = remote_client.read(cx).build_command( + Some(command), + &args, + &env, + None, + // todo + None, + None, + )?; + let mut command = std::process::Command::new(command_template.program); + command.args(command_template.args); + command.envs(command_template.env); + Ok(command) + } + None => { + let mut command = std::process::Command::new(command); + command.args(args); + command.envs(env); + if let Some(path) = path { + command.current_dir(path); } - }) - } else { - Task::ready(Some(format!( - "{activate_keyword} {activate_script_name} {name}; clear{line_ending}", - name = venv_settings.venv_name - ))) + Ok(command) + } } } - fn activate_python_virtual_environment( - &self, - command: Task>, - terminal_handle: &Entity, - cx: &mut App, - ) { - terminal_handle.update(cx, |_, cx| { - cx.spawn(async move |this, cx| { - if let Some(command) = command.await { - this.update(cx, |this, _| { - this.input(command.into_bytes()); - }) - .ok(); - } - }) - .detach() - }); - } - pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } @@ -544,6 +473,7 @@ fn create_remote_shell( env: &mut HashMap, working_directory: Option>, remote_client: Entity, + activation_script: Option, cx: &mut App, ) -> Result { // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed @@ -563,6 +493,7 @@ fn create_remote_shell( args.as_slice(), env, working_directory.map(|path| path.display().to_string()), + activation_script, None, )?; *env = command.env; @@ -576,57 +507,3 @@ fn create_remote_shell( title_override: Some(format!("{} — Terminal", host).into()), }) } - -fn add_environment_path(env: &mut HashMap, new_path: &Path) -> Result<()> { - let mut env_paths = vec![new_path.to_path_buf()]; - if let Some(path) = env.get("PATH").or(env::var("PATH").ok().as_ref()) { - let mut paths = std::env::split_paths(&path).collect::>(); - env_paths.append(&mut paths); - } - - let paths = std::env::join_paths(env_paths).context("failed to create PATH env variable")?; - env.insert("PATH".to_string(), paths.to_string_lossy().to_string()); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use collections::HashMap; - - #[test] - fn test_add_environment_path_with_existing_path() { - let tmp_path = std::path::PathBuf::from("/tmp/new"); - let mut env = HashMap::default(); - let old_path = if cfg!(windows) { - "/usr/bin;/usr/local/bin" - } else { - "/usr/bin:/usr/local/bin" - }; - env.insert("PATH".to_string(), old_path.to_string()); - env.insert("OTHER".to_string(), "aaa".to_string()); - - super::add_environment_path(&mut env, &tmp_path).unwrap(); - if cfg!(windows) { - assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new;{}", old_path)); - } else { - assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new:{}", old_path)); - } - assert_eq!(env.get("OTHER").unwrap(), "aaa"); - } - - #[test] - fn test_add_environment_path_with_empty_path() { - let tmp_path = std::path::PathBuf::from("/tmp/new"); - let mut env = HashMap::default(); - env.insert("OTHER".to_string(), "aaa".to_string()); - let os_path = std::env::var("PATH").unwrap(); - super::add_environment_path(&mut env, &tmp_path).unwrap(); - if cfg!(windows) { - assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new;{}", os_path)); - } else { - assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new:{}", os_path)); - } - assert_eq!(env.get("OTHER").unwrap(), "aaa"); - } -} diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 2997e302b6e62348eef6d65f158b22f00c992f7c..184d0e53d57bf06f5cd3667adea86ec4a2dde282 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -1,4 +1,5 @@ fn main() { + println!("cargo:rerun-if-changed=proto"); let mut build = prost_build::Config::new(); build .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index dd529ca87499b0daf2061fd990f7149828e3fce4..2b8d9e4a94fb9988e801c5ef9202ee603959d36b 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -757,6 +757,7 @@ impl RemoteClient { args: &[String], env: &HashMap, working_dir: Option, + activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result { let Some(connection) = self @@ -766,7 +767,14 @@ impl RemoteClient { else { return Err(anyhow!("no connection")); }; - connection.build_command(program, args, env, working_dir, port_forward) + connection.build_command( + program, + args, + env, + working_dir, + activation_script, + port_forward, + ) } pub fn upload_directory( @@ -998,6 +1006,7 @@ pub(crate) trait RemoteConnection: Send + Sync { args: &[String], env: &HashMap, working_dir: Option, + activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result; fn connection_options(&self) -> SshConnectionOptions; @@ -1364,6 +1373,7 @@ mod fake { args: &[String], env: &HashMap, _: Option, + _: Option, _: Option<(u16, String, u16)>, ) -> Result { let ssh_program = program.unwrap_or_else(|| "sh".to_string()); diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 193b96497375e5dee19aacaff114ba79d78a7191..750fc6dc586c227c6461ab4650c1b46b6bb01dc6 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -30,7 +30,10 @@ use std::{ time::Instant, }; use tempfile::TempDir; -use util::paths::{PathStyle, RemotePathBuf}; +use util::{ + get_default_system_shell, + paths::{PathStyle, RemotePathBuf}, +}; pub(crate) struct SshRemoteConnection { socket: SshSocket, @@ -113,6 +116,7 @@ impl RemoteConnection for SshRemoteConnection { input_args: &[String], input_env: &HashMap, working_dir: Option, + activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result { use std::fmt::Write as _; @@ -134,6 +138,9 @@ impl RemoteConnection for SshRemoteConnection { } else { write!(&mut script, "cd; ").unwrap(); }; + if let Some(activation_script) = activation_script { + write!(&mut script, " {activation_script};").unwrap(); + } for (k, v) in input_env.iter() { if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { @@ -155,7 +162,8 @@ impl RemoteConnection for SshRemoteConnection { write!(&mut script, "exec {shell} -l").unwrap(); }; - let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&script).unwrap()); + let sys_shell = get_default_system_shell(); + let shell_invocation = format!("{sys_shell} -c {}", shlex::try_quote(&script).unwrap()); let mut args = Vec::new(); args.extend(self.socket.ssh_args()); @@ -167,7 +175,6 @@ impl RemoteConnection for SshRemoteConnection { args.push("-t".into()); args.push(shell_invocation); - Ok(CommandTemplate { program: "ssh".into(), args, diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index f907bd1d9f3f02869b2eb4b6ea4d65663e0d00af..38a5a970b73c334892bc494f94b9b759b015677e 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,3 +1,7 @@ +use std::fmt; + +use util::get_system_shell; + use crate::Shell; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -11,9 +15,22 @@ pub enum ShellKind { Cmd, } +impl fmt::Display for ShellKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ShellKind::Posix => write!(f, "sh"), + ShellKind::Csh => write!(f, "csh"), + ShellKind::Fish => write!(f, "fish"), + ShellKind::Powershell => write!(f, "powershell"), + ShellKind::Nushell => write!(f, "nu"), + ShellKind::Cmd => write!(f, "cmd"), + } + } +} + impl ShellKind { pub fn system() -> Self { - Self::new(&system_shell()) + Self::new(&get_system_shell()) } pub fn new(program: &str) -> Self { @@ -22,12 +39,12 @@ impl ShellKind { #[cfg(not(windows))] let (_, program) = program.rsplit_once('/').unwrap_or(("", program)); if program == "powershell" - || program == "powershell.exe" + || program.ends_with("powershell.exe") || program == "pwsh" - || program == "pwsh.exe" + || program.ends_with("pwsh.exe") { ShellKind::Powershell - } else if program == "cmd" || program == "cmd.exe" { + } else if program == "cmd" || program.ends_with("cmd.exe") { ShellKind::Cmd } else if program == "nu" { ShellKind::Nushell @@ -178,18 +195,6 @@ impl ShellKind { } } -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 - // We could use `util::get_windows_system_shell()` here, but we are running tasks here, so leave it to `powershell.exe` - // should be okay. - "powershell.exe".to_string() - } else { - std::env::var("SHELL").unwrap_or("/bin/sh".to_string()) - } -} - /// ShellBuilder is used to turn a user-requested task into a /// program that can be executed by the shell. pub struct ShellBuilder { @@ -206,7 +211,7 @@ impl ShellBuilder { let (program, args) = match remote_system_shell { Some(program) => (program.to_string(), Vec::new()), None => match shell { - Shell::System => (system_shell(), Vec::new()), + Shell::System => (get_system_shell(), Vec::new()), Shell::Program(shell) => (shell.clone(), Vec::new()), Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), }, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b38a69f095049c80388d3c0ec2ab397fb4d2bec4..a5e0227533cf0e3ecbc9a8f2c6c55fa1254473e3 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -344,7 +344,6 @@ pub struct TerminalBuilder { impl TerminalBuilder { pub fn new( working_directory: Option, - python_venv_directory: Option, task: Option, shell: Shell, mut env: HashMap, @@ -353,8 +352,9 @@ impl TerminalBuilder { max_scroll_history_lines: Option, is_ssh_terminal: bool, window_id: u64, - completion_tx: Sender>, + completion_tx: Option>>, cx: &App, + activation_script: Option, ) -> Result { // If the parent environment doesn't have a locale set // (As is the case when launched from a .app on MacOS), @@ -428,13 +428,10 @@ impl TerminalBuilder { .clone() .or_else(|| Some(home_dir().to_path_buf())), drain_on_exit: true, - env: env.into_iter().collect(), + env: env.clone().into_iter().collect(), } }; - // Setup Alacritty's env, which modifies the current process's environment - alacritty_terminal::tty::setup_env(); - let default_cursor_style = AlacCursorStyle::from(cursor_shape); let scrolling_history = if task.is_some() { // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. @@ -517,11 +514,19 @@ impl TerminalBuilder { hyperlink_regex_searches: RegexSearches::new(), vi_mode_enabled: false, is_ssh_terminal, - python_venv_directory, last_mouse_move_time: Instant::now(), last_hyperlink_search_position: None, #[cfg(windows)] shell_program, + activation_script, + template: CopyTemplate { + shell, + env, + cursor_shape, + alternate_scroll, + max_scroll_history_lines, + window_id, + }, }; Ok(TerminalBuilder { @@ -683,7 +688,7 @@ pub enum SelectionPhase { pub struct Terminal { pty_tx: Notifier, - completion_tx: Sender>, + completion_tx: Option>>, term: Arc>>, term_config: Config, events: VecDeque, @@ -695,7 +700,6 @@ pub struct Terminal { pub breadcrumb_text: String, pub pty_info: PtyProcessInfo, title_override: Option, - pub python_venv_directory: Option, scroll_px: Pixels, next_link_id: usize, selection_phase: SelectionPhase, @@ -707,6 +711,17 @@ pub struct Terminal { last_hyperlink_search_position: Option>, #[cfg(windows)] shell_program: Option, + template: CopyTemplate, + activation_script: Option, +} + +struct CopyTemplate { + shell: Shell, + env: HashMap, + cursor_shape: CursorShape, + alternate_scroll: AlternateScroll, + max_scroll_history_lines: Option, + window_id: u64, } pub struct TaskState { @@ -1895,7 +1910,9 @@ impl Terminal { } }); - self.completion_tx.try_send(e).ok(); + if let Some(tx) = &self.completion_tx { + tx.try_send(e).ok(); + } let task = match &mut self.task { Some(task) => task, None => { @@ -1950,6 +1967,28 @@ impl Terminal { pub fn vi_mode_enabled(&self) -> bool { self.vi_mode_enabled } + + pub fn clone_builder( + &self, + cx: &App, + cwd: impl FnOnce() -> Option, + ) -> Result { + let working_directory = self.working_directory().or_else(cwd); + TerminalBuilder::new( + working_directory, + None, + self.template.shell.clone(), + self.template.env.clone(), + self.template.cursor_shape, + self.template.alternate_scroll, + self.template.max_scroll_history_lines, + self.is_ssh_terminal, + self.template.window_id, + None, + cx, + self.activation_script.clone(), + ) + } } // Helper function to convert a grid row to a string @@ -2164,7 +2203,6 @@ mod tests { let (completion_tx, completion_rx) = smol::channel::unbounded(); let terminal = cx.new(|cx| { TerminalBuilder::new( - None, None, None, task::Shell::WithArguments { @@ -2178,8 +2216,9 @@ mod tests { None, false, 0, - completion_tx, + Some(completion_tx), cx, + None, ) .unwrap() .subscribe(cx) diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index c7ebd314e4618300058b1a2e083d97d3bb569df3..9759fe8337bc4a870fb6fe0a903edf5c542f5d4f 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -3,9 +3,9 @@ use async_recursion::async_recursion; use collections::HashSet; use futures::{StreamExt as _, stream::FuturesUnordered}; use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity}; -use project::{Project, terminals::TerminalKind}; +use project::Project; use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; @@ -246,11 +246,9 @@ async fn deserialize_pane_group( .update(cx, |workspace, cx| default_working_directory(workspace, cx)) .ok() .flatten(); - let kind = TerminalKind::Shell( - working_directory.as_deref().map(Path::to_path_buf), - ); - let terminal = - project.update(cx, |project, cx| project.create_terminal(kind, cx)); + let terminal = project.update(cx, |project, cx| { + project.create_terminal_shell(working_directory, cx) + }); Some(Some(terminal)) } else { Some(None) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c3d7c4f793ed612a4dd819aca7552b48ccf6a3db..45e36c199048f9699c920939f7c6f8921d25c5e9 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -16,7 +16,7 @@ use gpui::{ Task, WeakEntity, Window, actions, }; use itertools::Itertools; -use project::{Fs, Project, ProjectEntryId, terminals::TerminalKind}; +use project::{Fs, Project, ProjectEntryId}; use search::{BufferSearchBar, buffer_search::DivRegistrar}; use settings::Settings; use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId}; @@ -376,14 +376,19 @@ impl TerminalPanel { } self.serialize(cx); } - pane::Event::Split(direction) => { - let Some(new_pane) = self.new_pane_with_cloned_active_terminal(window, cx) else { - return; - }; + &pane::Event::Split(direction) => { + let fut = self.new_pane_with_cloned_active_terminal(window, cx); let pane = pane.clone(); - let direction = *direction; - self.center.split(&pane, &new_pane, direction).log_err(); - window.focus(&new_pane.focus_handle(cx)); + cx.spawn_in(window, async move |panel, cx| { + let Some(new_pane) = fut.await else { + return; + }; + _ = panel.update_in(cx, |panel, window, cx| { + panel.center.split(&pane, &new_pane, direction).log_err(); + window.focus(&new_pane.focus_handle(cx)); + }); + }) + .detach(); } pane::Event::Focus => { self.active_pane = pane.clone(); @@ -400,57 +405,62 @@ impl TerminalPanel { &mut self, window: &mut Window, cx: &mut Context, - ) -> Option> { - let workspace = self.workspace.upgrade()?; + ) -> Task>> { + let Some(workspace) = self.workspace.upgrade() else { + return Task::ready(None); + }; let workspace = workspace.read(cx); let database_id = workspace.database_id(); let weak_workspace = self.workspace.clone(); let project = workspace.project().clone(); - let (working_directory, python_venv_directory) = self - .active_pane + let active_pane = &self.active_pane; + let terminal_view = active_pane .read(cx) .active_item() - .and_then(|item| item.downcast::()) - .map(|terminal_view| { - let terminal = terminal_view.read(cx).terminal().read(cx); - ( - terminal - .working_directory() - .or_else(|| default_working_directory(workspace, cx)), - terminal.python_venv_directory.clone(), - ) - }) - .unwrap_or((None, None)); - let kind = TerminalKind::Shell(working_directory); - let terminal = project - .update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, cx) - }) - .ok()?; - - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - weak_workspace.clone(), - database_id, - project.downgrade(), - window, - cx, - ) - })); - let pane = new_terminal_pane( - weak_workspace, - project, - self.active_pane.read(cx).is_zoomed(), - window, - cx, - ); - self.apply_tab_bar_buttons(&pane, cx); - pane.update(cx, |pane, cx| { - pane.add_item(terminal_view, true, true, None, window, cx); + .and_then(|item| item.downcast::()); + let working_directory = terminal_view.as_ref().and_then(|terminal_view| { + let terminal = terminal_view.read(cx).terminal().read(cx); + terminal + .working_directory() + .or_else(|| default_working_directory(workspace, cx)) }); + let is_zoomed = active_pane.read(cx).is_zoomed(); + cx.spawn_in(window, async move |panel, cx| { + let terminal = project + .update(cx, |project, cx| match terminal_view { + Some(view) => Task::ready(project.clone_terminal( + &view.read(cx).terminal.clone(), + cx, + || working_directory, + )), + None => project.create_terminal_shell(working_directory, cx), + }) + .ok()? + .await + .ok()?; - Some(pane) + panel + .update_in(cx, move |terminal_panel, window, cx| { + let terminal_view = Box::new(cx.new(|cx| { + TerminalView::new( + terminal.clone(), + weak_workspace.clone(), + database_id, + project.downgrade(), + window, + cx, + ) + })); + let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx); + terminal_panel.apply_tab_bar_buttons(&pane, cx); + pane.update(cx, |pane, cx| { + pane.add_item(terminal_view, true, true, None, window, cx); + }); + Some(pane) + }) + .ok() + .flatten() + }) } pub fn open_terminal( @@ -465,8 +475,8 @@ impl TerminalPanel { terminal_panel .update(cx, |panel, cx| { - panel.add_terminal( - TerminalKind::Shell(Some(action.working_directory.clone())), + panel.add_terminal_shell( + Some(action.working_directory.clone()), RevealStrategy::Always, window, cx, @@ -571,15 +581,16 @@ impl TerminalPanel { ) -> Task>> { let reveal = spawn_task.reveal; let reveal_target = spawn_task.reveal_target; - let kind = TerminalKind::Task(spawn_task); match reveal_target { RevealTarget::Center => self .workspace .update(cx, |workspace, cx| { - Self::add_center_terminal(workspace, kind, window, cx) + Self::add_center_terminal(workspace, window, cx, |project, cx| { + project.create_terminal_task(spawn_task, cx) + }) }) .unwrap_or_else(|e| Task::ready(Err(e))), - RevealTarget::Dock => self.add_terminal(kind, reveal, window, cx), + RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx), } } @@ -594,11 +605,14 @@ impl TerminalPanel { return; }; - let kind = TerminalKind::Shell(default_working_directory(workspace, cx)); - terminal_panel .update(cx, |this, cx| { - this.add_terminal(kind, RevealStrategy::Always, window, cx) + this.add_terminal_shell( + default_working_directory(workspace, cx), + RevealStrategy::Always, + window, + cx, + ) }) .detach_and_log_err(cx); } @@ -660,9 +674,13 @@ impl TerminalPanel { pub fn add_center_terminal( workspace: &mut Workspace, - kind: TerminalKind, window: &mut Window, cx: &mut Context, + create_terminal: impl FnOnce( + &mut Project, + &mut Context, + ) -> Task>> + + 'static, ) -> Task>> { if !is_enabled_in_workspace(workspace, cx) { return Task::ready(Err(anyhow!( @@ -671,9 +689,7 @@ impl TerminalPanel { } let project = workspace.project().downgrade(); cx.spawn_in(window, async move |workspace, cx| { - let terminal = project - .update(cx, |project, cx| project.create_terminal(kind, cx))? - .await?; + let terminal = project.update(cx, create_terminal)?.await?; workspace.update_in(cx, |workspace, window, cx| { let terminal_view = cx.new(|cx| { @@ -692,9 +708,9 @@ impl TerminalPanel { }) } - pub fn add_terminal( + pub fn add_terminal_task( &mut self, - kind: TerminalKind, + task: SpawnInTerminal, reveal_strategy: RevealStrategy, window: &mut Window, cx: &mut Context, @@ -710,7 +726,66 @@ impl TerminalPanel { })?; let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; let terminal = project - .update(cx, |project, cx| project.create_terminal(kind, cx))? + .update(cx, |project, cx| project.create_terminal_task(task, cx))? + .await?; + let result = workspace.update_in(cx, |workspace, window, cx| { + let terminal_view = Box::new(cx.new(|cx| { + TerminalView::new( + terminal.clone(), + workspace.weak_handle(), + workspace.database_id(), + workspace.project().downgrade(), + window, + cx, + ) + })); + + match reveal_strategy { + RevealStrategy::Always => { + workspace.focus_panel::(window, cx); + } + RevealStrategy::NoFocus => { + workspace.open_panel::(window, cx); + } + RevealStrategy::Never => {} + } + + pane.update(cx, |pane, cx| { + let focus = pane.has_focus(window, cx) + || matches!(reveal_strategy, RevealStrategy::Always); + pane.add_item(terminal_view, true, focus, None, window, cx); + }); + + Ok(terminal.downgrade()) + })?; + terminal_panel.update(cx, |terminal_panel, cx| { + terminal_panel.pending_terminals_to_add = + terminal_panel.pending_terminals_to_add.saturating_sub(1); + terminal_panel.serialize(cx) + })?; + result + }) + } + + pub fn add_terminal_shell( + &mut self, + cwd: Option, + reveal_strategy: RevealStrategy, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let workspace = self.workspace.clone(); + cx.spawn_in(window, async move |terminal_panel, cx| { + if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? { + anyhow::bail!("terminal not yet supported for remote projects"); + } + let pane = terminal_panel.update(cx, |terminal_panel, _| { + terminal_panel.pending_terminals_to_add += 1; + terminal_panel.active_pane.clone() + })?; + let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; + let terminal = project + .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? .await?; let result = workspace.update_in(cx, |workspace, window, cx| { let terminal_view = Box::new(cx.new(|cx| { @@ -819,7 +894,7 @@ impl TerminalPanel { })??; let new_terminal = project .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Task(spawn_task), cx) + project.create_terminal_task(spawn_task, cx) })? .await?; terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| { @@ -1248,18 +1323,29 @@ impl Render for TerminalPanel { let panes = terminal_panel.center.panes(); if let Some(&pane) = panes.get(action.0) { window.focus(&pane.read(cx).focus_handle(cx)); - } else if let Some(new_pane) = - terminal_panel.new_pane_with_cloned_active_terminal(window, cx) - { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - window.focus(&new_pane.focus_handle(cx)); + } else { + let future = + terminal_panel.new_pane_with_cloned_active_terminal(window, cx); + cx.spawn_in(window, async move |terminal_panel, cx| { + if let Some(new_pane) = future.await { + _ = terminal_panel.update_in( + cx, + |terminal_panel, window, cx| { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + let new_pane = new_pane.read(cx); + window.focus(&new_pane.focus_handle(cx)); + }, + ); + } + }) + .detach(); } }), ) @@ -1395,13 +1481,14 @@ impl Panel for TerminalPanel { return; } cx.defer_in(window, |this, window, cx| { - let Ok(kind) = this.workspace.update(cx, |workspace, cx| { - TerminalKind::Shell(default_working_directory(workspace, cx)) - }) else { + let Ok(kind) = this + .workspace + .update(cx, |workspace, cx| default_working_directory(workspace, cx)) + else { return; }; - this.add_terminal(kind, RevealStrategy::Always, window, cx) + this.add_terminal_shell(kind, RevealStrategy::Always, window, cx) .detach_and_log_err(cx) }) } diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs index e20df7f0010480d782eb16375ca3480d4f390742..226a8f4c3d9bca398df778fa2043fb5872383b56 100644 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ b/crates/terminal_view/src/terminal_path_like_target.rs @@ -364,7 +364,7 @@ fn possibly_open_target( mod tests { use super::*; use gpui::TestAppContext; - use project::{Project, terminals::TerminalKind}; + use project::Project; use serde_json::json; use std::path::{Path, PathBuf}; use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint}; @@ -405,8 +405,8 @@ mod tests { app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let terminal = project - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(None), cx) + .update(cx, |project: &mut Project, cx| { + project.create_terminal_shell(None, cx) }) .await .expect("Failed to create a terminal"); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9aa855acb7aa18a0431fcfc07e7a32932162e4f2..9e479464af224c4d85119d6ca2e0b25c360f9c3d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -15,7 +15,7 @@ use gpui::{ deferred, div, }; use persistence::TERMINAL_DB; -use project::{Project, search::SearchQuery, terminals::TerminalKind}; +use project::{Project, search::SearchQuery}; use schemars::JsonSchema; use task::TaskId; use terminal::{ @@ -204,12 +204,9 @@ impl TerminalView { cx: &mut Context, ) { let working_directory = default_working_directory(workspace, cx); - TerminalPanel::add_center_terminal( - workspace, - TerminalKind::Shell(working_directory), - window, - cx, - ) + TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| { + project.create_terminal_shell(working_directory, cx) + }) .detach_and_log_err(cx); } @@ -1333,16 +1330,10 @@ impl Item for TerminalView { let terminal = self .project .update(cx, |project, cx| { - let terminal = self.terminal().read(cx); - let working_directory = terminal - .working_directory() - .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf())); - let python_venv_directory = terminal.python_venv_directory.clone(); - project.create_terminal_with_venv( - TerminalKind::Shell(working_directory), - python_venv_directory, - cx, - ) + let cwd = project + .active_project_directory(cx) + .map(|it| it.to_path_buf()); + project.clone_terminal(self.terminal(), cx, || cwd) }) .ok()? .log_err()?; @@ -1498,9 +1489,7 @@ impl SerializableItem for TerminalView { .flatten(); let terminal = project - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(cwd), cx) - })? + .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? .await?; cx.update(|window, cx| { cx.new(|cx| { diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 69a2c88706bff8b459ec7b678976a84ae4f943bf..0aceec5d7ae4b672afc6111bd4f2389d7b1b6af7 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1057,6 +1057,18 @@ pub fn get_system_shell() -> String { } } +pub fn get_default_system_shell() -> String { + #[cfg(target_os = "windows")] + { + get_windows_system_shell() + } + + #[cfg(not(target_os = "windows"))] + { + "/bin/sh".to_string() + } +} + #[derive(Debug)] pub enum ConnectionResult { Timeout, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c4ba93bcec60f158d22904674996ba63202a64a6..3ef9ff65eb0fe5aedfd5e72aa18f1481a011fce7 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -824,7 +824,6 @@ impl WorkspaceDb { conn.exec_bound( sql!( DELETE FROM breakpoints WHERE workspace_id = ?1; - DELETE FROM toolchains WHERE workspace_id = ?1; ) )?(workspace.id).context("Clearing old breakpoints")?; @@ -1097,7 +1096,6 @@ impl WorkspaceDb { query! { pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> { - DELETE FROM toolchains WHERE workspace_id = ?1; DELETE FROM workspaces WHERE workspace_id IS ? } @@ -1424,24 +1422,24 @@ impl WorkspaceDb { &self, workspace_id: WorkspaceId, worktree_id: WorktreeId, - relative_path: String, + relative_worktree_path: String, language_name: LanguageName, ) -> Result> { self.write(move |this| { let mut select = this .select_bound(sql!( - SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_path = ? + SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_worktree_path = ? )) - .context("Preparing insertion")?; + .context("select toolchain")?; let toolchain: Vec<(String, String, String)> = - select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_path))?; + select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_worktree_path))?; Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain { 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()?, }))) }) .await @@ -1456,7 +1454,7 @@ impl WorkspaceDb { .select_bound(sql!( SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ? )) - .context("Preparing insertion")?; + .context("select toolchains")?; let toolchain: Vec<(String, String, u64, String, String, String)> = select(workspace_id)?; @@ -1465,7 +1463,7 @@ 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()?, }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect()) }) .await