diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 97dcd40afd..bf83196a97 100644 --- a/crates/terminal/src/modal.rs +++ b/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>, -} - -impl ProgramManager { - pub fn add_program(&mut self, window: usize, program: WeakModelHandle) { - 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( - &self, - window: &usize, - ) -> impl Iterator> + '_ { - self.window_to_programs - .get(window) - .into_iter() - .flat_map(|programs| { - programs - .iter() - .filter(|program| program.model_type() != TypeId::of::()) - .map(|program| program.downcast().unwrap()) - }) - } -} - -#[derive(Debug)] -struct StoredTerminal(ModelHandle); - pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext) { - // 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::, _, _>(|possible_connection, _| { - possible_connection.take() - }); + let possible_terminal = ProgramManager::remove::(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::>(Some(StoredTerminal(stored_terminal.clone()))); + ProgramManager::insert_or_replace::(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::>(Some(StoredTerminal( - terminal_handle.clone(), - ))); + ProgramManager::insert_or_replace::(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::>(Some(StoredTerminal(terminal_handle))); + ProgramManager::insert_or_replace::(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::>(None); + ProgramManager::remove::(cx.window_id(), cx); + if workspace.modal::().is_some() { workspace.dismiss_modal(cx) } diff --git a/crates/workspace/src/programs.rs b/crates/workspace/src/programs.rs new file mode 100644 index 0000000000..36169ea4c7 --- /dev/null +++ b/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, +} + +impl ProgramManager { + pub fn insert_or_replace( + window: usize, + program: ModelHandle, + cx: &mut ViewContext, + ) -> Option { + cx.update_global::(|pm, _| { + pm.insert_or_replace_internal::(window, program) + }) + } + + pub fn remove( + window: usize, + cx: &mut ViewContext, + ) -> Option> { + cx.update_global::(|pm, _| pm.remove_internal::(window)) + } + + pub fn new() -> Self { + Self { + modals: Default::default(), + } + } + + /// Inserts or replaces the model at the given location. + fn insert_or_replace_internal( + &mut self, + window: usize, + program: ModelHandle, + ) -> Option { + self.modals.insert(window, AnyModelHandle::from(program)) + } + + /// Remove the program associated with this window, if it's of the given type + fn remove_internal(&mut self, window: usize) -> Option> { + let program = self.modals.remove(&window); + if let Some(program) = program { + if program.is::() { + // 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 + } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ac24467343..94424f63fa 100644 --- a/crates/workspace/src/workspace.rs +++ b/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, 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);