From ca8ddcdeec4508b69a0fb29d7a6fa14960514da1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Jun 2022 10:59:50 +0200 Subject: [PATCH 1/6] Set window's `edited = true` when there are unsaved changes --- crates/gpui/src/app.rs | 7 +++++++ crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/window.rs | 11 +++++++++++ crates/gpui/src/platform/test.rs | 6 ++++++ crates/workspace/src/workspace.rs | 13 +++++++++++++ 5 files changed, 38 insertions(+) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 12338b75fbe0b8d08f512ae199bc810eedac838c..52e8e859ad64238ba9ada9415da7f472feadd7d5 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3339,6 +3339,13 @@ impl<'a, T: View> ViewContext<'a, T> { } } + pub fn set_window_edited(&mut self, edited: bool) { + let window_id = self.window_id(); + if let Some((_, window)) = self.presenters_and_platform_windows.get_mut(&window_id) { + window.set_edited(edited); + } + } + pub fn add_model(&mut self, build_model: F) -> ModelHandle where S: Entity, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 6d841f3962694317aec11d9b21f24faab4ab83ae..6c259537adcb23e48c9e4c4a6cff2c815e426a2f 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -97,6 +97,7 @@ pub trait Window: WindowContext { fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); fn set_title(&mut self, title: &str); + fn set_edited(&mut self, edited: bool); } pub trait WindowContext { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index ac5073d893b8343b65dc2f70c1c9e64659cc7704..e16bac5a0d7842cf52ddb9f38935e90a03b15172 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -397,6 +397,17 @@ impl platform::Window for Window { msg_send![app, changeWindowsItem:window title:title filename:false] } } + + fn set_edited(&mut self, edited: bool) { + unsafe { + let window = self.0.borrow().native_window; + msg_send![window, setDocumentEdited: edited as BOOL] + } + + // Changing the document edited state resets the traffic light position, + // so we have to move it again. + self.0.borrow().move_traffic_light(); + } } impl platform::WindowContext for Window { diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index b1b460ff70d96c894ba74583f52cf60efebbcd1c..fd07ef80a230105c81337c4abc4c67e583455f95 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -38,6 +38,7 @@ pub struct Window { resize_handlers: Vec>, close_handlers: Vec>, pub(crate) title: Option, + pub(crate) edited: bool, pub(crate) pending_prompts: RefCell>>, } @@ -191,6 +192,7 @@ impl Window { scale_factor: 1.0, current_scene: None, title: None, + edited: false, pending_prompts: Default::default(), } } @@ -258,6 +260,10 @@ impl super::Window for Window { fn set_title(&mut self, title: &str) { self.title = Some(title.to_string()) } + + fn set_edited(&mut self, edited: bool) { + self.edited = edited; + } } pub fn platform() -> Platform { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b1a4fbd293354f7934b529ebd49b59653cfd182a..55da35ca858ff95acbd5aa757e43375bc71d3343 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -731,6 +731,7 @@ pub struct Workspace { leader_state: LeaderState, follower_states_by_leader: FollowerStatesByLeader, last_leaders_by_pane: HashMap, PeerId>, + window_edited: bool, _observe_current_user: Task<()>, } @@ -846,6 +847,7 @@ impl Workspace { leader_state: Default::default(), follower_states_by_leader: Default::default(), last_leaders_by_pane: Default::default(), + window_edited: false, _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); @@ -1474,6 +1476,7 @@ impl Workspace { if pane == self.active_pane { self.active_item_path_changed(cx); } + self.update_window_edited(cx); } } } else { @@ -1778,6 +1781,16 @@ impl Workspace { cx.set_window_title(&title); } + fn update_window_edited(&mut self, cx: &mut ViewContext) { + let is_edited = self + .items(cx) + .any(|item| item.has_conflict(cx) || item.is_dirty(cx)); + if is_edited != self.window_edited { + self.window_edited = is_edited; + cx.set_window_edited(self.window_edited) + } + } + fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext) -> Vec { let mut collaborators = self .project From 06033d7fa9b14cf0defcd40b63c5f97cf21cf4e9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Jun 2022 11:43:19 +0200 Subject: [PATCH 2/6] Introduce `ViewContext::on_window_should_close` This is a new callback that can be used to interrupt closing the window when the user has unsaved changes. --- crates/gpui/src/app.rs | 45 ++++++++++++++++++++++++++ crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/window.rs | 23 +++++++++++++ crates/gpui/src/platform/test.rs | 12 +++++-- 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 52e8e859ad64238ba9ada9415da7f472feadd7d5..f5bfb390902ec1962f15963ad73b41b59022ab4c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -780,6 +780,7 @@ type GlobalObservationCallback = Box; type ReleaseObservationCallback = Box; type ActionObservationCallback = Box; type DeserializeActionCallback = fn(json: &str) -> anyhow::Result>; +type WindowShouldCloseSubscriptionCallback = Box bool>; pub struct MutableAppContext { weak_self: Option>>, @@ -2004,6 +2005,12 @@ impl MutableAppContext { Effect::ActionDispatchNotification { action_id } => { self.handle_action_dispatch_notification_effect(action_id) } + Effect::WindowShouldCloseSubscription { + window_id, + callback, + } => { + self.handle_window_should_close_subscription_effect(window_id, callback) + } } self.pending_notifications.clear(); self.remove_dropped_entities(); @@ -2451,6 +2458,17 @@ impl MutableAppContext { self.action_dispatch_observations.lock().extend(callbacks); } + fn handle_window_should_close_subscription_effect( + &mut self, + window_id: usize, + mut callback: WindowShouldCloseSubscriptionCallback, + ) { + let mut app = self.upgrade(); + if let Some((_, window)) = self.presenters_and_platform_windows.get_mut(&window_id) { + window.on_should_close(Box::new(move || app.update(|cx| callback(cx)))) + } + } + pub fn focus(&mut self, window_id: usize, view_id: Option) { if let Some(pending_focus_index) = self.pending_focus_index { self.pending_effects.remove(pending_focus_index); @@ -2828,6 +2846,10 @@ pub enum Effect { ActionDispatchNotification { action_id: TypeId, }, + WindowShouldCloseSubscription { + window_id: usize, + callback: WindowShouldCloseSubscriptionCallback, + }, } impl Debug for Effect { @@ -2921,6 +2943,10 @@ impl Debug for Effect { .field("is_active", is_active) .finish(), Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(), + Effect::WindowShouldCloseSubscription { window_id, .. } => f + .debug_struct("Effect::WindowShouldCloseSubscription") + .field("window_id", window_id) + .finish(), } } } @@ -3346,6 +3372,25 @@ impl<'a, T: View> ViewContext<'a, T> { } } + pub fn on_window_should_close(&mut self, mut callback: F) + where + F: 'static + FnMut(&mut T, &mut ViewContext) -> bool, + { + let window_id = self.window_id(); + let view = self.weak_handle(); + self.pending_effects + .push_back(Effect::WindowShouldCloseSubscription { + window_id, + callback: Box::new(move |cx| { + if let Some(view) = view.upgrade(cx) { + view.update(cx, |view, cx| callback(view, cx)) + } else { + true + } + }), + }); + } + pub fn add_model(&mut self, build_model: F) -> ModelHandle where S: Entity, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 6c259537adcb23e48c9e4c4a6cff2c815e426a2f..b4875df3f5708010172fc0447865fca7116faef5 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -93,6 +93,7 @@ pub trait Window: WindowContext { fn on_event(&mut self, callback: Box bool>); fn on_active_status_change(&mut self, callback: Box); fn on_resize(&mut self, callback: Box); + fn on_should_close(&mut self, callback: Box bool>); fn on_close(&mut self, callback: Box); fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index e16bac5a0d7842cf52ddb9f38935e90a03b15172..367a9d70c8f2956c00c2771eba843f2b7b7abf6b 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -81,6 +81,10 @@ unsafe fn build_classes() { sel!(windowDidResignKey:), window_did_change_key_status as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(windowShouldClose:), + window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, + ); decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel)); decl.register() }; @@ -167,6 +171,7 @@ struct WindowState { event_callback: Option bool>>, activate_callback: Option>, resize_callback: Option>, + should_close_callback: Option bool>>, close_callback: Option>, synthetic_drag_counter: usize, executor: Rc, @@ -242,6 +247,7 @@ impl Window { native_window, event_callback: None, resize_callback: None, + should_close_callback: None, close_callback: None, activate_callback: None, synthetic_drag_counter: 0, @@ -339,6 +345,10 @@ impl platform::Window for Window { self.0.as_ref().borrow_mut().resize_callback = Some(callback); } + fn on_should_close(&mut self, callback: Box bool>) { + self.0.as_ref().borrow_mut().should_close_callback = Some(callback); + } + fn on_close(&mut self, callback: Box) { self.0.as_ref().borrow_mut().close_callback = Some(callback); } @@ -674,6 +684,19 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) .detach(); } +extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if let Some(mut callback) = window_state_borrow.should_close_callback.take() { + drop(window_state_borrow); + let should_close = callback(); + window_state.borrow_mut().should_close_callback = Some(callback); + should_close as BOOL + } else { + YES + } +} + extern "C" fn close_window(this: &Object, _: Sel) { unsafe { let close_callback = { diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index fd07ef80a230105c81337c4abc4c67e583455f95..9ffd95ea0e9be1db4d24f394f13a38378b4d586b 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -37,6 +37,7 @@ pub struct Window { event_handlers: Vec bool>>, resize_handlers: Vec>, close_handlers: Vec>, + should_close_handler: Option bool>>, pub(crate) title: Option, pub(crate) edited: bool, pub(crate) pending_prompts: RefCell>>, @@ -186,9 +187,10 @@ impl Window { fn new(size: Vector2F) -> Self { Self { size, - event_handlers: Vec::new(), - resize_handlers: Vec::new(), - close_handlers: Vec::new(), + event_handlers: Default::default(), + resize_handlers: Default::default(), + close_handlers: Default::default(), + should_close_handler: Default::default(), scale_factor: 1.0, current_scene: None, title: None, @@ -264,6 +266,10 @@ impl super::Window for Window { fn set_edited(&mut self, edited: bool) { self.edited = edited; } + + fn on_should_close(&mut self, callback: Box bool>) { + self.should_close_handler = Some(callback); + } } pub fn platform() -> Platform { From d9b5357234db1d449468e11bc9d33ceb09e6a5c8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Jun 2022 11:44:14 +0200 Subject: [PATCH 3/6] Always prevent window from closing and manually invoke Workspace::close This ensures we ask the user to save their state if there are unsaved changes. --- crates/workspace/src/workspace.rs | 6 +++++- crates/zed/src/zed.rs | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 55da35ca858ff95acbd5aa757e43375bc71d3343..0a7c0c1e82c4a5ec2890e0b5bc24f24d1563d78a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -907,7 +907,11 @@ impl Workspace { } } - fn close(&mut self, _: &CloseWindow, cx: &mut ViewContext) -> Option>> { + pub fn close( + &mut self, + _: &CloseWindow, + cx: &mut ViewContext, + ) -> Option>> { let prepare = self.prepare_to_close(cx); Some(cx.spawn(|this, mut cx| async move { if prepare.await? { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bfccd42b6bcc0fa9057b14133cd8ef47b489e46e..35981bda0b638515305c36ad82450bec9ab98d13 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -222,6 +222,13 @@ pub fn initialize_workspace( }); auto_update::notify_of_any_new_update(cx.weak_handle(), cx); + + cx.on_window_should_close(|workspace, cx| { + if let Some(task) = workspace.close(&Default::default(), cx) { + task.detach_and_log_err(cx); + } + false + }); } pub fn build_window_options() -> WindowOptions<'static> { From 2dae0ddcdb13b394bad02e786b13a104136dac9f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Jun 2022 14:18:53 +0200 Subject: [PATCH 4/6] Add test to verify closing window via the mouse --- crates/gpui/src/app.rs | 66 +++++++++++++++++--------------- crates/gpui/src/platform/test.rs | 2 +- crates/zed/src/zed.rs | 34 +++++++++++++++- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f5bfb390902ec1962f15963ad73b41b59022ab4c..245ee0da1ed9895944aa022d945419df951e253f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -22,7 +22,7 @@ use smallvec::SmallVec; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, - cell::RefCell, + cell::{RefCell, RefMut}, collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque}, fmt::{self, Debug}, hash::{Hash, Hasher}, @@ -521,16 +521,8 @@ impl TestAppContext { pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) { use postage::prelude::Sink as _; - let mut state = self.cx.borrow_mut(); - let (_, window) = state - .presenters_and_platform_windows - .get_mut(&window_id) - .unwrap(); - let test_window = window - .as_any_mut() - .downcast_mut::() - .unwrap(); - let mut done_tx = test_window + let mut done_tx = self + .window_mut(window_id) .pending_prompts .borrow_mut() .pop_front() @@ -539,30 +531,28 @@ impl TestAppContext { } pub fn has_pending_prompt(&self, window_id: usize) -> bool { - let mut state = self.cx.borrow_mut(); - let (_, window) = state - .presenters_and_platform_windows - .get_mut(&window_id) - .unwrap(); - let test_window = window - .as_any_mut() - .downcast_mut::() - .unwrap(); - let prompts = test_window.pending_prompts.borrow_mut(); + let window = self.window_mut(window_id); + let prompts = window.pending_prompts.borrow_mut(); !prompts.is_empty() } pub fn current_window_title(&self, window_id: usize) -> Option { - let mut state = self.cx.borrow_mut(); - let (_, window) = state - .presenters_and_platform_windows - .get_mut(&window_id) - .unwrap(); - let test_window = window - .as_any_mut() - .downcast_mut::() - .unwrap(); - test_window.title.clone() + self.window_mut(window_id).title.clone() + } + + pub fn simulate_window_close(&self, window_id: usize) -> bool { + let handler = self.window_mut(window_id).should_close_handler.take(); + if let Some(mut handler) = handler { + let should_close = handler(); + self.window_mut(window_id).should_close_handler = Some(handler); + should_close + } else { + false + } + } + + pub fn is_window_edited(&self, window_id: usize) -> bool { + self.window_mut(window_id).edited } pub fn leak_detector(&self) -> Arc> { @@ -576,6 +566,20 @@ impl TestAppContext { .lock() .assert_dropped(handle.id()) } + + fn window_mut(&self, window_id: usize) -> RefMut { + RefMut::map(self.cx.borrow_mut(), |state| { + let (_, window) = state + .presenters_and_platform_windows + .get_mut(&window_id) + .unwrap(); + let test_window = window + .as_any_mut() + .downcast_mut::() + .unwrap(); + test_window + }) + } } impl AsyncAppContext { diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 9ffd95ea0e9be1db4d24f394f13a38378b4d586b..eb241797c3b8c570c5e5a1163bd9efd691d78f1a 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -37,7 +37,7 @@ pub struct Window { event_handlers: Vec bool>>, resize_handlers: Vec>, close_handlers: Vec>, - should_close_handler: Option bool>>, + pub(crate) should_close_handler: Option bool>>, pub(crate) title: Option, pub(crate) edited: bool, pub(crate) pending_prompts: RefCell>>, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 35981bda0b638515305c36ad82450bec9ab98d13..0f869d5b5b0c7482f11095991a29e6b880de46ce 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -371,7 +371,9 @@ mod tests { use super::*; use assets::Assets; use editor::{Autoscroll, DisplayPoint, Editor}; - use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle}; + use gpui::{ + executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle, + }; use project::ProjectPath; use serde_json::json; use std::{ @@ -439,6 +441,36 @@ mod tests { assert_eq!(cx.window_ids().len(), 2); } + #[gpui::test] + async fn test_closing_window_via_mouse(executor: Arc, cx: &mut TestAppContext) { + let app_state = init(cx); + app_state + .fs + .as_fake() + .insert_tree("/root", json!({"a": "hey"})) + .await; + + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx)) + .await; + assert_eq!(cx.window_ids().len(), 1); + + let workspace = cx.root_view::(cx.window_ids()[0]).unwrap(); + let editor = workspace.read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + + assert!(!cx.simulate_window_close(workspace.window_id())); + executor.run_until_parked(); + cx.simulate_prompt_answer(workspace.window_id(), 1); + executor.run_until_parked(); + assert_eq!(cx.window_ids().len(), 0); + } + #[gpui::test] async fn test_new_empty_workspace(cx: &mut TestAppContext) { let app_state = init(cx); From a21dbdd0d66234d7cd0e338d7d16ac1964750421 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Jun 2022 14:28:10 +0200 Subject: [PATCH 5/6] Update window edited status when pane item is removed --- crates/gpui/src/app.rs | 6 +++--- crates/workspace/src/pane.rs | 2 ++ crates/workspace/src/workspace.rs | 3 +++ crates/zed/src/zed.rs | 3 +++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 245ee0da1ed9895944aa022d945419df951e253f..5fa55448084368dc1f7b99fe119afaf645ff4f5d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -22,7 +22,7 @@ use smallvec::SmallVec; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, - cell::{RefCell, RefMut}, + cell::RefCell, collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque}, fmt::{self, Debug}, hash::{Hash, Hasher}, @@ -567,8 +567,8 @@ impl TestAppContext { .assert_dropped(handle.id()) } - fn window_mut(&self, window_id: usize) -> RefMut { - RefMut::map(self.cx.borrow_mut(), |state| { + fn window_mut(&self, window_id: usize) -> std::cell::RefMut { + std::cell::RefMut::map(self.cx.borrow_mut(), |state| { let (_, window) = state .presenters_and_platform_windows .get_mut(&window_id) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index f790912c5cf87747b856d300cacd0832660cc53d..09051d726629cb348590d178f3e5fc38d81c7953 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -110,6 +110,7 @@ pub enum Event { Activate, ActivateItem { local: bool }, Remove, + RemoveItem, Split(SplitDirection), ChangeItemTitle, } @@ -575,6 +576,7 @@ impl Pane { } let item = pane.items.remove(item_ix); + cx.emit(Event::RemoveItem); if pane.items.is_empty() { item.deactivated(cx); pane.update_toolbar(cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0a7c0c1e82c4a5ec2890e0b5bc24f24d1563d78a..942e9a091a14f56fee3cb443070f6a604cf2f1d2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1482,6 +1482,9 @@ impl Workspace { } self.update_window_edited(cx); } + pane::Event::RemoveItem => { + self.update_window_edited(cx); + } } } else { error!("pane {} not found", pane_id); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0f869d5b5b0c7482f11095991a29e6b880de46ce..0f5e561b04459af6583ca40f76680d887fd042a5 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -462,7 +462,10 @@ mod tests { .downcast::() .unwrap() }); + assert!(!cx.is_window_edited(workspace.window_id())); + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + assert!(cx.is_window_edited(workspace.window_id())); assert!(!cx.simulate_window_close(workspace.window_id())); executor.run_until_parked(); From 90102f22fd89c320ba006488e3733e2232b6c764 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Jun 2022 14:41:47 +0200 Subject: [PATCH 6/6] Improve testing of window edit state --- crates/zed/src/zed.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0f5e561b04459af6583ca40f76680d887fd042a5..242a8ef1092649003547d34492ce016668cceb5b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -442,7 +442,7 @@ mod tests { } #[gpui::test] - async fn test_closing_window_via_mouse(executor: Arc, cx: &mut TestAppContext) { + async fn test_window_edit_state(executor: Arc, cx: &mut TestAppContext) { let app_state = init(cx); app_state .fs @@ -454,6 +454,7 @@ mod tests { .await; assert_eq!(cx.window_ids().len(), 1); + // When opening the workspace, the window is not in a edited state. let workspace = cx.root_view::(cx.window_ids()[0]).unwrap(); let editor = workspace.read_with(cx, |workspace, cx| { workspace @@ -464,11 +465,51 @@ mod tests { }); assert!(!cx.is_window_edited(workspace.window_id())); + // Editing a buffer marks the window as edited. editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); assert!(cx.is_window_edited(workspace.window_id())); + // Undoing the edit restores the window's edited state. + editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx)); + assert!(!cx.is_window_edited(workspace.window_id())); + + // Redoing the edit marks the window as edited again. + editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx)); + assert!(cx.is_window_edited(workspace.window_id())); + + // Closing the item restores the window's edited state. + let close = workspace.update(cx, |workspace, cx| { + drop(editor); + Pane::close_active_item(workspace, &Default::default(), cx).unwrap() + }); + executor.run_until_parked(); + cx.simulate_prompt_answer(workspace.window_id(), 1); + close.await.unwrap(); + assert!(!cx.is_window_edited(workspace.window_id())); + + // Opening the buffer again doesn't impact the window's edited state. + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx)) + .await; + let editor = workspace.read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert!(!cx.is_window_edited(workspace.window_id())); + + // Editing the buffer marks the window as edited. + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + assert!(cx.is_window_edited(workspace.window_id())); + + // Ensure closing the window via the mouse gets preempted due to the + // buffer having unsaved changes. assert!(!cx.simulate_window_close(workspace.window_id())); executor.run_until_parked(); + assert_eq!(cx.window_ids().len(), 1); + + // The window is successfully closed after the user dismisses the prompt. cx.simulate_prompt_answer(workspace.window_id(), 1); executor.run_until_parked(); assert_eq!(cx.window_ids().len(), 0);