Add a way to join all panes into one (#17673)

Closes https://github.com/zed-industries/zed/issues/17536
Closes https://github.com/zed-industries/zed/pull/17548


Release Notes:

- Added a way to join all panes into one with `pane::JoinAll` action
([#17536](https://github.com/zed-industries/zed/issues/17536))

---------

Co-authored-by: Yogesh Dhamija <ydhamija96@gmail.com>
This commit is contained in:
Kirill Bulatov 2024-09-10 23:58:57 -04:00 committed by GitHub
parent 331d28d479
commit ec189fe884
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 173 additions and 47 deletions

View file

@ -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<Self>) {
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<Pane>, cx: &mut ViewContext<Self>) {
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<EntityId> = 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<Pane>,
destination: View<Pane>,
item_id_to_move: EntityId,
destination_index: usize,
cx: &mut ViewContext<Self>,
) {
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<Pane>,
@ -5944,6 +5918,79 @@ fn resize_edge(
}
}
fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, 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<Pane>, to_pane: &View<Pane>, 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::<Vec<_>>()
{
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<Pane>,
destination: &View<Pane>,
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<Workspace>,
item_id: u64,
) -> View<TestItem> {
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<Workspace>) -> View<Pane> {
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 {