Fix item closing overly triggering save dialogues (#21374)

Closes https://github.com/zed-industries/zed/issues/12029

Allows to introspect project items inside items more deeply, checking
them for being dirty.
For that:
* renames `project::Item` into `project::ProjectItem`
* adds an `is_dirty(&self) -> bool` method to the renamed trait
* changes the closing logic to only care about dirty project items when
checking for save prompts conditions
* save prompts are raised only if the item is singleton without a
project path; or if the item has dirty project items that are not open
elsewhere

Release Notes:

- Fixed item closing overly triggering save dialogues
This commit is contained in:
Kirill Bulatov 2024-12-01 01:48:31 +02:00 committed by GitHub
parent c2cd84a749
commit 28849dd2a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 600 additions and 85 deletions

View file

@ -1295,10 +1295,12 @@ impl Pane {
) -> Task<Result<()>> {
// Find the items to close.
let mut items_to_close = Vec::new();
let mut item_ids_to_close = HashSet::default();
let mut dirty_items = Vec::new();
for item in &self.items {
if should_close(item.item_id()) {
items_to_close.push(item.boxed_clone());
item_ids_to_close.insert(item.item_id());
if item.is_dirty(cx) {
dirty_items.push(item.boxed_clone());
}
@ -1339,16 +1341,23 @@ impl Pane {
}
}
let mut saved_project_items_ids = HashSet::default();
for item in items_to_close.clone() {
// Find the item's current index and its set of project item models. Avoid
for item_to_close in items_to_close {
// Find the item's current index and its set of dirty project item models. Avoid
// storing these in advance, in case they have changed since this task
// was started.
let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
(pane.index_for_item(&*item), item.project_item_model_ids(cx))
})?;
let item_ix = if let Some(ix) = item_ix {
ix
} else {
let mut dirty_project_item_ids = Vec::new();
let Some(item_ix) = pane.update(&mut cx, |pane, cx| {
item_to_close.for_each_project_item(
cx,
&mut |project_item_id, project_item| {
if project_item.is_dirty() {
dirty_project_item_ids.push(project_item_id);
}
},
);
pane.index_for_item(&*item_to_close)
})?
else {
continue;
};
@ -1356,27 +1365,34 @@ impl Pane {
// in the workspace, AND that the user has not already been prompted to save.
// If there are any such project entries, prompt the user to save this item.
let project = workspace.update(&mut cx, |workspace, cx| {
for item in workspace.items(cx) {
if !items_to_close
.iter()
.any(|item_to_close| item_to_close.item_id() == item.item_id())
{
let other_project_item_ids = item.project_item_model_ids(cx);
project_item_ids.retain(|id| !other_project_item_ids.contains(id));
for open_item in workspace.items(cx) {
let open_item_id = open_item.item_id();
if !item_ids_to_close.contains(&open_item_id) {
let other_project_item_ids = open_item.project_item_model_ids(cx);
dirty_project_item_ids
.retain(|id| !other_project_item_ids.contains(id));
}
}
workspace.project().clone()
})?;
let should_save = project_item_ids
let should_save = dirty_project_item_ids
.iter()
.any(|id| saved_project_items_ids.insert(*id));
.any(|id| saved_project_items_ids.insert(*id))
// Always propose to save singleton files without any project paths: those cannot be saved via multibuffer, as require a file path selection modal.
|| cx
.update(|cx| {
item_to_close.is_dirty(cx)
&& item_to_close.is_singleton(cx)
&& item_to_close.project_path(cx).is_none()
})
.unwrap_or(false);
if should_save
&& !Self::save_item(
project.clone(),
&pane,
item_ix,
&*item,
&*item_to_close,
save_intent,
&mut cx,
)
@ -1390,7 +1406,7 @@ impl Pane {
if let Some(item_ix) = pane
.items
.iter()
.position(|i| i.item_id() == item.item_id())
.position(|i| i.item_id() == item_to_close.item_id())
{
pane.remove_item(item_ix, false, true, cx);
}
@ -3725,9 +3741,18 @@ mod tests {
assert_item_labels(&pane, [], cx);
add_labeled_item(&pane, "A", true, cx);
add_labeled_item(&pane, "B", true, cx);
add_labeled_item(&pane, "C", true, cx);
add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
item.project_items
.push(TestProjectItem::new(1, "A.txt", cx))
});
add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
item.project_items
.push(TestProjectItem::new(2, "B.txt", cx))
});
add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
item.project_items
.push(TestProjectItem::new(3, "C.txt", cx))
});
assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
let save = pane
@ -3746,6 +3771,30 @@ mod tests {
cx.simulate_prompt_answer(2);
save.await.unwrap();
assert_item_labels(&pane, [], cx);
add_labeled_item(&pane, "A", true, cx);
add_labeled_item(&pane, "B", true, cx);
add_labeled_item(&pane, "C", true, cx);
assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
let save = pane
.update(cx, |pane, cx| {
pane.close_all_items(
&CloseAllItems {
save_intent: None,
close_pinned: false,
},
cx,
)
})
.unwrap();
cx.executor().run_until_parked();
cx.simulate_prompt_answer(2);
cx.executor().run_until_parked();
cx.simulate_prompt_answer(2);
cx.executor().run_until_parked();
save.await.unwrap();
assert_item_labels(&pane, ["A*^", "B^", "C^"], cx);
}
#[gpui::test]
@ -3833,14 +3882,14 @@ mod tests {
}
// Assert the item label, with the active item label suffixed with a '*'
#[track_caller]
fn assert_item_labels<const COUNT: usize>(
pane: &View<Pane>,
expected_states: [&str; COUNT],
cx: &mut VisualTestContext,
) {
pane.update(cx, |pane, cx| {
let actual_states = pane
.items
let actual_states = pane.update(cx, |pane, cx| {
pane.items
.iter()
.enumerate()
.map(|(ix, item)| {
@ -3859,12 +3908,11 @@ mod tests {
}
state
})
.collect::<Vec<_>>();
assert_eq!(
actual_states, expected_states,
"pane items do not match expectation"
);
})
.collect::<Vec<_>>()
});
assert_eq!(
actual_states, expected_states,
"pane items do not match expectation"
);
}
}