From 0ce4dbb64150755532369e4028cdc811c1da6fdd Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 18 Aug 2025 16:43:32 +0200 Subject: [PATCH 1/7] Split terminal API into shell and task --- crates/agent2/src/tools/terminal_tool.rs | 29 +- crates/assistant_tools/src/terminal_tool.rs | 29 +- crates/debugger_ui/src/session/running.rs | 17 +- crates/project/src/debugger/dap_store.rs | 1 - crates/project/src/terminals.rs | 788 ++++++------------ crates/terminal/src/terminal.rs | 14 +- crates/terminal_view/src/persistence.rs | 15 +- crates/terminal_view/src/terminal_panel.rs | 129 ++- .../src/terminal_path_like_target.rs | 7 +- crates/terminal_view/src/terminal_view.rs | 25 +- 10 files changed, 396 insertions(+), 658 deletions(-) diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index f41b909d0b..18f4571d65 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::{ @@ -141,21 +141,18 @@ impl AgentTool for TerminalTool { let program = program.await; let env = env.await; - let terminal = self - .project - .update(cx, |project, cx| { - project.create_terminal( - TerminalKind::Task(task::SpawnInTerminal { - command: Some(program), - args, - cwd: working_dir.clone(), - env, - ..Default::default() - }), - cx, - ) - })? - .await?; + let terminal = self.project.update(cx, |project, cx| { + project.create_terminal_task( + task::SpawnInTerminal { + command: Some(program), + args, + cwd: working_dir.clone(), + env, + ..Default::default() + }, + cx, + ) + })??; let acp_terminal = cx.new(|cx| { acp_thread::Terminal::new( input.command.clone(), diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index b28e55e78a..b8f09012cb 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,21 +213,18 @@ 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 { - command: Some(program), - args, - cwd, - env, - ..Default::default() - }), - cx, - ) - })? - .await + project.update(cx, |project, cx| { + 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 9991395f35..fd135596eb 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; @@ -1016,12 +1015,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?; + })??; let terminal_view = cx.new_window_entity(|window, cx| { TerminalView::new( @@ -1165,7 +1163,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(), @@ -1183,14 +1181,15 @@ 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?; + let terminal = terminal_task?; let terminal_view = cx.new_window_entity(|window, cx| { TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 2906c32ff4..77edd1e8a6 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -292,7 +292,6 @@ impl DapStore { .map(|command| (command, &binary.arguments)), binary.cwd.as_deref(), binary.envs, - None, path_style, ); diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b009b357fe..133f5d3e54 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,49 +1,27 @@ -use crate::{Project, ProjectPath}; -use anyhow::{Context as _, Result}; +use crate::Project; + +use anyhow::Result; use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; +use gpui::{App, AppContext as _, Context, Entity, WeakEntity}; use itertools::Itertools; -use language::LanguageName; use remote::{SshInfo, ssh_session::SshArgs}; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ borrow::Cow, - env::{self}, path::{Path, PathBuf}, sync::Arc, }; use task::{Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ - TaskState, TaskStatus, Terminal, TerminalBuilder, - terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, -}; -use util::{ - ResultExt, - paths::{PathStyle, RemotePathBuf}, -}; - -/// The directory inside a Python virtual environment that contains executables -const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") { - "Scripts" -} else { - "bin" + TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; +use util::paths::{PathStyle, RemotePathBuf}; 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), -} - /// SshCommand describes how to connect to a remote server #[derive(Debug, Clone, PartialEq, Eq)] pub struct SshCommand { @@ -108,46 +86,260 @@ impl Project { None } - 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) - } + ) -> Result> { + let this = &mut *self; + let ssh_details = this.ssh_details(cx); + let path: Option> = if let Some(cwd) = &spawn_task.cwd { + if ssh_details.is_some() { + 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 { + this.active_project_directory(cx) }; + let is_ssh_terminal = ssh_details.is_some(); + let mut settings_location = None; if let Some(path) = path.as_ref() - && let Some((worktree, _)) = self.find_worktree(path, cx) + && let Some((worktree, _)) = this.find_worktree(path, cx) { settings_location = Some(SettingsLocation { worktree_id: worktree.read(cx).id(), path, }); } - let venv = TerminalSettings::get(settings_location, cx) - .detect_venv - .clone(); + let settings = TerminalSettings::get(settings_location, cx).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 - } else { - None - }; - project.update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, cx) - })? + let (completion_tx, completion_rx) = bounded(1); + + // Start with the environment that we might have inherited from the Zed CLI. + let mut env = this + .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); + + let local_path = if is_ssh_terminal { None } else { path.clone() }; + + let (spawn_task, shell) = { + 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, + }); + + env.extend(spawn_task.env); + + match ssh_details { + Some(SshDetails { + host, + ssh_command, + envs, + path_style, + shell, + }) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + let (program, args) = wrap_for_ssh( + &shell, + &ssh_command, + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), + path.as_deref(), + env, + path_style, + ); + env = HashMap::default(); + if let Some(envs) = envs { + env.extend(envs); + } + ( + task_state, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) + } + None => { + let shell = if let Some(program) = spawn_task.command { + Shell::WithArguments { + program, + args: spawn_task.args, + title_override: None, + } + } else { + Shell::System + }; + (task_state, shell) + } + } + }; + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + spawn_task, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_ssh_terminal, + cx.entity_id().as_u64(), + Some(completion_tx), + cx, + ) + .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_shell( + &mut self, + cwd: Option, + cx: &mut Context, + ) -> Result> { + let path = cwd.map(|p| Arc::from(&*p)); + let this = &mut *self; + let ssh_details = this.ssh_details(cx); + + let is_ssh_terminal = ssh_details.is_some(); + + let mut settings_location = None; + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = this.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); + } + let settings = TerminalSettings::get(settings_location, cx).clone(); + + // Start with the environment that we might have inherited from the Zed CLI. + let mut env = this + .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); + + let local_path = if is_ssh_terminal { None } else { path.clone() }; + + let (spawn_task, shell) = { + match ssh_details { + Some(SshDetails { + host, + ssh_command, + envs, + path_style, + shell, + }) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + + // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed + // to properly display colors. + // We do not have the luxury of assuming the host has it installed, + // so we set it to a default that does not break the highlighting via ssh. + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + + let (program, args) = + wrap_for_ssh(&shell, &ssh_command, None, path.as_deref(), env, path_style); + env = HashMap::default(); + if let Some(envs) = envs { + env.extend(envs); + } + ( + Option::::None, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) + } + None => (None, settings.shell), + } + }; + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + spawn_task, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_ssh_terminal, + cx.entity_id().as_u64(), + None, + cx, + ) + .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 }) } @@ -199,7 +391,6 @@ impl Project { Some((&command, &args)), path.as_deref(), env, - None, path_style, ); let mut command = std::process::Command::new(command); @@ -221,432 +412,6 @@ impl Project { } } - pub fn create_terminal_with_venv( - &mut self, - kind: TerminalKind, - python_venv_directory: Option, - cx: &mut Context, - ) -> Result> { - let this = &mut *self; - let ssh_details = this.ssh_details(cx); - 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 ssh_details.is_some() { - 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 { - this.active_project_directory(cx) - } - } - }; - - let is_ssh_terminal = ssh_details.is_some(); - - let mut settings_location = None; - if let Some(path) = path.as_ref() - && let Some((worktree, _)) = this.find_worktree(path, cx) - { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, - }); - } - let settings = TerminalSettings::get(settings_location, cx).clone(); - - let (completion_tx, completion_rx) = bounded(1); - - // Start with the environment that we might have inherited from the Zed CLI. - let mut env = this - .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); - - let local_path = if is_ssh_terminal { None } else { path.clone() }; - - let mut python_venv_activate_command = Task::ready(None); - - let (spawn_task, shell) = match kind { - TerminalKind::Shell(_) => { - if let Some(python_venv_directory) = &python_venv_directory { - python_venv_activate_command = this.python_activate_command( - python_venv_directory, - &settings.detect_venv, - &settings.shell, - cx, - ); - } - - match ssh_details { - Some(SshDetails { - host, - ssh_command, - envs, - path_style, - shell, - }) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - - // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed - // to properly display colors. - // We do not have the luxury of assuming the host has it installed, - // so we set it to a default that does not break the highlighting via ssh. - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - - let (program, args) = wrap_for_ssh( - &shell, - &ssh_command, - None, - path.as_deref(), - env, - None, - path_style, - ); - env = HashMap::default(); - if let Some(envs) = envs { - env.extend(envs); - } - ( - Option::::None, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) - } - None => (None, settings.shell), - } - } - TerminalKind::Task(spawn_task) => { - 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, - }); - - env.extend(spawn_task.env); - - if let Some(venv_path) = &python_venv_directory { - env.insert( - "VIRTUAL_ENV".to_string(), - venv_path.to_string_lossy().to_string(), - ); - } - - match ssh_details { - Some(SshDetails { - host, - ssh_command, - envs, - path_style, - shell, - }) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - let (program, args) = wrap_for_ssh( - &shell, - &ssh_command, - spawn_task - .command - .as_ref() - .map(|command| (command, &spawn_task.args)), - path.as_deref(), - env, - python_venv_directory.as_deref(), - path_style, - ); - env = HashMap::default(); - if let Some(envs) = envs { - env.extend(envs); - } - ( - task_state, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) - } - None => { - if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join(PYTHON_VENV_BIN_DIR)) - .log_err(); - } - - let shell = if let Some(program) = spawn_task.command { - Shell::WithArguments { - program, - args: spawn_task.args, - title_override: None, - } - } else { - Shell::System - }; - (task_state, shell) - } - } - } - }; - 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_ssh_terminal, - cx.entity_id().as_u64(), - completion_tx, - cx, - ) - .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(); - - this.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, - } - } - - 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", - }; - - let line_ending = match std::env::consts::OS { - "windows" => "\r", - _ => "\n", - }; - - 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 - } - }) - } else { - Task::ready(Some(format!( - "{activate_keyword} {activate_script_name} {name}; clear{line_ending}", - name = venv_settings.venv_name - ))) - } - } - - 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 } @@ -658,7 +423,6 @@ pub fn wrap_for_ssh( command: Option<(&String, &Vec)>, path: Option<&Path>, env: HashMap, - venv_directory: Option<&Path>, path_style: PathStyle, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { @@ -675,12 +439,6 @@ pub fn wrap_for_ssh( env_changes.push_str(&format!("{}={} ", k, v)); } } - if let Some(venv_directory) = 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_changes.push_str(&format!("PATH={}:$PATH ", path)); - } let commands = if let Some(path) = path { let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string(); @@ -709,57 +467,3 @@ pub fn wrap_for_ssh( args.push(shell_invocation); (program, args) } - -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/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b38a69f095..ab6b6eaa73 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,7 +352,7 @@ impl TerminalBuilder { max_scroll_history_lines: Option, is_ssh_terminal: bool, window_id: u64, - completion_tx: Sender>, + completion_tx: Option>>, cx: &App, ) -> Result { // If the parent environment doesn't have a locale set @@ -517,7 +516,6 @@ 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)] @@ -683,7 +681,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 +693,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, @@ -1895,7 +1892,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 => { @@ -2164,7 +2163,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,7 +2176,7 @@ mod tests { None, false, 0, - completion_tx, + Some(completion_tx), cx, ) .unwrap() diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index b93b267f58..ab3aafd955 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -3,7 +3,7 @@ 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 ui::{App, Context, Pixels, Window}; @@ -242,11 +242,12 @@ 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.as_deref().map(Path::to_path_buf), + cx, + ) + }); Some(Some(terminal)) } else { Some(None) @@ -255,7 +256,7 @@ async fn deserialize_pane_group( .ok() .flatten()?; if let Some(terminal) = terminal { - let terminal = terminal.await.ok()?; + let terminal = terminal.ok()?; pane.update_in(cx, |pane, window, cx| { let terminal_view = Box::new(cx.new(|cx| { TerminalView::new( diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6b17911487..ba70440612 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}; @@ -406,25 +406,21 @@ impl TerminalPanel { let database_id = workspace.database_id(); let weak_workspace = self.workspace.clone(); let project = workspace.project().clone(); - let (working_directory, python_venv_directory) = self + let working_directory = self .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(), - ) + terminal + .working_directory() + .or_else(|| default_working_directory(workspace, cx)) }) - .unwrap_or((None, None)); - let kind = TerminalKind::Shell(working_directory); + .unwrap_or(None); let terminal = project .update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, cx) + project.create_terminal_shell(working_directory, cx) }) .ok()?; @@ -465,8 +461,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, @@ -560,15 +556,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), } } @@ -583,11 +580,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); } @@ -649,9 +649,10 @@ 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) -> Result> + + 'static, ) -> Task>> { if !is_enabled_in_workspace(workspace, cx) { return Task::ready(Err(anyhow!( @@ -660,9 +661,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)??; workspace.update_in(cx, |workspace, window, cx| { let terminal_view = cx.new(|cx| { @@ -681,9 +680,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, @@ -698,9 +697,66 @@ impl TerminalPanel { 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(kind, cx))? - .await?; + let terminal = + project.update(cx, |project, cx| project.create_terminal_task(task, cx))??; + 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))??; let result = workspace.update_in(cx, |workspace, window, cx| { let terminal_view = Box::new(cx.new(|cx| { TerminalView::new( @@ -806,11 +862,9 @@ impl TerminalPanel { this.workspace .update(cx, |workspace, _| workspace.project().clone()) })??; - let new_terminal = project - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Task(spawn_task), cx) - })? - .await?; + let new_terminal = project.update(cx, |project, cx| { + project.create_terminal_task(spawn_task, cx) + })??; terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| { terminal_to_replace.set_terminal(new_terminal.clone(), window, cx); })?; @@ -1384,13 +1438,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 e20df7f001..caaf3261d5 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,10 +405,9 @@ 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, None) }) - .await .expect("Failed to create a terminal"); let workspace_a = workspace.clone(); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9aa855acb7..c9adb8c18a 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); } @@ -1337,12 +1334,7 @@ impl Item for TerminalView { 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, - ) + project.create_terminal_shell(working_directory, cx) }) .ok()? .log_err()?; @@ -1497,11 +1489,8 @@ impl SerializableItem for TerminalView { .ok() .flatten(); - let terminal = project - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(cwd), cx) - })? - .await?; + let terminal = + project.update(cx, |project, cx| project.create_terminal_shell(cwd, cx))??; cx.update(|window, cx| { cx.new(|cx| { TerminalView::new( From b2ba2de7b4060edc033172aae2df8847ca4891e8 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 18 Aug 2025 20:01:40 +0200 Subject: [PATCH 2/7] Async terminal construction --- crates/project/src/terminals.rs | 90 +++++++++--- crates/terminal/src/terminal.rs | 44 +++++- crates/terminal_view/src/persistence.rs | 9 +- crates/terminal_view/src/terminal_panel.rs | 139 +++++++++++------- .../src/terminal_path_like_target.rs | 1 + crates/terminal_view/src/terminal_view.rs | 18 ++- 6 files changed, 206 insertions(+), 95 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 133f5d3e54..ead0bd64f4 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,9 +1,8 @@ -use crate::Project; - use anyhow::Result; use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, WeakEntity}; -use itertools::Itertools; +use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; +use itertools::Itertools as _; +use language::LanguageName; use remote::{SshInfo, ssh_session::SshArgs}; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; @@ -16,7 +15,12 @@ use task::{Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; -use util::paths::{PathStyle, RemotePathBuf}; +use util::{ + maybe, + paths::{PathStyle, RemotePathBuf}, +}; + +use crate::{Project, ProjectPath}; pub struct Terminals { pub(crate) local_handles: Vec>, @@ -239,7 +243,8 @@ impl Project { &mut self, cwd: Option, cx: &mut Context, - ) -> Result> { + project_path_context: Option, + ) -> Task>> { let path = cwd.map(|p| Arc::from(&*p)); let this = &mut *self; let ssh_details = this.ssh_details(cx); @@ -305,23 +310,66 @@ impl Project { None => (None, settings.shell), } }; - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - spawn_task, - shell, - env, - settings.cursor_shape.unwrap_or_default(), - settings.alternate_scroll, - settings.max_scroll_history_lines, - is_ssh_terminal, - cx.entity_id().as_u64(), - None, - cx, - ) - .map(|builder| { + 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?; + + Some(()) + }) + .await; + project.update(cx, move |this, cx| { + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + spawn_task, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_ssh_terminal, + cx.entity_id().as_u64(), + None, + cx, + ) + .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 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)); - this.terminals + self.terminals .local_handles .push(terminal_handle.downgrade()); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ab6b6eaa73..292b9f8728 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -427,13 +427,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. @@ -520,6 +517,14 @@ impl TerminalBuilder { last_hyperlink_search_position: None, #[cfg(windows)] shell_program, + template: CopyTemplate { + shell, + env, + cursor_shape, + alternate_scroll, + max_scroll_history_lines, + window_id, + }, }; Ok(TerminalBuilder { @@ -704,6 +709,16 @@ pub struct Terminal { last_hyperlink_search_position: Option>, #[cfg(windows)] shell_program: Option, + template: CopyTemplate, +} + +struct CopyTemplate { + shell: Shell, + env: HashMap, + cursor_shape: CursorShape, + alternate_scroll: AlternateScroll, + max_scroll_history_lines: Option, + window_id: u64, } pub struct TaskState { @@ -1949,6 +1964,27 @@ 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, + ) + } } // Helper function to convert a grid row to a string diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index ab3aafd955..328468dfb8 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -5,7 +5,7 @@ use futures::{StreamExt as _, stream::FuturesUnordered}; use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity}; 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 _; @@ -243,10 +243,7 @@ async fn deserialize_pane_group( .ok() .flatten(); let terminal = project.update(cx, |project, cx| { - project.create_terminal_shell( - working_directory.as_deref().map(Path::to_path_buf), - cx, - ) + project.create_terminal_shell(working_directory, cx, None) }); Some(Some(terminal)) } else { @@ -256,7 +253,7 @@ async fn deserialize_pane_group( .ok() .flatten()?; if let Some(terminal) = terminal { - let terminal = terminal.ok()?; + let terminal = terminal.await.ok()?; pane.update_in(cx, |pane, window, cx| { let terminal_view = Box::new(cx.new(|cx| { TerminalView::new( diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ba70440612..e50582222f 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -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,14 +405,16 @@ 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 = self - .active_pane + let active_pane = &self.active_pane; + let working_directory = active_pane .read(cx) .active_item() .and_then(|item| item.downcast::()) @@ -418,35 +425,38 @@ impl TerminalPanel { .or_else(|| default_working_directory(workspace, cx)) }) .unwrap_or(None); - let terminal = project - .update(cx, |project, cx| { - project.create_terminal_shell(working_directory, cx) - }) - .ok()?; + let is_zoomed = active_pane.read(cx).is_zoomed(); + cx.spawn_in(window, async move |panel, cx| { + let terminal = project + .update(cx, |project, cx| { + project.create_terminal_shell(working_directory, cx, None) + }) + .ok()? + .await + .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); - }); - - 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( @@ -561,7 +571,7 @@ impl TerminalPanel { .workspace .update(cx, |workspace, cx| { Self::add_center_terminal(workspace, window, cx, |project, cx| { - project.create_terminal_task(spawn_task, cx) + Task::ready(project.create_terminal_task(spawn_task, cx)) }) }) .unwrap_or_else(|e| Task::ready(Err(e))), @@ -651,7 +661,10 @@ impl TerminalPanel { workspace: &mut Workspace, window: &mut Window, cx: &mut Context, - create_terminal: impl FnOnce(&mut Project, &mut Context) -> Result> + create_terminal: impl FnOnce( + &mut Project, + &mut Context, + ) -> Task>> + 'static, ) -> Task>> { if !is_enabled_in_workspace(workspace, cx) { @@ -661,7 +674,7 @@ impl TerminalPanel { } let project = workspace.project().downgrade(); cx.spawn_in(window, async move |workspace, cx| { - let terminal = project.update(cx, create_terminal)??; + let terminal = project.update(cx, create_terminal)?.await?; workspace.update_in(cx, |workspace, window, cx| { let terminal_view = cx.new(|cx| { @@ -755,8 +768,11 @@ impl TerminalPanel { 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))??; + let terminal = project + .update(cx, |project, cx| { + project.create_terminal_shell(cwd, cx, None) + })? + .await?; let result = workspace.update_in(cx, |workspace, window, cx| { let terminal_view = Box::new(cx.new(|cx| { TerminalView::new( @@ -1291,18 +1307,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(); } }), ) diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs index caaf3261d5..f65cb482f7 100644 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ b/crates/terminal_view/src/terminal_path_like_target.rs @@ -408,6 +408,7 @@ mod tests { .update(cx, |project: &mut Project, cx| { project.create_terminal_shell(None, cx, None) }) + .await .expect("Failed to create a terminal"); let workspace_a = workspace.clone(); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index c9adb8c18a..36938142d0 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -205,7 +205,7 @@ impl TerminalView { ) { let working_directory = default_working_directory(workspace, cx); TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| { - project.create_terminal_shell(working_directory, cx) + project.create_terminal_shell(working_directory, cx, None) }) .detach_and_log_err(cx); } @@ -1330,11 +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())); - project.create_terminal_shell(working_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()?; @@ -1489,8 +1488,11 @@ impl SerializableItem for TerminalView { .ok() .flatten(); - let terminal = - project.update(cx, |project, cx| project.create_terminal_shell(cwd, cx))??; + let terminal = project + .update(cx, |project, cx| { + project.create_terminal_shell(cwd, cx, None) + })? + .await?; cx.update(|window, cx| { cx.new(|cx| { TerminalView::new( From 7b3d73d6fd79b49912dfbef40a5a529c907e2108 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 20 Aug 2025 09:50:33 +0200 Subject: [PATCH 3/7] fetch ssh shell --- crates/project/src/terminals.rs | 81 ++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index ead0bd64f4..5543033415 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -273,43 +273,6 @@ impl Project { env.extend(settings.env); let local_path = if is_ssh_terminal { None } else { path.clone() }; - - let (spawn_task, shell) = { - match ssh_details { - Some(SshDetails { - host, - ssh_command, - envs, - path_style, - shell, - }) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - - // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed - // to properly display colors. - // We do not have the luxury of assuming the host has it installed, - // so we set it to a default that does not break the highlighting via ssh. - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - - let (program, args) = - wrap_for_ssh(&shell, &ssh_command, None, path.as_deref(), env, path_style); - env = HashMap::default(); - if let Some(envs) = envs { - env.extend(envs); - } - ( - Option::::None, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) - } - None => (None, settings.shell), - } - }; let toolchain = project_path_context.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); cx.spawn(async move |project, cx| { @@ -319,6 +282,50 @@ impl Project { Some(()) }) .await; + + let (spawn_task, shell) = { + match ssh_details { + Some(SshDetails { + host, + ssh_command, + envs, + path_style, + shell, + }) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + + // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed + // to properly display colors. + // We do not have the luxury of assuming the host has it installed, + // so we set it to a default that does not break the highlighting via ssh. + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + + let (program, args) = wrap_for_ssh( + &shell, + &ssh_command, + None, + path.as_deref(), + env, + path_style, + ); + env = HashMap::default(); + if let Some(envs) = envs { + env.extend(envs); + } + ( + Option::::None, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) + } + None => (None, settings.shell), + } + }; + project.update(cx, move |this, cx| { TerminalBuilder::new( local_path.map(|path| path.to_path_buf()), From 8e11e6a03e464f74c0faa932e2816e121cd67004 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 19 Aug 2025 11:39:08 +0200 Subject: [PATCH 4/7] Add activation script support for terminals/toolchains --- crates/language/src/toolchain.rs | 31 ++++++++--- crates/languages/src/python.rs | 2 + crates/project/src/debugger/dap_store.rs | 1 + crates/project/src/project_tests.rs | 1 + crates/project/src/terminals.rs | 71 +++++++++++++++++------- crates/project/src/toolchain_store.rs | 6 ++ crates/proto/proto/toolchain.proto | 1 + crates/task/src/shell_builder.rs | 2 +- crates/task/src/task.rs | 2 +- crates/terminal/src/terminal.rs | 5 ++ crates/workspace/src/persistence.rs | 8 ++- 11 files changed, 98 insertions(+), 32 deletions(-) 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 From c9eb9a9e4150d7cb19d93e16934072463017e605 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 20 Aug 2025 12:22:57 +0200 Subject: [PATCH 5/7] Populate project path context on shell creation --- crates/language/src/toolchain.rs | 9 +- crates/languages/src/python.rs | 188 ++++++++++++++++++++- crates/project/src/project_tests.rs | 2 +- crates/project/src/terminals.rs | 8 +- crates/project/src/toolchain_store.rs | 37 +++- crates/task/src/shell_builder.rs | 15 ++ crates/terminal_view/src/terminal_panel.rs | 57 +++++-- crates/terminal_view/src/terminal_view.rs | 24 ++- crates/workspace/src/persistence.rs | 8 +- 9 files changed, 311 insertions(+), 37 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 0f49e2d12b..96f417aa86 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -13,6 +13,7 @@ use async_trait::async_trait; use collections::{FxHashMap, HashMap}; use gpui::{AsyncApp, SharedString}; use settings::WorktreeId; +use task::ShellKind; use crate::{LanguageName, ManifestName}; @@ -26,7 +27,7 @@ pub struct Toolchain { /// Full toolchain data (including language-specific details) pub as_json: serde_json::Value, /// shell -> script - pub startup_script: FxHashMap, + pub activation_script: FxHashMap, } impl std::hash::Hash for Toolchain { @@ -36,7 +37,7 @@ impl std::hash::Hash for Toolchain { path, language_name, as_json: _, - startup_script: _, + activation_script: _, } = self; name.hash(state); path.hash(state); @@ -51,7 +52,7 @@ impl PartialEq for Toolchain { path, language_name, as_json: _, - startup_script, + activation_script: 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. @@ -59,7 +60,7 @@ impl PartialEq for Toolchain { &other.name, &other.path, &other.language_name, - &other.startup_script, + &other.activation_script, )) } } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index a87049be22..9073960654 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -35,7 +35,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{TaskTemplate, TaskTemplates, VariableName}; +use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName}; use util::ResultExt; pub(crate) struct PyprojectTomlManifestProvider; @@ -872,14 +872,17 @@ impl ToolchainLister for PythonToolchainProvider { if let Some(nk) = name_and_kind { _ = write!(name, " {nk}"); } - Some(Toolchain { 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()?, - startup_script: std::iter::once(("fish".to_owned(), "test".to_owned())) - .collect(), + activation_script: std::iter::once(( + ShellKind::Fish, + "echo python recognized a venv and injected a fish startup command" + .to_owned(), + )) + .collect(), }) }) .collect(); @@ -1691,3 +1694,180 @@ mod tests { }); } } +/* +fn python_venv_directory( + 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() + .map_or(false, |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, + } +} + +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", + }; + + let line_ending = match std::env::consts::OS { + "windows" => "\r", + _ => "\n", + }; + + 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 + } + }) + } else { + Task::ready(Some(format!( + "{activate_keyword} {activate_script_name} {name}; clear{line_ending}", + name = venv_settings.venv_name + ))) + } +} +*/ diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a46b723bdf..6846df00bd 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9204,7 +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(), + activation_script: Default::default(), }) } } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 61f62f217a..9ed65f0377 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, system_shell}; +use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal, system_shell}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; @@ -293,10 +293,12 @@ impl Project { let scripts = maybe!(async { let toolchain = toolchain?.await?; - Some(toolchain.startup_script) + Some(toolchain.activation_script) }) .await; - let activation_script = scripts.as_ref().and_then(|it| it.get(&shell)); + let activation_script = scripts + .as_ref() + .and_then(|it| it.get(&ShellKind::new(&shell))); let shell = { match ssh_details { Some(SshDetails { diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 54610ad9a6..36d030b07e 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -20,6 +20,7 @@ use rpc::{ proto::{self, FromProto, ToProto}, }; use settings::WorktreeId; +use task::ShellKind; use util::ResultExt as _; use crate::{ @@ -138,7 +139,11 @@ 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(), + activation_script: toolchain + .activation_script + .into_iter() + .map(|(k, v)| (ShellKind::new(&k), v)) + .collect(), language_name, }; let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); @@ -179,7 +184,11 @@ 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(), + activation_script: toolchain + .activation_script + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), } }), }) @@ -223,7 +232,11 @@ 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(), + activation_script: toolchain + .activation_script + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), } }) .collect::>(); @@ -452,7 +465,11 @@ 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(), + activation_script: toolchain + .activation_script + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), }), path: Some(project_path.path.to_string_lossy().into_owned()), }) @@ -505,7 +522,11 @@ impl RemoteToolchainStore { .to_string() .into(), as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, - startup_script: toolchain.activation_script.into_iter().collect(), + activation_script: toolchain + .activation_script + .into_iter() + .map(|(k, v)| (ShellKind::new(&k), v)) + .collect(), }) }) .collect(); @@ -562,7 +583,11 @@ impl RemoteToolchainStore { .to_string() .into(), as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, - startup_script: toolchain.activation_script.into_iter().collect(), + activation_script: toolchain + .activation_script + .into_iter() + .map(|(k, v)| (ShellKind::new(&k), v)) + .collect(), }) }) }) diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 0ebba1b3c8..1195e59c93 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,3 +1,5 @@ +use std::fmt; + use crate::Shell; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -11,6 +13,19 @@ 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()) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index e50582222f..a673e321db 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1,4 +1,11 @@ -use std::{cmp, ops::ControlFlow, path::PathBuf, process::ExitStatus, sync::Arc, time::Duration}; +use std::{ + cmp, + ops::ControlFlow, + path::{Path, PathBuf}, + process::ExitStatus, + sync::Arc, + time::Duration, +}; use crate::{ TerminalView, default_working_directory, @@ -414,22 +421,36 @@ impl TerminalPanel { let weak_workspace = self.workspace.clone(); let project = workspace.project().clone(); let active_pane = &self.active_pane; - let working_directory = 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)) - }) - .unwrap_or(None); + .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| { - project.create_terminal_shell(working_directory, cx, None) + .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, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ), }) .ok()? .await @@ -770,7 +791,17 @@ impl TerminalPanel { let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; let terminal = project .update(cx, |project, cx| { - project.create_terminal_shell(cwd, cx, None) + project.create_terminal_shell( + cwd, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ) })? .await?; let result = workspace.update_in(cx, |workspace, window, cx| { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 36938142d0..a719c4055a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -205,7 +205,17 @@ impl TerminalView { ) { let working_directory = default_working_directory(workspace, cx); TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| { - project.create_terminal_shell(working_directory, cx, None) + project.create_terminal_shell( + working_directory, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ) }) .detach_and_log_err(cx); } @@ -1490,7 +1500,17 @@ impl SerializableItem for TerminalView { let terminal = project .update(cx, |project, cx| { - project.create_terminal_shell(cwd, cx, None) + project.create_terminal_shell( + cwd, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ) })? .await?; cx.update(|window, cx| { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index dc3a502ef8..28f0153d9e 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1389,6 +1389,7 @@ impl WorkspaceDb { relative_path: String, language_name: LanguageName, ) -> Result> { + return Ok(None); self.write(move |this| { let mut select = this .select_bound(sql!( @@ -1404,8 +1405,7 @@ impl WorkspaceDb { path: path.into(), language_name, as_json: serde_json::Value::from_str(&raw_json).ok()?, - // todo refresh? - startup_script: Default::default(), + activation_script: Default::default(), }))) }) .await @@ -1415,6 +1415,7 @@ impl WorkspaceDb { &self, workspace_id: WorkspaceId, ) -> Result)>> { + return Ok(vec![]); self.write(move |this| { let mut select = this .select_bound(sql!( @@ -1430,8 +1431,7 @@ impl WorkspaceDb { path: path.into(), language_name: LanguageName::new(&language_name), as_json: serde_json::Value::from_str(&raw_json).ok()?, - // todo refresh? - startup_script: Default::default(), + activation_script: Default::default(), }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect()) }) .await From d2f2142127fdc265b27ccc19d6d8747d88300dab Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 21 Aug 2025 13:23:56 +0200 Subject: [PATCH 6/7] Setup activation scripts in python toolchain --- Cargo.lock | 1 + crates/languages/Cargo.toml | 1 + crates/languages/src/python.rs | 249 +++++----------------- crates/project/src/terminals.rs | 34 ++- crates/task/src/shell_builder.rs | 24 +-- crates/task/src/task.rs | 2 +- crates/terminal_view/src/persistence.rs | 20 +- crates/terminal_view/src/terminal_view.rs | 27 ++- 8 files changed, 109 insertions(+), 249 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c835b503ad..ef06443b5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9269,6 +9269,7 @@ dependencies = [ "snippet_provider", "task", "tempfile", + "terminal", "text", "theme", "toml 0.8.20", diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 8e25818070..e431573088 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -71,6 +71,7 @@ settings.workspace = true smol.workspace = true snippet_provider.workspace = true task.workspace = true +terminal.workspace = true tempfile.workspace = true toml.workspace = true tree-sitter = { workspace = true, optional = true } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 9073960654..ab36f47704 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] @@ -791,7 +792,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 @@ -856,7 +857,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}"); } @@ -876,13 +877,46 @@ 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()?, - activation_script: std::iter::once(( - ShellKind::Fish, - "echo python recognized a venv and injected a fish startup command" - .to_owned(), - )) - .collect(), + as_json: serde_json::to_value(toolchain.clone()).ok()?, + activation_script: match (toolchain.kind, toolchain.prefix) { + (Some(PythonEnvironmentKind::Venv), Some(prefix)) => [ + ( + ShellKind::Fish, + "source", + prefix.join(BINARY_DIR).join("activate.fish"), + ), + ( + ShellKind::Powershell, + ".", + prefix.join(BINARY_DIR).join("activate.ps1"), + ), + ( + ShellKind::Nushell, + "overlay use", + prefix.join(BINARY_DIR).join("activate.nu"), + ), + ( + ShellKind::Posix, + ".", + prefix.join(BINARY_DIR).join("activate"), + ), + ( + ShellKind::Cmd, + ".", + prefix.join(BINARY_DIR).join("activate.bat"), + ), + ( + ShellKind::Csh, + ".", + prefix.join(BINARY_DIR).join("activate.csh"), + ), + ] + .into_iter() + .filter(|(_, _, path)| path.exists() && path.is_file()) + .map(|(kind, cmd, path)| (kind, format!("{cmd} {}", path.display()))) + .collect(), + _ => Default::default(), + }, }) }) .collect(); @@ -1694,180 +1728,3 @@ mod tests { }); } } -/* -fn python_venv_directory( - 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() - .map_or(false, |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, - } -} - -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", - }; - - let line_ending = match std::env::consts::OS { - "windows" => "\r", - _ => "\n", - }; - - 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 - } - }) - } else { - Task::ready(Some(format!( - "{activate_keyword} {activate_script_name} {name}; clear{line_ending}", - name = venv_settings.venv_name - ))) - } -} -*/ diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 9ed65f0377..a0ab1a4bff 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -11,12 +11,12 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal, system_shell}; +use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; use util::{ - maybe, + get_system_shell, maybe, paths::{PathStyle, RemotePathBuf}, }; @@ -287,18 +287,17 @@ impl Project { args: _, title_override: _, } => program.clone(), - Shell::System => system_shell(), + Shell::System => get_system_shell(), }, }; + let shell_kind = ShellKind::new(&shell); let scripts = maybe!(async { let toolchain = toolchain?.await?; Some(toolchain.activation_script) }) .await; - let activation_script = scripts - .as_ref() - .and_then(|it| it.get(&ShellKind::new(&shell))); + let activation_script = scripts.as_ref().and_then(|it| it.get(&shell_kind)); let shell = { match ssh_details { Some(SshDetails { @@ -336,19 +335,17 @@ impl Project { title_override: Some(format!("{} — Terminal", host).into()), } } - 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 => match activation_script { + Some(activation_script) => Shell::WithArguments { + program: shell.clone(), + args: vec![ + "-c".to_owned(), + format!("{activation_script}; exec {shell} -l",), + ], + title_override: None, + }, + None => settings.shell, }, - None => settings.shell, } }; project.update(cx, move |this, cx| { @@ -508,6 +505,7 @@ pub fn wrap_for_ssh( path_style: PathStyle, activation_script: Option<&str>, ) -> (String, Vec) { + // todo make this shell aware let to_run = if let Some((command, args)) = command { let command: Option> = shlex::try_quote(command).ok(); let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok()); diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 1195e59c93..2d30b3f769 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,5 +1,7 @@ use std::fmt; +use util::get_system_shell; + use crate::Shell; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -28,7 +30,7 @@ impl fmt::Display for ShellKind { impl ShellKind { pub fn system() -> Self { - Self::new(&system_shell()) + Self::new(&get_system_shell()) } pub fn new(program: &str) -> Self { @@ -37,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 @@ -193,18 +195,6 @@ impl ShellKind { } } -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 - // 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 { @@ -221,7 +211,7 @@ impl ShellBuilder { let (program, args) = match shell { Shell::System => match remote_system_shell { Some(remote_shell) => (remote_shell.to_string(), Vec::new()), - None => (system_shell(), Vec::new()), + None => (get_system_shell(), Vec::new()), }, Shell::Program(shell) => (shell.clone(), Vec::new()), Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index 2dd771b9e5..eb9e59f087 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, system_shell}; +pub use shell_builder::{ShellBuilder, ShellKind}; pub use task_template::{ DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index 328468dfb8..b90f0fc7d3 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -3,9 +3,12 @@ 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; +use project::{Project, ProjectPath}; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; @@ -242,8 +245,19 @@ async fn deserialize_pane_group( .update(cx, |workspace, cx| default_working_directory(workspace, cx)) .ok() .flatten(); + let p = workspace + .update(cx, |workspace, cx| { + let worktree = workspace.worktrees(cx).next()?.read(cx); + worktree.root_dir()?; + Some(ProjectPath { + worktree_id: worktree.id(), + path: Arc::from(Path::new("")), + }) + }) + .ok() + .flatten(); let terminal = project.update(cx, |project, cx| { - project.create_terminal_shell(working_directory, cx, None) + project.create_terminal_shell(working_directory, cx, p) }); Some(Some(terminal)) } else { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index a719c4055a..09daab1bc1 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}; +use project::{Project, ProjectPath, search::SearchQuery}; use schemars::JsonSchema; use task::TaskId; use terminal::{ @@ -1498,20 +1498,19 @@ impl SerializableItem for TerminalView { .ok() .flatten(); + let p = workspace + .update(cx, |workspace, cx| { + let worktree = workspace.worktrees(cx).next()?.read(cx); + worktree.root_dir()?; + Some(ProjectPath { + worktree_id: worktree.id(), + path: Arc::from(Path::new("")), + }) + }) + .ok() + .flatten(); let terminal = project - .update(cx, |project, cx| { - project.create_terminal_shell( - cwd, - cx, - project - .active_entry() - .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) - .map(|worktree_id| project::ProjectPath { - worktree_id, - path: Arc::from(Path::new("")), - }), - ) - })? + .update(cx, |project, cx| project.create_terminal_shell(cwd, cx, p))? .await?; cx.update(|window, cx| { cx.new(|cx| { From 333354024d7e8899db694fc1bc1a4dfc05d2177f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 22 Aug 2025 12:06:07 +0200 Subject: [PATCH 7/7] Load python venv for tasks --- crates/agent2/src/tools/terminal_tool.rs | 34 ++-- crates/assistant_tools/src/terminal_tool.rs | 33 ++-- crates/debugger_ui/src/session/running.rs | 26 ++- crates/language/src/toolchain.rs | 3 + crates/project/src/terminals.rs | 204 ++++++++++++-------- crates/terminal/src/terminal_settings.rs | 1 + crates/terminal_view/src/terminal_panel.rs | 47 ++++- crates/workspace/src/persistence.rs | 3 +- 8 files changed, 236 insertions(+), 115 deletions(-) diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 18f4571d65..9bd2a9b58a 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -141,18 +141,28 @@ impl AgentTool for TerminalTool { let program = program.await; let env = env.await; - let terminal = self.project.update(cx, |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(program), - args, - cwd: working_dir.clone(), - env, - ..Default::default() - }, - cx, - ) - })??; + let terminal = self + .project + .update(cx, |project, cx| { + project.create_terminal_task( + task::SpawnInTerminal { + command: Some(program), + args, + cwd: working_dir.clone(), + env, + ..Default::default() + }, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ) + })? + .await?; let acp_terminal = cx.new(|cx| { acp_thread::Terminal::new( input.command.clone(), diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index b8f09012cb..49584d3c9c 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -213,18 +213,27 @@ impl Tool for TerminalTool { async move |cx| { let program = program.await; let env = env.await; - project.update(cx, |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(program), - args, - cwd, - env, - ..Default::default() - }, - cx, - ) - })? + project + .update(cx, |project, cx| { + project.create_terminal_task( + task::SpawnInTerminal { + command: Some(program), + args, + cwd, + env, + ..Default::default() + }, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ) + })? + .await } }); diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index fd135596eb..0881cdcb22 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1018,8 +1018,15 @@ impl RunningState { project.create_terminal_task( task_with_shell.clone(), cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(std::path::Path::new("")), + }), ) - })??; + })?.await?; let terminal_view = cx.new_window_entity(|window, cx| { TerminalView::new( @@ -1186,10 +1193,21 @@ impl RunningState { let workspace = self.workspace.clone(); let weak_project = project.downgrade(); - let terminal_task = - project.update(cx, |project, cx| project.create_terminal_task(kind, cx)); + let terminal_task = project.update(cx, |project, cx| { + project.create_terminal_task( + kind, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(std::path::Path::new("")), + }), + ) + }); let terminal_task = cx.spawn_in(window, async move |_, cx| { - let terminal = terminal_task?; + let terminal = terminal_task.await?; let terminal_view = cx.new_window_entity(|window, cx| { TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 96f417aa86..5798863a00 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -28,6 +28,9 @@ pub struct Toolchain { pub as_json: serde_json::Value, /// shell -> script pub activation_script: FxHashMap, + // Option + // sh activate -c "user shell -l" + // check if this work with powershell } impl std::hash::Hash for Toolchain { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index a0ab1a4bff..149db2c51c 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -94,7 +94,8 @@ impl Project { &mut self, spawn_task: SpawnInTerminal, cx: &mut Context, - ) -> Result> { + project_path_context: Option, + ) -> Task>> { let this = &mut *self; let ssh_details = this.ssh_details(cx); let path: Option> = if let Some(cwd) = &spawn_task.cwd { @@ -135,8 +136,14 @@ impl Project { env.extend(settings.env); let local_path = if is_ssh_terminal { None } else { path.clone() }; - - let (spawn_task, shell) = { + let toolchain = + project_path_context.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); + cx.spawn(async move |project, cx| { + let scripts = maybe!(async { + let toolchain = toolchain?.await?; + Some(toolchain.activation_script) + }) + .await; let task_state = Some(TaskState { id: spawn_task.id, full_label: spawn_task.full_label, @@ -150,94 +157,132 @@ impl Project { completion_rx, }); - env.extend(spawn_task.env); + let shell = { + env.extend(spawn_task.env); + // todo(lw): Use shell builder + 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 => get_system_shell(), + }, + }; + let shell_kind = ShellKind::new(&shell); + let activation_script = scripts.as_ref().and_then(|it| it.get(&shell_kind)); - match ssh_details { - Some(SshDetails { - host, - ssh_command, - envs, - path_style, - shell, - }) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - let (program, args) = wrap_for_ssh( - &shell, - &ssh_command, - spawn_task - .command - .as_ref() - .map(|command| (command, &spawn_task.args)), - path.as_deref(), - env, + match ssh_details { + Some(SshDetails { + host, + ssh_command, + envs, path_style, - None, - ); - env = HashMap::default(); - if let Some(envs) = envs { - env.extend(envs); - } - ( - task_state, + shell, + }) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + + let (program, args) = wrap_for_ssh( + &shell, + &ssh_command, + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), + path.as_deref(), + env, + path_style, + activation_script.map(String::as_str), + ); + env = HashMap::default(); + if let Some(envs) = envs { + env.extend(envs); + } Shell::WithArguments { program, args, title_override: Some(format!("{} — Terminal", host).into()), - }, - ) - } - None => { - let shell = if let Some(program) = spawn_task.command { - Shell::WithArguments { - program, - args: spawn_task.args, - title_override: None, } - } else { - Shell::System - }; - (task_state, shell) + } + None => match activation_script { + 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: 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()), - spawn_task, - shell, - env, - settings.cursor_shape.unwrap_or_default(), - settings.alternate_scroll, - settings.max_scroll_history_lines, - is_ssh_terminal, - cx.entity_id().as_u64(), - Some(completion_tx), - cx, - None, - ) - .map(|builder| { - let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + }; + project.update(cx, move |this, cx| { + 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_ssh_terminal, + cx.entity_id().as_u64(), + Some(completion_tx), + cx, + None, + ) + .map(|builder| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); - this.terminals - .local_handles - .push(terminal_handle.downgrade()); + 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; + 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(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } + }) + .detach(); - terminal_handle + terminal_handle + }) + })? }) } @@ -278,6 +323,7 @@ impl Project { let toolchain = project_path_context.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); cx.spawn(async move |project, cx| { + // todo(lw): Use shell builder let shell = match &ssh_details { Some(ssh) => ssh.shell.clone(), None => match &settings.shell { diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 635e3e2ca5..133be9e623 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -95,6 +95,7 @@ pub enum VenvSettings { /// to the current working directory. We recommend overriding this /// in your project's settings, rather than globally. activate_script: Option, + // deprecate but use venv_name: Option, directories: Option>, }, diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index a673e321db..7d8377579a 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -592,7 +592,17 @@ impl TerminalPanel { .workspace .update(cx, |workspace, cx| { Self::add_center_terminal(workspace, window, cx, |project, cx| { - Task::ready(project.create_terminal_task(spawn_task, cx)) + project.create_terminal_task( + spawn_task, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ) }) }) .unwrap_or_else(|e| Task::ready(Err(e))), @@ -731,8 +741,21 @@ impl TerminalPanel { 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_task(task, cx))??; + let terminal = project + .update(cx, |project, cx| { + project.create_terminal_task( + task, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ) + })? + .await?; let result = workspace.update_in(cx, |workspace, window, cx| { let terminal_view = Box::new(cx.new(|cx| { TerminalView::new( @@ -909,9 +932,21 @@ impl TerminalPanel { this.workspace .update(cx, |workspace, _| workspace.project().clone()) })??; - let new_terminal = project.update(cx, |project, cx| { - project.create_terminal_task(spawn_task, cx) - })??; + let new_terminal = project + .update(cx, |project, cx| { + project.create_terminal_task( + spawn_task, + cx, + project + .active_entry() + .and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx)) + .map(|worktree_id| project::ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }), + ) + })? + .await?; terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| { terminal_to_replace.set_terminal(new_terminal.clone(), window, cx); })?; diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 28f0153d9e..c293bd0e49 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1389,7 +1389,6 @@ impl WorkspaceDb { relative_path: String, language_name: LanguageName, ) -> Result> { - return Ok(None); self.write(move |this| { let mut select = this .select_bound(sql!( @@ -1415,7 +1414,6 @@ impl WorkspaceDb { &self, workspace_id: WorkspaceId, ) -> Result)>> { - return Ok(vec![]); self.write(move |this| { let mut select = this .select_bound(sql!( @@ -1426,6 +1424,7 @@ impl WorkspaceDb { let toolchain: Vec<(String, String, u64, String, String, String)> = select(workspace_id)?; + // todo look into re-serializing these if we fix up Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain { name: name.into(), path: path.into(),