settings: Add max tabs option (#18933)

Add a `max_tabs` option to the settings that ensure no more than this
amount of tabs are open in a pane. If set to `null`, there is no limit.

Closes #4784

Release Notes:

- Added a `max_tabs` option to cap the maximum number of open tabs.
This commit is contained in:
Ulysse Buonomo 2024-12-14 05:32:55 +01:00 committed by GitHub
parent 0be7cf8ea8
commit cd5d8b4173
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 107 additions and 0 deletions

View file

@ -896,6 +896,8 @@ impl Pane {
destination_index: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.close_items_over_max_tabs(cx);
if item.is_singleton(cx) {
if let Some(&entry_id) = item.project_entry_ids(cx).first() {
let project = self.project.read(cx);
@ -1298,6 +1300,43 @@ impl Pane {
))
}
pub fn close_items_over_max_tabs(&mut self, cx: &mut ViewContext<Self>) {
let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
return;
};
// Reduce over the activation history to get every dirty items up to max_tabs
// count.
let mut index_list = Vec::new();
let mut items_len = self.items_len();
let mut indexes: HashMap<EntityId, usize> = HashMap::default();
for (index, item) in self.items.iter().enumerate() {
indexes.insert(item.item_id(), index);
}
for entry in self.activation_history.iter() {
if items_len < max_tabs {
break;
}
let Some(&index) = indexes.get(&entry.entity_id) else {
continue;
};
if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
continue;
}
index_list.push(index);
items_len -= 1;
}
// The sort and reverse is necessary since we remove items
// using their index position, hence removing from the end
// of the list first to avoid changing indexes.
index_list.sort_unstable();
index_list
.iter()
.rev()
.for_each(|&index| self._remove_item(index, false, false, None, cx));
}
pub(super) fn file_names_for_prompt(
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
all_dirty_items: usize,
@ -3282,6 +3321,8 @@ impl Render for DraggedTab {
#[cfg(test)]
mod tests {
use std::num::NonZero;
use super::*;
use crate::item::test::{TestItem, TestProjectItem};
use gpui::{TestAppContext, VisualTestContext};
@ -3305,6 +3346,54 @@ mod tests {
});
}
#[gpui::test]
async fn test_add_item_capped_to_max_tabs(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());
for i in 0..7 {
add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
}
set_max_tabs(cx, Some(5));
add_labeled_item(&pane, "7", false, cx);
// Remove items to respect the max tab cap.
assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
pane.update(cx, |pane, cx| {
pane.activate_item(0, false, false, cx);
});
add_labeled_item(&pane, "X", false, cx);
// Respect activation order.
assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
for i in 0..7 {
add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
}
// Keeps dirty items, even over max tab cap.
assert_item_labels(
&pane,
["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
cx,
);
set_max_tabs(cx, None);
for i in 0..7 {
add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
}
// No cap when max tabs is None.
assert_item_labels(
&pane,
[
"D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
"N5", "N6*",
],
cx,
);
}
#[gpui::test]
async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
init_test(cx);
@ -3984,6 +4073,14 @@ mod tests {
});
}
fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
});
});
}
fn add_labeled_item(
pane: &View<Pane>,
label: &str,