@@ -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,
@@ -1,3 +1,5 @@
+use std::num::NonZeroUsize;
+
use anyhow::Result;
use collections::HashMap;
use gpui::AppContext;
@@ -20,6 +22,7 @@ pub struct WorkspaceSettings {
pub use_system_path_prompts: bool,
pub command_aliases: HashMap<String, String>,
pub show_user_picture: bool,
+ pub max_tabs: Option<NonZeroUsize>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -133,6 +136,11 @@ pub struct WorkspaceSettingsContent {
///
/// Default: true
pub show_user_picture: Option<bool>,
+ // Maximum open tabs in a pane. Will not close an unsaved
+ // tab. Set to `None` for unlimited tabs.
+ //
+ // Default: none
+ pub max_tabs: Option<NonZeroUsize>,
}
#[derive(Deserialize)]