From 67cb0462988ae2fcb145d3587b5bd3b7a1a647b1 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Fri, 7 Apr 2023 19:36:10 -0400 Subject: [PATCH 1/3] Add tab context menu --- crates/workspace/src/item.rs | 2 +- crates/workspace/src/pane.rs | 333 ++++++++++++++++++++++++++++++----- crates/zed/src/zed.rs | 12 +- 3 files changed, 300 insertions(+), 47 deletions(-) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index f9b3550003473778435e143c5a785db2bbe83572..f7ffe64f972d99ccb9b172f3fc70a4a0203f91b4 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -402,7 +402,7 @@ impl ItemHandle for ViewHandle { 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; } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 4d2ff32ef3e09bdc67562ea9389a3a3684286b49..d489f1b723987ee8c5823127ca2e1d9e9e6431a2 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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, +} + +#[derive(Clone, PartialEq)] +pub struct CloseItemsToTheLeftById { + pub item_id: usize, + pub pane: WeakViewHandle, +} + +#[derive(Clone, PartialEq)] +pub struct CloseItemsToTheRightById { + pub item_id: usize, + pub pane: WeakViewHandle, +} + actions!( pane, [ @@ -57,12 +75,6 @@ actions!( ] ); -#[derive(Clone, PartialEq)] -pub struct CloseItem { - pub item_id: usize, - pub pane: WeakViewHandle, -} - #[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, +} + 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>, toolbar: ViewHandle, tab_bar_context_menu: TabBarContextMenu, + tab_context_menu: ViewHandle, docked: Option, _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, + item_id_to_close: usize, + cx: &mut ViewContext, + ) -> Task> { + 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,20 +858,35 @@ impl Pane { let pane = pane_handle.read(cx); let active_item_id = pane.items[pane.active_item_index].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?; + Ok(()) + })) + } + + pub fn close_items_to_the_left_by_id( + workspace: &mut Workspace, + pane: ViewHandle, + item_id: usize, + cx: &mut ViewContext, + ) -> Task> { let item_ids: Vec<_> = pane + .read(cx) .items() - .take_while(|item| item.id() != active_item_id) + .take_while(|item| item.id() != item_id) .map(|item| item.id()) .collect(); - let task = Self::close_items(workspace, pane_handle, cx, move |item_id| { + let task = Self::close_items(workspace, pane, cx, move |item_id| { item_ids.contains(&item_id) }); - Some(cx.foreground().spawn(async move { + cx.foreground().spawn(async move { task.await?; Ok(()) - })) + }) } pub fn close_items_to_the_right( @@ -829,21 +898,36 @@ impl Pane { let pane = pane_handle.read(cx); let active_item_id = pane.items[pane.active_item_index].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?; + Ok(()) + })) + } + + pub fn close_items_to_the_right_by_id( + workspace: &mut Workspace, + pane: ViewHandle, + item_id: usize, + cx: &mut ViewContext, + ) -> Task> { let item_ids: Vec<_> = pane + .read(cx) .items() .rev() - .take_while(|item| item.id() != active_item_id) + .take_while(|item| item.id() != item_id) .map(|item| item.id()) .collect(); - let task = Self::close_items(workspace, pane_handle, cx, move |item_id| { + let task = Self::close_items(workspace, pane, cx, move |item_id| { item_ids.contains(&item_id) }); - Some(cx.foreground().spawn(async move { + cx.foreground().spawn(async move { task.await?; Ok(()) - })) + }) } pub fn close_all_items( @@ -861,17 +945,6 @@ impl Pane { })) } - pub fn close_item( - workspace: &mut Workspace, - pane: ViewHandle, - item_id_to_close: usize, - cx: &mut ViewContext, - ) -> Task> { - Self::close_items(workspace, pane, cx, move |view_id| { - view_id == item_id_to_close - }) - } - pub fn close_items( workspace: &mut Workspace, pane: ViewHandle, @@ -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, + ) { + 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 { &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, 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, 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, + 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, + 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, 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, pane: &ViewHandle, label: &str, + is_dirty: bool, cx: &mut TestAppContext, ) -> Box> { 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, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8aa8bca07b3f38f9ecd0cbbbbb93d0fa5e17fc63..36703fae316361b41e7230c7d1a5389550350895 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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(); From c39764487cc40c62712f5a6fc83a814834fe7b79 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 11 Apr 2023 15:45:02 -0400 Subject: [PATCH 2/3] Construct context menu in a more clear way --- crates/workspace/src/pane.rs | 88 ++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index d489f1b723987ee8c5823127ca2e1d9e9e6431a2..31d099c04b78d9c1a5139c7ba82d4f2f4f05df25 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1290,52 +1290,52 @@ impl Pane { 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)); + // The `CloseInactiveItems` action should really be called "CloseOthers" 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 self.tab_context_menu.update(cx, |menu, cx| { - menu.show(action.position, AnchorCorner::TopLeft, options, cx); + menu.show( + action.position, + AnchorCorner::TopLeft, + if is_active_item { + vec![ + ContextMenuItem::item("Close Active Item", CloseActiveItem), + ContextMenuItem::item("Close Inactive Items", CloseInactiveItems), + ContextMenuItem::item("Close Clean Items", CloseCleanItems), + ContextMenuItem::item("Close Items To The Left", CloseItemsToTheLeft), + ContextMenuItem::item("Close Items To The Right", CloseItemsToTheRight), + ContextMenuItem::item("Close All Items", CloseAllItems), + ] + } else { + // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command. + vec![ + ContextMenuItem::item( + "Close Inactive Item", + CloseItemById { + item_id: target_item_id, + pane: target_pane.clone(), + }, + ), + ContextMenuItem::item("Close Inactive Items", CloseInactiveItems), + ContextMenuItem::item("Close Clean Items", CloseCleanItems), + ContextMenuItem::item( + "Close Items To The Left", + CloseItemsToTheLeftById { + item_id: target_item_id, + pane: target_pane.clone(), + }, + ), + ContextMenuItem::item( + "Close Items To The Right", + CloseItemsToTheRightById { + item_id: target_item_id, + pane: target_pane.clone(), + }, + ), + ContextMenuItem::item("Close All Items", CloseAllItems), + ] + }, + cx, + ); }); } From 0b52308c99417272dd3f31b98576035ed72dd161 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 11 Apr 2023 16:25:42 -0400 Subject: [PATCH 3/3] Represent dirty state in item-testing code --- crates/workspace/src/pane.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 31d099c04b78d9c1a5139c7ba82d4f2f4f05df25..a54aed96f464bdcfff03e526ecdb8cd9f5c37963 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2429,17 +2429,17 @@ mod tests { 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, "C", true, 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); + 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); + assert_item_labels(&pane, ["A^", "C*^"], cx); } #[gpui::test] @@ -2597,6 +2597,9 @@ mod tests { if ix == pane.active_item_index { state.push('*'); } + if item.is_dirty(cx) { + state.push('^'); + } state }) .collect::>();