diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index cc752f7aec..d0fa411381 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3,6 +3,7 @@ use crate::{ ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams, WeakItemHandle, }, + move_item, notifications::NotifyResultExt, toolbar::Toolbar, workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, @@ -149,6 +150,7 @@ actions!( GoBack, GoForward, JoinIntoNext, + JoinAll, ReopenClosedItem, SplitLeft, SplitUp, @@ -188,6 +190,7 @@ pub enum Event { item_id: EntityId, }, Split(SplitDirection), + JoinAll, JoinIntoNext, ChangeItemTitle, Focus, @@ -220,6 +223,7 @@ impl fmt::Debug for Event { .debug_struct("Split") .field("direction", direction) .finish(), + Event::JoinAll => f.write_str("JoinAll"), Event::JoinIntoNext => f.write_str("JoinIntoNext"), Event::ChangeItemTitle => f.write_str("ChangeItemTitle"), Event::Focus => f.write_str("Focus"), @@ -679,6 +683,10 @@ impl Pane { cx.emit(Event::JoinIntoNext); } + fn join_all(&mut self, cx: &mut ViewContext) { + cx.emit(Event::JoinAll); + } + fn history_updated(&mut self, cx: &mut ViewContext) { self.toolbar.update(cx, |_, cx| cx.notify()); } @@ -1757,9 +1765,7 @@ impl Pane { self.workspace .update(cx, |_, cx| { - cx.defer(move |this, cx| { - this.move_item(pane.clone(), pane, id, destination_index, cx) - }); + cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx)); }) .ok()?; @@ -1777,9 +1783,7 @@ impl Pane { self.workspace .update(cx, |_, cx| { - cx.defer(move |this, cx| { - this.move_item(pane.clone(), pane, id, destination_index, cx) - }); + cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx)); }) .ok()?; @@ -2349,7 +2353,7 @@ impl Pane { } }) } - workspace.move_item(from_pane.clone(), to_pane.clone(), item_id, ix, cx); + move_item(&from_pane, &to_pane, item_id, ix, cx); }); }) .log_err(); @@ -2556,6 +2560,7 @@ impl Render for Pane { .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx))) .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx))) .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx))) + .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx))) .on_action(cx.listener(Pane::toggle_zoom)) .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| { pane.activate_item(action.0, true, true, cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 50b92326b2..e6358cfdb9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2965,6 +2965,7 @@ impl Workspace { self.split_and_clone(pane, *direction, cx); } pane::Event::JoinIntoNext => self.join_pane_into_next(pane, cx), + pane::Event::JoinAll => self.join_all_panes(cx), pane::Event::Remove { focus_on_pane } => { self.remove_pane(pane, focus_on_pane.clone(), cx) } @@ -3094,7 +3095,7 @@ impl Workspace { }; let new_pane = self.add_pane(cx); - self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx); + move_item(&from, &new_pane, item_id_to_move, 0, cx); self.center .split(&pane_to_split, &new_pane, split_direction) .unwrap(); @@ -3122,6 +3123,17 @@ impl Workspace { })) } + pub fn join_all_panes(&mut self, cx: &mut ViewContext) { + let active_item = self.active_pane.read(cx).active_item(); + for pane in &self.panes { + join_pane_into_active(&self.active_pane, pane, cx); + } + if let Some(active_item) = active_item { + self.activate_item(active_item.as_ref(), true, true, cx); + } + cx.notify(); + } + pub fn join_pane_into_next(&mut self, pane: View, cx: &mut ViewContext) { let next_pane = self .find_pane_in_direction(SplitDirection::Right, cx) @@ -3131,48 +3143,10 @@ impl Workspace { let Some(next_pane) = next_pane else { return; }; - - let item_ids: Vec = pane.read(cx).items().map(|item| item.item_id()).collect(); - for item_id in item_ids { - self.move_item(pane.clone(), next_pane.clone(), item_id, 0, cx); - } + move_all_items(&pane, &next_pane, cx); cx.notify(); } - pub fn move_item( - &mut self, - source: View, - destination: View, - item_id_to_move: EntityId, - destination_index: usize, - cx: &mut ViewContext, - ) { - let Some((item_ix, item_handle)) = source - .read(cx) - .items() - .enumerate() - .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move) - else { - // Tab was closed during drag - return; - }; - - let item_handle = item_handle.clone(); - - if source != destination { - // Close item from previous pane - source.update(cx, |source, cx| { - source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx); - }); - } - - // This automatically removes duplicate items in the pane - destination.update(cx, |destination, cx| { - destination.add_item(item_handle, true, true, Some(destination_index), cx); - destination.focus(cx) - }); - } - fn remove_pane( &mut self, pane: View, @@ -5944,6 +5918,79 @@ fn resize_edge( } } +fn join_pane_into_active(active_pane: &View, pane: &View, cx: &mut WindowContext<'_>) { + if pane == active_pane { + return; + } else if pane.read(cx).items_len() == 0 { + pane.update(cx, |_, cx| { + cx.emit(pane::Event::Remove { + focus_on_pane: None, + }); + }) + } else { + move_all_items(pane, active_pane, cx); + } +} + +fn move_all_items(from_pane: &View, to_pane: &View, cx: &mut WindowContext<'_>) { + let destination_is_different = from_pane != to_pane; + let mut moved_items = 0; + for (item_ix, item_handle) in from_pane + .read(cx) + .items() + .enumerate() + .map(|(ix, item)| (ix, item.clone())) + .collect::>() + { + let ix = item_ix - moved_items; + if destination_is_different { + // Close item from previous pane + from_pane.update(cx, |source, cx| { + source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx); + }); + moved_items += 1; + } + + // This automatically removes duplicate items in the pane + to_pane.update(cx, |destination, cx| { + destination.add_item(item_handle, true, true, None, cx); + destination.focus(cx) + }); + } +} + +pub fn move_item( + source: &View, + destination: &View, + item_id_to_move: EntityId, + destination_index: usize, + cx: &mut WindowContext<'_>, +) { + let Some((item_ix, item_handle)) = source + .read(cx) + .items() + .enumerate() + .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move) + .map(|(ix, item)| (ix, item.clone())) + else { + // Tab was closed during drag + return; + }; + + if source != destination { + // Close item from previous pane + source.update(cx, |source, cx| { + source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx); + }); + } + + // This automatically removes duplicate items in the pane + destination.update(cx, |destination, cx| { + destination.add_item(item_handle, true, true, Some(destination_index), cx); + destination.focus(cx) + }); +} + #[cfg(test)] mod tests { use std::{cell::RefCell, rc::Rc}; @@ -6855,6 +6902,80 @@ mod tests { }); } + fn add_an_item_to_active_pane( + cx: &mut VisualTestContext, + workspace: &View, + item_id: u64, + ) -> View { + let item = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new( + item_id, + "item{item_id}.txt", + cx, + )]) + }); + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx); + }); + return item; + } + + fn split_pane(cx: &mut VisualTestContext, workspace: &View) -> View { + return workspace.update(cx, |workspace, cx| { + let new_pane = + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + new_pane + }); + } + + #[gpui::test] + async fn test_join_all_panes(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + + add_an_item_to_active_pane(cx, &workspace, 1); + split_pane(cx, &workspace); + add_an_item_to_active_pane(cx, &workspace, 2); + split_pane(cx, &workspace); // empty pane + split_pane(cx, &workspace); + let last_item = add_an_item_to_active_pane(cx, &workspace, 3); + + cx.executor().run_until_parked(); + + workspace.update(cx, |workspace, cx| { + let num_panes = workspace.panes().len(); + let num_items_in_current_pane = workspace.active_pane().read(cx).items().count(); + let active_item = workspace + .active_pane() + .read(cx) + .active_item() + .expect("item is in focus"); + + assert_eq!(num_panes, 4); + assert_eq!(num_items_in_current_pane, 1); + assert_eq!(active_item.item_id(), last_item.item_id()); + }); + + workspace.update(cx, |workspace, cx| { + workspace.join_all_panes(cx); + }); + + workspace.update(cx, |workspace, cx| { + let num_panes = workspace.panes().len(); + let num_items_in_current_pane = workspace.active_pane().read(cx).items().count(); + let active_item = workspace + .active_pane() + .read(cx) + .active_item() + .expect("item is in focus"); + + assert_eq!(num_panes, 1); + assert_eq!(num_items_in_current_pane, 3); + assert_eq!(active_item.item_id(), last_item.item_id()); + }); + } struct TestModal(FocusHandle); impl TestModal {