Populate project path context on shell creation

This commit is contained in:
Lukas Wirth 2025-08-20 12:22:57 +02:00
parent 8e11e6a03e
commit c9eb9a9e41
9 changed files with 311 additions and 37 deletions

View file

@ -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<String, String>,
pub activation_script: FxHashMap<ShellKind, String>,
}
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,
))
}
}

View file

@ -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<Path>,
venv_settings: VenvSettings,
cx: &Context<Project>,
) -> Task<Option<PathBuf>> {
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<PathBuf> {
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<PathBuf> {
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<Option<String>> {
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
)))
}
}
*/

View file

@ -9204,7 +9204,7 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
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(),
})
}
}

View file

@ -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 {

View file

@ -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::<Vec<_>>();
@ -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(),
})
})
})

View file

@ -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())

View file

@ -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::<TerminalView>())
.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::<TerminalView>());
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| {

View file

@ -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| {

View file

@ -1389,6 +1389,7 @@ impl WorkspaceDb {
relative_path: String,
language_name: LanguageName,
) -> Result<Option<Toolchain>> {
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<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
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