WIP

Joseph T. Lyons and Mikayla Maki created

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

assets/settings/default.json                 |  27 ++-
crates/project/src/terminals.rs              |  59 +++----
crates/terminal/src/terminal.rs              | 122 ----------------
crates/terminal/src/terminal_settings.rs     | 163 ++++++++++++++++++++++
crates/terminal_view/src/terminal_element.rs |   3 
crates/terminal_view/src/terminal_panel.rs   |   2 
crates/terminal_view/src/terminal_view.rs    |   5 
7 files changed, 213 insertions(+), 168 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -333,6 +333,24 @@
     //           "custom": 2
     //         },
     "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",
@@ -340,15 +358,6 @@
     // the terminal will default to matching the buffer's font family.
     // "font_family": "Zed Mono",
     // ---
-    // Whether or not to automatically search for, and activate, Python virtual
-    //  environments.
-    // Current limitations:
-    //     - Only ".env", "env", ".venv", and "venv" are searched for at the
-    //       root of the project
-    //     - Only works with Posix-complaint shells
-    //     - Only activates the first virtual environment it finds, regardless
-    //       of the nunber of projects in the workspace.
-    "activate_python_virtual_environment": false
   },
   // Difference settings for semantic_index
   "semantic_index": {

crates/project/src/terminals.rs 🔗

@@ -1,7 +1,10 @@
 use crate::Project;
 use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle};
-use std::path::PathBuf;
-use terminal::{Shell, 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;
@@ -23,8 +26,7 @@ impl Project {
             ));
         } else {
             let settings = settings::get::<TerminalSettings>(cx);
-            let activate_python_virtual_environment =
-                settings.activate_python_virtual_environment.clone();
+            let python_settings = settings.detect_venv.clone();
             let shell = settings.shell.clone();
 
             let terminal = TerminalBuilder::new(
@@ -53,15 +55,15 @@ impl Project {
                 })
                 .detach();
 
-                if activate_python_virtual_environment {
-                    let activate_script_path = self.find_activate_script_path(&shell, cx);
+                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
             });
 
@@ -71,37 +73,26 @@ impl Project {
 
     pub fn find_activate_script_path(
         &mut self,
-        shell: &Shell,
-        cx: &mut ModelContext<Project>,
+        settings: &VenvSettingsContent,
+        working_directory: Option<PathBuf>,
     ) -> Option<PathBuf> {
-        let program = match shell {
-            terminal::Shell::System => "Figure this out",
-            terminal::Shell::Program(program) => program,
-            terminal::Shell::WithArguments { program, args: _ } => program,
+        // 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",
         };
 
-        // This is so hacky - find a better way to do this
-        let script_name = if program.contains("fish") {
-            "activate.fish"
-        } else {
-            "activate"
-        };
+        for virtual_environment_name in settings.directories {
+            let mut path = working_directory.join(virtual_environment_name);
+            path.push("bin/");
+            path.push(activate_script_name);
 
-        let worktree_paths = self
-            .worktrees(cx)
-            .map(|worktree| worktree.read(cx).abs_path());
-
-        const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"];
-
-        for worktree_path in worktree_paths {
-            for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES {
-                let mut path = worktree_path.join(virtual_environment_name);
-                path.push("bin/");
-                path.push(script_name);
-
-                if path.exists() {
-                    return Some(path);
-                }
+            if path.exists() {
+                return Some(path);
             }
         }
 

crates/terminal/src/terminal.rs 🔗

@@ -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,124 +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,
-    pub activate_python_virtual_environment: bool,
-}
-
-#[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 activate_python_virtual_environment: Option<bool>,
-}
-
-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,

crates/terminal/src/terminal_settings.rs 🔗

@@ -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 },
+}

crates/terminal_view/src/terminal_element.rs 🔗

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

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -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},

crates/terminal_view/src/terminal_view.rs 🔗

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