Display prompt when trying to save a conflicting file

Antonio Scandurra created

Change summary

gpui/src/app.rs                 | 35 +++++++++++++++++++++++++++
gpui/src/lib.rs                 |  2 
gpui/src/platform/mac/window.rs | 44 ++++++++++++++++++++++++++++++++++
gpui/src/platform/mod.rs        | 13 ++++++++++
gpui/src/platform/test.rs       |  2 +
zed/src/workspace.rs            | 42 +++++++++++++++++++++++++-------
6 files changed, 125 insertions(+), 13 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     elements::ElementBox,
     executor,
     keymap::{self, Keystroke},
-    platform::{self, WindowOptions},
+    platform::{self, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::{post_inc, timeout},
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
@@ -578,6 +578,31 @@ impl MutableAppContext {
         self.platform.set_menus(menus);
     }
 
+    pub fn prompt<F>(
+        &self,
+        window_id: usize,
+        level: PromptLevel,
+        msg: &str,
+        answers: &[&str],
+        done_fn: F,
+    ) where
+        F: 'static + FnOnce(usize, &mut MutableAppContext),
+    {
+        let app = self.weak_self.as_ref().unwrap().upgrade().unwrap();
+        let foreground = self.foreground.clone();
+        let (_, window) = &self.presenters_and_platform_windows[&window_id];
+        window.prompt(
+            level,
+            msg,
+            answers,
+            Box::new(move |answer| {
+                foreground
+                    .spawn(async move { (done_fn)(answer, &mut *app.borrow_mut()) })
+                    .detach();
+            }),
+        );
+    }
+
     pub fn prompt_for_paths<F>(&self, options: PathPromptOptions, done_fn: F)
     where
         F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),
@@ -1766,6 +1791,14 @@ impl<'a, T: View> ViewContext<'a, T> {
         &self.app.ctx.background
     }
 
+    pub fn prompt<F>(&self, level: PromptLevel, msg: &str, answers: &[&str], done_fn: F)
+    where
+        F: 'static + FnOnce(usize, &mut MutableAppContext),
+    {
+        self.app
+            .prompt(self.window_id, level, msg, answers, done_fn)
+    }
+
     pub fn prompt_for_paths<F>(&self, options: PathPromptOptions, done_fn: F)
     where
         F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),

gpui/src/lib.rs 🔗

@@ -24,7 +24,7 @@ pub mod color;
 pub mod json;
 pub mod keymap;
 mod platform;
