diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 55916f3345..a50c105171 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -492,7 +492,7 @@ impl AssistantPanel { cx: &mut ViewContext, ) { let update_model_summary = match event { - pane::Event::Remove => { + pane::Event::Remove { .. } => { cx.emit(PanelEvent::Close); false } diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index fe8227f661..e8966ac5b9 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -173,16 +173,16 @@ impl TabSwitcherDelegate { }; cx.subscribe(&pane, |tab_switcher, _, event, cx| { match event { - PaneEvent::AddItem { .. } | PaneEvent::RemovedItem { .. } | PaneEvent::Remove => { - tab_switcher.picker.update(cx, |picker, cx| { - let selected_item_id = picker.delegate.selected_item_id(); - picker.delegate.update_matches(cx); - if let Some(item_id) = selected_item_id { - picker.delegate.select_item(item_id, cx); - } - cx.notify(); - }) - } + PaneEvent::AddItem { .. } + | PaneEvent::RemovedItem { .. } + | PaneEvent::Remove { .. } => tab_switcher.picker.update(cx, |picker, cx| { + let selected_item_id = picker.delegate.selected_item_id(); + picker.delegate.update_matches(cx); + if let Some(item_id) = selected_item_id { + picker.delegate.select_item(item_id, cx); + } + cx.notify(); + }), _ => {} }; }) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 14ad7181fc..b0527fdd29 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -347,7 +347,7 @@ impl TerminalPanel { match event { pane::Event::ActivateItem { .. } => self.serialize(cx), pane::Event::RemovedItem { .. } => self.serialize(cx), - pane::Event::Remove => cx.emit(PanelEvent::Close), + pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3a0ed23d8b..c358372066 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -147,6 +147,7 @@ actions!( CloseItemsToTheRight, GoBack, GoForward, + JoinIntoNext, ReopenClosedItem, SplitLeft, SplitUp, @@ -175,7 +176,9 @@ pub enum Event { ActivateItem { local: bool, }, - Remove, + Remove { + focus_on_pane: Option>, + }, RemoveItem { idx: usize, }, @@ -183,6 +186,7 @@ pub enum Event { item_id: EntityId, }, Split(SplitDirection), + JoinIntoNext, ChangeItemTitle, Focus, ZoomIn, @@ -204,7 +208,7 @@ impl fmt::Debug for Event { .debug_struct("ActivateItem") .field("local", local) .finish(), - Event::Remove => f.write_str("Remove"), + Event::Remove { .. } => f.write_str("Remove"), Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(), Event::RemovedItem { item_id } => f .debug_struct("RemovedItem") @@ -214,6 +218,7 @@ impl fmt::Debug for Event { .debug_struct("Split") .field("direction", direction) .finish(), + Event::JoinIntoNext => f.write_str("JoinIntoNext"), Event::ChangeItemTitle => f.write_str("ChangeItemTitle"), Event::Focus => f.write_str("Focus"), Event::ZoomIn => f.write_str("ZoomIn"), @@ -668,6 +673,10 @@ impl Pane { } } + fn join_into_next(&mut self, cx: &mut ViewContext) { + cx.emit(Event::JoinIntoNext); + } + fn history_updated(&mut self, cx: &mut ViewContext) { self.toolbar.update(cx, |_, cx| cx.notify()); } @@ -1318,6 +1327,33 @@ impl Pane { activate_pane: bool, close_pane_if_empty: bool, cx: &mut ViewContext, + ) { + self._remove_item(item_index, activate_pane, close_pane_if_empty, None, cx) + } + + pub fn remove_item_and_focus_on_pane( + &mut self, + item_index: usize, + activate_pane: bool, + focus_on_pane_if_closed: View, + cx: &mut ViewContext, + ) { + self._remove_item( + item_index, + activate_pane, + true, + Some(focus_on_pane_if_closed), + cx, + ) + } + + fn _remove_item( + &mut self, + item_index: usize, + activate_pane: bool, + close_pane_if_empty: bool, + focus_on_pane_if_closed: Option>, + cx: &mut ViewContext, ) { self.activation_history .retain(|entry| entry.entity_id != self.items[item_index].item_id()); @@ -1354,7 +1390,9 @@ impl Pane { item.deactivated(cx); if close_pane_if_empty { self.update_toolbar(cx); - cx.emit(Event::Remove); + cx.emit(Event::Remove { + focus_on_pane: focus_on_pane_if_closed, + }); } } @@ -2259,6 +2297,7 @@ impl Render for Pane { .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))) .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::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/persistence/model.rs b/crates/workspace/src/persistence/model.rs index a697149861..ebe1905f46 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -360,7 +360,9 @@ impl SerializedPaneGroup { } else { let pane = pane.upgrade()?; workspace - .update(cx, |workspace, cx| workspace.force_remove_pane(&pane, cx)) + .update(cx, |workspace, cx| { + workspace.force_remove_pane(&pane, &None, cx) + }) .log_err()?; None } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1f574e9ce4..2d3bb3ee2a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2955,7 +2955,10 @@ impl Workspace { pane::Event::Split(direction) => { self.split_and_clone(pane, *direction, cx); } - pane::Event::Remove => self.remove_pane(pane, cx), + pane::Event::JoinIntoNext => self.join_pane_into_next(pane, cx), + pane::Event::Remove { focus_on_pane } => { + self.remove_pane(pane, focus_on_pane.clone(), cx) + } pane::Event::ActivateItem { local } => { pane.model.update(cx, |pane, _| { pane.track_alternate_file_items(); @@ -3103,6 +3106,23 @@ impl Workspace { })) } + pub fn join_pane_into_next(&mut self, pane: View, cx: &mut ViewContext) { + let next_pane = self + .find_pane_in_direction(SplitDirection::Right, cx) + .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx)) + .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx)) + .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx)); + 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); + } + cx.notify(); + } + pub fn move_item( &mut self, source: View, @@ -3126,7 +3146,7 @@ impl Workspace { if source != destination { // Close item from previous pane source.update(cx, |source, cx| { - source.remove_item(item_ix, false, true, cx); + source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx); }); } @@ -3137,9 +3157,14 @@ impl Workspace { }); } - fn remove_pane(&mut self, pane: View, cx: &mut ViewContext) { + fn remove_pane( + &mut self, + pane: View, + focus_on: Option>, + cx: &mut ViewContext, + ) { if self.center.remove(&pane).unwrap() { - self.force_remove_pane(&pane, cx); + self.force_remove_pane(&pane, &focus_on, cx); self.unfollow_in_pane(&pane, cx); self.last_leaders_by_pane.remove(&pane.downgrade()); for removed_item in pane.read(cx).items() { @@ -3932,7 +3957,7 @@ impl Workspace { } } Member::Pane(pane) => { - self.force_remove_pane(&pane, cx); + self.force_remove_pane(&pane, &None, cx); } } } @@ -3942,12 +3967,21 @@ impl Workspace { self.serialize_workspace_internal(cx) } - fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { + fn force_remove_pane( + &mut self, + pane: &View, + focus_on: &Option>, + cx: &mut ViewContext, + ) { self.panes.retain(|p| p != pane); - self.panes - .last() - .unwrap() - .update(cx, |pane, cx| pane.focus(cx)); + if let Some(focus_on) = focus_on { + focus_on.update(cx, |pane, cx| pane.focus(cx)); + } else { + self.panes + .last() + .unwrap() + .update(cx, |pane, cx| pane.focus(cx)); + } if self.last_active_center_pane == Some(pane.downgrade()) { self.last_active_center_pane = None; } @@ -6449,6 +6483,143 @@ mod tests { }); } + #[gpui::test] + async fn test_join_pane_into_next(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)); + + // Let's arrange the panes like this: + // + // +-----------------------+ + // | top | + // +------+--------+-------+ + // | left | center | right | + // +------+--------+-------+ + // | bottom | + // +-----------------------+ + + let top_item = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)]) + }); + let bottom_item = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)]) + }); + let left_item = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)]) + }); + let right_item = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)]) + }); + let center_item = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)]) + }); + + let top_pane_id = workspace.update(cx, |workspace, cx| { + let top_pane_id = workspace.active_pane().entity_id(); + workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx); + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx); + top_pane_id + }); + let bottom_pane_id = workspace.update(cx, |workspace, cx| { + let bottom_pane_id = workspace.active_pane().entity_id(); + workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx); + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx); + bottom_pane_id + }); + let left_pane_id = workspace.update(cx, |workspace, cx| { + let left_pane_id = workspace.active_pane().entity_id(); + workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx); + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + left_pane_id + }); + let right_pane_id = workspace.update(cx, |workspace, cx| { + let right_pane_id = workspace.active_pane().entity_id(); + workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx); + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx); + right_pane_id + }); + let center_pane_id = workspace.update(cx, |workspace, cx| { + let center_pane_id = workspace.active_pane().entity_id(); + workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx); + center_pane_id + }); + cx.executor().run_until_parked(); + + workspace.update(cx, |workspace, cx| { + assert_eq!(center_pane_id, workspace.active_pane().entity_id()); + + // Join into next from center pane into right + workspace.join_pane_into_next(workspace.active_pane().clone(), cx); + }); + + workspace.update(cx, |workspace, cx| { + let active_pane = workspace.active_pane(); + assert_eq!(right_pane_id, active_pane.entity_id()); + assert_eq!(2, active_pane.read(cx).items_len()); + let item_ids_in_pane = + HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); + assert!(item_ids_in_pane.contains(¢er_item.item_id())); + assert!(item_ids_in_pane.contains(&right_item.item_id())); + + // Join into next from right pane into bottom + workspace.join_pane_into_next(workspace.active_pane().clone(), cx); + }); + + workspace.update(cx, |workspace, cx| { + let active_pane = workspace.active_pane(); + assert_eq!(bottom_pane_id, active_pane.entity_id()); + assert_eq!(3, active_pane.read(cx).items_len()); + let item_ids_in_pane = + HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); + assert!(item_ids_in_pane.contains(¢er_item.item_id())); + assert!(item_ids_in_pane.contains(&right_item.item_id())); + assert!(item_ids_in_pane.contains(&bottom_item.item_id())); + + // Join into next from bottom pane into left + workspace.join_pane_into_next(workspace.active_pane().clone(), cx); + }); + + workspace.update(cx, |workspace, cx| { + let active_pane = workspace.active_pane(); + assert_eq!(left_pane_id, active_pane.entity_id()); + assert_eq!(4, active_pane.read(cx).items_len()); + let item_ids_in_pane = + HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); + assert!(item_ids_in_pane.contains(¢er_item.item_id())); + assert!(item_ids_in_pane.contains(&right_item.item_id())); + assert!(item_ids_in_pane.contains(&bottom_item.item_id())); + assert!(item_ids_in_pane.contains(&left_item.item_id())); + + // Join into next from left pane into top + workspace.join_pane_into_next(workspace.active_pane().clone(), cx); + }); + + workspace.update(cx, |workspace, cx| { + let active_pane = workspace.active_pane(); + assert_eq!(top_pane_id, active_pane.entity_id()); + assert_eq!(5, active_pane.read(cx).items_len()); + let item_ids_in_pane = + HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); + assert!(item_ids_in_pane.contains(¢er_item.item_id())); + assert!(item_ids_in_pane.contains(&right_item.item_id())); + assert!(item_ids_in_pane.contains(&bottom_item.item_id())); + assert!(item_ids_in_pane.contains(&left_item.item_id())); + assert!(item_ids_in_pane.contains(&top_item.item_id())); + + // Single pane left: no-op + workspace.join_pane_into_next(workspace.active_pane().clone(), cx) + }); + + workspace.update(cx, |workspace, _cx| { + let active_pane = workspace.active_pane(); + assert_eq!(top_pane_id, active_pane.entity_id()); + }); + } + struct TestModal(FocusHandle); impl TestModal {