From 0ce4dbb64150755532369e4028cdc811c1da6fdd Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 18 Aug 2025 16:43:32 +0200 Subject: [PATCH] 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(