@@ -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<Pane>,
}
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+pub struct CloseActiveItem {
+ pub save_behavior: Option<SaveBehavior>,
+}
+
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<Self>,
) -> Option<Task<Result<()>>> {
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<Self>,
) -> 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(
@@ -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<Pane>,
+ save_behavior: SaveBehavior,
should_close: impl 'static + Fn(usize) -> bool,
) -> Task<Result<()>> {
// 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<Pane>,
item_ix: usize,
item: &dyn ItemHandle,
- should_prompt_for_save: bool,
+ save_behavior: SaveBehavior,
cx: &mut AsyncAppContext,
) -> Result<bool> {
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);
}
@@ -1308,13 +1308,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(SaveBehavior::PromptOnWrite, cx)
+ })?
.await?)
})
}
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 {
save_all.await?;
Ok(())
@@ -1323,7 +1325,7 @@ impl Workspace {
fn save_all_internal(
&mut self,
- should_prompt_to_save: bool,
+ save_behaviour: SaveBehavior,
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
if self.project.read(cx).is_read_only() {
@@ -1358,7 +1360,7 @@ impl Workspace {
&pane,
ix,
&*item,
- should_prompt_to_save,
+ save_behaviour,
&mut cx,
)
.await?
@@ -4358,7 +4360,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 +4497,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 +4615,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 +4638,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));
@@ -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::<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();
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