pane: Improve close active item to better handle pinned tabs (#23488)

Closes #22247

- [x] Do not close pinned tab on keyboard shortcuts like `ctrl+w` or
`alt+f4`
- [x] Close pinned tab on context menu action, menu bar action, or vim
bang
- [x] While closing pinned tab via shortcut (where it won't close),
instead activate any other non-pinned tab in same pane
- [x] Else, if any other pane contains non-pinned tab, activate that
- [x] Tests

Co-authored-by: uncenter <47499684+uncenter@users.noreply.github.com>

Release Notes:

- Pinned tab now stay open when using close shortcuts, auto focuses to
any other non-pinned tab instead.
This commit is contained in:
smit 2025-02-07 22:54:57 +05:30 committed by GitHub
parent f0565b4e2e
commit 44c6a54f95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 298 additions and 36 deletions

View file

@ -99,6 +99,8 @@ pub struct ActivateItem(pub usize);
#[serde(deny_unknown_fields)]
pub struct CloseActiveItem {
pub save_intent: Option<SaveIntent>,
#[serde(default)]
pub close_pinned: bool,
}
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
@ -1224,6 +1226,37 @@ impl Pane {
return None;
}
if self.is_tab_pinned(self.active_item_index) && !action.close_pinned {
// Activate any non-pinned tab in same pane
let non_pinned_tab_index = self
.items()
.enumerate()
.find(|(index, _item)| !self.is_tab_pinned(*index))
.map(|(index, _item)| index);
if let Some(index) = non_pinned_tab_index {
self.activate_item(index, false, false, window, cx);
return None;
}
// Activate any non-pinned tab in different pane
let current_pane = cx.entity();
self.workspace
.update(cx, |workspace, cx| {
let panes = workspace.center.panes();
let pane_with_unpinned_tab = panes.iter().find(|pane| {
if **pane == &current_pane {
return false;
}
pane.read(cx).has_unpinned_tabs()
});
if let Some(pane) = pane_with_unpinned_tab {
pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx));
}
})
.ok();
return None;
};
let active_item_id = self.items[self.active_item_index].item_id();
Some(self.close_item_by_id(
active_item_id,
@ -2105,6 +2138,24 @@ impl Pane {
self.pinned_tab_count != 0
}
fn has_unpinned_tabs(&self) -> bool {
self.pinned_tab_count < self.items.len()
}
fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.items.is_empty() {
return;
}
let Some(index) = self
.items()
.enumerate()
.find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
else {
return;
};
self.activate_item(index, true, true, window, cx);
}
fn render_tab(
&self,
ix: usize,
@ -2280,7 +2331,10 @@ impl Pane {
pane.unpin_tab_at(ix, window, cx);
}))
} else {
end_slot_action = &CloseActiveItem { save_intent: None };
end_slot_action = &CloseActiveItem {
save_intent: None,
close_pinned: false,
};
end_slot_tooltip_text = "Close Tab";
IconButton::new("close tab", IconName::Close)
.when(!always_show_close_button, |button| {
@ -2350,7 +2404,10 @@ impl Pane {
menu = menu
.entry(
"Close",
Some(Box::new(CloseActiveItem { save_intent: None })),
Some(Box::new(CloseActiveItem {
save_intent: None,
close_pinned: true,
})),
window.handler_for(&pane, move |pane, window, cx| {
pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
.detach_and_log_err(cx);
@ -2991,14 +3048,9 @@ impl Pane {
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
}
})
.enumerate()
.filter(|(index, _item)| self.is_tab_pinned(*index))
.map(|(_, item)| item.item_id())
.collect()
}
@ -3561,7 +3613,14 @@ mod tests {
pane.update_in(cx, |pane, window, cx| {
assert!(pane
.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false
},
window,
cx
)
.is_none())
});
}
@ -3902,7 +3961,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -3915,7 +3981,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -3923,7 +3996,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B*", "C"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -3931,7 +4011,14 @@ mod tests {
assert_item_labels(&pane, ["A", "C*"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -3967,7 +4054,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -3980,7 +4074,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -3988,7 +4089,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B", "C*"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -3996,7 +4104,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B*"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -4032,7 +4147,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -4045,7 +4167,14 @@ mod tests {
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -4058,7 +4187,14 @@ mod tests {
assert_item_labels(&pane, ["A*", "B", "C"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -4066,7 +4202,14 @@ mod tests {
assert_item_labels(&pane, ["B*", "C"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.unwrap()
.await
@ -4300,7 +4443,7 @@ mod tests {
let project = Project::test(fs, None, cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let item_a = add_labeled_item(&pane, "A", false, cx);
@ -4326,6 +4469,71 @@ mod tests {
assert_item_labels(&pane, [], cx);
}
#[gpui::test]
async fn test_close_pinned_tab_with_non_pinned_in_same_pane(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(|window, cx| Workspace::test_new(project, window, cx));
// Non-pinned tabs in same pane
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
add_labeled_item(&pane, "A", false, cx);
add_labeled_item(&pane, "B", false, cx);
add_labeled_item(&pane, "C", false, cx);
pane.update_in(cx, |pane, window, cx| {
pane.pin_tab_at(0, window, cx);
});
set_labeled_items(&pane, ["A*", "B", "C"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
);
});
// Non-pinned tab should be active
assert_item_labels(&pane, ["A", "B*", "C"], cx);
}
#[gpui::test]
async fn test_close_pinned_tab_with_non_pinned_in_different_pane(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(|window, cx| Workspace::test_new(project, window, cx));
// No non-pinned tabs in same pane, non-pinned tabs in another pane
let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let pane2 = workspace.update_in(cx, |workspace, window, cx| {
workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
});
add_labeled_item(&pane1, "A", false, cx);
pane1.update_in(cx, |pane, window, cx| {
pane.pin_tab_at(0, window, cx);
});
set_labeled_items(&pane1, ["A*"], cx);
add_labeled_item(&pane2, "B", false, cx);
set_labeled_items(&pane2, ["B"], cx);
pane1.update_in(cx, |pane, window, cx| {
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
);
});
// Non-pinned tab of other pane should be active
assert_item_labels(&pane2, ["B*"], cx);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View file

@ -8175,6 +8175,7 @@ mod tests {
pane.close_active_item(
&CloseActiveItem {
save_intent: Some(SaveIntent::Close),
close_pinned: false,
},
window,
cx,
@ -8279,7 +8280,14 @@ mod tests {
});
let close_singleton_buffer_task = pane
.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.expect("should have active singleton buffer to close");
cx.background_executor.run_until_parked();
@ -8385,7 +8393,14 @@ mod tests {
});
let _close_multi_buffer_task = pane
.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.expect("should have active multi buffer to close");
cx.background_executor.run_until_parked();
@ -8476,7 +8491,14 @@ mod tests {
});
let close_multi_buffer_task = pane
.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
pane.close_active_item(
&CloseActiveItem {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
})
.expect("should have active multi buffer to close");
cx.background_executor.run_until_parked();