WIP

Joseph T. Lyons and Julia created

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>

Change summary

assets/settings/default.json    | 18 +++++++++---
crates/project/src/terminals.rs | 49 +++++++++++++++++++++++++++++++++++
crates/terminal/src/terminal.rs | 14 ++++++++++
3 files changed, 76 insertions(+), 5 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -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,23 @@
     //         "line_height": {
     //           "custom": 2
     //         },
-    "line_height": "comfortable"
+    "line_height": "comfortable",
     // 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",
+    // ---
+    // 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.
+    "automatically_activate_python_virtual_environment": false
   },
   // Difference settings for semantic_index
   "semantic_index": {

crates/project/src/terminals.rs 🔗

@@ -3,6 +3,9 @@ use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle};
 use std::path::PathBuf;
 use terminal::{Terminal, TerminalBuilder, TerminalSettings};
 
+#[cfg(target_os = "macos")]
+use std::os::unix::ffi::OsStrExt;
+
 pub struct Terminals {
     pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
 }
@@ -47,6 +50,12 @@ impl Project {
                 })
                 .detach();
 
+                let setting = settings::get::<TerminalSettings>(cx);
+
+                if setting.automatically_activate_python_virtual_environment {
+                    self.set_up_python_virtual_environment(&terminal_handle, cx);
+                }
+
                 terminal_handle
             });
 
@@ -54,6 +63,46 @@ impl Project {
         }
     }
 
+    fn set_up_python_virtual_environment(
+        &mut self,
+        terminal_handle: &ModelHandle<Terminal>,
+        cx: &mut ModelContext<Project>,
+    ) {
+        let virtual_environment = self.find_python_virtual_environment(cx);
+        if let Some(virtual_environment) = virtual_environment {
+            // 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(virtual_environment.as_os_str().as_bytes());
+            command.push(b'\n');
+
+            terminal_handle.update(cx, |this, _| this.input_bytes(command));
+        }
+    }
+
+    pub fn find_python_virtual_environment(
+        &mut self,
+        cx: &mut ModelContext<Project>,
+    ) -> Option<PathBuf> {
+        const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"];
+
+        let worktree_paths = self
+            .worktrees(cx)
+            .map(|worktree| worktree.read(cx).abs_path());
+
+        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/activate");
+
+                if path.exists() {
+                    return Some(path);
+                }
+            }
+        }
+
+        None
+    }
+
     pub fn local_terminal_handles(&self) -> &Vec<WeakModelHandle<terminal::Terminal>> {
         &self.terminals.local_handles
     }

crates/terminal/src/terminal.rs 🔗

@@ -158,6 +158,7 @@ pub struct TerminalSettings {
     pub dock: TerminalDockPosition,
     pub default_width: f32,
     pub default_height: f32,
+    pub automatically_activate_python_virtual_environment: bool,
 }
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -176,6 +177,7 @@ pub struct TerminalSettingsContent {
     pub dock: Option<TerminalDockPosition>,
     pub default_width: Option<f32>,
     pub default_height: Option<f32>,
+    pub automatically_activate_python_virtual_environment: Option<bool>,
 }
 
 impl TerminalSettings {
@@ -1018,6 +1020,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 +1032,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 {