Slowly disconnect venv handling from project/terminals.rs

This commit is contained in:
Lukas Wirth 2025-08-14 14:00:37 +02:00
parent b756853407
commit dc0e81b13b
4 changed files with 117 additions and 138 deletions

View file

@ -123,13 +123,86 @@ 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))?
.update(cx, |this, cx| {
this.python_venv_directory(path, venv.clone(), cx)
})?
.await
.zip(Some(venv))
} else {
None
};
project.update(cx, |project, cx| {
project.create_terminal_with_venv(kind, python_venv_directory, cx)
// 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()?;
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 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 line_ending = match std::env::consts::OS {
"windows" => "\r",
_ => "\n",
};
if venv_settings.venv_name.is_empty() {
let path = python_venv_directory
.join(match std::env::consts::OS {
"windows" => "Scripts",
_ => "bin",
})
.join(activate_script_name)
.to_string_lossy()
.to_string();
let quoted = shlex::try_quote(&path).ok()?;
Some(format!(
"{} {} ; clear{}",
activate_keyword, quoted, line_ending
))
} else {
Some(format!(
"{activate_keyword} {activate_script_name} {name}; clear{line_ending}",
name = venv_settings.venv_name
))
}
};
project.create_terminal_with_startup(kind, startup_script, cx)
})?
})
}
@ -199,10 +272,30 @@ impl Project {
}
}
pub fn create_terminal_with_venv(
pub fn clone_terminal(
&mut self,
terminal: &Entity<Terminal>,
cx: &mut Context<Self>,
) -> Result<Entity<Terminal>> {
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<PathBuf>,
startup_script: impl FnOnce(&Shell) -> Option<String> + 'static,
cx: &mut Context<Self>,
) -> Result<Entity<Terminal>> {
let this = &mut *self;
@ -353,17 +446,10 @@ impl Project {
}
};
let python_venv_activate_command = if let Some(python_venv_directory) =
&python_venv_directory
{
this.python_activate_command(python_venv_directory, &settings.detect_venv, &shell, cx)
} else {
Task::ready(None)
};
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,
@ -396,12 +482,6 @@ impl Project {
})
.detach();
this.activate_python_virtual_environment(
python_venv_activate_command,
&terminal_handle,
cx,
);
terminal_handle
})
}
@ -514,104 +594,6 @@ impl Project {
}
}
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(match std::env::consts::OS {
"windows" => "Scripts",
_ => "bin",
})
.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<Option<String>>,
terminal_handle: &Entity<Terminal>,
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<WeakEntity<terminal::Terminal>> {
&self.terminals.local_handles
}

View file

@ -344,7 +344,7 @@ pub struct TerminalBuilder {
impl TerminalBuilder {
pub fn new(
working_directory: Option<PathBuf>,
python_venv_directory: Option<PathBuf>,
startup_script: Option<String>,
task: Option<TaskState>,
shell: Shell,
mut env: HashMap<String, String>,
@ -492,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,
@ -514,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,
@ -684,6 +688,8 @@ pub struct Terminal {
term: Arc<FairMutex<Term<ZedListener>>>,
term_config: Config,
events: VecDeque<InternalEvent>,
// TODO lw: type this better
pub startup_script: Option<String>,
/// This is only used for mouse mode cell change detection
last_mouse: Option<(AlacPoint, AlacDirection)>,
pub matches: Vec<RangeInclusive<AlacPoint>>,
@ -692,7 +698,6 @@ pub struct Terminal {
pub breadcrumb_text: String,
pub pty_info: PtyProcessInfo,
title_override: Option<SharedString>,
pub python_venv_directory: Option<PathBuf>,
scroll_px: Pixels,
next_link_id: usize,
selection_phase: SelectionPhase,

View file

@ -416,22 +416,18 @@ impl TerminalPanel {
let database_id = workspace.database_id();
let weak_workspace = self.workspace.clone();
let project = workspace.project().clone();
let working_directory = self
let terminal = self
.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);
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, None, 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()?;

View file

@ -1668,11 +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()));
project.create_terminal_with_venv(TerminalKind::Shell(working_directory), None, cx)
project.clone_terminal(self.terminal(), cx)
})
.ok()?
.log_err()?;