Enable toolchain venv in new terminals (#21388)

Sebastian Nickels and Piotr Osiewicz created

Fixes part of issue #7808 

> This venv should be the one we automatically activate when opening new
terminals, if the detect_venv setting is on.

Release Notes:

- Selected Python toolchains (virtual environments) are now automatically activated in new terminals.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>

Change summary

crates/project/src/terminals.rs            | 354 +++++++++++++----------
crates/terminal/src/terminal_settings.rs   |   2 
crates/terminal_view/src/persistence.rs    |  56 ++-
crates/terminal_view/src/terminal_panel.rs | 279 +++++++++++-------
crates/terminal_view/src/terminal_view.rs  |  67 ++-
5 files changed, 446 insertions(+), 312 deletions(-)

Detailed changes

crates/project/src/terminals.rs 🔗

@@ -1,8 +1,9 @@
 use crate::Project;
-use anyhow::Context as _;
+use anyhow::{Context as _, Result};
 use collections::HashMap;
-use gpui::{AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, WeakModel};
+use gpui::{AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, Task, WeakModel};
 use itertools::Itertools;
+use language::LanguageName;
 use settings::{Settings, SettingsLocation};
 use smol::channel::bounded;
 use std::{
@@ -10,10 +11,11 @@ use std::{
     env::{self},
     iter,
     path::{Path, PathBuf},
+    sync::Arc,
 };
 use task::{Shell, SpawnInTerminal};
 use terminal::{
-    terminal_settings::{self, TerminalSettings},
+    terminal_settings::{self, TerminalSettings, VenvSettings},
     TaskState, TaskStatus, Terminal, TerminalBuilder,
 };
 use util::ResultExt;
@@ -42,7 +44,7 @@ pub struct SshCommand {
 }
 
 impl Project {
-    pub fn active_project_directory(&self, cx: &AppContext) -> Option<PathBuf> {
+    pub fn active_project_directory(&self, cx: &AppContext) -> Option<Arc<Path>> {
         let worktree = self
             .active_entry()
             .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
@@ -53,7 +55,7 @@ impl Project {
                 worktree
                     .root_entry()
                     .filter(|entry| entry.is_dir())
-                    .map(|_| worktree.abs_path().to_path_buf())
+                    .map(|_| worktree.abs_path().clone())
             });
         worktree
     }
@@ -87,12 +89,12 @@ impl Project {
         kind: TerminalKind,
         window: AnyWindowHandle,
         cx: &mut ModelContext<Self>,
-    ) -> anyhow::Result<Model<Terminal>> {
-        let path = match &kind {
-            TerminalKind::Shell(path) => path.as_ref().map(|path| path.to_path_buf()),
+    ) -> Task<Result<Model<Terminal>>> {
+        let path: Option<Arc<Path>> = match &kind {
+            TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
             TerminalKind::Task(spawn_task) => {
                 if let Some(cwd) = &spawn_task.cwd {
-                    Some(cwd.clone())
+                    Some(Arc::from(cwd.as_ref()))
                 } else {
                     self.active_project_directory(cx)
                 }
@@ -109,7 +111,7 @@ impl Project {
                 });
             }
         }
-        let settings = TerminalSettings::get(settings_location, cx);
+        let settings = TerminalSettings::get(settings_location, cx).clone();
 
         let (completion_tx, completion_rx) = bounded(1);
 
@@ -128,160 +130,206 @@ impl Project {
         } else {
             None
         };
-        let python_venv_directory = path
-            .as_ref()
-            .and_then(|path| self.python_venv_directory(path, settings, cx));
-        let mut python_venv_activate_command = None;
-
-        let (spawn_task, shell) = match kind {
-            TerminalKind::Shell(_) => {
-                if let Some(python_venv_directory) = python_venv_directory {
-                    python_venv_activate_command =
-                        self.python_activate_command(&python_venv_directory, settings);
-                }
 
-                match &ssh_details {
-                    Some((host, ssh_command)) => {
-                        log::debug!("Connecting to a remote server: {ssh_command:?}");
-
-                        // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
-                        // to properly display colors.
-                        // We do not have the luxury of assuming the host has it installed,
-                        // so we set it to a default that does not break the highlighting via ssh.
-                        env.entry("TERM".to_string())
-                            .or_insert_with(|| "xterm-256color".to_string());
-
-                        let (program, args) =
-                            wrap_for_ssh(ssh_command, None, path.as_deref(), env, None);
-                        env = HashMap::default();
-                        (
-                            None,
-                            Shell::WithArguments {
-                                program,
-                                args,
-                                title_override: Some(format!("{} — Terminal", host).into()),
-                            },
-                        )
+        cx.spawn(move |this, mut cx| async move {
+            let python_venv_directory = if let Some(path) = path.clone() {
+                this.update(&mut cx, |this, cx| {
+                    this.python_venv_directory(path, settings.detect_venv.clone(), cx)
+                })?
+                .await
+            } else {
+                None
+            };
+            let mut python_venv_activate_command = None;
+
+            let (spawn_task, shell) = match kind {
+                TerminalKind::Shell(_) => {
+                    if let Some(python_venv_directory) = python_venv_directory {
+                        python_venv_activate_command = this
+                            .update(&mut cx, |this, _| {
+                                this.python_activate_command(
+                                    &python_venv_directory,
+                                    &settings.detect_venv,
+                                )
+                            })
+                            .ok()
+                            .flatten();
                     }
-                    None => (None, settings.shell.clone()),
-                }
-            }
-            TerminalKind::Task(spawn_task) => {
-                let task_state = Some(TaskState {
-                    id: spawn_task.id,
-                    full_label: spawn_task.full_label,
-                    label: spawn_task.label,
-                    command_label: spawn_task.command_label,
-                    hide: spawn_task.hide,
-                    status: TaskStatus::Running,
-                    show_summary: spawn_task.show_summary,
-                    show_command: spawn_task.show_command,
-                    completion_rx,
-                });
-
-                env.extend(spawn_task.env);
 
-                if let Some(venv_path) = &python_venv_directory {
-                    env.insert(
-                        "VIRTUAL_ENV".to_string(),
-                        venv_path.to_string_lossy().to_string(),
-                    );
+                    match &ssh_details {
+                        Some((host, ssh_command)) => {
+                            log::debug!("Connecting to a remote server: {ssh_command:?}");
+
+                            // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
+                            // to properly display colors.
+                            // We do not have the luxury of assuming the host has it installed,
+                            // so we set it to a default that does not break the highlighting via ssh.
+                            env.entry("TERM".to_string())
+                                .or_insert_with(|| "xterm-256color".to_string());
+
+                            let (program, args) =
+                                wrap_for_ssh(ssh_command, None, path.as_deref(), env, None);
+                            env = HashMap::default();
+                            (
+                                Option::<TaskState>::None,
+                                Shell::WithArguments {
+                                    program,
+                                    args,
+                                    title_override: Some(format!("{} — Terminal", host).into()),
+                                },
+                            )
+                        }
+                        None => (None, settings.shell.clone()),
+                    }
                 }
-
-                match &ssh_details {
-                    Some((host, ssh_command)) => {
-                        log::debug!("Connecting to a remote server: {ssh_command:?}");
-                        env.entry("TERM".to_string())
-                            .or_insert_with(|| "xterm-256color".to_string());
-                        let (program, args) = wrap_for_ssh(
-                            ssh_command,
-                            Some((&spawn_task.command, &spawn_task.args)),
-                            path.as_deref(),
-                            env,
-                            python_venv_directory,
+                TerminalKind::Task(spawn_task) => {
+                    let task_state = Some(TaskState {
+                        id: spawn_task.id,
+                        full_label: spawn_task.full_label,
+                        label: spawn_task.label,
+                        command_label: spawn_task.command_label,
+                        hide: spawn_task.hide,
+                        status: TaskStatus::Running,
+                        show_summary: spawn_task.show_summary,
+                        show_command: spawn_task.show_command,
+                        completion_rx,
+                    });
+
+                    env.extend(spawn_task.env);
+
+                    if let Some(venv_path) = &python_venv_directory {
+                        env.insert(
+                            "VIRTUAL_ENV".to_string(),
+                            venv_path.to_string_lossy().to_string(),
                         );
-                        env = HashMap::default();
-                        (
-                            task_state,
-                            Shell::WithArguments {
-                                program,
-                                args,
-                                title_override: Some(format!("{} — Terminal", host).into()),
-                            },
-                        )
                     }
-                    None => {
-                        if let Some(venv_path) = &python_venv_directory {
-                            add_environment_path(&mut env, &venv_path.join("bin")).log_err();
-                        }
 
-                        (
-                            task_state,
-                            Shell::WithArguments {
-                                program: spawn_task.command,
-                                args: spawn_task.args,
-                                title_override: None,
-                            },
-                        )
+                    match &ssh_details {
+                        Some((host, ssh_command)) => {
+                            log::debug!("Connecting to a remote server: {ssh_command:?}");
+                            env.entry("TERM".to_string())
+                                .or_insert_with(|| "xterm-256color".to_string());
+                            let (program, args) = wrap_for_ssh(
+                                ssh_command,
+                                Some((&spawn_task.command, &spawn_task.args)),
+                                path.as_deref(),
+                                env,
+                                python_venv_directory,
+                            );
+                            env = HashMap::default();
+                            (
+                                task_state,
+                                Shell::WithArguments {
+                                    program,
+                                    args,
+                                    title_override: Some(format!("{} — Terminal", host).into()),
+                                },
+                            )
+                        }
+                        None => {
+                            if let Some(venv_path) = &python_venv_directory {
+                                add_environment_path(&mut env, &venv_path.join("bin")).log_err();
+                            }
+
+                            (
+                                task_state,
+                                Shell::WithArguments {
+                                    program: spawn_task.command,
+                                    args: spawn_task.args,
+                                    title_override: None,
+                                },
+                            )
+                        }
                     }
                 }
-            }
-        };
-
-        let terminal = TerminalBuilder::new(
-            local_path,
-            spawn_task,
-            shell,
-            env,
-            settings.cursor_shape.unwrap_or_default(),
-            settings.alternate_scroll,
-            settings.max_scroll_history_lines,
-            ssh_details.is_some(),
-            window,
-            completion_tx,
-            cx,
-        )
-        .map(|builder| {
-            let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
-
-            self.terminals
-                .local_handles
-                .push(terminal_handle.downgrade());
-
-            let id = terminal_handle.entity_id();
-            cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
-                let handles = &mut project.terminals.local_handles;
-
-                if let Some(index) = handles
-                    .iter()
-                    .position(|terminal| terminal.entity_id() == id)
-                {
-                    handles.remove(index);
-                    cx.notify();
-                }
-            })
-            .detach();
+            };
+            let terminal = this.update(&mut cx, |this, cx| {
+                TerminalBuilder::new(
+                    local_path.map(|path| path.to_path_buf()),
+                    spawn_task,
+                    shell,
+                    env,
+                    settings.cursor_shape.unwrap_or_default(),
+                    settings.alternate_scroll,
+                    settings.max_scroll_history_lines,
+                    ssh_details.is_some(),
+                    window,
+                    completion_tx,
+                    cx,
+                )
+                .map(|builder| {
+                    let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
+
+                    this.terminals
+                        .local_handles
+                        .push(terminal_handle.downgrade());
+
+                    let id = terminal_handle.entity_id();
+                    cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
+                        let handles = &mut project.terminals.local_handles;
+
+                        if let Some(index) = handles
+                            .iter()
+                            .position(|terminal| terminal.entity_id() == id)
+                        {
+                            handles.remove(index);
+                            cx.notify();
+                        }
+                    })
+                    .detach();
 
-            if let Some(activate_command) = python_venv_activate_command {
-                self.activate_python_virtual_environment(activate_command, &terminal_handle, cx);
-            }
-            terminal_handle
-        });
+                    if let Some(activate_command) = python_venv_activate_command {
+                        this.activate_python_virtual_environment(
+                            activate_command,
+                            &terminal_handle,
+                            cx,
+                        );
+                    }
+                    terminal_handle
+                })
+            })?;
 
-        terminal
+            terminal
+        })
     }
 
-    pub fn python_venv_directory(
+    fn python_venv_directory(
         &self,
-        abs_path: &Path,
-        settings: &TerminalSettings,
-        cx: &AppContext,
-    ) -> Option<PathBuf> {
-        let venv_settings = settings.detect_venv.as_option()?;
-        if let Some(path) = self.find_venv_in_worktree(abs_path, &venv_settings, cx) {
-            return Some(path);
-        }
-        self.find_venv_on_filesystem(abs_path, &venv_settings, cx)
+        abs_path: Arc<Path>,
+        venv_settings: VenvSettings,
+        cx: &ModelContext<Project>,
+    ) -> Task<Option<PathBuf>> {
+        cx.spawn(move |this, mut cx| async move {
+            if let Some((worktree, _)) = this
+                .update(&mut cx, |this, cx| this.find_worktree(&abs_path, cx))
+                .ok()?
+            {
+                let toolchain = this
+                    .update(&mut cx, |this, cx| {
+                        this.active_toolchain(
+                            worktree.read(cx).id(),
+                            LanguageName::new("Python"),
+                            cx,
+                        )
+                    })
+                    .ok()?
+                    .await;
+
+                if let Some(toolchain) = toolchain {
+                    let toolchain_path = Path::new(toolchain.path.as_ref());
+                    return Some(toolchain_path.parent()?.parent()?.to_path_buf());
+                }
+            }
+            let venv_settings = venv_settings.as_option()?;
+            this.update(&mut cx, move |this, cx| {
+                if let Some(path) = this.find_venv_in_worktree(&abs_path, &venv_settings, cx) {
+                    return Some(path);
+                }
+                this.find_venv_on_filesystem(&abs_path, &venv_settings, cx)
+            })
+            .ok()
+            .flatten()
+        })
     }
 
     fn find_venv_in_worktree(
@@ -337,9 +385,9 @@ impl Project {
     fn python_activate_command(
         &self,
         venv_base_directory: &Path,
-        settings: &TerminalSettings,
+        venv_settings: &VenvSettings,
     ) -> Option<String> {
-        let venv_settings = settings.detect_venv.as_option()?;
+        let venv_settings = venv_settings.as_option()?;
         let activate_keyword = match venv_settings.activate_script {
             terminal_settings::ActivateScript::Default => match std::env::consts::OS {
                 "windows" => ".",
@@ -441,7 +489,7 @@ pub fn wrap_for_ssh(
     (program, args)
 }
 
-fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> anyhow::Result<()> {
+fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> Result<()> {
     let mut env_paths = vec![new_path.to_path_buf()];
     if let Some(path) = env.get("PATH").or(env::var("PATH").ok().as_ref()) {
         let mut paths = std::env::split_paths(&path).collect::<Vec<_>>();

crates/terminal/src/terminal_settings.rs 🔗

@@ -24,7 +24,7 @@ pub struct Toolbar {
     pub breadcrumbs: bool,
 }
 
-#[derive(Debug, Deserialize)]
+#[derive(Clone, Debug, Deserialize)]
 pub struct TerminalSettings {
     pub shell: Shell,
     pub working_directory: WorkingDirectory,

crates/terminal_view/src/persistence.rs 🔗

@@ -5,7 +5,7 @@ use futures::{stream::FuturesUnordered, StreamExt as _};
 use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView};
 use project::{terminals::TerminalKind, Project};
 use serde::{Deserialize, Serialize};
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use ui::{Pixels, ViewContext, VisualContext as _, WindowContext};
 use util::ResultExt as _;
 
@@ -219,33 +219,39 @@ async fn deserialize_pane_group(
                 })
                 .log_err()?;
             let active_item = serialized_pane.active_item;
-            pane.update(cx, |pane, cx| {
-                populate_pane_items(pane, new_items, active_item, cx);
-                // Avoid blank panes in splits
-                if pane.items_len() == 0 {
-                    let working_directory = workspace
-                        .update(cx, |workspace, cx| default_working_directory(workspace, cx))
-                        .ok()
-                        .flatten();
-                    let kind = TerminalKind::Shell(working_directory);
-                    let window = cx.window_handle();
-                    let terminal = project
-                        .update(cx, |project, cx| project.create_terminal(kind, window, cx))
-                        .log_err()?;
+
+            let terminal = pane
+                .update(cx, |pane, cx| {
+                    populate_pane_items(pane, new_items, active_item, cx);
+                    // Avoid blank panes in splits
+                    if pane.items_len() == 0 {
+                        let working_directory = workspace
+                            .update(cx, |workspace, cx| default_working_directory(workspace, cx))
+                            .ok()
+                            .flatten();
+                        let kind = TerminalKind::Shell(
+                            working_directory.as_deref().map(Path::to_path_buf),
+                        );
+                        let window = cx.window_handle();
+                        let terminal = project
+                            .update(cx, |project, cx| project.create_terminal(kind, window, cx));
+                        Some(Some(terminal))
+                    } else {
+                        Some(None)
+                    }
+                })
+                .ok()
+                .flatten()?;
+            if let Some(terminal) = terminal {
+                let terminal = terminal.await.ok()?;
+                pane.update(cx, |pane, cx| {
                     let terminal_view = Box::new(cx.new_view(|cx| {
-                        TerminalView::new(
-                            terminal.clone(),
-                            workspace.clone(),
-                            Some(workspace_id),
-                            cx,
-                        )
+                        TerminalView::new(terminal, workspace.clone(), Some(workspace_id), cx)
                     }));
                     pane.add_item(terminal_view, true, false, None, cx);
-                }
-                Some(())
-            })
-            .ok()
-            .flatten()?;
+                })
+                .ok()?;
+            }
             Some((Member::Pane(pane.clone()), active.then_some(pane)))
         }
     }

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -318,10 +318,19 @@ impl TerminalPanel {
                 }
             }
             pane::Event::Split(direction) => {
-                let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else {
-                    return;
-                };
-                self.center.split(&pane, &new_pane, *direction).log_err();
+                let new_pane = self.new_pane_with_cloned_active_terminal(cx);
+                let pane = pane.clone();
+                let direction = *direction;
+                cx.spawn(move |this, mut cx| async move {
+                    let Some(new_pane) = new_pane.await else {
+                        return;
+                    };
+                    this.update(&mut cx, |this, _| {
+                        this.center.split(&pane, &new_pane, direction).log_err();
+                    })
+                    .ok();
+                })
+                .detach();
             }
             pane::Event::Focus => {
                 self.active_pane = pane.clone();
@@ -334,8 +343,12 @@ impl TerminalPanel {
     fn new_pane_with_cloned_active_terminal(
         &mut self,
         cx: &mut ViewContext<Self>,
-    ) -> Option<View<Pane>> {
-        let workspace = self.workspace.clone().upgrade()?;
+    ) -> Task<Option<View<Pane>>> {
+        let Some(workspace) = self.workspace.clone().upgrade() else {
+            return Task::ready(None);
+        };
+        let database_id = workspace.read(cx).database_id();
+        let weak_workspace = self.workspace.clone();
         let project = workspace.read(cx).project().clone();
         let working_directory = self
             .active_pane
@@ -352,21 +365,37 @@ impl TerminalPanel {
             .or_else(|| default_working_directory(workspace.read(cx), cx));
         let kind = TerminalKind::Shell(working_directory);
         let window = cx.window_handle();
-        let terminal = project
-            .update(cx, |project, cx| project.create_terminal(kind, window, cx))
-            .log_err()?;
-        let database_id = workspace.read(cx).database_id();
-        let terminal_view = Box::new(cx.new_view(|cx| {
-            TerminalView::new(terminal.clone(), self.workspace.clone(), database_id, cx)
-        }));
-        let pane = new_terminal_pane(self.workspace.clone(), project, cx);
-        self.apply_tab_bar_buttons(&pane, cx);
-        pane.update(cx, |pane, cx| {
-            pane.add_item(terminal_view, true, true, None, cx);
-        });
-        cx.focus_view(&pane);
+        cx.spawn(move |this, mut cx| async move {
+            let terminal = project
+                .update(&mut cx, |project, cx| {
+                    project.create_terminal(kind, window, cx)
+                })
+                .log_err()?
+                .await
+                .log_err()?;
+
+            let terminal_view = Box::new(
+                cx.new_view(|cx| {
+                    TerminalView::new(terminal.clone(), weak_workspace.clone(), database_id, cx)
+                })
+                .ok()?,
+            );
+            let pane = this
+                .update(&mut cx, |this, cx| {
+                    let pane = new_terminal_pane(weak_workspace, project, cx);
+                    this.apply_tab_bar_buttons(&pane, cx);
+                    pane
+                })
+                .ok()?;
+
+            pane.update(&mut cx, |pane, cx| {
+                pane.add_item(terminal_view, true, true, None, cx);
+            })
+            .ok()?;
+            cx.focus_view(&pane).ok()?;
 
-        Some(pane)
+            Some(pane)
+        })
     }
 
     pub fn open_terminal(
@@ -489,43 +518,58 @@ impl TerminalPanel {
             .last()
             .expect("covered no terminals case above")
             .clone();
-        if allow_concurrent_runs {
-            debug_assert!(
-                !use_new_terminal,
-                "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
-            );
-            self.replace_terminal(
-                spawn_task,
-                task_pane,
-                existing_item_index,
-                existing_terminal,
-                cx,
-            );
-        } else {
-            self.deferred_tasks.insert(
-                spawn_in_terminal.id.clone(),
-                cx.spawn(|terminal_panel, mut cx| async move {
-                    wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
-                    terminal_panel
-                        .update(&mut cx, |terminal_panel, cx| {
-                            if use_new_terminal {
-                                terminal_panel
-                                    .spawn_in_new_terminal(spawn_task, cx)
-                                    .detach_and_log_err(cx);
-                            } else {
-                                terminal_panel.replace_terminal(
-                                    spawn_task,
-                                    task_pane,
-                                    existing_item_index,
-                                    existing_terminal,
-                                    cx,
-                                );
-                            }
-                        })
-                        .ok();
-                }),
-            );
-        }
+        let id = spawn_in_terminal.id.clone();
+        cx.spawn(move |this, mut cx| async move {
+            if allow_concurrent_runs {
+                debug_assert!(
+                    !use_new_terminal,
+                    "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
+                );
+                this.update(&mut cx, |this, cx| {
+                    this.replace_terminal(
+                        spawn_task,
+                        task_pane,
+                        existing_item_index,
+                        existing_terminal,
+                        cx,
+                    )
+                })?
+                .await;
+            } else {
+                this.update(&mut cx, |this, cx| {
+                    this.deferred_tasks.insert(
+                        id,
+                        cx.spawn(|terminal_panel, mut cx| async move {
+                            wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
+                            let Ok(Some(new_terminal_task)) =
+                                terminal_panel.update(&mut cx, |terminal_panel, cx| {
+                                    if use_new_terminal {
+                                        terminal_panel
+                                            .spawn_in_new_terminal(spawn_task, cx)
+                                            .detach_and_log_err(cx);
+                                        None
+                                    } else {
+                                        Some(terminal_panel.replace_terminal(
+                                            spawn_task,
+                                            task_pane,
+                                            existing_item_index,
+                                            existing_terminal,
+                                            cx,
+                                        ))
+                                    }
+                                })
+                            else {
+                                return;
+                            };
+                            new_terminal_task.await;
+                        }),
+                    );
+                })
+                .ok();
+            }
+            anyhow::Result::<_, anyhow::Error>::Ok(())
+        })
+        .detach()
     }
 
     pub fn spawn_in_new_terminal(
@@ -611,11 +655,14 @@ impl TerminalPanel {
 
         cx.spawn(|terminal_panel, mut cx| async move {
             let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?;
+            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
+            let window = cx.window_handle();
+            let terminal = project
+                .update(&mut cx, |project, cx| {
+                    project.create_terminal(kind, window, cx)
+                })?
+                .await?;
             let result = workspace.update(&mut cx, |workspace, cx| {
-                let window = cx.window_handle();
-                let terminal = workspace
-                    .project()
-                    .update(cx, |project, cx| project.create_terminal(kind, window, cx))?;
                 let terminal_view = Box::new(cx.new_view(|cx| {
                     TerminalView::new(
                         terminal.clone(),
@@ -695,48 +742,64 @@ impl TerminalPanel {
         terminal_item_index: usize,
         terminal_to_replace: View<TerminalView>,
         cx: &mut ViewContext<'_, Self>,
-    ) -> Option<()> {
-        let project = self
-            .workspace
-            .update(cx, |workspace, _| workspace.project().clone())
-            .ok()?;
-
+    ) -> Task<Option<()>> {
         let reveal = spawn_task.reveal;
         let window = cx.window_handle();
-        let new_terminal = project.update(cx, |project, cx| {
-            project
-                .create_terminal(TerminalKind::Task(spawn_task), window, cx)
-                .log_err()
-        })?;
-        terminal_to_replace.update(cx, |terminal_to_replace, cx| {
-            terminal_to_replace.set_terminal(new_terminal, cx);
-        });
-
-        match reveal {
-            RevealStrategy::Always => {
-                self.activate_terminal_view(&task_pane, terminal_item_index, true, cx);
-                let task_workspace = self.workspace.clone();
-                cx.spawn(|_, mut cx| async move {
-                    task_workspace
-                        .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
+        let task_workspace = self.workspace.clone();
+        cx.spawn(move |this, mut cx| async move {
+            let project = this
+                .update(&mut cx, |this, cx| {
+                    this.workspace
+                        .update(cx, |workspace, _| workspace.project().clone())
                         .ok()
                 })
-                .detach();
-            }
-            RevealStrategy::NoFocus => {
-                self.activate_terminal_view(&task_pane, terminal_item_index, false, cx);
-                let task_workspace = self.workspace.clone();
-                cx.spawn(|_, mut cx| async move {
-                    task_workspace
-                        .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
-                        .ok()
+                .ok()
+                .flatten()?;
+            let new_terminal = project
+                .update(&mut cx, |project, cx| {
+                    project.create_terminal(TerminalKind::Task(spawn_task), window, cx)
                 })
-                .detach();
+                .ok()?
+                .await
+                .log_err()?;
+            terminal_to_replace
+                .update(&mut cx, |terminal_to_replace, cx| {
+                    terminal_to_replace.set_terminal(new_terminal, cx);
+                })
+                .ok()?;
+
+            match reveal {
+                RevealStrategy::Always => {
+                    this.update(&mut cx, |this, cx| {
+                        this.activate_terminal_view(&task_pane, terminal_item_index, true, cx)
+                    })
+                    .ok()?;
+
+                    cx.spawn(|mut cx| async move {
+                        task_workspace
+                            .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
+                            .ok()
+                    })
+                    .detach();
+                }
+                RevealStrategy::NoFocus => {
+                    this.update(&mut cx, |this, cx| {
+                        this.activate_terminal_view(&task_pane, terminal_item_index, false, cx)
+                    })
+                    .ok()?;
+
+                    cx.spawn(|mut cx| async move {
+                        task_workspace
+                            .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
+                            .ok()
+                    })
+                    .detach();
+                }
+                RevealStrategy::Never => {}
             }
-            RevealStrategy::Never => {}
-        }
 
-        Some(())
+            Some(())
+        })
     }
 
     fn has_no_terminals(&self, cx: &WindowContext) -> bool {
@@ -998,18 +1061,18 @@ impl Render for TerminalPanel {
                     if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
                         cx.focus_view(&pane);
                     } else {
-                        if let Some(new_pane) =
-                            terminal_panel.new_pane_with_cloned_active_terminal(cx)
-                        {
-                            terminal_panel
-                                .center
-                                .split(
-                                    &terminal_panel.active_pane,
-                                    &new_pane,
-                                    SplitDirection::Right,
-                                )
-                                .log_err();
-                        }
+                        let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx);
+                        cx.spawn(|this, mut cx| async move {
+                            if let Some(new_pane) = new_pane.await {
+                                this.update(&mut cx, |this, _| {
+                                    this.center
+                                        .split(&this.active_pane, &new_pane, SplitDirection::Right)
+                                        .log_err();
+                                })
+                                .ok();
+                            }
+                        })
+                        .detach();
                     }
                 }))
                 .on_action(cx.listener(

crates/terminal_view/src/terminal_view.rs 🔗

@@ -136,24 +136,36 @@ impl TerminalView {
         let working_directory = default_working_directory(workspace, cx);
 
         let window = cx.window_handle();
-        let terminal = workspace
-            .project()
-            .update(cx, |project, cx| {
-                project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
-            })
-            .notify_err(workspace, cx);
-
-        if let Some(terminal) = terminal {
-            let view = cx.new_view(|cx| {
-                TerminalView::new(
-                    terminal,
-                    workspace.weak_handle(),
-                    workspace.database_id(),
-                    cx,
-                )
-            });
-            workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
-        }
+        let project = workspace.project().downgrade();
+        cx.spawn(move |workspace, mut cx| async move {
+            let terminal = project
+                .update(&mut cx, |project, cx| {
+                    project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
+                })
+                .ok()?
+                .await;
+            let terminal = workspace
+                .update(&mut cx, |workspace, cx| terminal.notify_err(workspace, cx))
+                .ok()
+                .flatten()?;
+
+            workspace
+                .update(&mut cx, |workspace, cx| {
+                    let view = cx.new_view(|cx| {
+                        TerminalView::new(
+                            terminal,
+                            workspace.weak_handle(),
+                            workspace.database_id(),
+                            cx,
+                        )
+                    });
+                    workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
+                })
+                .ok();
+
+            Some(())
+        })
+        .detach()
     }
 
     pub fn new(
@@ -1231,9 +1243,11 @@ impl SerializableItem for TerminalView {
                 .ok()
                 .flatten();
 
-            let terminal = project.update(&mut cx, |project, cx| {
-                project.create_terminal(TerminalKind::Shell(cwd), window, cx)
-            })??;
+            let terminal = project
+                .update(&mut cx, |project, cx| {
+                    project.create_terminal(TerminalKind::Shell(cwd), window, cx)
+                })?
+                .await?;
             cx.update(|cx| {
                 cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx))
             })
@@ -1362,11 +1376,14 @@ impl SearchableItem for TerminalView {
 
 ///Gets the working directory for the given workspace, respecting the user's settings.
 /// None implies "~" on whichever machine we end up on.
-pub fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+pub(crate) fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
     match &TerminalSettings::get_global(cx).working_directory {
-        WorkingDirectory::CurrentProjectDirectory => {
-            workspace.project().read(cx).active_project_directory(cx)
-        }
+        WorkingDirectory::CurrentProjectDirectory => workspace
+            .project()
+            .read(cx)
+            .active_project_directory(cx)
+            .as_deref()
+            .map(Path::to_path_buf),
         WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
         WorkingDirectory::AlwaysHome => None,
         WorkingDirectory::Always { directory } => {