Add "Save As" command

Max Brunsfeld created

Change summary

assets/keymaps/default.json       |  1 +
crates/workspace/src/workspace.rs | 16 +++++++++++++---
crates/zed/src/menus.rs           |  4 ++++
crates/zed/src/zed.rs             | 10 +++++-----
4 files changed, 23 insertions(+), 8 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -16,6 +16,7 @@
             "cmd-w": "pane::CloseActiveItem",
             "alt-cmd-w": "pane::CloseInactiveItems",
             "cmd-s": "workspace::Save",
+            "cmd-shift-S": "workspace::SaveAs",
             "cmd-=": "zed::IncreaseBufferFontSize",
             "cmd--": "zed::DecreaseBufferFontSize",
             "cmd-,": "zed::OpenSettings",

crates/workspace/src/workspace.rs 🔗

@@ -80,6 +80,7 @@ actions!(
         AddFolderToProject,
         Unfollow,
         Save,
+        SaveAs,
         ActivatePreviousPane,
         ActivateNextPane,
         FollowNextCollaborator,
@@ -150,7 +151,12 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     );
     cx.add_action(
         |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
-            workspace.save_active_item(cx).detach_and_log_err(cx);
+            workspace.save_active_item(false, cx).detach_and_log_err(cx);
+        },
+    );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
+            workspace.save_active_item(true, cx).detach_and_log_err(cx);
         },
     );
     cx.add_action(Workspace::toggle_sidebar_item);
@@ -1064,10 +1070,14 @@ impl Workspace {
         self.active_item(cx).and_then(|item| item.project_path(cx))
     }
 
-    pub fn save_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
+    pub fn save_active_item(
+        &mut self,
+        force_name_change: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
         let project = self.project.clone();
         if let Some(item) = self.active_item(cx) {
-            if item.can_save(cx) {
+            if !force_name_change && item.can_save(cx) {
                 if item.has_conflict(cx.as_ref()) {
                     const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 

crates/zed/src/menus.rs 🔗

@@ -50,6 +50,10 @@ pub fn menus() -> Vec<Menu<'static>> {
                     name: "Save",
                     action: Box::new(workspace::Save),
                 },
+                MenuItem::Action {
+                    name: "Save As…",
+                    action: Box::new(workspace::SaveAs),
+                },
                 MenuItem::Action {
                     name: "Close Editor",
                     action: Box::new(workspace::CloseActiveItem),

crates/zed/src/zed.rs 🔗

@@ -391,7 +391,7 @@ mod tests {
             assert!(editor.text(cx).is_empty());
         });
 
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
+        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
         app_state.fs.as_fake().insert_dir("/root").await;
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
         save_task.await.unwrap();
@@ -666,7 +666,7 @@ mod tests {
             .await;
         cx.read(|cx| assert!(editor.is_dirty(cx)));
 
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
+        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
         cx.simulate_prompt_answer(window_id, 0);
         save_task.await.unwrap();
         editor.read_with(cx, |editor, cx| {
@@ -707,7 +707,7 @@ mod tests {
         });
 
         // Save the buffer. This prompts for a filename.
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
+        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
         cx.simulate_new_path_selection(|parent_dir| {
             assert_eq!(parent_dir, Path::new("/root"));
             Some(parent_dir.join("the-new-name.rs"))
@@ -731,7 +731,7 @@ mod tests {
             editor.handle_input(&editor::Input(" there".into()), cx);
             assert_eq!(editor.is_dirty(cx.as_ref()), true);
         });
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
+        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
         save_task.await.unwrap();
         assert!(!cx.did_prompt_for_new_path());
         editor.read_with(cx, |editor, cx| {
@@ -793,7 +793,7 @@ mod tests {
         });
 
         // Save the buffer. This prompts for a filename.
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
+        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
         save_task.await.unwrap();
         // The buffer is not dirty anymore and the language is assigned based on the path.