diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 178b8f9b69f4b1e43065bfb145d4968b0d482d9a..db3455598bcb36e41acbaaacf66d3a93186e5111 100644 --- a/gpui/src/app.rs +++ b/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( + &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(&self, options: PathPromptOptions, done_fn: F) where F: 'static + FnOnce(Option>, &mut MutableAppContext), @@ -1766,6 +1791,14 @@ impl<'a, T: View> ViewContext<'a, T> { &self.app.ctx.background } + pub fn prompt(&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(&self, options: PathPromptOptions, done_fn: F) where F: 'static + FnOnce(Option>, &mut MutableAppContext), diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index edf14bc65c8ae4bc1332ee7016685d5bd3e389d7..72e3c5a2f5f698a78ebc298c3467d4bca07d03d9 100644 --- a/gpui/src/lib.rs +++ b/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, diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index 0c81ca623c324051d2c12568f49f1ddd53060e51..2c6df244e1d69633a34f672661794005b9514a95 100644 --- a/gpui/src/platform/mac/window.rs +++ b/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) { self.0.as_ref().borrow_mut().close_callback = Some(callback); } + + fn prompt( + &self, + level: platform::PromptLevel, + msg: &str, + answers: &[&str], + done_fn: Box, + ) { + 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() +} diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index 2ad5ce42026daedaba801546bfe4f83bb8b1a803..f75351744957483f1990758a6592cfed4e1a9f9d 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -71,6 +71,13 @@ pub trait Window: WindowContext { fn on_event(&mut self, callback: Box); fn on_resize(&mut self, callback: Box); fn on_close(&mut self, callback: Box); + fn prompt( + &self, + level: PromptLevel, + msg: &str, + answers: &[&str], + done_fn: Box, + ); } 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>; fn select_font( diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 255484e2a6fe51899a7fd21851a30b3a0270559f..98c36b110f081d0cbb7f769cf8af16aa33a70da5 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -163,6 +163,8 @@ impl super::Window for Window { fn on_close(&mut self, callback: Box) { self.close_handlers.push(callback); } + + fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], _: Box) {} } pub(crate) fn platform() -> Platform { diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index b60cd63487df9d838ad4a8cf544cca29d2a586e0..cd299daf9cf9ad001262c32127b98ee7488b038b 100644 --- a/zed/src/workspace.rs +++ b/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(); + } } }