pane: Apply max_tabs
change immediately (#32447)
Closes #32217 Follow up of https://github.com/zed-industries/zed/pull/32301, sorry about the messy rebase in the previous PR. Release Notes: - Fixed `max_tabs` setting not applying immediately when changed TODO: - [x] Fix the off-by-one bug (currently closing one more tab than the max_tabs setting) while perserving "+1 Tab Allowance" feature. - [x] Investigate Double Invocation of `settings_changed` - [x] Write test that: - Sets max_tabs to `n` - Opens `n` buffers - Changes max_tabs to `n-1` - Asserts we have exactly `n-1` buffers remaining --------- Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
This commit is contained in:
parent
c7ee635853
commit
d1ca6db756
1 changed files with 89 additions and 8 deletions
|
@ -32,6 +32,7 @@ use settings::{Settings, SettingsStore};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
cmp, fmt, mem,
|
cmp, fmt, mem,
|
||||||
|
num::NonZeroUsize,
|
||||||
ops::ControlFlow,
|
ops::ControlFlow,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
@ -323,6 +324,7 @@ pub struct Pane {
|
||||||
>,
|
>,
|
||||||
render_tab_bar: Rc<dyn Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement>,
|
render_tab_bar: Rc<dyn Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement>,
|
||||||
show_tab_bar_buttons: bool,
|
show_tab_bar_buttons: bool,
|
||||||
|
max_tabs: Option<NonZeroUsize>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
tab_bar_scroll_handle: ScrollHandle,
|
tab_bar_scroll_handle: ScrollHandle,
|
||||||
/// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
|
/// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
|
||||||
|
@ -425,11 +427,12 @@ impl Pane {
|
||||||
cx.on_focus(&focus_handle, window, Pane::focus_in),
|
cx.on_focus(&focus_handle, window, Pane::focus_in),
|
||||||
cx.on_focus_in(&focus_handle, window, Pane::focus_in),
|
cx.on_focus_in(&focus_handle, window, Pane::focus_in),
|
||||||
cx.on_focus_out(&focus_handle, window, Pane::focus_out),
|
cx.on_focus_out(&focus_handle, window, Pane::focus_out),
|
||||||
cx.observe_global::<SettingsStore>(Self::settings_changed),
|
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||||
cx.subscribe(&project, Self::project_events),
|
cx.subscribe(&project, Self::project_events),
|
||||||
];
|
];
|
||||||
|
|
||||||
let handle = cx.entity().downgrade();
|
let handle = cx.entity().downgrade();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
alternate_file_items: (None, None),
|
alternate_file_items: (None, None),
|
||||||
focus_handle,
|
focus_handle,
|
||||||
|
@ -440,6 +443,7 @@ impl Pane {
|
||||||
zoomed: false,
|
zoomed: false,
|
||||||
active_item_index: 0,
|
active_item_index: 0,
|
||||||
preview_item_id: None,
|
preview_item_id: None,
|
||||||
|
max_tabs: WorkspaceSettings::get_global(cx).max_tabs,
|
||||||
last_focus_handle_by_item: Default::default(),
|
last_focus_handle_by_item: Default::default(),
|
||||||
nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
|
nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
|
||||||
mode: NavigationMode::Normal,
|
mode: NavigationMode::Normal,
|
||||||
|
@ -620,17 +624,25 @@ impl Pane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings_changed(&mut self, cx: &mut Context<Self>) {
|
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let tab_bar_settings = TabBarSettings::get_global(cx);
|
let tab_bar_settings = TabBarSettings::get_global(cx);
|
||||||
|
let new_max_tabs = WorkspaceSettings::get_global(cx).max_tabs;
|
||||||
|
|
||||||
if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
|
if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
|
||||||
*display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons;
|
*display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons;
|
self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons;
|
||||||
|
|
||||||
if !PreviewTabsSettings::get_global(cx).enabled {
|
if !PreviewTabsSettings::get_global(cx).enabled {
|
||||||
self.preview_item_id = None;
|
self.preview_item_id = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if new_max_tabs != self.max_tabs {
|
||||||
|
self.max_tabs = new_max_tabs;
|
||||||
|
self.close_items_on_settings_change(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
self.update_diagnostics(cx);
|
self.update_diagnostics(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -924,7 +936,7 @@ impl Pane {
|
||||||
.any(|existing_item| existing_item.item_id() == item.item_id());
|
.any(|existing_item| existing_item.item_id() == item.item_id());
|
||||||
|
|
||||||
if !item_already_exists {
|
if !item_already_exists {
|
||||||
self.close_items_over_max_tabs(window, cx);
|
self.close_items_on_item_open(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.is_singleton(cx) {
|
if item.is_singleton(cx) {
|
||||||
|
@ -932,6 +944,7 @@ impl Pane {
|
||||||
let Some(project) = self.project.upgrade() else {
|
let Some(project) = self.project.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let project = project.read(cx);
|
let project = project.read(cx);
|
||||||
if let Some(project_path) = project.path_for_entry(entry_id, cx) {
|
if let Some(project_path) = project.path_for_entry(entry_id, cx) {
|
||||||
let abs_path = project.absolute_path(&project_path, cx);
|
let abs_path = project.absolute_path(&project_path, cx);
|
||||||
|
@ -1409,29 +1422,59 @@ impl Pane {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_items_over_max_tabs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn close_items_on_item_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
|
let target = self.max_tabs.map(|m| m.get());
|
||||||
|
let protect_active_item = false;
|
||||||
|
self.close_items_to_target_count(target, protect_active_item, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_items_on_settings_change(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let target = self.max_tabs.map(|m| m.get() + 1);
|
||||||
|
// The active item in this case is the settings.json file, which should be protected from being closed
|
||||||
|
let protect_active_item = true;
|
||||||
|
self.close_items_to_target_count(target, protect_active_item, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_items_to_target_count(
|
||||||
|
&mut self,
|
||||||
|
target_count: Option<usize>,
|
||||||
|
protect_active_item: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(target_count) = target_count else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reduce over the activation history to get every dirty items up to max_tabs
|
|
||||||
// count.
|
|
||||||
let mut index_list = Vec::new();
|
let mut index_list = Vec::new();
|
||||||
let mut items_len = self.items_len();
|
let mut items_len = self.items_len();
|
||||||
let mut indexes: HashMap<EntityId, usize> = HashMap::default();
|
let mut indexes: HashMap<EntityId, usize> = HashMap::default();
|
||||||
|
let active_ix = self.active_item_index();
|
||||||
|
|
||||||
for (index, item) in self.items.iter().enumerate() {
|
for (index, item) in self.items.iter().enumerate() {
|
||||||
indexes.insert(item.item_id(), index);
|
indexes.insert(item.item_id(), index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close least recently used items to reach target count.
|
||||||
|
// The target count is allowed to be exceeded, as we protect pinned
|
||||||
|
// items, dirty items, and sometimes, the active item.
|
||||||
for entry in self.activation_history.iter() {
|
for entry in self.activation_history.iter() {
|
||||||
if items_len < max_tabs {
|
if items_len < target_count {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(&index) = indexes.get(&entry.entity_id) else {
|
let Some(&index) = indexes.get(&entry.entity_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if protect_active_item && index == active_ix {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
|
if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.is_tab_pinned(index) {
|
if self.is_tab_pinned(index) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -3848,6 +3891,7 @@ mod tests {
|
||||||
for i in 0..7 {
|
for i in 0..7 {
|
||||||
add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
|
add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
set_max_tabs(cx, Some(5));
|
set_max_tabs(cx, Some(5));
|
||||||
add_labeled_item(&pane, "7", false, cx);
|
add_labeled_item(&pane, "7", false, cx);
|
||||||
// Remove items to respect the max tab cap.
|
// Remove items to respect the max tab cap.
|
||||||
|
@ -3884,6 +3928,43 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_reduce_max_tabs_closes_existing_items(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.clone(), window, cx));
|
||||||
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
|
add_labeled_item(&pane, "A", false, cx);
|
||||||
|
add_labeled_item(&pane, "B", false, cx);
|
||||||
|
let item_c = add_labeled_item(&pane, "C", false, cx);
|
||||||
|
let item_d = add_labeled_item(&pane, "D", false, cx);
|
||||||
|
add_labeled_item(&pane, "E", false, cx);
|
||||||
|
add_labeled_item(&pane, "Settings", false, cx);
|
||||||
|
assert_item_labels(&pane, ["A", "B", "C", "D", "E", "Settings*"], cx);
|
||||||
|
|
||||||
|
set_max_tabs(cx, Some(5));
|
||||||
|
assert_item_labels(&pane, ["B", "C", "D", "E", "Settings*"], cx);
|
||||||
|
|
||||||
|
set_max_tabs(cx, Some(4));
|
||||||
|
assert_item_labels(&pane, ["C", "D", "E", "Settings*"], cx);
|
||||||
|
|
||||||
|
pane.update_in(cx, |pane, window, cx| {
|
||||||
|
let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
|
||||||
|
pane.pin_tab_at(ix, window, cx);
|
||||||
|
|
||||||
|
let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
|
||||||
|
pane.pin_tab_at(ix, window, cx);
|
||||||
|
});
|
||||||
|
assert_item_labels(&pane, ["C!", "D!", "E", "Settings*"], cx);
|
||||||
|
|
||||||
|
set_max_tabs(cx, Some(2));
|
||||||
|
assert_item_labels(&pane, ["C!", "D!", "Settings*"], cx);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
|
async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue