Test prompting when saving while there's a conflict

Antonio Scandurra created

Change summary

gpui/src/app.rs                 | 17 ++++++++++
gpui/src/platform/mac/window.rs |  5 +++
gpui/src/platform/mod.rs        |  1 
gpui/src/platform/test.rs       | 10 +++++
zed/src/workspace.rs            | 55 ++++++++++++++++++++++++++++++++++
5 files changed, 86 insertions(+), 2 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -343,6 +343,23 @@ impl TestAppContext {
     pub fn did_prompt_for_new_path(&self) -> bool {
         self.1.as_ref().did_prompt_for_new_path()
     }
+
+    pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
+        let mut state = self.0.borrow_mut();
+        let (_, window) = state
+            .presenters_and_platform_windows
+            .get_mut(&window_id)
+            .unwrap();
+        let test_window = window
+            .as_any_mut()
+            .downcast_mut::<platform::test::Window>()
+            .unwrap();
+        let callback = test_window
+            .last_prompt
+            .take()
+            .expect("prompt was not called");
+        (callback)(answer);
+    }
 }
 
 impl UpdateModel for TestAppContext {

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

@@ -27,6 +27,7 @@ use objc::{
 use pathfinder_geometry::vector::vec2f;
 use smol::Timer;
 use std::{
+    any::Any,
     cell::{Cell, RefCell},
     convert::TryInto,
     ffi::c_void,
@@ -263,6 +264,10 @@ impl Drop for Window {
 }
 
 impl platform::Window for Window {
+    fn as_any_mut(&mut self) -> &mut dyn Any {
+        self
+    }
+
     fn on_event(&mut self, callback: Box<dyn FnMut(Event)>) {
         self.0.as_ref().borrow_mut().event_callback = Some(callback);
     }

gpui/src/platform/mod.rs 🔗

@@ -68,6 +68,7 @@ pub trait Dispatcher: Send + Sync {
 }
 
 pub trait Window: WindowContext {
+    fn as_any_mut(&mut self) -> &mut dyn Any;
     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()>);

gpui/src/platform/test.rs 🔗

@@ -24,6 +24,7 @@ pub struct Window {
     event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
     resize_handlers: Vec<Box<dyn FnMut(&mut dyn super::WindowContext)>>,
     close_handlers: Vec<Box<dyn FnOnce()>>,
+    pub(crate) last_prompt: RefCell<Option<Box<dyn FnOnce(usize)>>>,
 }
 
 impl Platform {
@@ -123,6 +124,7 @@ impl Window {
             close_handlers: Vec::new(),
             scale_factor: 1.0,
             current_scene: None,
+            last_prompt: RefCell::new(None),
         }
     }
 }
@@ -152,6 +154,10 @@ impl super::WindowContext for Window {
 }
 
 impl super::Window for Window {
+    fn as_any_mut(&mut self) -> &mut dyn Any {
+        self
+    }
+
     fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event)>) {
         self.event_handlers.push(callback);
     }
@@ -164,7 +170,9 @@ impl super::Window for Window {
         self.close_handlers.push(callback);
     }
 
-    fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], _: Box<dyn FnOnce(usize)>) {}
+    fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], f: Box<dyn FnOnce(usize)>) {
+        self.last_prompt.replace(Some(f));
+    }
 }
 
 pub(crate) fn platform() -> Platform {

zed/src/workspace.rs 🔗

@@ -761,7 +761,7 @@ mod tests {
     use crate::{editor::BufferView, settings, test::temp_tree};
     use gpui::App;
     use serde_json::json;
-    use std::collections::HashSet;
+    use std::{collections::HashSet, fs};
     use tempdir::TempDir;
 
     #[test]
@@ -1101,6 +1101,59 @@ mod tests {
         });
     }
 
+    #[test]
+    fn test_save_conflicting_item() {
+        App::test_async((), |mut app| async move {
+            let dir = temp_tree(json!({
+                "a.txt": "",
+            }));
+
+            let settings = settings::channel(&app.font_cache()).unwrap().1;
+            let (window_id, workspace) = app.add_window(|ctx| {
+                let mut workspace = Workspace::new(0, settings, ctx);
+                workspace.add_worktree(dir.path(), ctx);
+                workspace
+            });
+            let tree = app.read(|ctx| {
+                let mut trees = workspace.read(ctx).worktrees().iter();
+                trees.next().unwrap().clone()
+            });
+            tree.flush_fs_events(&app).await;
+
+            // Open a file within an existing worktree.
+            app.update(|ctx| {
+                workspace.update(ctx, |view, ctx| {
+                    view.open_paths(&[dir.path().join("a.txt")], ctx)
+                })
+            })
+            .await;
+            let editor = app.read(|ctx| {
+                let pane = workspace.read(ctx).active_pane().read(ctx);
+                let item = pane.active_item().unwrap();
+                item.to_any().downcast::<BufferView>().unwrap()
+            });
+
+            app.update(|ctx| {
+                editor.update(ctx, |editor, ctx| editor.insert(&"x".to_string(), ctx))
+            });
+            fs::write(dir.path().join("a.txt"), "changed").unwrap();
+            tree.flush_fs_events(&app).await;
+            app.read(|ctx| {
+                assert!(editor.is_dirty(ctx));
+                assert!(editor.has_conflict(ctx));
+            });
+
+            app.update(|ctx| workspace.update(ctx, |w, ctx| w.save_active_item(&(), ctx)));
+            app.simulate_prompt_answer(window_id, 0);
+            tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
+                .await;
+            app.read(|ctx| {
+                assert!(!editor.is_dirty(ctx));
+                assert!(!editor.has_conflict(ctx));
+            });
+        });
+    }
+
     #[test]
     fn test_pane_actions() {
         App::test_async((), |mut app| async move {