vim: Add ZZ and ZQ

The major change here is a refactoring to allow controling the save
behaviour when closing items, which is pre-work needed for vim command
palette.

For zed-industries/community#1868
This commit is contained in:
Conrad Irwin 2023-09-08 13:55:13 -06:00
parent 5d782b6cf0
commit ba1c350dad
9 changed files with 258 additions and 89 deletions

View file

@ -198,6 +198,18 @@
"z c": "editor::Fold", "z c": "editor::Fold",
"z o": "editor::UnfoldLines", "z o": "editor::UnfoldLines",
"z f": "editor::FoldSelectedRanges", "z f": "editor::FoldSelectedRanges",
"shift-z shift-q": [
"pane::CloseActiveItem",
{
"saveBehavior": "dontSave"
}
],
"shift-z shift-z": [
"pane::CloseActiveItem",
{
"saveBehavior": "promptOnConflict"
}
],
// Count support // Count support
"1": [ "1": [
"vim::Number", "vim::Number",

View file

@ -1530,7 +1530,9 @@ async fn test_host_disconnect(
// Ensure client B is not prompted to save edits when closing window after disconnecting. // Ensure client B is not prompted to save edits when closing window after disconnecting.
let can_close = workspace_b let can_close = workspace_b
.update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) .update(cx_b, |workspace, cx| {
workspace.prepare_to_close(true, workspace::SaveBehavior::PromptOnWrite, cx)
})
.await .await
.unwrap(); .unwrap();
assert!(can_close); assert!(can_close);

View file

@ -1528,8 +1528,13 @@ mod tests {
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
active_pane active_pane
.update(cx, |pane, cx| { .update(cx, |pane, cx| {
pane.close_active_item(&workspace::CloseActiveItem, cx) pane.close_active_item(
.unwrap() &workspace::CloseActiveItem {
save_behavior: None,
},
cx,
)
.unwrap()
}) })
.await .await
.unwrap(); .unwrap();

View file

@ -283,7 +283,12 @@ impl TerminalView {
pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) { pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
let menu_entries = vec![ let menu_entries = vec![
ContextMenuItem::action("Clear", Clear), ContextMenuItem::action("Clear", Clear),
ContextMenuItem::action("Close", pane::CloseActiveItem), ContextMenuItem::action(
"Close",
pane::CloseActiveItem {
save_behavior: None,
},
),
]; ];
self.context_menu.update(cx, |menu, cx| { self.context_menu.update(cx, |menu, cx| {

View file

@ -474,8 +474,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
for item_event in T::to_item_events(event).into_iter() { for item_event in T::to_item_events(event).into_iter() {
match item_event { match item_event {
ItemEvent::CloseItem => { ItemEvent::CloseItem => {
pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx)) pane.update(cx, |pane, cx| {
.detach_and_log_err(cx); pane.close_item_by_id(
item.id(),
crate::SaveBehavior::PromptOnWrite,
cx,
)
})
.detach_and_log_err(cx);
return; return;
} }

View file

@ -43,6 +43,19 @@ use std::{
}; };
use theme::{Theme, ThemeSettings}; use theme::{Theme, ThemeSettings};
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum SaveBehavior {
/// ask before overwriting conflicting files (used by default with %s)
PromptOnConflict,
/// ask before writing any file that wouldn't be auto-saved (used by default with %w)
PromptOnWrite,
/// never prompt, write on conflict (used with vim's :w!)
SilentlyOverwrite,
/// skip all save-related behaviour (used with vim's :cq)
DontSave,
}
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
pub struct ActivateItem(pub usize); pub struct ActivateItem(pub usize);
@ -64,13 +77,17 @@ pub struct CloseItemsToTheRightById {
pub pane: WeakViewHandle<Pane>, pub pane: WeakViewHandle<Pane>,
} }
#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
pub struct CloseActiveItem {
pub save_behavior: Option<SaveBehavior>,
}
actions!( actions!(
pane, pane,
[ [
ActivatePrevItem, ActivatePrevItem,
ActivateNextItem, ActivateNextItem,
ActivateLastItem, ActivateLastItem,
CloseActiveItem,
CloseInactiveItems, CloseInactiveItems,
CloseCleanItems, CloseCleanItems,
CloseItemsToTheLeft, CloseItemsToTheLeft,
@ -86,7 +103,7 @@ actions!(
] ]
); );
impl_actions!(pane, [ActivateItem]); impl_actions!(pane, [ActivateItem, CloseActiveItem]);
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
@ -696,22 +713,29 @@ impl Pane {
pub fn close_active_item( pub fn close_active_item(
&mut self, &mut self,
_: &CloseActiveItem, action: &CloseActiveItem,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
if self.items.is_empty() { if self.items.is_empty() {
return None; return None;
} }
let active_item_id = self.items[self.active_item_index].id(); let active_item_id = self.items[self.active_item_index].id();
Some(self.close_item_by_id(active_item_id, cx)) Some(self.close_item_by_id(
active_item_id,
action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
cx,
))
} }
pub fn close_item_by_id( pub fn close_item_by_id(
&mut self, &mut self,
item_id_to_close: usize, item_id_to_close: usize,
save_behavior: SaveBehavior,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
self.close_items(cx, move |view_id| view_id == item_id_to_close) self.close_items(cx, save_behavior, move |view_id| {
view_id == item_id_to_close
})
} }
pub fn close_inactive_items( pub fn close_inactive_items(
@ -724,7 +748,11 @@ impl Pane {
} }
let active_item_id = self.items[self.active_item_index].id(); let active_item_id = self.items[self.active_item_index].id();
Some(self.close_items(cx, move |item_id| item_id != active_item_id)) Some(
self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
item_id != active_item_id
}),
)
} }
pub fn close_clean_items( pub fn close_clean_items(
@ -737,7 +765,11 @@ impl Pane {
.filter(|item| !item.is_dirty(cx)) .filter(|item| !item.is_dirty(cx))
.map(|item| item.id()) .map(|item| item.id())
.collect(); .collect();
Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id))) Some(
self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
item_ids.contains(&item_id)
}),
)
} }
pub fn close_items_to_the_left( pub fn close_items_to_the_left(
@ -762,7 +794,9 @@ impl Pane {
.take_while(|item| item.id() != item_id) .take_while(|item| item.id() != item_id)
.map(|item| item.id()) .map(|item| item.id())
.collect(); .collect();
self.close_items(cx, move |item_id| item_ids.contains(&item_id)) self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
item_ids.contains(&item_id)
})
} }
pub fn close_items_to_the_right( pub fn close_items_to_the_right(
@ -788,7 +822,9 @@ impl Pane {
.take_while(|item| item.id() != item_id) .take_while(|item| item.id() != item_id)
.map(|item| item.id()) .map(|item| item.id())
.collect(); .collect();
self.close_items(cx, move |item_id| item_ids.contains(&item_id)) self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
item_ids.contains(&item_id)
})
} }
pub fn close_all_items( pub fn close_all_items(
@ -800,12 +836,13 @@ impl Pane {
return None; return None;
} }
Some(self.close_items(cx, move |_| true)) Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true))
} }
pub fn close_items( pub fn close_items(
&mut self, &mut self,
cx: &mut ViewContext<Pane>, cx: &mut ViewContext<Pane>,
save_behavior: SaveBehavior,
should_close: impl 'static + Fn(usize) -> bool, should_close: impl 'static + Fn(usize) -> bool,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
// Find the items to close. // Find the items to close.
@ -858,8 +895,15 @@ impl Pane {
.any(|id| saved_project_items_ids.insert(*id)); .any(|id| saved_project_items_ids.insert(*id));
if should_save if should_save
&& !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx) && !Self::save_item(
.await? project.clone(),
&pane,
item_ix,
&*item,
save_behavior,
&mut cx,
)
.await?
{ {
break; break;
} }
@ -954,13 +998,17 @@ impl Pane {
pane: &WeakViewHandle<Pane>, pane: &WeakViewHandle<Pane>,
item_ix: usize, item_ix: usize,
item: &dyn ItemHandle, item: &dyn ItemHandle,
should_prompt_for_save: bool, save_behavior: SaveBehavior,
cx: &mut AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<bool> { ) -> Result<bool> {
const CONFLICT_MESSAGE: &str = const CONFLICT_MESSAGE: &str =
"This file has changed on disk since you started editing it. Do you want to overwrite it?"; "This file has changed on disk since you started editing it. Do you want to overwrite it?";
const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?"; const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
if save_behavior == SaveBehavior::DontSave {
return Ok(true);
}
let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| { let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
( (
item.has_conflict(cx), item.has_conflict(cx),
@ -971,18 +1019,22 @@ impl Pane {
}); });
if has_conflict && can_save { if has_conflict && can_save {
let mut answer = pane.update(cx, |pane, cx| { if save_behavior == SaveBehavior::SilentlyOverwrite {
pane.activate_item(item_ix, true, true, cx); pane.update(cx, |_, cx| item.save(project, cx))?.await?;
cx.prompt( } else {
PromptLevel::Warning, let mut answer = pane.update(cx, |pane, cx| {
CONFLICT_MESSAGE, pane.activate_item(item_ix, true, true, cx);
&["Overwrite", "Discard", "Cancel"], cx.prompt(
) PromptLevel::Warning,
})?; CONFLICT_MESSAGE,
match answer.next().await { &["Overwrite", "Discard", "Cancel"],
Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, )
Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, })?;
_ => return Ok(false), match answer.next().await {
Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
_ => return Ok(false),
}
} }
} else if is_dirty && (can_save || is_singleton) { } else if is_dirty && (can_save || is_singleton) {
let will_autosave = cx.read(|cx| { let will_autosave = cx.read(|cx| {
@ -991,7 +1043,7 @@ impl Pane {
AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
) && Self::can_autosave_item(&*item, cx) ) && Self::can_autosave_item(&*item, cx)
}); });
let should_save = if should_prompt_for_save && !will_autosave { let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave {
let mut answer = pane.update(cx, |pane, cx| { let mut answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx); pane.activate_item(item_ix, true, true, cx);
cx.prompt( cx.prompt(
@ -1113,7 +1165,12 @@ impl Pane {
AnchorCorner::TopLeft, AnchorCorner::TopLeft,
if is_active_item { if is_active_item {
vec![ vec![
ContextMenuItem::action("Close Active Item", CloseActiveItem), ContextMenuItem::action(
"Close Active Item",
CloseActiveItem {
save_behavior: None,
},
),
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems), ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::action("Close Clean Items", CloseCleanItems), ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft), ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
@ -1128,8 +1185,12 @@ impl Pane {
move |cx| { move |cx| {
if let Some(pane) = pane.upgrade(cx) { if let Some(pane) = pane.upgrade(cx) {
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
pane.close_item_by_id(target_item_id, cx) pane.close_item_by_id(
.detach_and_log_err(cx); target_item_id,
SaveBehavior::PromptOnWrite,
cx,
)
.detach_and_log_err(cx);
}) })
} }
} }
@ -1278,7 +1339,12 @@ impl Pane {
.on_click(MouseButton::Middle, { .on_click(MouseButton::Middle, {
let item_id = item.id(); let item_id = item.id();
move |_, pane, cx| { move |_, pane, cx| {
pane.close_item_by_id(item_id, cx).detach_and_log_err(cx); pane.close_item_by_id(
item_id,
SaveBehavior::PromptOnWrite,
cx,
)
.detach_and_log_err(cx);
} }
}) })
.on_down( .on_down(
@ -1486,7 +1552,8 @@ impl Pane {
cx.window_context().defer(move |cx| { cx.window_context().defer(move |cx| {
if let Some(pane) = pane.upgrade(cx) { if let Some(pane) = pane.upgrade(cx) {
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
pane.close_item_by_id(item_id, cx).detach_and_log_err(cx); pane.close_item_by_id(item_id, SaveBehavior::PromptOnWrite, cx)
.detach_and_log_err(cx);
}); });
} }
}); });
@ -2089,7 +2156,14 @@ mod tests {
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
assert!(pane.close_active_item(&CloseActiveItem, cx).is_none()) assert!(pane
.close_active_item(
&CloseActiveItem {
save_behavior: None
},
cx
)
.is_none())
}); });
} }
@ -2339,31 +2413,59 @@ mod tests {
add_labeled_item(&pane, "1", false, cx); add_labeled_item(&pane, "1", false, cx);
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) pane.update(cx, |pane, cx| {
.unwrap() pane.close_active_item(
.await &CloseActiveItem {
.unwrap(); save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A", "B*", "C", "D"], cx); assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx)); pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) pane.update(cx, |pane, cx| {
.unwrap() pane.close_active_item(
.await &CloseActiveItem {
.unwrap(); save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A", "B*", "C"], cx); assert_item_labels(&pane, ["A", "B*", "C"], cx);
pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) pane.update(cx, |pane, cx| {
.unwrap() pane.close_active_item(
.await &CloseActiveItem {
.unwrap(); save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A", "C*"], cx); assert_item_labels(&pane, ["A", "C*"], cx);
pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) pane.update(cx, |pane, cx| {
.unwrap() pane.close_active_item(
.await &CloseActiveItem {
.unwrap(); save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A*"], cx); assert_item_labels(&pane, ["A*"], cx);
} }

View file

@ -1258,7 +1258,7 @@ impl Workspace {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
let window = cx.window(); let window = cx.window();
let prepare = self.prepare_to_close(false, cx); let prepare = self.prepare_to_close(false, SaveBehavior::PromptOnWrite, cx);
Some(cx.spawn(|_, mut cx| async move { Some(cx.spawn(|_, mut cx| async move {
if prepare.await? { if prepare.await? {
window.remove(&mut cx); window.remove(&mut cx);
@ -1270,6 +1270,7 @@ impl Workspace {
pub fn prepare_to_close( pub fn prepare_to_close(
&mut self, &mut self,
quitting: bool, quitting: bool,
save_behavior: SaveBehavior,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> { ) -> Task<Result<bool>> {
let active_call = self.active_call().cloned(); let active_call = self.active_call().cloned();
@ -1308,13 +1309,15 @@ impl Workspace {
} }
Ok(this Ok(this
.update(&mut cx, |this, cx| this.save_all_internal(true, cx))? .update(&mut cx, |this, cx| {
this.save_all_internal(save_behavior, cx)
})?
.await?) .await?)
}) })
} }
fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> { fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
let save_all = self.save_all_internal(false, cx); let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx);
Some(cx.foreground().spawn(async move { Some(cx.foreground().spawn(async move {
save_all.await?; save_all.await?;
Ok(()) Ok(())
@ -1323,7 +1326,7 @@ impl Workspace {
fn save_all_internal( fn save_all_internal(
&mut self, &mut self,
should_prompt_to_save: bool, save_behaviour: SaveBehavior,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> { ) -> Task<Result<bool>> {
if self.project.read(cx).is_read_only() { if self.project.read(cx).is_read_only() {
@ -1358,7 +1361,7 @@ impl Workspace {
&pane, &pane,
ix, ix,
&*item, &*item,
should_prompt_to_save, save_behaviour,
&mut cx, &mut cx,
) )
.await? .await?
@ -1404,7 +1407,7 @@ impl Workspace {
let close_task = if is_remote || has_worktree || has_dirty_items { let close_task = if is_remote || has_worktree || has_dirty_items {
None None
} else { } else {
Some(self.prepare_to_close(false, cx)) Some(self.prepare_to_close(false, SaveBehavior::PromptOnWrite, cx))
}; };
let app_state = self.app_state.clone(); let app_state = self.app_state.clone();
@ -4099,7 +4102,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
// If the user cancels any save prompt, then keep the app open. // If the user cancels any save prompt, then keep the app open.
for window in workspace_windows { for window in workspace_windows {
if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx) workspace.prepare_to_close(true, SaveBehavior::PromptOnWrite, cx)
}) { }) {
if !should_close.await? { if !should_close.await? {
return Ok(()); return Ok(());
@ -4289,7 +4292,9 @@ mod tests {
// When there are no dirty items, there's nothing to do. // When there are no dirty items, there's nothing to do.
let item1 = window.add_view(cx, |_| TestItem::new()); let item1 = window.add_view(cx, |_| TestItem::new());
workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); let task = workspace.update(cx, |w, cx| {
w.prepare_to_close(false, SaveBehavior::PromptOnWrite, cx)
});
assert!(task.await.unwrap()); assert!(task.await.unwrap());
// When there are dirty untitled items, prompt to save each one. If the user // When there are dirty untitled items, prompt to save each one. If the user
@ -4304,7 +4309,9 @@ mod tests {
w.add_item(Box::new(item2.clone()), cx); w.add_item(Box::new(item2.clone()), cx);
w.add_item(Box::new(item3.clone()), cx); w.add_item(Box::new(item3.clone()), cx);
}); });
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); let task = workspace.update(cx, |w, cx| {
w.prepare_to_close(false, SaveBehavior::PromptOnWrite, cx)
});
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
window.simulate_prompt_answer(2, cx); // cancel window.simulate_prompt_answer(2, cx); // cancel
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
@ -4358,7 +4365,9 @@ mod tests {
let item1_id = item1.id(); let item1_id = item1.id();
let item3_id = item3.id(); let item3_id = item3.id();
let item4_id = item4.id(); let item4_id = item4.id();
pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id)) pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| {
[item1_id, item3_id, item4_id].contains(&id)
})
}); });
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
@ -4493,7 +4502,9 @@ mod tests {
// once for project entry 0, and once for project entry 2. After those two // once for project entry 0, and once for project entry 2. After those two
// prompts, the task should complete. // prompts, the task should complete.
let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true)); let close = left_pane.update(cx, |pane, cx| {
pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true)
});
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
left_pane.read_with(cx, |pane, cx| { left_pane.read_with(cx, |pane, cx| {
assert_eq!( assert_eq!(
@ -4609,9 +4620,11 @@ mod tests {
item.is_dirty = true; item.is_dirty = true;
}); });
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)) pane.update(cx, |pane, cx| {
.await pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
.unwrap(); })
.await
.unwrap();
assert!(!window.has_pending_prompt(cx)); assert!(!window.has_pending_prompt(cx));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
@ -4630,8 +4643,9 @@ mod tests {
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
// Ensure autosave is prevented for deleted files also when closing the buffer. // Ensure autosave is prevented for deleted files also when closing the buffer.
let _close_items = let _close_items = pane.update(cx, |pane, cx| {
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)); pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
});
deterministic.run_until_parked(); deterministic.run_until_parked();
assert!(window.has_pending_prompt(cx)); assert!(window.has_pending_prompt(cx));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));

View file

@ -41,7 +41,12 @@ pub fn menus() -> Vec<Menu<'static>> {
MenuItem::action("Save", workspace::Save), MenuItem::action("Save", workspace::Save),
MenuItem::action("Save As…", workspace::SaveAs), MenuItem::action("Save As…", workspace::SaveAs),
MenuItem::action("Save All", workspace::SaveAll), MenuItem::action("Save All", workspace::SaveAll),
MenuItem::action("Close Editor", workspace::CloseActiveItem), MenuItem::action(
"Close Editor",
workspace::CloseActiveItem {
save_behavior: None,
},
),
MenuItem::action("Close Window", workspace::CloseWindow), MenuItem::action("Close Window", workspace::CloseWindow),
], ],
}, },

View file

@ -438,7 +438,7 @@ fn quit(_: &Quit, cx: &mut gpui::AppContext) {
// If the user cancels any save prompt, then keep the app open. // If the user cancels any save prompt, then keep the app open.
for window in workspace_windows { for window in workspace_windows {
if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx) workspace.prepare_to_close(true, workspace::SaveBehavior::PromptOnWrite, cx)
}) { }) {
if !should_close.await? { if !should_close.await? {
return Ok(()); return Ok(());
@ -733,7 +733,7 @@ mod tests {
use theme::{ThemeRegistry, ThemeSettings}; use theme::{ThemeRegistry, ThemeSettings};
use workspace::{ use workspace::{
item::{Item, ItemHandle}, item::{Item, ItemHandle},
open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle, open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle,
}; };
#[gpui::test] #[gpui::test]
@ -1495,7 +1495,12 @@ mod tests {
pane2_item.downcast::<Editor>().unwrap().downgrade() pane2_item.downcast::<Editor>().unwrap().downgrade()
}); });
cx.dispatch_action(window.into(), workspace::CloseActiveItem); cx.dispatch_action(
window.into(),
workspace::CloseActiveItem {
save_behavior: None,
},
);
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
workspace.read_with(cx, |workspace, _| { workspace.read_with(cx, |workspace, _| {
@ -1503,7 +1508,12 @@ mod tests {
assert_eq!(workspace.active_pane(), &pane_1); assert_eq!(workspace.active_pane(), &pane_1);
}); });
cx.dispatch_action(window.into(), workspace::CloseActiveItem); cx.dispatch_action(
window.into(),
workspace::CloseActiveItem {
save_behavior: None,
},
);
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
window.simulate_prompt_answer(1, cx); window.simulate_prompt_answer(1, cx);
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
@ -1661,7 +1671,7 @@ mod tests {
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
let editor3_id = editor3.id(); let editor3_id = editor3.id();
drop(editor3); drop(editor3);
pane.close_item_by_id(editor3_id, cx) pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -1696,7 +1706,7 @@ mod tests {
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
let editor2_id = editor2.id(); let editor2_id = editor2.id();
drop(editor2); drop(editor2);
pane.close_item_by_id(editor2_id, cx) pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -1852,24 +1862,32 @@ mod tests {
assert_eq!(active_path(&workspace, cx), Some(file4.clone())); assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
// Close all the pane items in some arbitrary order. // Close all the pane items in some arbitrary order.
pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx)) pane.update(cx, |pane, cx| {
.await pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx)
.unwrap(); })
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file4.clone())); assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx)) pane.update(cx, |pane, cx| {
.await pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx)
.unwrap(); })
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file3.clone())); assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx)) pane.update(cx, |pane, cx| {
.await pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx)
.unwrap(); })
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file3.clone())); assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx)) pane.update(cx, |pane, cx| {
.await pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx)
.unwrap(); })
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), None); assert_eq!(active_path(&workspace, cx), None);
// Reopen all the closed items, ensuring they are reopened in the same order // Reopen all the closed items, ensuring they are reopened in the same order