Add setting to automatically enable virtual environment (#2882)

This isn't ready to go - I'm opening a PR to ask for some advice. When
activating a python virtual environment, the typical command used is
`source path_to_venv/bin/activate`. The problem is, the activatate
script isn't portable to all shells, so some additional scripts are
bundled in the env, for example, `activate.fish`. We don't have a good
way of knowing what shell we are in, in order to know what script to
run.

Julia gave the alternative of simply activating the virtual environment
while in the zsh context, before the user's custom shell is launched,
which I think does work, but because we activate the virtual environment
before we launch the custom shell, the shell isn't really aware that we
are in the virtual environment and it fails to display the information
in the prompt that is typically shown after activating.

Is there a clean way for us to know for a fact what shell is being ran,
so we know what script to run?

Check out the code comments below for more context.

---


https://github.com/zed-industries/zed/assets/19867440/ddb76aaa-152b-4c93-a513-3cd580b7c40f

I've used Zed to write Python scripts, but working on an actual project
has really magnified where Python dev is falling short. A huge
quality-of-life thing we can do is provide a setting to automaticaly
search for and activate virtual environments when found, when terminals
are created. Manually starting these up in every terminal instance is
such a drag.

A few quirks:
- We don't have a way of knowing if the prompt is ready before we try
run the command, which means we see the text inserted at the top of the
terminal and on the prompt - I dont think this should be a blocker
though.
- If a user has multiple python projects with mutliple virtual
environments, we only detect and activate the first one, since can't
really make any assumptions about which one to activate. I dont think
this should be a blocker either, as I think most users will have a
single project open in Zed.

Release Notes:

- Added a `detect_venv` setting for the terminal. When configured, the
Zed terminal will automatically activate Python virtual environments on
terminal creation.
This commit is contained in:
Joseph T. Lyons 2023-08-25 15:14:24 -04:00 committed by GitHub
commit c1fd648390
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 268 additions and 131 deletions

View file

@ -1,7 +1,13 @@
use crate::Project;
use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle};
use std::path::PathBuf;
use terminal::{Terminal, TerminalBuilder, TerminalSettings};
use std::path::{Path, PathBuf};
use terminal::{
terminal_settings::{self, TerminalSettings, VenvSettingsContent},
Terminal, TerminalBuilder,
};
#[cfg(target_os = "macos")]
use std::os::unix::ffi::OsStrExt;
pub struct Terminals {
pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
@ -20,10 +26,12 @@ impl Project {
));
} else {
let settings = settings::get::<TerminalSettings>(cx);
let python_settings = settings.detect_venv.clone();
let shell = settings.shell.clone();
let terminal = TerminalBuilder::new(
working_directory.clone(),
settings.shell.clone(),
shell.clone(),
settings.env.clone(),
Some(settings.blinking.clone()),
settings.alternate_scroll,
@ -47,6 +55,15 @@ impl Project {
})
.detach();
if let Some(python_settings) = &python_settings.as_option() {
let activate_script_path =
self.find_activate_script_path(&python_settings, working_directory);
self.activate_python_virtual_environment(
activate_script_path,
&terminal_handle,
cx,
);
}
terminal_handle
});
@ -54,6 +71,50 @@ impl Project {
}
}
pub fn find_activate_script_path(
&mut self,
settings: &VenvSettingsContent,
working_directory: Option<PathBuf>,
) -> Option<PathBuf> {
// When we are unable to resolve the working directory, the terminal builder
// defaults to '/'. We should probably encode this directly somewhere, but for
// now, let's just hard code it here.
let working_directory = working_directory.unwrap_or_else(|| Path::new("/").to_path_buf());
let activate_script_name = match settings.activate_script {
terminal_settings::ActivateScript::Default => "activate",
terminal_settings::ActivateScript::Csh => "activate.csh",
terminal_settings::ActivateScript::Fish => "activate.fish",
};
for virtual_environment_name in settings.directories {
let mut path = working_directory.join(virtual_environment_name);
path.push("bin/");
path.push(activate_script_name);
if path.exists() {
return Some(path);
}
}
None
}
fn activate_python_virtual_environment(
&mut self,
activate_script: Option<PathBuf>,
terminal_handle: &ModelHandle<Terminal>,
cx: &mut ModelContext<Project>,
) {
if let Some(activate_script) = activate_script {
// Paths are not strings so we need to jump through some hoops to format the command without `format!`
let mut command = Vec::from("source ".as_bytes());
command.extend_from_slice(activate_script.as_os_str().as_bytes());
command.push(b'\n');
terminal_handle.update(cx, |this, _| this.input_bytes(command));
}
}
pub fn local_terminal_handles(&self) -> &Vec<WeakModelHandle<terminal::Terminal>> {
&self.terminals.local_handles
}