Refactor item-closing actions (#31838)

While working on 

- https://github.com/zed-industries/zed/pull/31783
- https://github.com/zed-industries/zed/pull/31786

... I noticed some areas that could be improved through refactoring. The
bug in https://github.com/zed-industries/zed/pull/31783 came from having
duplicate code. The fix had been applied to one version, but not the
duplicated code.

This PR attempts to do some initial clean up, through some refactoring.

Release Notes:

- N/A
This commit is contained in:
Joseph T. Lyons 2025-05-31 19:38:32 -04:00 committed by GitHub
parent cc536655a1
commit 24e4446cd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 184 additions and 178 deletions

View file

@ -1010,7 +1010,6 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
workspace_b.update_in(cx_b, |workspace, window, cx| { workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| { workspace.active_pane().update(cx, |pane, cx| {
pane.close_inactive_items(&Default::default(), window, cx) pane.close_inactive_items(&Default::default(), window, cx)
.unwrap()
.detach(); .detach();
}); });
}); });

View file

@ -20016,7 +20016,6 @@ println!("5");
pane_1 pane_1
.update_in(cx, |pane, window, cx| { .update_in(cx, |pane, window, cx| {
pane.close_inactive_items(&CloseInactiveItems::default(), window, cx) pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
.unwrap()
}) })
.await .await
.unwrap(); .unwrap();
@ -20053,7 +20052,6 @@ println!("5");
pane_2 pane_2
.update_in(cx, |pane, window, cx| { .update_in(cx, |pane, window, cx| {
pane.close_inactive_items(&CloseInactiveItems::default(), window, cx) pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
.unwrap()
}) })
.await .await
.unwrap(); .unwrap();
@ -20229,7 +20227,6 @@ println!("5");
}); });
pane.update_in(cx, |pane, window, cx| { pane.update_in(cx, |pane, window, cx| {
pane.close_all_items(&CloseAllItems::default(), window, cx) pane.close_all_items(&CloseAllItems::default(), window, cx)
.unwrap()
}) })
.await .await
.unwrap(); .unwrap();

View file

