Exclude pinned tabs when closing items (#19593)

Closing multiple items will no longer closed pinned tabs.

Closes #19560

Release Notes:

- Fixed close actions closing pinned tabs.
This commit is contained in:
Axel Carlsson 2024-11-07 10:20:19 +01:00 committed by GitHub
parent b33ae888c0
commit 4f62ebe4be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 220 additions and 47 deletions

View file

@ -251,10 +251,10 @@
"ctrl-shift-pagedown": "pane::SwapItemRight", "ctrl-shift-pagedown": "pane::SwapItemRight",
"ctrl-w": "pane::CloseActiveItem", "ctrl-w": "pane::CloseActiveItem",
"ctrl-f4": "pane::CloseActiveItem", "ctrl-f4": "pane::CloseActiveItem",
"alt-ctrl-t": "pane::CloseInactiveItems", "alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes", "alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-k u": "pane::CloseCleanItems", "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"ctrl-k w": "pane::CloseAllItems", "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
"ctrl-shift-f": "project_search::ToggleFocus", "ctrl-shift-f": "project_search::ToggleFocus",
"ctrl-alt-g": "search::SelectNextMatch", "ctrl-alt-g": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch", "ctrl-alt-shift-g": "search::SelectPrevMatch",

View file

@ -291,10 +291,10 @@
"ctrl-shift-pageup": "pane::SwapItemLeft", "ctrl-shift-pageup": "pane::SwapItemLeft",
"ctrl-shift-pagedown": "pane::SwapItemRight", "ctrl-shift-pagedown": "pane::SwapItemRight",
"cmd-w": "pane::CloseActiveItem", "cmd-w": "pane::CloseActiveItem",
"alt-cmd-t": "pane::CloseInactiveItems", "alt-cmd-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
"cmd-k u": "pane::CloseCleanItems", "cmd-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"cmd-k cmd-w": "pane::CloseAllItems", "cmd-k cmd-w": ["pane::CloseAllItems", { "close_pinned": false }],
"cmd-f": "project_search::ToggleFocus", "cmd-f": "project_search::ToggleFocus",
"cmd-g": "search::SelectNextMatch", "cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch", "cmd-shift-g": "search::SelectPrevMatch",

View file

@ -628,10 +628,12 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
("tabo", "nly"), ("tabo", "nly"),
workspace::CloseInactiveItems { workspace::CloseInactiveItems {
save_intent: Some(SaveIntent::Close), save_intent: Some(SaveIntent::Close),
close_pinned: false,
}, },
) )
.bang(workspace::CloseInactiveItems { .bang(workspace::CloseInactiveItems {
save_intent: Some(SaveIntent::Skip), save_intent: Some(SaveIntent::Skip),
close_pinned: false,
}), }),
VimCommand::new( VimCommand::new(
("on", "ly"), ("on", "ly"),

View file

@ -105,12 +105,37 @@ pub struct CloseActiveItem {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CloseInactiveItems { pub struct CloseInactiveItems {
pub save_intent: Option<SaveIntent>, pub save_intent: Option<SaveIntent>,
#[serde(default)]
pub close_pinned: bool,
} }
#[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CloseAllItems { pub struct CloseAllItems {
pub save_intent: Option<SaveIntent>, pub save_intent: Option<SaveIntent>,
#[serde(default)]
pub close_pinned: bool,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CloseCleanItems {
#[serde(default)]
pub close_pinned: bool,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CloseItemsToTheRight {
#[serde(default)]
pub close_pinned: bool,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CloseItemsToTheLeft {
#[serde(default)]
pub close_pinned: bool,
} }
#[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
@ -130,6 +155,9 @@ impl_actions!(
[ [
CloseAllItems, CloseAllItems,
CloseActiveItem, CloseActiveItem,
CloseCleanItems,
CloseItemsToTheLeft,
CloseItemsToTheRight,
CloseInactiveItems, CloseInactiveItems,
ActivateItem, ActivateItem,
RevealInProjectPanel, RevealInProjectPanel,
@ -144,9 +172,6 @@ actions!(
ActivateNextItem, ActivateNextItem,
ActivateLastItem, ActivateLastItem,
AlternateFile, AlternateFile,
CloseCleanItems,
CloseItemsToTheLeft,
CloseItemsToTheRight,
GoBack, GoBack,
GoForward, GoForward,
JoinIntoNext, JoinIntoNext,
@ -1120,16 +1145,17 @@ impl Pane {
} }
let active_item_id = self.items[self.active_item_index].item_id(); 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);
Some(self.close_items( Some(self.close_items(
cx, cx,
action.save_intent.unwrap_or(SaveIntent::Close), action.save_intent.unwrap_or(SaveIntent::Close),
move |item_id| item_id != active_item_id, move |item_id| item_id != active_item_id && !non_closeable_items.contains(&item_id),
)) ))
} }
pub fn close_clean_items( pub fn close_clean_items(
&mut self, &mut self,
_: &CloseCleanItems, action: &CloseCleanItems,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
let item_ids: Vec<_> = self let item_ids: Vec<_> = self
@ -1137,26 +1163,29 @@ impl Pane {
.filter(|item| !item.is_dirty(cx)) .filter(|item| !item.is_dirty(cx))
.map(|item| item.item_id()) .map(|item| item.item_id())
.collect(); .collect();
let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
Some(self.close_items(cx, SaveIntent::Close, move |item_id| { Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id) item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
})) }))
} }
pub fn close_items_to_the_left( pub fn close_items_to_the_left(
&mut self, &mut self,
_: &CloseItemsToTheLeft, action: &CloseItemsToTheLeft,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
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.items[self.active_item_index].item_id();
Some(self.close_items_to_the_left_by_id(active_item_id, cx)) let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
Some(self.close_items_to_the_left_by_id(active_item_id, non_closeable_items, cx))
} }
pub fn close_items_to_the_left_by_id( pub fn close_items_to_the_left_by_id(
&mut self, &mut self,
item_id: EntityId, item_id: EntityId,
non_closeable_items: Vec<EntityId>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let item_ids: Vec<_> = self let item_ids: Vec<_> = self
@ -1165,25 +1194,27 @@ impl Pane {
.map(|item| item.item_id()) .map(|item| item.item_id())
.collect(); .collect();
self.close_items(cx, SaveIntent::Close, move |item_id| { self.close_items(cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id) item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
}) })
} }
pub fn close_items_to_the_right( pub fn close_items_to_the_right(
&mut self, &mut self,
_: &CloseItemsToTheRight, action: &CloseItemsToTheRight,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
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.items[self.active_item_index].item_id();
Some(self.close_items_to_the_right_by_id(active_item_id, cx)) let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
Some(self.close_items_to_the_right_by_id(active_item_id, non_closeable_items, cx))
} }
pub fn close_items_to_the_right_by_id( pub fn close_items_to_the_right_by_id(
&mut self, &mut self,
item_id: EntityId, item_id: EntityId,
non_closeable_items: Vec<EntityId>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let item_ids: Vec<_> = self let item_ids: Vec<_> = self
@ -1193,7 +1224,7 @@ impl Pane {
.map(|item| item.item_id()) .map(|item| item.item_id())
.collect(); .collect();
self.close_items(cx, SaveIntent::Close, move |item_id| { self.close_items(cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id) item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
}) })
} }
@ -1206,11 +1237,12 @@ impl Pane {
return None; return None;
} }
Some( let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { Some(self.close_items(
true cx,
}), action.save_intent.unwrap_or(SaveIntent::Close),
) |item_id| !non_closeable_items.contains(&item_id),
))
} }
pub(super) fn file_names_for_prompt( pub(super) fn file_names_for_prompt(
@ -2023,7 +2055,10 @@ impl Pane {
) )
.entry( .entry(
"Close Others", "Close Others",
Some(Box::new(CloseInactiveItems { save_intent: None })), Some(Box::new(CloseInactiveItems {
save_intent: None,
close_pinned: false,
})),
cx.handler_for(&pane, move |pane, cx| { cx.handler_for(&pane, move |pane, cx| {
pane.close_items(cx, SaveIntent::Close, |id| id != item_id) pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
.detach_and_log_err(cx); .detach_and_log_err(cx);
@ -2032,37 +2067,63 @@ impl Pane {
.separator() .separator()
.entry( .entry(
"Close Left", "Close Left",
Some(Box::new(CloseItemsToTheLeft)), Some(Box::new(CloseItemsToTheLeft {
close_pinned: false,
})),
cx.handler_for(&pane, move |pane, cx| { cx.handler_for(&pane, move |pane, cx| {
pane.close_items_to_the_left_by_id(item_id, cx) pane.close_items_to_the_left_by_id(
.detach_and_log_err(cx); item_id,
pane.get_non_closeable_item_ids(false),
cx,
)
.detach_and_log_err(cx);
}), }),
) )
.entry( .entry(
"Close Right", "Close Right",
Some(Box::new(CloseItemsToTheRight)), Some(Box::new(CloseItemsToTheRight {
close_pinned: false,
})),
cx.handler_for(&pane, move |pane, cx| { cx.handler_for(&pane, move |pane, cx| {
pane.close_items_to_the_right_by_id(item_id, cx) pane.close_items_to_the_right_by_id(
.detach_and_log_err(cx); item_id,
pane.get_non_closeable_item_ids(false),
cx,
)
.detach_and_log_err(cx);
}), }),
) )
.separator() .separator()
.entry( .entry(
"Close Clean", "Close Clean",
Some(Box::new(CloseCleanItems)), Some(Box::new(CloseCleanItems {
close_pinned: false,
})),
cx.handler_for(&pane, move |pane, cx| { cx.handler_for(&pane, move |pane, cx| {
if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) { if let Some(task) = pane.close_clean_items(
&CloseCleanItems {
close_pinned: false,
},
cx,
) {
task.detach_and_log_err(cx) task.detach_and_log_err(cx)
} }
}), }),
) )
.entry( .entry(
"Close All", "Close All",
Some(Box::new(CloseAllItems { save_intent: None })), Some(Box::new(CloseAllItems {
save_intent: None,
close_pinned: false,
})),
cx.handler_for(&pane, |pane, cx| { cx.handler_for(&pane, |pane, cx| {
if let Some(task) = if let Some(task) = pane.close_all_items(
pane.close_all_items(&CloseAllItems { save_intent: None }, cx) &CloseAllItems {
{ save_intent: None,
close_pinned: false,
},
cx,
) {
task.detach_and_log_err(cx) task.detach_and_log_err(cx)
} }
}), }),
@ -2557,6 +2618,24 @@ impl Pane {
pub fn display_nav_history_buttons(&mut self, display: Option<bool>) { pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
self.display_nav_history_buttons = display; self.display_nav_history_buttons = display;
} }
fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
if close_pinned {
return vec![];
}
self.items
.iter()
.map(|item| item.item_id())
.filter(|item_id| {
if let Some(ix) = self.index_for_item_id(*item_id) {
self.is_tab_pinned(ix)
} else {
true
}
})
.collect()
}
} }
impl FocusableView for Pane { impl FocusableView for Pane {
@ -3442,7 +3521,13 @@ mod tests {
set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx) pane.close_inactive_items(
&CloseInactiveItems {
save_intent: None,
close_pinned: false,
},
cx,
)
}) })
.unwrap() .unwrap()
.await .await
@ -3466,10 +3551,17 @@ mod tests {
add_labeled_item(&pane, "E", false, cx); add_labeled_item(&pane, "E", false, cx);
assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) pane.update(cx, |pane, cx| {
.unwrap() pane.close_clean_items(
.await &CloseCleanItems {
.unwrap(); close_pinned: false,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A^", "C*^"], cx); assert_item_labels(&pane, ["A^", "C*^"], cx);
} }
@ -3485,7 +3577,12 @@ mod tests {
set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) pane.close_items_to_the_left(
&CloseItemsToTheLeft {
close_pinned: false,
},
cx,
)
}) })
.unwrap() .unwrap()
.await .await
@ -3505,7 +3602,12 @@ mod tests {
set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
pane.close_items_to_the_right(&CloseItemsToTheRight, cx) pane.close_items_to_the_right(
&CloseItemsToTheRight {
close_pinned: false,
},
cx,
)
}) })
.unwrap() .unwrap()
.await .await
@ -3522,17 +3624,42 @@ mod tests {
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
add_labeled_item(&pane, "A", false, cx); let item_a = add_labeled_item(&pane, "A", false, cx);
add_labeled_item(&pane, "B", false, cx); add_labeled_item(&pane, "B", false, cx);
add_labeled_item(&pane, "C", false, cx); add_labeled_item(&pane, "C", false, cx);
assert_item_labels(&pane, ["A", "B", "C*"], cx); assert_item_labels(&pane, ["A", "B", "C*"], cx);
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
pane.close_all_items(&CloseAllItems { save_intent: None }, cx) let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
pane.pin_tab_at(ix, cx);
pane.close_all_items(
&CloseAllItems {
save_intent: None,
close_pinned: false,
},
cx,
)
}) })
.unwrap() .unwrap()
.await .await
.unwrap(); .unwrap();
assert_item_labels(&pane, ["A*"], cx);
pane.update(cx, |pane, cx| {
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
pane.unpin_tab_at(ix, cx);
pane.close_all_items(
&CloseAllItems {
save_intent: None,
close_pinned: false,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, [], cx); assert_item_labels(&pane, [], cx);
add_labeled_item(&pane, "A", true, cx); add_labeled_item(&pane, "A", true, cx);
@ -3542,7 +3669,13 @@ mod tests {
let save = pane let save = pane
.update(cx, |pane, cx| { .update(cx, |pane, cx| {
pane.close_all_items(&CloseAllItems { save_intent: None }, cx) pane.close_all_items(
&CloseAllItems {
save_intent: None,
close_pinned: false,
},
cx,
)
}) })
.unwrap(); .unwrap();
@ -3552,6 +3685,37 @@ mod tests {
assert_item_labels(&pane, [], cx); assert_item_labels(&pane, [], cx);
} }
#[gpui::test]
async fn test_close_all_items_including_pinned(cx: &mut 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.clone(), cx));
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let item_a = add_labeled_item(&pane, "A", false, cx);
add_labeled_item(&pane, "B", false, cx);
add_labeled_item(&pane, "C", false, cx);
assert_item_labels(&pane, ["A", "B", "C*"], cx);
pane.update(cx, |pane, cx| {
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
pane.pin_tab_at(ix, cx);
pane.close_all_items(
&CloseAllItems {
save_intent: None,
close_pinned: true,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, [], cx);
}
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);

View file

@ -2219,7 +2219,13 @@ impl Workspace {
if retain_active_pane { if retain_active_pane {
if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx) pane.close_inactive_items(
&CloseInactiveItems {
save_intent: None,
close_pinned: false,
},
cx,
)
}) { }) {
tasks.push(current_pane_close); tasks.push(current_pane_close);
}; };
@ -2234,6 +2240,7 @@ impl Workspace {
pane.close_all_items( pane.close_all_items(
&CloseAllItems { &CloseAllItems {
save_intent: Some(save_intent), save_intent: Some(save_intent),
close_pinned: false,
}, },
cx, cx,
) )