Only allow save_as for pane if can_save_as is true (#25028)

Dylan and Conrad Irwin created

When saving an item, some logic is done to determine whether one can
save it. In the special case where the intent is to `SaveAs`, it was
previously allowed to proceed as long as the buffer was a singleton
(presumably since it only makes sense to provide a save path for a
single file). However, we need to _also_ check that this item can be
"saved as" at all.

For this, we resurrect the `ItemHandle`/`Item` trait method
`can_save_as`. We have given it the default implementation of returning
`false`, and then overridden this in the implementation for
`TerminalView`.

Closes #25023


Release Notes:

- Fixed crash when trying to save terminal buffer

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/editor/src/items.rs                |  4 +++
crates/terminal_view/src/terminal_view.rs |  4 +++
crates/workspace/src/item.rs              | 12 +++++++++
crates/workspace/src/pane.rs              | 31 ++++++++++++++----------
4 files changed, 38 insertions(+), 13 deletions(-)

Detailed changes

crates/editor/src/items.rs 🔗

@@ -706,6 +706,10 @@ impl Item for Editor {
         self.buffer.read(cx).is_singleton()
     }
 
+    fn can_save_as(&self, cx: &App) -> bool {
+        self.buffer.read(cx).is_singleton()
+    }
+
     fn clone_on_split(
         &self,
         _workspace_id: Option<WorkspaceId>,

crates/workspace/src/item.rs 🔗

@@ -276,6 +276,9 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
     fn can_save(&self, _cx: &App) -> bool {
         false
     }
+    fn can_save_as(&self, _: &App) -> bool {
+        false
+    }
     fn save(
         &mut self,
         _format: bool,
@@ -477,6 +480,7 @@ pub trait ItemHandle: 'static + Send {
     fn has_deleted_file(&self, cx: &App) -> bool;
     fn has_conflict(&self, cx: &App) -> bool;
     fn can_save(&self, cx: &App) -> bool;
+    fn can_save_as(&self, cx: &App) -> bool;
     fn save(
         &self,
         format: bool,
@@ -890,6 +894,10 @@ impl<T: Item> ItemHandle for Entity<T> {
         self.read(cx).can_save(cx)
     }
 
+    fn can_save_as(&self, cx: &App) -> bool {
+        self.read(cx).can_save_as(cx)
+    }
+
     fn save(
         &self,
         format: bool,
@@ -1468,6 +1476,10 @@ pub mod test {
                     .all(|item| item.read(cx).entry_id.is_some())
         }
 
+        fn can_save_as(&self, _cx: &App) -> bool {
+            self.is_singleton
+        }
+
         fn save(
             &mut self,
             _: bool,

crates/workspace/src/pane.rs 🔗

@@ -1794,18 +1794,23 @@ impl Pane {
             return Ok(true);
         };
 
-        let (mut has_conflict, mut is_dirty, mut can_save, is_singleton, has_deleted_file) = cx
-            .update(|_window, cx| {
-                (
-                    item.has_conflict(cx),
-                    item.is_dirty(cx),
-                    item.can_save(cx),
-                    item.is_singleton(cx),
-                    item.has_deleted_file(cx),
-                )
-            })?;
-
-        let can_save_as = is_singleton;
+        let (
+            mut has_conflict,
+            mut is_dirty,
+            mut can_save,
+            can_save_as,
+            is_singleton,
+            has_deleted_file,
+        ) = cx.update(|_window, cx| {
+            (
+                item.has_conflict(cx),
+                item.is_dirty(cx),
+                item.can_save(cx),
+                item.can_save_as(cx),
+                item.is_singleton(cx),
+                item.has_deleted_file(cx),
+            )
+        })?;
 
         // when saving a single buffer, we ignore whether or not it's dirty.
         if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
@@ -1939,7 +1944,7 @@ impl Pane {
                     item.save(should_format, project, window, cx)
                 })?
                 .await?;
-            } else if can_save_as {
+            } else if can_save_as && is_singleton {
                 let abs_path = pane.update_in(cx, |pane, window, cx| {
                     pane.activate_item(item_ix, true, true, window, cx);
                     pane.workspace.update(cx, |workspace, cx| {