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

@ -284,8 +284,6 @@
// "directory": "~/zed/projects/"
// }
// }
//
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
@ -334,13 +332,32 @@
// "line_height": {
// "custom": 2
// },
"line_height": "comfortable"
"line_height": "comfortable",
// Activate the python virtual environment, if one is found, in the
// terminal's working directory (as resolved by the working_directory
// setting). Set this to "off" to disable this behavior.
"detect_venv": {
"on": {
// Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this
// in your project's settings, rather than globally.
"directories": [
".env",
"env",
".venv",
"venv"
],
// Can also be 'csh' and 'fish'
"activate_script": "default"
}
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// "font_size": "15",
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
// "font_family": "Zed Mono",
// ---
},
// Difference settings for semantic_index
"semantic_index": {

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
}

View File

@ -1,5 +1,6 @@
pub mod mappings;
pub use alacritty_terminal;
pub mod terminal_settings;
use alacritty_terminal::{
ansi::{ClearMode, Handler},
@ -31,8 +32,8 @@ use mappings::mouse::{
};
use procinfo::LocalProcessInfo;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
use util::truncate_and_trailoff;
use std::{
@ -48,7 +49,6 @@ use std::{
use thiserror::Error;
use gpui::{
fonts,
geometry::vector::{vec2f, Vector2F},
keymap_matcher::Keystroke,
platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase},
@ -134,122 +134,6 @@ pub fn init(cx: &mut AppContext) {
settings::register::<TerminalSettings>(cx);
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TerminalDockPosition {
Left,
Bottom,
Right,
}
#[derive(Deserialize)]
pub struct TerminalSettings {
pub shell: Shell,
pub working_directory: WorkingDirectory,
font_size: Option<f32>,
pub font_family: Option<String>,
pub line_height: TerminalLineHeight,
pub font_features: Option<fonts::Features>,
pub env: HashMap<String, String>,
pub blinking: TerminalBlink,
pub alternate_scroll: AlternateScroll,
pub option_as_meta: bool,
pub copy_on_select: bool,
pub dock: TerminalDockPosition,
pub default_width: f32,
pub default_height: f32,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct TerminalSettingsContent {
pub shell: Option<Shell>,
pub working_directory: Option<WorkingDirectory>,
pub font_size: Option<f32>,
pub font_family: Option<String>,
pub line_height: Option<TerminalLineHeight>,
pub font_features: Option<fonts::Features>,
pub env: Option<HashMap<String, String>>,
pub blinking: Option<TerminalBlink>,
pub alternate_scroll: Option<AlternateScroll>,
pub option_as_meta: Option<bool>,
pub copy_on_select: Option<bool>,
pub dock: Option<TerminalDockPosition>,
pub default_width: Option<f32>,
pub default_height: Option<f32>,
}
impl TerminalSettings {
pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
self.font_size
.map(|size| theme::adjusted_font_size(size, cx))
}
}
impl settings::Setting for TerminalSettings {
const KEY: Option<&'static str> = Some("terminal");
type FileContent = TerminalSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &AppContext,
) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum TerminalLineHeight {
#[default]
Comfortable,
Standard,
Custom(f32),
}
impl TerminalLineHeight {
pub fn value(&self) -> f32 {
match self {
TerminalLineHeight::Comfortable => 1.618,
TerminalLineHeight::Standard => 1.3,
TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TerminalBlink {
Off,
TerminalControlled,
On,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Shell {
System,
Program(String),
WithArguments { program: String, args: Vec<String> },
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AlternateScroll {
On,
Off,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectory {
CurrentProjectDirectory,
FirstProjectDirectory,
AlwaysHome,
Always { directory: String },
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct TerminalSize {
pub cell_width: f32,
@ -1018,6 +902,10 @@ impl Terminal {
self.pty_tx.notify(input.into_bytes());
}
fn write_bytes_to_pty(&self, input: Vec<u8>) {
self.pty_tx.notify(input);
}
pub fn input(&mut self, input: String) {
self.events
.push_back(InternalEvent::Scroll(AlacScroll::Bottom));
@ -1026,6 +914,14 @@ impl Terminal {
self.write_to_pty(input);
}
pub fn input_bytes(&mut self, input: Vec<u8>) {
self.events
.push_back(InternalEvent::Scroll(AlacScroll::Bottom));
self.events.push_back(InternalEvent::SetSelection(None));
self.write_bytes_to_pty(input);
}
pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
if let Some(esc) = esc {

View File

@ -0,0 +1,163 @@
use std::{collections::HashMap, path::PathBuf};
use gpui::{fonts, AppContext};
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TerminalDockPosition {
Left,
Bottom,
Right,
}
#[derive(Deserialize)]
pub struct TerminalSettings {
pub shell: Shell,
pub working_directory: WorkingDirectory,
font_size: Option<f32>,
pub font_family: Option<String>,
pub line_height: TerminalLineHeight,
pub font_features: Option<fonts::Features>,
pub env: HashMap<String, String>,
pub blinking: TerminalBlink,
pub alternate_scroll: AlternateScroll,
pub option_as_meta: bool,
pub copy_on_select: bool,
pub dock: TerminalDockPosition,
pub default_width: f32,
pub default_height: f32,
pub detect_venv: VenvSettings,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum VenvSettings {
#[default]
Off,
On {
activate_script: Option<ActivateScript>,
directories: Option<Vec<PathBuf>>,
},
}
pub struct VenvSettingsContent<'a> {
pub activate_script: ActivateScript,
pub directories: &'a [PathBuf],
}
impl VenvSettings {
pub fn as_option(&self) -> Option<VenvSettingsContent> {
match self {
VenvSettings::Off => None,
VenvSettings::On {
activate_script,
directories,
} => Some(VenvSettingsContent {
activate_script: activate_script.unwrap_or(ActivateScript::Default),
directories: directories.as_deref().unwrap_or(&[]),
}),
}
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ActivateScript {
#[default]
Default,
Csh,
Fish,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct TerminalSettingsContent {
pub shell: Option<Shell>,
pub working_directory: Option<WorkingDirectory>,
pub font_size: Option<f32>,
pub font_family: Option<String>,
pub line_height: Option<TerminalLineHeight>,
pub font_features: Option<fonts::Features>,
pub env: Option<HashMap<String, String>>,
pub blinking: Option<TerminalBlink>,
pub alternate_scroll: Option<AlternateScroll>,
pub option_as_meta: Option<bool>,
pub copy_on_select: Option<bool>,
pub dock: Option<TerminalDockPosition>,
pub default_width: Option<f32>,
pub default_height: Option<f32>,
pub detect_venv: Option<VenvSettings>,
}
impl TerminalSettings {
pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
self.font_size
.map(|size| theme::adjusted_font_size(size, cx))
}
}
impl settings::Setting for TerminalSettings {
const KEY: Option<&'static str> = Some("terminal");
type FileContent = TerminalSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum TerminalLineHeight {
#[default]
Comfortable,
Standard,
Custom(f32),
}
impl TerminalLineHeight {
pub fn value(&self) -> f32 {
match self {
TerminalLineHeight::Comfortable => 1.618,
TerminalLineHeight::Standard => 1.3,
TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TerminalBlink {
Off,
TerminalControlled,
On,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Shell {
System,
Program(String),
WithArguments { program: String, args: Vec<String> },
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AlternateScroll {
On,
Off,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectory {
CurrentProjectDirectory,
FirstProjectDirectory,
AlwaysHome,
Always { directory: String },
}

View File

@ -25,7 +25,8 @@ use terminal::{
term::{cell::Flags, TermMode},
},
mappings::colors::convert_color,
IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize,
terminal_settings::TerminalSettings,
IndexedCell, Terminal, TerminalContent, TerminalSize,
};
use theme::{TerminalStyle, ThemeSettings};
use util::ResultExt;

View File

@ -9,7 +9,7 @@ use gpui::{
use project::Fs;
use serde::{Deserialize, Serialize};
use settings::SettingsStore;
use terminal::{TerminalDockPosition, TerminalSettings};
use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},

View File

@ -33,7 +33,8 @@ use terminal::{
index::Point,
term::{search::RegexSearch, TermMode},
},
Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory,
terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
Event, MaybeNavigationTarget, Terminal,
};
use util::{paths::PathLikeWithPosition, ResultExt};
use workspace::{
@ -44,8 +45,6 @@ use workspace::{
NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
};
pub use terminal::TerminalSettings;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
///Event to transmit the scroll from the element to the view