Began program manager, made terminal modals per-window

Mikayla Maki created

Change summary

crates/terminal/src/modal.rs      | 83 ++++----------------------------
crates/workspace/src/programs.rs  | 77 ++++++++++++++++++++++++++++++
crates/workspace/src/workspace.rs |  4 +
3 files changed, 93 insertions(+), 71 deletions(-)

Detailed changes

crates/terminal/src/modal.rs 🔗

@@ -1,11 +1,6 @@
-use std::{
-    any::TypeId,
-    collections::{HashMap, HashSet},
-};
-
-use gpui::{AnyWeakModelHandle, Entity, ModelHandle, ViewContext, WeakModelHandle};
+use gpui::{ModelHandle, ViewContext};
 use settings::{Settings, WorkingDirectory};
-use workspace::Workspace;
+use workspace::{programs::ProgramManager, Workspace};
 
 use crate::{
     terminal_container_view::{
@@ -14,73 +9,20 @@ use crate::{
     Event, Terminal,
 };
 
-// TODO: Need to put this basic structure in workspace, and make 'program handles'
-// based off of the 'searchable item' pattern except with models this way, the workspace's clients
-// can register their models as programs.
-// Programs are:
-//  - Kept alive by the program manager, they need to emit an event to get dropped from it
-//  - Can be interacted with directly, (closed, activated), etc, bypassing associated view(s)
-//  - Have special rendering methods that the program manager offers to fill out the status bar
-//  - Can emit events for the program manager which:
-//    - Add a jewel (notification, change, etc.)
-//    - Drop the program
-//    - ???
-//  - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles
-//  - Start by making up the infrastructure, then just render the first item as the modal terminal in it's spot
-// update),
-
-struct ProgramManager {
-    window_to_programs: HashMap<usize, HashSet<AnyWeakModelHandle>>,
-}
-
-impl ProgramManager {
-    pub fn add_program<T: Entity>(&mut self, window: usize, program: WeakModelHandle<T>) {
-        let mut programs = if let Some(programs) = self.window_to_programs.remove(&window) {
-            programs
-        } else {
-            HashSet::default()
-        };
-
-        programs.insert(AnyWeakModelHandle::from(program));
-        self.window_to_programs.insert(window, programs);
-    }
-
-    pub fn get_programs<T: Entity>(
-        &self,
-        window: &usize,
-    ) -> impl Iterator<Item = WeakModelHandle<T>> + '_ {
-        self.window_to_programs
-            .get(window)
-            .into_iter()
-            .flat_map(|programs| {
-                programs
-                    .iter()
-                    .filter(|program| program.model_type() != TypeId::of::<T>())
-                    .map(|program| program.downcast().unwrap())
-            })
-    }
-}
-
-#[derive(Debug)]
-struct StoredTerminal(ModelHandle<Terminal>);
-
 pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
-    // cx.window_id()
+    let window = cx.window_id();
 
     // Pull the terminal connection out of the global if it has been stored
-    let possible_terminal =
-        cx.update_default_global::<Option<StoredTerminal>, _, _>(|possible_connection, _| {
-            possible_connection.take()
-        });
+    let possible_terminal = ProgramManager::remove::<Terminal, _>(window, cx);
 
-    if let Some(StoredTerminal(stored_terminal)) = possible_terminal {
+    if let Some(terminal_handle) = possible_terminal {
         workspace.toggle_modal(cx, |_, cx| {
             // Create a view from the stored connection if the terminal modal is not already shown
-            cx.add_view(|cx| TerminalContainer::from_terminal(stored_terminal.clone(), true, cx))
+            cx.add_view(|cx| TerminalContainer::from_terminal(terminal_handle.clone(), true, cx))
         });
         // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
         // store the terminal back in the global
-        cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(stored_terminal.clone())));
+        ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
     } else {
         // No connection was stored, create a new terminal
         if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
@@ -101,21 +43,19 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
                 cx.subscribe(&terminal_handle, on_event).detach();
                 // Set the global immediately if terminal construction was successful,
                 // in case the user opens the command palette
-                cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(
-                    terminal_handle.clone(),
-                )));
+                ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
             }
 
             this
         }) {
-            // Terminal modal was dismissed. Store terminal if the terminal view is connected
+            // Terminal modal was dismissed and the terminal view is connected, store the terminal
             if let TerminalContainerContent::Connected(connected) =
                 &closed_terminal_handle.read(cx).content
             {
                 let terminal_handle = connected.read(cx).handle();
                 // Set the global immediately if terminal construction was successful,
                 // in case the user opens the command palette
-                cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(terminal_handle)));
+                ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
             }
         }
     }