@ -1035,6 +1035,10 @@ impl Pane {
self.items.get(self.active_item_index).cloned() self.items.get(self.active_item_index).cloned()
} }
fn active_item_id(&self) -> EntityId {
self.items[self.active_item_index].item_id()
}
pub fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> { pub fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
self.items self.items
.get(self.active_item_index)? .get(self.active_item_index)?
@ -1244,7 +1248,7 @@ impl Pane {
return None; return None;
}; };
let active_item_id = self.items[self.active_item_index].item_id(); let active_item_id = self.active_item_id();
Some(self.close_item_by_id( Some(self.close_item_by_id(
active_item_id, active_item_id,
action.save_intent.unwrap_or(SaveIntent::Close), action.save_intent.unwrap_or(SaveIntent::Close),
@ -1270,19 +1274,23 @@ impl Pane {
action: &CloseInactiveItems, action: &CloseInactiveItems,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> { ) -> Task<Result<()>> {
if self.items.is_empty() { if self.items.is_empty() {
return None; return Task::ready(Ok(()));
} }
let active_item_id = self.items[self.active_item_index].item_id(); let active_item_id = self.active_item_id();
let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned); let pinned_item_ids = self.pinned_item_ids();
Some(self.close_items(
self.close_items(
window, window,
cx, cx,
action.save_intent.unwrap_or(SaveIntent::Close), action.save_intent.unwrap_or(SaveIntent::Close),
move |item_id| item_id != active_item_id && !non_closeable_items.contains(&item_id), move |item_id| {
)) item_id != active_item_id
&& (action.close_pinned || !pinned_item_ids.contains(&item_id))
},
)
} }
pub fn close_clean_items( pub fn close_clean_items(
@ -1290,18 +1298,18 @@ impl Pane {
action: &CloseCleanItems, action: &CloseCleanItems,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> { ) -> Task<Result<()>> {
let item_ids: Vec<_> = self if self.items.is_empty() {
.items() return Task::ready(Ok(()));
.filter(|item| !item.is_dirty(cx)) }
.map(|item| item.item_id())
.collect(); let clean_item_ids = self.clean_item_ids(cx);
let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned); let pinned_item_ids = self.pinned_item_ids();
Some(
self.close_items(window, cx, SaveIntent::Close, move |item_id| { self.close_items(window, cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id) clean_item_ids.contains(&item_id)
}), && (action.close_pinned || !pinned_item_ids.contains(&item_id))
) })
} }
pub fn close_items_to_the_left( pub fn close_items_to_the_left(
@ -1313,12 +1321,14 @@ impl Pane {
if self.items.is_empty() { if self.items.is_empty() {
return None; return None;
} }
let active_item_id = self.items[self.active_item_index].item_id();
let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned); let active_item_id = self.active_item_id();
let pinned_item_ids = self.pinned_item_ids();
Some(self.close_items_to_the_left_by_id( Some(self.close_items_to_the_left_by_id(
active_item_id, active_item_id,
action, action,
non_closeable_items, pinned_item_ids,
window, window,
cx, cx,
)) ))
@ -1328,19 +1338,19 @@ impl Pane {
&mut self, &mut self,
item_id: EntityId, item_id: EntityId,
action: &CloseItemsToTheLeft, action: &CloseItemsToTheLeft,
non_closeable_items: HashSet<EntityId>, pinned_item_ids: HashSet<EntityId>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let item_ids: Vec<_> = self if self.items.is_empty() {
.items() return Task::ready(Ok(()));
.take_while(|item| item.item_id() != item_id) }
.map(|item| item.item_id())
.collect(); let to_the_left_item_ids = self.to_the_left_item_ids(item_id);
self.close_items(window, cx, SaveIntent::Close, move |item_id| { self.close_items(window, cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id) to_the_left_item_ids.contains(&item_id)
&& !action.close_pinned && (action.close_pinned || !pinned_item_ids.contains(&item_id))
&& !non_closeable_items.contains(&item_id)
}) })
} }
@ -1353,12 +1363,12 @@ impl Pane {
if self.items.is_empty() { if self.items.is_empty() {
return None; return None;
} }
let active_item_id = self.items[self.active_item_index].item_id(); let active_item_id = self.active_item_id();
let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned); let pinned_item_ids = self.pinned_item_ids();
Some(self.close_items_to_the_right_by_id( Some(self.close_items_to_the_right_by_id(
active_item_id, active_item_id,
action, action,
non_closeable_items, pinned_item_ids,
window, window,
cx, cx,
)) ))
@ -1368,20 +1378,19 @@ impl Pane {
&mut self, &mut self,
item_id: EntityId, item_id: EntityId,
action: &CloseItemsToTheRight, action: &CloseItemsToTheRight,
non_closeable_items: HashSet<EntityId>, pinned_item_ids: HashSet<EntityId>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let item_ids: Vec<_> = self if self.items.is_empty() {
.items() return Task::ready(Ok(()));
.rev() }
.take_while(|item| item.item_id() != item_id)
.map(|item| item.item_id()) let to_the_right_item_ids = self.to_the_right_item_ids(item_id);
.collect();
self.close_items(window, cx, SaveIntent::Close, move |item_id| { self.close_items(window, cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id) to_the_right_item_ids.contains(&item_id)
&& !action.close_pinned && (action.close_pinned || !pinned_item_ids.contains(&item_id))
&& !non_closeable_items.contains(&item_id)
}) })
} }
@ -1390,18 +1399,19 @@ impl Pane {
action: &CloseAllItems, action: &CloseAllItems,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> { ) -> Task<Result<()>> {
if self.items.is_empty() { if self.items.is_empty() {
return None; return Task::ready(Ok(()));
} }
let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned); let pinned_item_ids = self.pinned_item_ids();
Some(self.close_items(
self.close_items(
window, window,
cx, cx,
action.save_intent.unwrap_or(SaveIntent::Close), action.save_intent.unwrap_or(SaveIntent::Close),
|item_id| !non_closeable_items.contains(&item_id), |item_id| action.close_pinned || !pinned_item_ids.contains(&item_id),
)) )
} }
pub fn close_items_over_max_tabs(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn close_items_over_max_tabs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@ -1498,7 +1508,7 @@ impl Pane {
} }
pub fn close_items( pub fn close_items(
&mut self, &self,
window: &mut Window, window: &mut Window,
cx: &mut Context<Pane>, cx: &mut Context<Pane>,
mut save_intent: SaveIntent, mut save_intent: SaveIntent,
@ -2383,14 +2393,32 @@ impl Pane {
let pane = pane.clone(); let pane = pane.clone();
let menu_context = menu_context.clone(); let menu_context = menu_context.clone();
ContextMenu::build(window, cx, move |mut menu, window, cx| { ContextMenu::build(window, cx, move |mut menu, window, cx| {
let close_active_item_action = CloseActiveItem {
save_intent: None,
close_pinned: true,
};
let close_inactive_items_action = CloseInactiveItems {
save_intent: None,
close_pinned: false,
};
let close_items_to_the_left_action = CloseItemsToTheLeft {
close_pinned: false,
};
let close_items_to_the_right_action = CloseItemsToTheRight {
close_pinned: false,
};
let close_clean_items_action = CloseCleanItems {
close_pinned: false,
};
let close_all_items_action = CloseAllItems {
save_intent: None,
close_pinned: false,
};
if let Some(pane) = pane.upgrade() { if let Some(pane) = pane.upgrade() {
menu = menu menu = menu
.entry( .entry(
"Close", "Close",
Some(Box::new(CloseActiveItem { Some(Box::new(close_active_item_action)),
save_intent: None,
close_pinned: true,
})),
window.handler_for(&pane, move |pane, window, cx| { window.handler_for(&pane, move |pane, window, cx| {
pane.close_item_by_id(item_id, SaveIntent::Close, window, cx) pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
@ -2398,34 +2426,27 @@ impl Pane {
) )
.item(ContextMenuItem::Entry( .item(ContextMenuItem::Entry(
ContextMenuEntry::new("Close Others") ContextMenuEntry::new("Close Others")
.action(Box::new(CloseInactiveItems { .action(Box::new(close_inactive_items_action.clone()))
save_intent: None,
close_pinned: false,
}))
.disabled(total_items == 1) .disabled(total_items == 1)
.handler(window.handler_for(&pane, move |pane, window, cx| { .handler(window.handler_for(&pane, move |pane, window, cx| {
let non_closeable_ids = pane.close_inactive_items(
pane.get_non_closeable_item_ids(false); &close_inactive_items_action,
pane.close_items(window, cx, SaveIntent::Close, |id| { window,
id != item_id && !non_closeable_ids.contains(&id) cx,
}) )
.detach_and_log_err(cx); .detach_and_log_err(cx);
})), })),
)) ))
.separator() .separator()
.item(ContextMenuItem::Entry( .item(ContextMenuItem::Entry(
ContextMenuEntry::new("Close Left") ContextMenuEntry::new("Close Left")
.action(Box::new(CloseItemsToTheLeft { .action(Box::new(close_items_to_the_left_action.clone()))
close_pinned: false,
}))
.disabled(!has_items_to_left) .disabled(!has_items_to_left)
.handler(window.handler_for(&pane, move |pane, window, cx| { .handler(window.handler_for(&pane, move |pane, window, cx| {
pane.close_items_to_the_left_by_id( pane.close_items_to_the_left_by_id(
item_id, item_id,
&CloseItemsToTheLeft { &close_items_to_the_left_action,
close_pinned: false, pane.pinned_item_ids(),
},
pane.get_non_closeable_item_ids(false),
window, window,
cx, cx,
) )
@ -2434,17 +2455,13 @@ impl Pane {
)) ))
.item(ContextMenuItem::Entry( .item(ContextMenuItem::Entry(
ContextMenuEntry::new("Close Right") ContextMenuEntry::new("Close Right")
.action(Box::new(CloseItemsToTheRight { .action(Box::new(close_items_to_the_right_action.clone()))
close_pinned: false,
}))
.disabled(!has_items_to_right) .disabled(!has_items_to_right)
.handler(window.handler_for(&pane, move |pane, window, cx| { .handler(window.handler_for(&pane, move |pane, window, cx| {
pane.close_items_to_the_right_by_id( pane.close_items_to_the_right_by_id(
item_id, item_id,
&CloseItemsToTheRight { &close_items_to_the_right_action,
close_pinned: false, pane.pinned_item_ids(),
},
pane.get_non_closeable_item_ids(false),
window, window,
cx, cx,
) )
@ -2454,38 +2471,18 @@ impl Pane {
.separator() .separator()
.entry( .entry(
"Close Clean", "Close Clean",
Some(Box::new(CloseCleanItems { Some(Box::new(close_clean_items_action.clone())),
close_pinned: false,
})),
window.handler_for(&pane, move |pane, window, cx| { window.handler_for(&pane, move |pane, window, cx| {
if let Some(task) = pane.close_clean_items( pane.close_clean_items(&close_clean_items_action, window, cx)
&CloseCleanItems { .detach_and_log_err(cx)
close_pinned: false,
},
window,
cx,
) {
task.detach_and_log_err(cx)
}
}), }),
) )
.entry( .entry(
"Close All", "Close All",
Some(Box::new(CloseAllItems { Some(Box::new(close_all_items_action.clone())),
save_intent: None, window.handler_for(&pane, move |pane, window, cx| {
close_pinned: false, pane.close_all_items(&close_all_items_action, window, cx)
})), .detach_and_log_err(cx)
window.handler_for(&pane, |pane, window, cx| {
if let Some(task) = pane.close_all_items(
&CloseAllItems {
save_intent: None,
close_pinned: false,
},
window,
cx,
) {
task.detach_and_log_err(cx)
}
}), }),
); );
@ -3087,16 +3084,44 @@ impl Pane {
self.display_nav_history_buttons = display; self.display_nav_history_buttons = display;
} }
fn get_non_closeable_item_ids(&self, close_pinned: bool) -> HashSet<EntityId> { fn pinned_item_ids(&self) -> HashSet<EntityId> {
if close_pinned {
return HashSet::from_iter([]);
}
self.items self.items
.iter() .iter()
.enumerate() .enumerate()
.filter(|(index, _item)| self.is_tab_pinned(*index)) .filter_map(|(index, item)| {
.map(|(_, item)| item.item_id()) if self.is_tab_pinned(index) {
return Some(item.item_id());
}
None
})
.collect()
}
fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
self.items()
.filter_map(|item| {
if !item.is_dirty(cx) {
return Some(item.item_id());
}
None
})
.collect()
}
fn to_the_left_item_ids(&self, item_id: EntityId) -> HashSet<EntityId> {
self.items()
.take_while(|item| item.item_id() != item_id)
.map(|item| item.item_id())
.collect()
}
fn to_the_right_item_ids(&self, item_id: EntityId) -> HashSet<EntityId> {
self.items()
.rev()
.take_while(|item| item.item_id() != item_id)
.map(|item| item.item_id())
.collect() .collect()
} }
@ -3305,16 +3330,14 @@ impl Render for Pane {
) )
.on_action( .on_action(
cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| { cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
if let Some(task) = pane.close_inactive_items(action, window, cx) { pane.close_inactive_items(action, window, cx)
task.detach_and_log_err(cx) .detach_and_log_err(cx);
}
}), }),
) )
.on_action( .on_action(
cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| { cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
if let Some(task) = pane.close_clean_items(action, window, cx) { pane.close_clean_items(action, window, cx)
task.detach_and_log_err(cx) .detach_and_log_err(cx)
}
}), }),
) )
.on_action(cx.listener( .on_action(cx.listener(
@ -3333,9 +3356,8 @@ impl Render for Pane {
)) ))
.on_action( .on_action(
cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| { cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
if let Some(task) = pane.close_all_items(action, window, cx) { pane.close_all_items(action, window, cx)
task.detach_and_log_err(cx) .detach_and_log_err(cx)
}
}), }),
) )
.on_action( .on_action(
@ -4413,7 +4435,6 @@ mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert_item_labels(&pane, ["A!", "B!", "E*"], cx); assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
@ -4445,7 +4466,6 @@ mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert_item_labels(&pane, ["A^", "C*^"], cx); assert_item_labels(&pane, ["A^", "C*^"], cx);
@ -4532,7 +4552,6 @@ mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert_item_labels(&pane, ["A*!"], cx); assert_item_labels(&pane, ["A*!"], cx);
@ -4549,7 +4568,6 @@ mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
@ -4569,18 +4587,16 @@ mod tests {
}); });
assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
let save = pane let save = pane.update_in(cx, |pane, window, cx| {
.update_in(cx, |pane, window, cx| { pane.close_all_items(
pane.close_all_items( &CloseAllItems {
&CloseAllItems { save_intent: None,
save_intent: None, close_pinned: false,
close_pinned: false, },
}, window,
window, cx,
cx, )
) });
})
.unwrap();
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.simulate_prompt_answer("Save all"); cx.simulate_prompt_answer("Save all");
@ -4591,18 +4607,16 @@ mod tests {
add_labeled_item(&pane, "B", true, cx); add_labeled_item(&pane, "B", true, cx);
add_labeled_item(&pane, "C", true, cx); add_labeled_item(&pane, "C", true, cx);
assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
let save = pane let save = pane.update_in(cx, |pane, window, cx| {
.update_in(cx, |pane, window, cx| { pane.close_all_items(
pane.close_all_items( &CloseAllItems {
&CloseAllItems { save_intent: None,
save_intent: None, close_pinned: false,
close_pinned: false, },
}, window,
window, cx,
cx, )
) });
})
.unwrap();
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.simulate_prompt_answer("Discard all"); cx.simulate_prompt_answer("Discard all");
@ -4642,7 +4656,6 @@ mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
@ -4681,7 +4694,6 @@ mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert_item_labels(&pane, [], cx); assert_item_labels(&pane, [], cx);

View file

@ -2658,7 +2658,7 @@ impl Workspace {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
if retain_active_pane { if retain_active_pane {
if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { let current_pane_close = current_pane.update(cx, |pane, cx| {
pane.close_inactive_items( pane.close_inactive_items(
&CloseInactiveItems { &CloseInactiveItems {
save_intent: None, save_intent: None,
@ -2667,9 +2667,9 @@ impl Workspace {
window, window,
cx, cx,
) )
}) { });
tasks.push(current_pane_close);
}; tasks.push(current_pane_close);
} }
for pane in self.panes() { for pane in self.panes() {
@ -2677,7 +2677,7 @@ impl Workspace {
continue; continue;
} }
if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| { let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
pane.close_all_items( pane.close_all_items(
&CloseAllItems { &CloseAllItems {
save_intent: Some(save_intent), save_intent: Some(save_intent),
@ -2686,9 +2686,9 @@ impl Workspace {
window, window,
cx, cx,
) )
}) { });
tasks.push(close_pane_items)
} tasks.push(close_pane_items)
} }
if tasks.is_empty() { if tasks.is_empty() {
@ -8082,7 +8082,7 @@ mod tests {
assert!(!msg.contains("4.txt")); assert!(!msg.contains("4.txt"));
cx.simulate_prompt_answer("Cancel"); cx.simulate_prompt_answer("Cancel");
close.await.unwrap(); close.await;
left_pane left_pane
.update_in(cx, |left_pane, window, cx| { .update_in(cx, |left_pane, window, cx| {
@ -8114,7 +8114,7 @@ mod tests {
cx.simulate_prompt_answer("Save all"); cx.simulate_prompt_answer("Save all");
cx.executor().run_until_parked(); cx.executor().run_until_parked();
close.await.unwrap(); close.await;
right_pane.read_with(cx, |pane, _| { right_pane.read_with(cx, |pane, _| {
assert_eq!(pane.items_len(), 0); assert_eq!(pane.items_len(), 0);
}); });
@ -9040,18 +9040,16 @@ mod tests {
"Should select the multi buffer in the pane" "Should select the multi buffer in the pane"
); );
}); });
let close_all_but_multi_buffer_task = pane let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
.update_in(cx, |pane, window, cx| { pane.close_inactive_items(
pane.close_inactive_items( &CloseInactiveItems {
&CloseInactiveItems { save_intent: Some(SaveIntent::Save),
save_intent: Some(SaveIntent::Save), close_pinned: true,
close_pinned: true, },
}, window,
window, cx,
cx, )
) });
})
.expect("should have inactive files to close");
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
assert!(!cx.has_pending_prompt()); assert!(!cx.has_pending_prompt());
close_all_but_multi_buffer_task close_all_but_multi_buffer_task