diff --git a/Cargo.lock b/Cargo.lock index a4f8c521a1..dce3f77cce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9266,6 +9266,7 @@ dependencies = [ "snippet_provider", "task", "tempfile", + "terminal", "text", "theme", "toml 0.8.20", diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index af740d9901..0a67e0ad32 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -333,7 +333,7 @@ impl NativeAgent { Err(err) => ( None, Some(RulesLoadingError { - message: format!("{err}").into(), + message: format!("{err:#}").into(), }), ), }; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 4ad156b9fb..921ffe11db 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -557,7 +557,7 @@ impl ChannelStore { }; cx.background_spawn(async move { task.await.map_err(|error| { - anyhow!("{error}").context(format!("failed to open channel {resource_name}")) + anyhow!(error).context(format!("failed to open channel {resource_name}")) }) }) } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f09c012a85..653e27b8d4 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -686,7 +686,7 @@ impl Client { } ConnectionResult::Result(r) => { if let Err(error) = r { - log::error!("failed to connect: {error}"); + log::error!("failed to connect: {error:?}"); } else { break; } diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 2e6b4719d1..f3925be184 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -874,7 +874,7 @@ fn operation_from_storage( _format_version: i32, ) -> Result { let operation = - storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{error}"))?; + storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!(error))?; let version = version_from_storage(&operation.version); Ok(if operation.is_undo { proto::operation::Variant::Undo(proto::operation::Undo { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index ef749ac9b7..2c1bf0a6c2 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1312,7 +1312,7 @@ pub async fn handle_metrics(Extension(server): Extension>) -> Result let metric_families = prometheus::gather(); let encoded_metrics = encoder .encode_to_string(&metric_families) - .map_err(|err| anyhow!("{err}"))?; + .map_err(|err| anyhow!(err))?; Ok(encoded_metrics) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index a2bd934311..dc4cefc17e 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,5 +1,5 @@ use crate::*; -use anyhow::Context as _; +use anyhow::{Context as _, anyhow, bail}; use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use fs::RemoveOptions; use futures::{StreamExt, TryStreamExt}; @@ -24,7 +24,7 @@ use util::{ResultExt, maybe}; #[derive(Default)] pub(crate) struct PythonDebugAdapter { - debugpy_whl_base_path: OnceCell, String>>, + debugpy_whl_base_path: OnceCell>, } impl PythonDebugAdapter { @@ -91,13 +91,13 @@ impl PythonDebugAdapter { }) } - async fn fetch_wheel(delegate: &Arc) -> Result, String> { + async fn fetch_wheel(delegate: &Arc) -> Result> { let system_python = Self::system_python_name(delegate) .await - .ok_or_else(|| String::from("Could not find a Python installation"))?; + .ok_or_else(|| anyhow!("Could not find a Python installation"))?; let command: &OsStr = system_python.as_ref(); let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels"); - std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?; + std::fs::create_dir_all(&download_dir)?; let installation_succeeded = util::command::new_smol_command(command) .args([ "-m", @@ -109,32 +109,27 @@ impl PythonDebugAdapter { download_dir.to_string_lossy().as_ref(), ]) .output() - .await - .map_err(|e| format!("{e}"))? + .await? .status .success(); if !installation_succeeded { - return Err("debugpy installation failed".into()); + bail!("debugpy installation failed"); } - let wheel_path = std::fs::read_dir(&download_dir) - .map_err(|e| e.to_string())? + let wheel_path = std::fs::read_dir(&download_dir)? .find_map(|entry| { entry.ok().filter(|e| { e.file_type().is_ok_and(|typ| typ.is_file()) && Path::new(&e.file_name()).extension() == Some("whl".as_ref()) }) }) - .ok_or_else(|| String::from("Did not find a .whl in {download_dir}"))?; + .ok_or_else(|| anyhow!("Did not find a .whl in {download_dir:?}"))?; util::archive::extract_zip( &debug_adapters_dir().join(Self::ADAPTER_NAME), - File::open(&wheel_path.path()) - .await - .map_err(|e| e.to_string())?, + File::open(&wheel_path.path()).await?, ) - .await - .map_err(|e| e.to_string())?; + .await?; Ok(Arc::from(wheel_path.path())) } @@ -198,20 +193,17 @@ impl PythonDebugAdapter { .await; } - async fn fetch_debugpy_whl( - &self, - delegate: &Arc, - ) -> Result, String> { + async fn fetch_debugpy_whl(&self, delegate: &Arc) -> Arc { self.debugpy_whl_base_path .get_or_init(|| async move { Self::maybe_fetch_new_wheel(delegate).await; - Ok(Arc::from( + Arc::from( debug_adapters_dir() .join(Self::ADAPTER_NAME) .join("debugpy") .join("adapter") .as_ref(), - )) + ) }) .await .clone() @@ -704,10 +696,7 @@ impl DebugAdapter for PythonDebugAdapter { ) .await; - let debugpy_path = self - .fetch_debugpy_whl(delegate) - .await - .map_err(|e| anyhow::anyhow!("{e}"))?; + let debugpy_path = self.fetch_debugpy_whl(delegate).await; if let Some(toolchain) = &toolchain { log::debug!( "Found debugpy in toolchain environment: {}", diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 8e25818070..6610d06bce 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -72,6 +72,7 @@ smol.workspace = true snippet_provider.workspace = true task.workspace = true tempfile.workspace = true +terminal.workspace = true toml.workspace = true tree-sitter = { workspace = true, optional = true } tree-sitter-bash = { workspace = true, optional = true } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index b61ad2d36c..708952275d 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1,4 +1,4 @@ -use anyhow::{Context as _, ensure}; +use anyhow::{Context as _, Error, ensure}; use anyhow::{Result, anyhow}; use async_trait::async_trait; use collections::HashMap; @@ -22,6 +22,7 @@ use project::lsp_store::language_server_settings; use serde_json::{Value, json}; use smol::lock::OnceCell; use std::cmp::Ordering; +use std::ffi::OsStr; use parking_lot::Mutex; use std::str::FromStr; @@ -335,36 +336,20 @@ impl LspAdapter for PythonLspAdapter { } let object = user_settings.as_object_mut().unwrap(); - let interpreter_path = toolchain.path.to_string(); + let interpreter_path = toolchain.path.as_ref(); - // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() { - if let Some(venv_dir) = interpreter_dir.parent() { - // Check if this looks like a virtual environment - if venv_dir.join("pyvenv.cfg").exists() - || venv_dir.join("bin/activate").exists() - || venv_dir.join("Scripts/activate.bat").exists() - { - // Set venvPath and venv at the root level - // This matches the format of a pyrightconfig.json file - if let Some(parent) = venv_dir.parent() { - // Use relative path if the venv is inside the workspace - let venv_path = if parent == adapter.worktree_root_path() { - ".".to_string() - } else { - parent.to_string_lossy().into_owned() - }; - object.insert("venvPath".to_string(), Value::String(venv_path)); - } - - if let Some(venv_name) = venv_dir.file_name() { - object.insert( - "venv".to_owned(), - Value::String(venv_name.to_string_lossy().into_owned()), - ); - } - } - } + let (venv_path, venv) = detect_venv(&**adapter, interpreter_path); + if let Some(venv) = venv { + object.insert( + "venv".to_owned(), + Value::String(venv.to_string_lossy().into_owned()), + ); + } + if let Some(venv_path) = venv_path { + object.insert( + "venvPath".to_string(), + Value::String(venv_path.to_string_lossy().into_owned()), + ); } // Always set the python interpreter path @@ -378,11 +363,11 @@ impl LspAdapter for PythonLspAdapter { // Set both pythonPath and defaultInterpreterPath for compatibility python.insert( "pythonPath".to_owned(), - Value::String(interpreter_path.clone()), + Value::String(interpreter_path.to_owned()), ); python.insert( "defaultInterpreterPath".to_owned(), - Value::String(interpreter_path), + Value::String(interpreter_path.to_owned()), ); } @@ -967,7 +952,7 @@ impl pet_core::os_environment::Environment for EnvironmentApi<'_> { } pub(crate) struct PyLspAdapter { - python_venv_base: OnceCell, String>>, + python_venv_base: OnceCell, Arc>>, } impl PyLspAdapter { const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp"); @@ -1009,13 +994,9 @@ impl PyLspAdapter { None } - async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result, String> { + async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result, Arc> { self.python_venv_base - .get_or_init(move || async move { - Self::ensure_venv(delegate) - .await - .map_err(|e| format!("{e}")) - }) + .get_or_init(move || async move { Self::ensure_venv(delegate).await.map_err(Arc::new) }) .await .clone() } @@ -1027,6 +1008,12 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") { "bin" }; +const ACTIVATE_PATH: &str = if cfg!(target_os = "windows") { + "Scripts/activate.bat" +} else { + "bin/activate" +}; + #[async_trait(?Send)] impl LspAdapter for PyLspAdapter { fn name(&self) -> LanguageServerName { @@ -1263,7 +1250,7 @@ impl LspAdapter for PyLspAdapter { } pub(crate) struct BasedPyrightLspAdapter { - python_venv_base: OnceCell, String>>, + python_venv_base: OnceCell, Arc>>, } impl BasedPyrightLspAdapter { @@ -1310,13 +1297,9 @@ impl BasedPyrightLspAdapter { None } - async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result, String> { + async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result, Arc> { self.python_venv_base - .get_or_init(move || async move { - Self::ensure_venv(delegate) - .await - .map_err(|e| format!("{e}")) - }) + .get_or_init(move || async move { Self::ensure_venv(delegate).await.map_err(Arc::new) }) .await .clone() } @@ -1523,36 +1506,20 @@ impl LspAdapter for BasedPyrightLspAdapter { } let object = user_settings.as_object_mut().unwrap(); - let interpreter_path = toolchain.path.to_string(); + let interpreter_path = toolchain.path.as_ref(); - // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() { - if let Some(venv_dir) = interpreter_dir.parent() { - // Check if this looks like a virtual environment - if venv_dir.join("pyvenv.cfg").exists() - || venv_dir.join("bin/activate").exists() - || venv_dir.join("Scripts/activate.bat").exists() - { - // Set venvPath and venv at the root level - // This matches the format of a pyrightconfig.json file - if let Some(parent) = venv_dir.parent() { - // Use relative path if the venv is inside the workspace - let venv_path = if parent == adapter.worktree_root_path() { - ".".to_string() - } else { - parent.to_string_lossy().into_owned() - }; - object.insert("venvPath".to_string(), Value::String(venv_path)); - } - - if let Some(venv_name) = venv_dir.file_name() { - object.insert( - "venv".to_owned(), - Value::String(venv_name.to_string_lossy().into_owned()), - ); - } - } - } + let (venv_path, venv) = detect_venv(&**adapter, interpreter_path); + if let Some(venv) = venv { + object.insert( + "venv".to_owned(), + Value::String(venv.to_string_lossy().into_owned()), + ); + } + if let Some(venv_path) = venv_path { + object.insert( + "venvPath".to_string(), + Value::String(venv_path.to_string_lossy().into_owned()), + ); } // Always set the python interpreter path @@ -1566,11 +1533,11 @@ impl LspAdapter for BasedPyrightLspAdapter { // Set both pythonPath and defaultInterpreterPath for compatibility python.insert( "pythonPath".to_owned(), - Value::String(interpreter_path.clone()), + Value::String(interpreter_path.to_owned()), ); python.insert( "defaultInterpreterPath".to_owned(), - Value::String(interpreter_path), + Value::String(interpreter_path.to_owned()), ); } @@ -1583,6 +1550,38 @@ impl LspAdapter for BasedPyrightLspAdapter { } } +/// Detect if the interpreter path belongs to a virtual environment +fn detect_venv<'p>( + adapter: &dyn LspAdapterDelegate, + interpreter_path: &'p str, +) -> (Option<&'p Path>, Option<&'p OsStr>) { + let mut venv_path = None; + let mut venv = None; + // Detect if this is a virtual environment + if let Some(interpreter_dir) = Path::new(interpreter_path).parent() { + if let Some(venv_dir) = interpreter_dir.parent() { + // Check if this looks like a virtual environment + if venv_dir.join("pyvenv.cfg").exists() || venv_dir.join(ACTIVATE_PATH).exists() { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + venv_path = Some(if parent == adapter.worktree_root_path() { + Path::new(".") + } else { + parent + }); + } + + if let Some(venv_name) = venv_dir.file_name() { + venv = Some(venv_name); + } + } + } + } + (venv_path, venv) +} + #[cfg(test)] mod tests { use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext}; diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 6f834b5dc0..f857c63e1b 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -287,7 +287,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 5ea7b87fbe..9b793a5229 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,7 @@ use crate::{Project, ProjectPath}; -use anyhow::{Context as _, Result}; +use anyhow::Result; use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; +use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::LanguageName; use remote::ssh_session::SshArgs; @@ -9,7 +9,6 @@ use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ borrow::Cow, - env::{self}, path::{Path, PathBuf}, sync::Arc, }; @@ -18,10 +17,7 @@ use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, }; -use util::{ - ResultExt, - paths::{PathStyle, RemotePathBuf}, -}; +use util::paths::{PathStyle, RemotePathBuf}; pub struct Terminals { pub(crate) local_handles: Vec>, @@ -126,14 +122,104 @@ impl Project { 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 + match project.upgrade() { + Some(project) => { + let venv_dir = + Self::python_venv_directory(project.clone(), path, venv.clone(), cx) + .await?; + match venv_dir { + Some(venv_dir) + if project + .update(cx, |project, cx| { + project.resolve_abs_path( + &venv_dir + .join(BINARY_DIR) + .join("activate") + .to_string_lossy(), + cx, + ) + })? + .await + .is_some_and(|m| m.is_file()) => + { + Some((venv_dir, venv)) + } + _ => None, + } + } + None => None, + } } else { None }; + // todo lw: move all this out of here? + let startup_script = move |shell: &_| { + let Some((python_venv_directory, venv)) = &python_venv_directory else { + return None; + }; + // todo: If this returns `None` we may don't have a venv, but we may still have a python toolchiain + // we should modify the PATH here still + let venv_settings = venv.as_option()?; + // todo: handle ssh! + let script_kind = if venv_settings.activate_script + == terminal_settings::ActivateScript::Default + { + match shell { + Shell::Program(program) => ActivateScript::by_shell(program), + Shell::WithArguments { + program, + args: _, + title_override: _, + } => ActivateScript::by_shell(program), + Shell::System => None, + } + } else { + Some(venv_settings.activate_script) + } + .or_else(|| ActivateScript::by_env()); + let script_kind = match script_kind { + Some(activate) => activate, + None => ActivateScript::Default, + }; + + 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 activate_keyword = match script_kind { + terminal_settings::ActivateScript::Nushell => "overlay use", + terminal_settings::ActivateScript::PowerShell => ".", + terminal_settings::ActivateScript::Pyenv => "pyenv", + terminal_settings::ActivateScript::Default + | terminal_settings::ActivateScript::Csh + | terminal_settings::ActivateScript::Fish => "source", + }; + + let line_ending = if cfg!(windows) { '\r' } else { '\n' }; + + if venv_settings.venv_name.is_empty() { + let path = python_venv_directory + .join(BINARY_DIR) + .join(activate_script_name) + .to_string_lossy() + .to_string(); + let quoted = shlex::try_quote(&path).ok()?; + Some(format!("{activate_keyword} {quoted}{line_ending}",)) + } else { + Some(format!( + "{activate_keyword} {activate_script_name} {name}{line_ending}", + name = venv_settings.venv_name + )) + } + }; + project.update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, cx) + project.create_terminal_with_startup(kind, startup_script, cx) })? }) } @@ -182,7 +268,6 @@ impl Project { Some((&command, &args)), path.as_deref(), env, - None, path_style, ); let mut command = std::process::Command::new(command); @@ -204,10 +289,30 @@ impl Project { } } - pub fn create_terminal_with_venv( + pub fn clone_terminal( + &mut self, + terminal: &Entity, + cx: &mut Context, + ) -> Result> { + let terminal = terminal.read(cx); + let working_directory = terminal + .working_directory() + .or_else(|| Some(self.active_project_directory(cx)?.to_path_buf())); + let startup_script = terminal.startup_script.clone(); + self.create_terminal_with_startup( + TerminalKind::Shell(working_directory), + |_| { + // The shell shouldn't change here + startup_script + }, + cx, + ) + } + + pub fn create_terminal_with_startup( &mut self, kind: TerminalKind, - python_venv_directory: Option, + startup_script: impl FnOnce(&Shell) -> Option + 'static, cx: &mut Context, ) -> Result> { let this = &mut *self; @@ -256,19 +361,8 @@ impl Project { 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, @@ -285,14 +379,8 @@ impl Project { env.entry("TERM".to_string()) .or_insert_with(|| "xterm-256color".to_string()); - let (program, args) = wrap_for_ssh( - &ssh_command, - None, - path.as_deref(), - env, - None, - path_style, - ); + let (program, args) = + wrap_for_ssh(&ssh_command, None, path.as_deref(), env, path_style); env = HashMap::default(); if let Some(envs) = envs { env.extend(envs); @@ -325,13 +413,6 @@ impl Project { 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, @@ -342,6 +423,7 @@ impl Project { 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( &ssh_command, spawn_task @@ -350,7 +432,6 @@ impl Project { .map(|command| (command, &spawn_task.args)), path.as_deref(), env, - python_venv_directory.as_deref(), path_style, ); env = HashMap::default(); @@ -367,10 +448,6 @@ impl Project { ) } None => { - if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join("bin")).log_err(); - } - let shell = if let Some(program) = spawn_task.command { Shell::WithArguments { program, @@ -385,9 +462,11 @@ impl Project { } } }; + + let startup_script = startup_script(&shell); TerminalBuilder::new( local_path.map(|path| path.to_path_buf()), - python_venv_directory, + startup_script, spawn_task, shell, env, @@ -420,220 +499,92 @@ impl Project { }) .detach(); - this.activate_python_virtual_environment( - python_venv_activate_command, - &terminal_handle, - cx, - ); - terminal_handle }) } - fn python_venv_directory( - &self, + async fn python_venv_directory( + this: Entity, 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 { - let bin_dir_name = match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", + cx: &mut AsyncApp, + ) -> Result> { + let Some((worktree, relative_path)) = + this.update(cx, |this, cx| this.find_worktree(&abs_path, cx))? + else { + return Ok(None); }; - venv_settings - .directories - .iter() - .map(|name| abs_path.join(name)) - .find(|venv_path| { - let bin_path = venv_path.join(bin_dir_name); - 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()) - }) - } + 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, + ) + })? + .await; - 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(); - let bin_dir_name = match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", - }; - venv_settings - .directories - .iter() - .map(|name| abs_path.join(name)) - .find(|venv_path| { - let bin_path = venv_path.join(bin_dir_name); - // 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, + if let Some(toolchain) = toolchain { + let toolchain_path = Path::new(toolchain.path.as_ref()); + return Ok(toolchain_path + .parent() + .and_then(|p| Some(p.parent()?.to_path_buf()))); } - } - 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", + return Ok(None); }; - let line_ending = match std::env::consts::OS { - "windows" => "\r", - _ => "\n", - }; - - if venv_settings.venv_name.is_empty() { - let path = venv_base_directory - .join(match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", + let tool = this.update(cx, |this, cx| { + venv_settings + .directories + .iter() + .map(|name| abs_path.join(name)) + .find(|venv_path| { + let bin_path = venv_path.join(BINARY_DIR); + this.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()) }) - .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 - ))) + if let Some(toolchain_path) = tool { + return Ok(Some(toolchain_path)); } - } - 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(); + let r = this.update(cx, move |_, cx| { + let map: Vec<_> = venv_settings + .directories + .iter() + .map(|name| abs_path.join(name)) + .collect(); + Some(cx.spawn(async move |project, cx| { + for venv_path in map { + let bin_path = venv_path.join(BINARY_DIR); + let exists = project + .upgrade()? + .update(cx, |project, cx| { + project.resolve_abs_path(&bin_path.to_string_lossy(), cx) + }) + .ok()? + .await + .map_or(false, |meta| meta.is_dir()); + if exists { + return Some(venv_path); + } } - }) - .detach() - }); + None + })) + })?; + Ok(match r { + Some(task) => task.await, + None => None, + }) } pub fn local_terminal_handles(&self) -> &Vec> { @@ -641,12 +592,17 @@ impl Project { } } +const BINARY_DIR: &str = if cfg!(target_os = "windows") { + "Scripts" +} else { + "bin" +}; + pub fn wrap_for_ssh( ssh_command: &SshCommand, 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 { @@ -665,13 +621,7 @@ pub fn wrap_for_ssh( let mut env_changes = String::new(); for (k, v) in env.iter() { if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { - env_changes.push_str(&format!("{}={} ", k, v)); - } - } - if let Some(venv_directory) = venv_directory { - if 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)); + env_changes.push_str(&format!("{k}={v} ")); } } @@ -702,57 +652,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 86728cc11c..dd46b85468 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -344,7 +344,7 @@ pub struct TerminalBuilder { impl TerminalBuilder { pub fn new( working_directory: Option, - python_venv_directory: Option, + startup_script: Option, task: Option, shell: Shell, mut env: HashMap, @@ -432,9 +432,6 @@ impl TerminalBuilder { } }; - // 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. @@ -495,8 +492,8 @@ impl TerminalBuilder { //Kick things off let pty_tx = event_loop.channel(); let _io_thread = event_loop.spawn(); // DANGER - - let terminal = Terminal { + let activate = startup_script.clone(); + let mut terminal = Terminal { task, pty_tx: Notifier(pty_tx), completion_tx, @@ -517,13 +514,17 @@ impl TerminalBuilder { hyperlink_regex_searches: RegexSearches::new(), vi_mode_enabled: false, is_ssh_terminal, - python_venv_directory, + startup_script, last_mouse_move_time: Instant::now(), last_hyperlink_search_position: None, #[cfg(windows)] shell_program, }; + if let Some(activate) = activate { + terminal.input(activate.into_bytes()); + } + Ok(TerminalBuilder { terminal, events_rx, @@ -687,6 +688,8 @@ pub struct Terminal { term: Arc>>, term_config: Config, events: VecDeque, + // TODO lw: type this better + pub startup_script: Option, /// This is only used for mouse mode cell change detection last_mouse: Option<(AlacPoint, AlacDirection)>, pub matches: Vec>, @@ -695,7 +698,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, diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 3f89afffab..899f8ca0f2 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -135,6 +135,27 @@ pub enum ActivateScript { Pyenv, } +impl ActivateScript { + pub fn by_shell(shell: &str) -> Option { + Some(match shell { + "fish" => ActivateScript::Fish, + "tcsh" => ActivateScript::Csh, + "nu" => ActivateScript::Nushell, + "powershell" | "pwsh" => ActivateScript::PowerShell, + "sh" => ActivateScript::Default, + _ => return None, + }) + } + + pub fn by_env() -> Option { + let shell = std::env::var("SHELL").ok()?; + let shell = std::path::Path::new(&shell) + .file_name() + .and_then(|name| name.to_str())?; + Self::by_shell(shell) + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct TerminalSettingsContent { /// What shell to use when opening a terminal. diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 568dc1db2e..dd1fa90064 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -416,25 +416,18 @@ 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 terminal = 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(), - ) - }) - .unwrap_or((None, None)); - let kind = TerminalKind::Shell(working_directory); + .map(|terminal_view| terminal_view.read(cx).terminal().clone()); let terminal = project - .update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, cx) + .update(cx, |project, cx| match terminal { + Some(terminal) => project.clone_terminal(&terminal, cx), + None => { + project.create_terminal_with_startup(TerminalKind::Shell(None), |_| None, cx) + } }) .ok()?; diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 534c0a8051..fd72ac1224 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1668,16 +1668,7 @@ impl Item for TerminalView { let terminal = self .project .update(cx, |project, cx| { - let terminal = self.terminal().read(cx); - let working_directory = terminal - .working_directory() - .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf())); - let python_venv_directory = terminal.python_venv_directory.clone(); - project.create_terminal_with_venv( - TerminalKind::Shell(working_directory), - python_venv_directory, - cx, - ) + project.clone_terminal(self.terminal(), cx) }) .ok()? .log_err()?;