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:
parent
0be7cf8ea8
commit
cd5d8b4173
3 changed files with 107 additions and 0 deletions
|
@ -552,6 +552,8 @@
|
||||||
// 4. Save when idle for a certain amount of time:
|
// 4. Save when idle for a certain amount of time:
|
||||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||||
"autosave": "off",
|
"autosave": "off",
|
||||||
|
// Maximum number of tabs per pane. Unset for unlimited.
|
||||||
|
"max_tabs": null,
|
||||||
// Settings related to the editor's tab bar.
|
// Settings related to the editor's tab bar.
|
||||||
"tab_bar": {
|
"tab_bar": {
|
||||||
// Whether or not to show the tab bar in the editor
|
// Whether or not to show the tab bar in the editor
|
||||||
|
|
|
@ -896,6 +896,8 @@ impl Pane {
|
||||||
destination_index: Option<usize>,
|
destination_index: Option<usize>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
self.close_items_over_max_tabs(cx);
|
||||||
|
|
||||||
if item.is_singleton(cx) {
|
if item.is_singleton(cx) {
|
||||||
if let Some(&entry_id) = item.project_entry_ids(cx).first() {
|
if let Some(&entry_id) = item.project_entry_ids(cx).first() {
|
||||||
let project = self.project.read(cx);
|
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(
|
pub(super) fn file_names_for_prompt(
|
||||||
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
|
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
|
||||||
all_dirty_items: usize,
|
all_dirty_items: usize,
|
||||||
|
@ -3282,6 +3321,8 @@ impl Render for DraggedTab {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::num::NonZero;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::item::test::{TestItem, TestProjectItem};
|
use crate::item::test::{TestItem, TestProjectItem};
|
||||||
use gpui::{TestAppContext, VisualTestContext};
|
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]
|
#[gpui::test]
|
||||||
async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
|
async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
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(
|
fn add_labeled_item(
|
||||||
pane: &View<Pane>,
|
pane: &View<Pane>,
|
||||||
label: &str,
|
label: &str,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
|
@ -20,6 +22,7 @@ pub struct WorkspaceSettings {
|
||||||
pub use_system_path_prompts: bool,
|
pub use_system_path_prompts: bool,
|
||||||
pub command_aliases: HashMap<String, String>,
|
pub command_aliases: HashMap<String, String>,
|
||||||
pub show_user_picture: bool,
|
pub show_user_picture: bool,
|
||||||
|
pub max_tabs: Option<NonZeroUsize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
@ -133,6 +136,11 @@ pub struct WorkspaceSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: true
|
/// Default: true
|
||||||
pub show_user_picture: Option<bool>,
|
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)]
|
#[derive(Deserialize)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue