From ba1c350dad27cc73e0887c243cfc9c7ac2a7ce5b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 8 Sep 2023 13:55:13 -0600 Subject: [PATCH] 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 --- assets/keymaps/vim.json | 12 ++ crates/collab/src/tests/integration_tests.rs | 4 +- crates/file_finder/src/file_finder.rs | 9 +- crates/terminal_view/src/terminal_view.rs | 7 +- crates/workspace/src/item.rs | 10 +- crates/workspace/src/pane.rs | 198 ++++++++++++++----- crates/workspace/src/workspace.rs | 46 +++-- crates/zed/src/menus.rs | 7 +- crates/zed/src/zed.rs | 54 +++-- 9 files changed, 258 insertions(+), 89 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 45891adee6..b47907783e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -198,6 +198,18 @@ "z c": "editor::Fold", "z o": "editor::UnfoldLines", "z f": "editor::FoldSelectedRanges", + "shift-z shift-q": [ + "pane::CloseActiveItem", + { + "saveBehavior": "dontSave" + } + ], + "shift-z shift-z": [ + "pane::CloseActiveItem", + { + "saveBehavior": "promptOnConflict" + } + ], // Count support "1": [ "vim::Number", diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 8121b0ac91..405956e5db 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1530,7 +1530,9 @@ async fn test_host_disconnect( // Ensure client B is not prompted to save edits when closing window after disconnecting. 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 .unwrap(); assert!(can_close); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 523d6e8a5c..c2d8cc52b2 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1528,8 +1528,13 @@ mod tests { let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); active_pane .update(cx, |pane, cx| { - pane.close_active_item(&workspace::CloseActiveItem, cx) - .unwrap() + pane.close_active_item( + &workspace::CloseActiveItem { + save_behavior: None, + }, + cx, + ) + .unwrap() }) .await .unwrap(); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 104d181a7b..a12f9d3c3c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -283,7 +283,12 @@ impl TerminalView { pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext) { let menu_entries = vec![ ContextMenuItem::action("Clear", Clear), - ContextMenuItem::action("Close", pane::CloseActiveItem), + ContextMenuItem::action( + "Close", + pane::CloseActiveItem { + save_behavior: None, + }, + ), ]; self.context_menu.update(cx, |menu, cx| { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 4e24c831f4..ea747b3a36 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -474,8 +474,14 @@ impl ItemHandle for ViewHandle { for item_event in T::to_item_events(event).into_iter() { match item_event { ItemEvent::CloseItem => { - pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx)) - .detach_and_log_err(cx); + pane.update(cx, |pane, cx| { + pane.close_item_by_id( + item.id(), + crate::SaveBehavior::PromptOnWrite, + cx, + ) + }) + .detach_and_log_err(cx); return; } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fe3173ac9b..eb78e30e98 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -43,6 +43,19 @@ use std::{ }; 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)] pub struct ActivateItem(pub usize); @@ -64,13 +77,17 @@ pub struct CloseItemsToTheRightById { pub pane: WeakViewHandle, } +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +pub struct CloseActiveItem { + pub save_behavior: Option, +} + actions!( pane, [ ActivatePrevItem, ActivateNextItem, ActivateLastItem, - CloseActiveItem, CloseInactiveItems, CloseCleanItems, CloseItemsToTheLeft, @@ -86,7 +103,7 @@ actions!( ] ); -impl_actions!(pane, [ActivateItem]); +impl_actions!(pane, [ActivateItem, CloseActiveItem]); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; @@ -696,22 +713,29 @@ impl Pane { pub fn close_active_item( &mut self, - _: &CloseActiveItem, + action: &CloseActiveItem, cx: &mut ViewContext, ) -> Option>> { if self.items.is_empty() { return None; } 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( &mut self, item_id_to_close: usize, + save_behavior: SaveBehavior, cx: &mut ViewContext, ) -> Task> { - 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( @@ -724,7 +748,11 @@ impl Pane { } 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( @@ -737,7 +765,11 @@ impl Pane { .filter(|item| !item.is_dirty(cx)) .map(|item| item.id()) .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( @@ -762,7 +794,9 @@ impl Pane { .take_while(|item| item.id() != item_id) .map(|item| item.id()) .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( @@ -788,7 +822,9 @@ impl Pane { .take_while(|item| item.id() != item_id) .map(|item| item.id()) .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( @@ -800,12 +836,13 @@ impl Pane { return None; } - Some(self.close_items(cx, move |_| true)) + Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true)) } pub fn close_items( &mut self, cx: &mut ViewContext, + save_behavior: SaveBehavior, should_close: impl 'static + Fn(usize) -> bool, ) -> Task> { // Find the items to close. @@ -858,8 +895,15 @@ impl Pane { .any(|id| saved_project_items_ids.insert(*id)); if should_save - && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx) - .await? + && !Self::save_item( + project.clone(), + &pane, + item_ix, + &*item, + save_behavior, + &mut cx, + ) + .await? { break; } @@ -954,13 +998,17 @@ impl Pane { pane: &WeakViewHandle, item_ix: usize, item: &dyn ItemHandle, - should_prompt_for_save: bool, + save_behavior: SaveBehavior, cx: &mut AsyncAppContext, ) -> Result { const CONFLICT_MESSAGE: &str = "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?"; + if save_behavior == SaveBehavior::DontSave { + return Ok(true); + } + let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| { ( item.has_conflict(cx), @@ -971,18 +1019,22 @@ impl Pane { }); if has_conflict && can_save { - let mut answer = pane.update(cx, |pane, cx| { - pane.activate_item(item_ix, true, true, cx); - cx.prompt( - PromptLevel::Warning, - CONFLICT_MESSAGE, - &["Overwrite", "Discard", "Cancel"], - ) - })?; - 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), + if save_behavior == SaveBehavior::SilentlyOverwrite { + pane.update(cx, |_, cx| item.save(project, cx))?.await?; + } else { + let mut answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + cx.prompt( + PromptLevel::Warning, + CONFLICT_MESSAGE, + &["Overwrite", "Discard", "Cancel"], + ) + })?; + 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) { let will_autosave = cx.read(|cx| { @@ -991,7 +1043,7 @@ impl Pane { AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange ) && 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| { pane.activate_item(item_ix, true, true, cx); cx.prompt( @@ -1113,7 +1165,12 @@ impl Pane { AnchorCorner::TopLeft, if is_active_item { 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 Clean Items", CloseCleanItems), ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft), @@ -1128,8 +1185,12 @@ impl Pane { move |cx| { if let Some(pane) = pane.upgrade(cx) { pane.update(cx, |pane, cx| { - pane.close_item_by_id(target_item_id, cx) - .detach_and_log_err(cx); + pane.close_item_by_id( + target_item_id, + SaveBehavior::PromptOnWrite, + cx, + ) + .detach_and_log_err(cx); }) } } @@ -1278,7 +1339,12 @@ impl Pane { .on_click(MouseButton::Middle, { let item_id = item.id(); 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( @@ -1486,7 +1552,8 @@ impl Pane { cx.window_context().defer(move |cx| { if let Some(pane) = pane.upgrade(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()); 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); assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); - pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) - .unwrap() - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_active_item( + &CloseActiveItem { + save_behavior: None, + }, + cx, + ) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "B*", "C", "D"], cx); pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx)); assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) - .unwrap() - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_active_item( + &CloseActiveItem { + save_behavior: None, + }, + cx, + ) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "B*", "C"], cx); - pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) - .unwrap() - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_active_item( + &CloseActiveItem { + save_behavior: None, + }, + cx, + ) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "C*"], cx); - pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) - .unwrap() - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_active_item( + &CloseActiveItem { + save_behavior: None, + }, + cx, + ) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A*"], cx); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index be8148256d..abbed1fcf0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1258,7 +1258,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Option>> { 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 { if prepare.await? { window.remove(&mut cx); @@ -1270,6 +1270,7 @@ impl Workspace { pub fn prepare_to_close( &mut self, quitting: bool, + save_behavior: SaveBehavior, cx: &mut ViewContext, ) -> Task> { let active_call = self.active_call().cloned(); @@ -1308,13 +1309,15 @@ impl Workspace { } 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?) }) } fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext) -> Option>> { - 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 { save_all.await?; Ok(()) @@ -1323,7 +1326,7 @@ impl Workspace { fn save_all_internal( &mut self, - should_prompt_to_save: bool, + save_behaviour: SaveBehavior, cx: &mut ViewContext, ) -> Task> { if self.project.read(cx).is_read_only() { @@ -1358,7 +1361,7 @@ impl Workspace { &pane, ix, &*item, - should_prompt_to_save, + save_behaviour, &mut cx, ) .await? @@ -1404,7 +1407,7 @@ impl Workspace { let close_task = if is_remote || has_worktree || has_dirty_items { None } else { - Some(self.prepare_to_close(false, cx)) + Some(self.prepare_to_close(false, SaveBehavior::PromptOnWrite, cx)) }; 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. for window in workspace_windows { 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? { return Ok(()); @@ -4289,7 +4292,9 @@ mod tests { // When there are no dirty items, there's nothing to do. let item1 = window.add_view(cx, |_| TestItem::new()); 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()); // 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(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(); window.simulate_prompt_answer(2, cx); // cancel cx.foreground().run_until_parked(); @@ -4358,7 +4365,9 @@ mod tests { let item1_id = item1.id(); let item3_id = item3.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(); @@ -4493,7 +4502,9 @@ mod tests { // once for project entry 0, and once for project entry 2. After those two // 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(); left_pane.read_with(cx, |pane, cx| { assert_eq!( @@ -4609,9 +4620,11 @@ mod tests { item.is_dirty = true; }); - pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id) + }) + .await + .unwrap(); assert!(!window.has_pending_prompt(cx)); 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)); // Ensure autosave is prevented for deleted files also when closing the buffer. - let _close_items = - pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)); + let _close_items = pane.update(cx, |pane, cx| { + pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id) + }); deterministic.run_until_parked(); assert!(window.has_pending_prompt(cx)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 22a260b588..6b5f7b3a35 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -41,7 +41,12 @@ pub fn menus() -> Vec> { MenuItem::action("Save", workspace::Save), MenuItem::action("Save As…", workspace::SaveAs), 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), ], }, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 424bce60f2..c00bb7ee0d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -438,7 +438,7 @@ fn quit(_: &Quit, cx: &mut gpui::AppContext) { // If the user cancels any save prompt, then keep the app open. for window in workspace_windows { 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? { return Ok(()); @@ -733,7 +733,7 @@ mod tests { use theme::{ThemeRegistry, ThemeSettings}; use workspace::{ item::{Item, ItemHandle}, - open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle, + open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle, }; #[gpui::test] @@ -1495,7 +1495,12 @@ mod tests { pane2_item.downcast::().unwrap().downgrade() }); - cx.dispatch_action(window.into(), workspace::CloseActiveItem); + cx.dispatch_action( + window.into(), + workspace::CloseActiveItem { + save_behavior: None, + }, + ); cx.foreground().run_until_parked(); workspace.read_with(cx, |workspace, _| { @@ -1503,7 +1508,12 @@ mod tests { 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(); window.simulate_prompt_answer(1, cx); cx.foreground().run_until_parked(); @@ -1661,7 +1671,7 @@ mod tests { pane.update(cx, |pane, cx| { let editor3_id = editor3.id(); drop(editor3); - pane.close_item_by_id(editor3_id, cx) + pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx) }) .await .unwrap(); @@ -1696,7 +1706,7 @@ mod tests { pane.update(cx, |pane, cx| { let editor2_id = editor2.id(); drop(editor2); - pane.close_item_by_id(editor2_id, cx) + pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx) }) .await .unwrap(); @@ -1852,24 +1862,32 @@ mod tests { assert_eq!(active_path(&workspace, cx), Some(file4.clone())); // Close all the pane items in some arbitrary order. - pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx)) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx) + }) + .await + .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx)) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx) + }) + .await + .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx)) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx) + }) + .await + .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx)) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx) + }) + .await + .unwrap(); assert_eq!(active_path(&workspace, cx), None); // Reopen all the closed items, ensuring they are reopened in the same order