Skip to content

Commit

Permalink
settings: Add max tabs option (#18933)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
BuonOmo authored Dec 14, 2024
1 parent 0be7cf8 commit cd5d8b4
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 0 deletions.
2 changes: 2 additions & 0 deletions assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,8 @@
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Maximum number of tabs per pane. Unset for unlimited.
"max_tabs": null,
// Settings related to the editor's tab bar.
"tab_bar": {
// Whether or not to show the tab bar in the editor
Expand Down
97 changes: 97 additions & 0 deletions crates/workspace/src/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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};
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions crates/workspace/src/workspace_settings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::num::NonZeroUsize;

use anyhow::Result;
use collections::HashMap;
use gpui::AppContext;
Expand All @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down

0 comments on commit cd5d8b4

Please sign in to comment.