Add tab context menu

This commit is contained in:
Joseph Lyons 2023-04-07 19:36:10 -04:00
parent 035189a2a1
commit 67cb046298
3 changed files with 311 additions and 58 deletions

View File

@ -402,7 +402,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
for item_event in T::to_item_events(event).into_iter() {
match item_event {
ItemEvent::CloseItem => {
Pane::close_item(workspace, pane, item.id(), cx)
Pane::close_item_by_id(workspace, pane, item.id(), cx)
.detach_and_log_err(cx);
return;
}

View File

@ -37,6 +37,24 @@ use util::ResultExt;
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivateItem(pub usize);
#[derive(Clone, PartialEq)]
pub struct CloseItemById {
pub item_id: usize,
pub pane: WeakViewHandle<Pane>,
}
#[derive(Clone, PartialEq)]
pub struct CloseItemsToTheLeftById {
pub item_id: usize,
pub pane: WeakViewHandle<Pane>,
}
#[derive(Clone, PartialEq)]
pub struct CloseItemsToTheRightById {
pub item_id: usize,
pub pane: WeakViewHandle<Pane>,
}
actions!(
pane,
[
@ -57,12 +75,6 @@ actions!(
]
);
#[derive(Clone, PartialEq)]
pub struct CloseItem {
pub item_id: usize,
pub pane: WeakViewHandle<Pane>,
}
#[derive(Clone, PartialEq)]
pub struct MoveItem {
pub item_id: usize,
@ -92,11 +104,21 @@ pub struct DeployDockMenu;
#[derive(Clone, PartialEq)]
pub struct DeployNewMenu;
#[derive(Clone, PartialEq)]
pub struct DeployTabContextMenu {
pub position: Vector2F,
pub item_id: usize,
pub pane: WeakViewHandle<Pane>,
}
impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
impl_internal_actions!(
pane,
[
CloseItem,
CloseItemById,
CloseItemsToTheLeftById,
CloseItemsToTheRightById,
DeployTabContextMenu,
DeploySplitMenu,
DeployNewMenu,
DeployDockMenu,
@ -127,14 +149,34 @@ pub fn init(cx: &mut AppContext) {
cx.add_async_action(Pane::close_items_to_the_left);
cx.add_async_action(Pane::close_items_to_the_right);
cx.add_async_action(Pane::close_all_items);
cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
cx.add_async_action(|workspace: &mut Workspace, action: &CloseItemById, cx| {
let pane = action.pane.upgrade(cx)?;
let task = Pane::close_item(workspace, pane, action.item_id, cx);
let task = Pane::close_item_by_id(workspace, pane, action.item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
});
cx.add_async_action(
|workspace: &mut Workspace, action: &CloseItemsToTheLeftById, cx| {
let pane = action.pane.upgrade(cx)?;
let task = Pane::close_items_to_the_left_by_id(workspace, pane, action.item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
},
);
cx.add_async_action(
|workspace: &mut Workspace, action: &CloseItemsToTheRightById, cx| {
let pane = action.pane.upgrade(cx)?;
let task = Pane::close_items_to_the_right_by_id(workspace, pane, action.item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
},
);
cx.add_action(
|workspace,
MoveItem {
@ -168,6 +210,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Pane::deploy_split_menu);
cx.add_action(Pane::deploy_dock_menu);
cx.add_action(Pane::deploy_new_menu);
cx.add_action(Pane::deploy_tab_context_menu);
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
Pane::reopen_closed_item(workspace, cx).detach();
});
@ -214,6 +257,7 @@ pub struct Pane {
nav_history: Rc<RefCell<NavHistory>>,
toolbar: ViewHandle<Toolbar>,
tab_bar_context_menu: TabBarContextMenu,
tab_context_menu: ViewHandle<ContextMenu>,
docked: Option<DockAnchor>,
_background_actions: BackgroundActions,
_workspace_id: usize,
@ -319,6 +363,7 @@ impl Pane {
kind: TabBarContextMenuKind::New,
handle: context_menu,
},
tab_context_menu: cx.add_view(ContextMenu::new),
docked,
_background_actions: background_actions,
_workspace_id: workspace_id,
@ -742,9 +787,7 @@ impl Pane {
let pane = pane_handle.read(cx);
let active_item_id = pane.items[pane.active_item_index].id();
let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
item_id == active_item_id
});
let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
@ -752,6 +795,17 @@ impl Pane {
}))
}
pub fn close_item_by_id(
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
item_id_to_close: usize,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> {
Self::close_items(workspace, pane, cx, move |view_id| {
view_id == item_id_to_close
})
}
pub fn close_inactive_items(
workspace: &mut Workspace,
_: &CloseInactiveItems,
@ -804,15 +858,7 @@ impl Pane {
let pane = pane_handle.read(cx);
let active_item_id = pane.items[pane.active_item_index].id();
let item_ids: Vec<_> = pane
.items()
.take_while(|item| item.id() != active_item_id)
.map(|item| item.id())
.collect();
let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
item_ids.contains(&item_id)
});
let task = Self::close_items_to_the_left_by_id(workspace, pane_handle, active_item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
@ -820,6 +866,29 @@ impl Pane {
}))
}
pub fn close_items_to_the_left_by_id(
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
item_id: usize,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> {
let item_ids: Vec<_> = pane
.read(cx)
.items()
.take_while(|item| item.id() != item_id)
.map(|item| item.id())
.collect();
let task = Self::close_items(workspace, pane, cx, move |item_id| {
item_ids.contains(&item_id)
});
cx.foreground().spawn(async move {
task.await?;
Ok(())
})
}
pub fn close_items_to_the_right(
workspace: &mut Workspace,
_: &CloseItemsToTheRight,
@ -829,16 +898,7 @@ impl Pane {
let pane = pane_handle.read(cx);
let active_item_id = pane.items[pane.active_item_index].id();
let item_ids: Vec<_> = pane
.items()
.rev()
.take_while(|item| item.id() != active_item_id)
.map(|item| item.id())
.collect();
let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
item_ids.contains(&item_id)
});
let task = Self::close_items_to_the_right_by_id(workspace, pane_handle, active_item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
@ -846,6 +906,30 @@ impl Pane {
}))
}
pub fn close_items_to_the_right_by_id(
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
item_id: usize,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> {
let item_ids: Vec<_> = pane
.read(cx)
.items()
.rev()
.take_while(|item| item.id() != item_id)
.map(|item| item.id())
.collect();
let task = Self::close_items(workspace, pane, cx, move |item_id| {
item_ids.contains(&item_id)
});
cx.foreground().spawn(async move {
task.await?;
Ok(())
})
}
pub fn close_all_items(
workspace: &mut Workspace,
_: &CloseAllItems,
@ -861,17 +945,6 @@ impl Pane {
}))
}
pub fn close_item(
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
item_id_to_close: usize,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> {
Self::close_items(workspace, pane, cx, move |view_id| {
view_id == item_id_to_close
})
}
pub fn close_items(
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
@ -1207,6 +1280,65 @@ impl Pane {
self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
}
fn deploy_tab_context_menu(
&mut self,
action: &DeployTabContextMenu,
cx: &mut ViewContext<Self>,
) {
let target_item_id = action.item_id;
let target_pane = action.pane.clone();
let active_item_id = self.items[self.active_item_index].id();
let is_active_item = target_item_id == active_item_id;
let mut options = Vec::new();
// TODO: Explain why we are doing this - for the key bindings
options.push(if is_active_item {
ContextMenuItem::item("Close Active Item", CloseActiveItem)
} else {
ContextMenuItem::item(
"Close Inactive Item",
CloseItemById {
item_id: target_item_id,
pane: target_pane.clone(),
},
)
});
// This should really be called "close others" and the behaviour should be dynamically based on the tab the action is ran on. Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
options.push(ContextMenuItem::item(
"Close Inactive Items",
CloseInactiveItems,
));
options.push(ContextMenuItem::item("Close Clean Items", CloseCleanItems));
options.push(if is_active_item {
ContextMenuItem::item("Close Items To The Left", CloseItemsToTheLeft)
} else {
ContextMenuItem::item(
"Close Items To The Left",
CloseItemsToTheLeftById {
item_id: target_item_id,
pane: target_pane.clone(),
},
)
});
options.push(if is_active_item {
ContextMenuItem::item("Close Items To The Right", CloseItemsToTheRight)
} else {
ContextMenuItem::item(
"Close Items To The Right",
CloseItemsToTheRightById {
item_id: target_item_id,
pane: target_pane.clone(),
},
)
});
options.push(ContextMenuItem::item("Close All Items", CloseAllItems));
self.tab_context_menu.update(cx, |menu, cx| {
menu.show(action.position, AnchorCorner::TopLeft, options, cx);
});
}
pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
&self.toolbar
}
@ -1277,13 +1409,22 @@ impl Pane {
})
.on_click(MouseButton::Middle, {
let item = item.clone();
let pane = pane.clone();
move |_, cx: &mut EventContext| {
cx.dispatch_action(CloseItem {
cx.dispatch_action(CloseItemById {
item_id: item.id(),
pane: pane.clone(),
})
}
})
.on_down(MouseButton::Right, move |e, cx| {
let item = item.clone();
cx.dispatch_action(DeployTabContextMenu {
position: e.position,
item_id: item.id(),
pane: pane.clone(),
});
})
.boxed()
}
});
@ -1454,7 +1595,7 @@ impl Pane {
.on_click(MouseButton::Left, {
let pane = pane.clone();
move |_, cx| {
cx.dispatch_action(CloseItem {
cx.dispatch_action(CloseItemById {
item_id,
pane: pane.clone(),
})
@ -1624,6 +1765,7 @@ impl View for Pane {
.flex(1., true)
.boxed()
})
.with_child(ChildView::new(&self.tab_context_menu, cx).boxed())
.boxed()
} else {
enum EmptyPane {}
@ -2219,14 +2361,14 @@ mod tests {
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
add_labled_item(&workspace, &pane, "A", cx);
add_labled_item(&workspace, &pane, "B", cx);
add_labled_item(&workspace, &pane, "C", cx);
add_labled_item(&workspace, &pane, "D", cx);
add_labeled_item(&workspace, &pane, "A", false, cx);
add_labeled_item(&workspace, &pane, "B", false, cx);
add_labeled_item(&workspace, &pane, "C", false, cx);
add_labeled_item(&workspace, &pane, "D", false, cx);
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
add_labled_item(&workspace, &pane, "1", cx);
add_labeled_item(&workspace, &pane, "1", false, cx);
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
workspace.update(cx, |workspace, cx| {
@ -2257,14 +2399,125 @@ mod tests {
assert_item_labels(&pane, ["A*"], cx);
}
fn add_labled_item(
#[gpui::test]
async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
workspace.update(cx, |workspace, cx| {
Pane::close_inactive_items(workspace, &CloseInactiveItems, cx);
});
deterministic.run_until_parked();
assert_item_labels(&pane, ["C*"], cx);
}
#[gpui::test]
async fn test_close_clean_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
add_labeled_item(&workspace, &pane, "A", true, cx);
add_labeled_item(&workspace, &pane, "B", false, cx);
add_labeled_item(&workspace, &pane, "C", false, cx);
add_labeled_item(&workspace, &pane, "D", false, cx);
add_labeled_item(&workspace, &pane, "E", false, cx);
assert_item_labels(&pane, ["A", "B", "C", "D", "E*"], cx);
workspace.update(cx, |workspace, cx| {
Pane::close_clean_items(workspace, &CloseCleanItems, cx);
});
deterministic.run_until_parked();
assert_item_labels(&pane, ["A*"], cx);
}
#[gpui::test]
async fn test_close_items_to_the_left(
deterministic: Arc<Deterministic>,
cx: &mut TestAppContext,
) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
workspace.update(cx, |workspace, cx| {
Pane::close_items_to_the_left(workspace, &CloseItemsToTheLeft, cx);
});
deterministic.run_until_parked();
assert_item_labels(&pane, ["C*", "D", "E"], cx);
}
#[gpui::test]
async fn test_close_items_to_the_right(
deterministic: Arc<Deterministic>,
cx: &mut TestAppContext,
) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
workspace.update(cx, |workspace, cx| {
Pane::close_items_to_the_right(workspace, &CloseItemsToTheRight, cx);
});
deterministic.run_until_parked();
assert_item_labels(&pane, ["A", "B", "C*"], cx);
}
#[gpui::test]
async fn test_close_all_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
add_labeled_item(&workspace, &pane, "A", false, cx);
add_labeled_item(&workspace, &pane, "B", false, cx);
add_labeled_item(&workspace, &pane, "C", false, cx);
assert_item_labels(&pane, ["A", "B", "C*"], cx);
workspace.update(cx, |workspace, cx| {
Pane::close_all_items(workspace, &CloseAllItems, cx);
});
deterministic.run_until_parked();
assert_item_labels(&pane, [], cx);
}
fn add_labeled_item(
workspace: &ViewHandle<Workspace>,
pane: &ViewHandle<Pane>,
label: &str,
is_dirty: bool,
cx: &mut TestAppContext,
) -> Box<ViewHandle<TestItem>> {
workspace.update(cx, |workspace, cx| {
let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
let labeled_item =
Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
Pane::add_item(
workspace,

View File

@ -1548,7 +1548,7 @@ mod tests {
.update(cx, |workspace, cx| {
let editor3_id = editor3.id();
drop(editor3);
Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor3_id, cx)
})
.await
.unwrap();
@ -1581,7 +1581,7 @@ mod tests {
.update(cx, |workspace, cx| {
let editor2_id = editor2.id();
drop(editor2);
Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor2_id, cx)
})
.await
.unwrap();
@ -1731,7 +1731,7 @@ mod tests {
// Close all the pane items in some arbitrary order.
workspace
.update(cx, |workspace, cx| {
Pane::close_item(workspace, pane.clone(), file1_item_id, cx)
Pane::close_item_by_id(workspace, pane.clone(), file1_item_id, cx)
})
.await
.unwrap();
@ -1739,7 +1739,7 @@ mod tests {
workspace
.update(cx, |workspace, cx| {
Pane::close_item(workspace, pane.clone(), file4_item_id, cx)
Pane::close_item_by_id(workspace, pane.clone(), file4_item_id, cx)
})
.await
.unwrap();
@ -1747,7 +1747,7 @@ mod tests {
workspace
.update(cx, |workspace, cx| {
Pane::close_item(workspace, pane.clone(), file2_item_id, cx)
Pane::close_item_by_id(workspace, pane.clone(), file2_item_id, cx)
})
.await
.unwrap();
@ -1755,7 +1755,7 @@ mod tests {
workspace
.update(cx, |workspace, cx| {
Pane::close_item(workspace, pane.clone(), file3_item_id, cx)
Pane::close_item_by_id(workspace, pane.clone(), file3_item_id, cx)
})
.await
.unwrap();