-pub use platform::{Event, PathPromptOptions};
+pub use platform::{Event, PathPromptOptions, PromptLevel};
 pub use presenter::{
     AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext,
     SizeConstraint, Vector2FExt,

gpui/src/platform/mac/window.rs 🔗

@@ -5,6 +5,7 @@ use crate::{
     platform::{self, Event, WindowContext},
     Scene,
 };
+use block::ConcreteBlock;
 use cocoa::{
     appkit::{
         NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
@@ -26,7 +27,8 @@ use objc::{
 use pathfinder_geometry::vector::vec2f;
 use smol::Timer;
 use std::{
-    cell::RefCell,
+    cell::{Cell, RefCell},
+    convert::TryInto,
     ffi::c_void,
     mem, ptr,
     rc::{Rc, Weak},
@@ -272,6 +274,42 @@ impl platform::Window for Window {
     fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
         self.0.as_ref().borrow_mut().close_callback = Some(callback);
     }
+
+    fn prompt(
+        &self,
+        level: platform::PromptLevel,
+        msg: &str,
+        answers: &[&str],
+        done_fn: Box<dyn FnOnce(usize)>,
+    ) {
+        unsafe {
+            let alert: id = msg_send![class!(NSAlert), alloc];
+            let alert: id = msg_send![alert, init];
+            let alert_style = match level {
+                platform::PromptLevel::Info => 1,
+                platform::PromptLevel::Warning => 0,
+                platform::PromptLevel::Critical => 2,
+            };
+            let _: () = msg_send![alert, setAlertStyle: alert_style];
+            let _: () = msg_send![alert, setMessageText: ns_string(msg)];
+            for (ix, answer) in answers.into_iter().enumerate() {
+                let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
+                let _: () = msg_send![button, setTag: ix as NSInteger];
+            }
+            let done_fn = Cell::new(Some(done_fn));
+            let block = ConcreteBlock::new(move |answer: NSInteger| {
+                if let Some(done_fn) = done_fn.take() {
+                    (done_fn)(answer.try_into().unwrap());
+                }
+            });
+            let block = block.copy();
+            let _: () = msg_send![
+                alert,
+                beginSheetModalForWindow: self.0.borrow().native_window
+                completionHandler: block
+            ];
+        }
+    }
 }
 
 impl platform::WindowContext for Window {
@@ -515,3 +553,7 @@ async fn synthetic_drag(
         }
     }
 }
+
+unsafe fn ns_string(string: &str) -> id {
+    NSString::alloc(nil).init_str(string).autorelease()
+}

gpui/src/platform/mod.rs 🔗

@@ -71,6 +71,13 @@ pub trait Window: WindowContext {
     fn on_event(&mut self, callback: Box<dyn FnMut(Event)>);
     fn on_resize(&mut self, callback: Box<dyn FnMut(&mut dyn WindowContext)>);
     fn on_close(&mut self, callback: Box<dyn FnOnce()>);
+    fn prompt(
+        &self,
+        level: PromptLevel,
+        msg: &str,
+        answers: &[&str],
+        done_fn: Box<dyn FnOnce(usize)>,
+    );
 }
 
 pub trait WindowContext {
@@ -90,6 +97,12 @@ pub struct PathPromptOptions {
     pub multiple: bool,
 }
 
+pub enum PromptLevel {
+    Info,
+    Warning,
+    Critical,
+}
+
 pub trait FontSystem: Send + Sync {
     fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
     fn select_font(

gpui/src/platform/test.rs 🔗

@@ -163,6 +163,8 @@ impl super::Window for Window {
     fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
         self.close_handlers.push(callback);
     }
+
+    fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], _: Box<dyn FnOnce(usize)>) {}
 }
 
 pub(crate) fn platform() -> Platform {

zed/src/workspace.rs 🔗

@@ -9,8 +9,8 @@ use crate::{
 use futures_core::{future::LocalBoxFuture, Future};
 use gpui::{
     color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
-    ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions, View,
-    ViewContext, ViewHandle, WeakModelHandle,
+    ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions,
+    PromptLevel, View, ViewContext, ViewHandle, WeakModelHandle,
 };
 use log::error;
 pub use pane::*;
@@ -573,15 +573,37 @@ impl Workspace {
                     }
                 });
                 return;
-            }
+            } else if item.has_conflict(ctx.as_ref()) {
+                const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 
-            let task = item.save(None, ctx.as_mut());
-            ctx.spawn(task, |_, result, _| {
-                if let Err(e) = result {
-                    error!("failed to save item: {:?}, ", e);
-                }
-            })
-            .detach()
+                let handle = ctx.handle();
+                ctx.prompt(
+                    PromptLevel::Warning,
+                    CONFLICT_MESSAGE,
+                    &["Overwrite", "Cancel"],
+                    move |answer, ctx| {
+                        if answer == 0 {
+                            handle.update(ctx, move |_, ctx| {
+                                let task = item.save(None, ctx.as_mut());
+                                ctx.spawn(task, |_, result, _| {
+                                    if let Err(e) = result {
+                                        error!("failed to save item: {:?}, ", e);
+                                    }
+                                })
+                                .detach();
+                            });
+                        }
+                    },
+                );
+            } else {
+                let task = item.save(None, ctx.as_mut());
+                ctx.spawn(task, |_, result, _| {
+                    if let Err(e) = result {
+                        error!("failed to save item: {:?}, ", e);
+                    }
+                })
+                .detach();
+            }
         }
     }