@@ -129,7 +69,8 @@ pub fn on_event(
 ) {
     // Dismiss the modal if the terminal quit
     if let Event::CloseTerminal = event {
-        cx.set_global::<Option<StoredTerminal>>(None);
+        ProgramManager::remove::<Terminal, _>(cx.window_id(), cx);
+
         if workspace.modal::<TerminalContainer>().is_some() {
             workspace.dismiss_modal(cx)
         }

crates/workspace/src/programs.rs 🔗

@@ -0,0 +1,77 @@
+// TODO: Need to put this basic structure in workspace, and make 'program handles'
+// based off of the 'searchable item' pattern except with models. This way, the workspace's clients
+// can register their models as programs with a specific identity and capable of notifying the workspace
+// Programs are:
+//  - Kept alive by the program manager, they need to emit an event to get dropped from it
+//  - Can be interacted with directly, (closed, activated, etc.) by the program manager, bypassing
+//    associated view(s)
+//  - Have special rendering methods that the program manager requires them to implement to fill out
+//    the status bar
+//  - Can emit events for the program manager which:
+//    - Add a jewel (notification, change, etc.)
+//    - Drop the program
+//    - ???
+//  - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles
+
+use collections::HashMap;
+use gpui::{AnyModelHandle, Entity, ModelHandle, View, ViewContext};
+
+/// This struct is going to be the starting point for the 'program manager' feature that will
+/// eventually be implemented to provide a collaborative way of engaging with identity-having
+/// features like the terminal.
+pub struct ProgramManager {
+    // TODO: Make this a hashset or something
+    modals: HashMap<usize, AnyModelHandle>,
+}
+
+impl ProgramManager {
+    pub fn insert_or_replace<T: Entity, V: View>(
+        window: usize,
+        program: ModelHandle<T>,
+        cx: &mut ViewContext<V>,
+    ) -> Option<AnyModelHandle> {
+        cx.update_global::<ProgramManager, _, _>(|pm, _| {
+            pm.insert_or_replace_internal::<T>(window, program)
+        })
+    }
+
+    pub fn remove<T: Entity, V: View>(
+        window: usize,
+        cx: &mut ViewContext<V>,
+    ) -> Option<ModelHandle<T>> {
+        cx.update_global::<ProgramManager, _, _>(|pm, _| pm.remove_internal::<T>(window))
+    }
+
+    pub fn new() -> Self {
+        Self {
+            modals: Default::default(),
+        }
+    }
+
+    /// Inserts or replaces the model at the given location.
+    fn insert_or_replace_internal<T: Entity>(
+        &mut self,
+        window: usize,
+        program: ModelHandle<T>,
+    ) -> Option<AnyModelHandle> {
+        self.modals.insert(window, AnyModelHandle::from(program))
+    }
+
+    /// Remove the program associated with this window, if it's of the given type
+    fn remove_internal<T: Entity>(&mut self, window: usize) -> Option<ModelHandle<T>> {
+        let program = self.modals.remove(&window);
+        if let Some(program) = program {
+            if program.is::<T>() {
+                // Guaranteed to be some, but leave it in the option
+                // anyway for the API
+                program.downcast()
+            } else {
+                // Model is of the incorrect type, put it back
+                self.modals.insert(window, program);
+                None
+            }
+        } else {
+            None
+        }
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -5,6 +5,7 @@
 /// specific locations.
 pub mod pane;
 pub mod pane_group;
+pub mod programs;
 pub mod searchable;
 pub mod sidebar;
 mod status_bar;
@@ -36,6 +37,7 @@ use log::error;
 pub use pane::*;
 pub use pane_group::*;
 use postage::prelude::Stream;
+use programs::ProgramManager;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
 use searchable::SearchableItemHandle;
 use serde::Deserialize;
@@ -146,6 +148,8 @@ impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     pane::init(cx);
 
+    cx.set_global(ProgramManager::new());
+
     cx.add_global_action(open);
     cx.add_global_action({
         let app_state = Arc::downgrade(&app_state);