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