Keep track of open terminals

Joseph Lyons , Petros Amoiridis , and Mikayla Maki created

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

Change summary

crates/project/src/project.rs           | 39 ++++------------
crates/project/src/terminals.rs         | 63 +++++++++++++++++++++++++++
crates/workspace/src/terminal_button.rs |  4 +
3 files changed, 76 insertions(+), 30 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -1,6 +1,7 @@
 mod ignore;
 mod lsp_command;
 pub mod search;
+pub mod terminals;
 pub mod worktree;
 
 #[cfg(test)]
@@ -61,7 +62,8 @@ use std::{
     },
     time::{Duration, Instant, SystemTime},
 };
-use terminal::{Terminal, TerminalBuilder};
+use terminals::Terminals;
+
 use util::{debug_panic, defer, post_inc, ResultExt, TryFutureExt as _};
 
 pub use fs::*;
@@ -123,6 +125,7 @@ pub struct Project {
     buffers_being_formatted: HashSet<usize>,
     nonce: u128,
     _maintain_buffer_languages: Task<()>,
+    terminals: Terminals,
 }
 
 enum OpenBuffer {
@@ -439,6 +442,9 @@ impl Project {
             buffers_being_formatted: Default::default(),
             next_language_server_id: 0,
             nonce: StdRng::from_entropy().gen(),
+            terminals: Terminals {
+                local_handles: Vec::new(),
+            },
         })
     }
 
@@ -516,6 +522,9 @@ impl Project {
                 buffers_being_formatted: Default::default(),
                 buffer_snapshots: Default::default(),
                 nonce: StdRng::from_entropy().gen(),
+                terminals: Terminals {
+                    local_handles: Vec::new(),
+                },
             };
             for worktree in worktrees {
                 let _ = this.add_worktree(&worktree, cx);
@@ -1184,34 +1193,6 @@ impl Project {
         !self.is_local()
     }
 
-    pub fn create_terminal(
-        &mut self,
-        working_directory: Option<PathBuf>,
-        window_id: usize,
-        cx: &mut ModelContext<Self>,
-    ) -> Result<ModelHandle<Terminal>> {
-        if self.is_remote() {
-            return Err(anyhow!(
-                "creating terminals as a guest is not supported yet"
-            ));
-        } else {
-            let settings = cx.global::<Settings>();
-            let shell = settings.terminal_shell();
-            let envs = settings.terminal_env();
-            let scroll = settings.terminal_scroll();
-
-            TerminalBuilder::new(
-                working_directory.clone(),
-                shell,
-                envs,
-                settings.terminal_overrides.blinking.clone(),
-                scroll,
-                window_id,
-            )
-            .map(|builder| cx.add_model(|cx| builder.subscribe(cx)))
-        }
-    }
-
     pub fn create_buffer(
         &mut self,
         text: &str,

crates/project/src/terminals.rs 🔗

@@ -0,0 +1,63 @@
+use std::path::PathBuf;
+
+use gpui::{ModelContext, ModelHandle, WeakModelHandle};
+use settings::Settings;
+use terminal::{Terminal, TerminalBuilder};
+
+use crate::Project;
+
+pub struct Terminals {
+    pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
+}
+
+impl Project {
+    pub fn create_terminal(
+        &mut self,
+        working_directory: Option<PathBuf>,
+        window_id: usize,
+        cx: &mut ModelContext<Self>,
+    ) -> anyhow::Result<ModelHandle<Terminal>> {
+        if self.is_remote() {
+            return Err(anyhow::anyhow!(
+                "creating terminals as a guest is not supported yet"
+            ));
+        } else {
+            let settings = cx.global::<Settings>();
+            let shell = settings.terminal_shell();
+            let envs = settings.terminal_env();
+            let scroll = settings.terminal_scroll();
+
+            let terminal = TerminalBuilder::new(
+                working_directory.clone(),
+                shell,
+                envs,
+                settings.terminal_overrides.blinking.clone(),
+                scroll,
+                window_id,
+            )
+            .map(|builder| {
+                let terminal_handle = cx.add_model(|cx| builder.subscribe(cx));
+
+                self.terminals
+                    .local_handles
+                    .push(terminal_handle.downgrade());
+
+                let id = terminal_handle.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.id() == id) {
+                        handles.remove(index);
+                    }
+                })
+                .detach();
+
+                terminal_handle
+            });
+
+            terminal
+        }
+    }
+}
+
+// TODO: Add a few tests for adding and removing terminal tabs

crates/workspace/src/terminal_button.rs 🔗

@@ -11,9 +11,10 @@ pub struct TerminalButton {
     workspace: WeakViewHandle<Workspace>,
 }
 
+// TODO: Rename this to `DeployTerminalButton`
 impl TerminalButton {
     pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
-        // When dock moves, redraw so that the icon and toggle status matches.
+        // When terminal moves, redraw so that the icon and toggle status matches.
         cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
 
         Self {
@@ -63,6 +64,7 @@ impl View for TerminalButton {
         })
         .with_cursor_style(CursorStyle::PointingHand)
         .on_up(MouseButton::Left, move |_, _| {
+            // TODO: Do we need this stuff?
             // let dock_pane = workspace.read(cx.app).dock_pane();
             // let drop_index = dock_pane.read(cx.app).items_len() + 1;
             // handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);