Merge pull request #1291 from zed-industries/fix-autosave-on-close

Fix autosave when closing a tab
This commit is contained in:
Max Brunsfeld 2022-07-06 10:54:44 -07:00 committed by GitHub
commit 00d1c2e56f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 318 additions and 181 deletions

View file

@ -568,10 +568,11 @@ impl workspace::Item for ProjectDiagnosticsEditor {
}
fn should_update_tab_on_event(event: &Event) -> bool {
matches!(
event,
Event::Saved | Event::DirtyChanged | Event::TitleChanged
)
Editor::should_update_tab_on_event(event)
}
fn is_edit_event(event: &Self::Event) -> bool {
Editor::is_edit_event(event)
}
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {

View file

@ -18,7 +18,6 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
pub use display_map::DisplayPoint;
use display_map::*;
pub use element::*;
use futures::{channel::oneshot, FutureExt};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
@ -51,7 +50,7 @@ use ordered_float::OrderedFloat;
use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
use settings::{Autosave, Settings};
use settings::Settings;
use smallvec::SmallVec;
use smol::Timer;
use snippet::Snippet;
@ -439,8 +438,6 @@ pub struct Editor {
leader_replica_id: Option<u16>,
hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState,
pending_autosave: Option<Task<Option<()>>>,
cancel_pending_autosave: Option<oneshot::Sender<()>>,
_subscriptions: Vec<Subscription>,
}
@ -1028,13 +1025,10 @@ impl Editor {
leader_replica_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
pending_autosave: Default::default(),
cancel_pending_autosave: Default::default(),
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed),
cx.observe_window_activation(Self::on_window_activation_changed),
],
};
this.end_selection(cx);
@ -5584,33 +5578,6 @@ impl Editor {
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
cx.emit(Event::BufferEdited);
if let Autosave::AfterDelay { milliseconds } = cx.global::<Settings>().autosave {
let pending_autosave =
self.pending_autosave.take().unwrap_or(Task::ready(None));
if let Some(cancel_pending_autosave) = self.cancel_pending_autosave.take() {
let _ = cancel_pending_autosave.send(());
}
let (cancel_tx, mut cancel_rx) = oneshot::channel();
self.cancel_pending_autosave = Some(cancel_tx);
self.pending_autosave = Some(cx.spawn_weak(|this, mut cx| async move {
let mut timer = cx
.background()
.timer(Duration::from_millis(milliseconds))
.fuse();
pending_autosave.await;
futures::select_biased! {
_ = cancel_rx => return None,
_ = timer => {}
}
this.upgrade(&cx)?
.update(&mut cx, |this, cx| this.autosave(cx))
.await
.log_err();
None
}));
}
}
language::Event::Reparsed => cx.emit(Event::Reparsed),
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
@ -5629,25 +5596,6 @@ impl Editor {
cx.notify();
}
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if !active && cx.global::<Settings>().autosave == Autosave::OnWindowChange {
self.autosave(cx).detach_and_log_err(cx);
}
}
fn autosave(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
if let Some(project) = self.project.clone() {
if self.buffer.read(cx).is_dirty(cx)
&& !self.buffer.read(cx).has_conflict(cx)
&& workspace::Item::can_save(self, cx)
{
return workspace::Item::save(self, project, cx);
}
}
Task::ready(Ok(()))
}
pub fn set_searchable(&mut self, searchable: bool) {
self.searchable = searchable;
}
@ -5865,10 +5813,6 @@ impl View for Editor {
hide_hover(self, cx);
cx.emit(Event::Blurred);
cx.notify();
if cx.global::<Settings>().autosave == Autosave::OnFocusChange {
self.autosave(cx).detach_and_log_err(cx);
}
}
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
@ -6282,23 +6226,22 @@ mod tests {
use super::*;
use futures::StreamExt;
use gpui::{
executor::Deterministic,
geometry::rect::RectF,
platform::{WindowBounds, WindowOptions},
};
use indoc::indoc;
use language::{FakeLspAdapter, LanguageConfig};
use lsp::FakeLanguageServer;
use project::{FakeFs, Fs};
use project::FakeFs;
use settings::LanguageSettings;
use std::{cell::RefCell, path::Path, rc::Rc, time::Instant};
use std::{cell::RefCell, rc::Rc, time::Instant};
use text::Point;
use unindent::Unindent;
use util::{
assert_set_eq,
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
};
use workspace::{FollowableItem, Item, ItemHandle};
use workspace::{FollowableItem, ItemHandle};
#[gpui::test]
fn test_edit_events(cx: &mut MutableAppContext) {
@ -9562,72 +9505,6 @@ mod tests {
save.await.unwrap();
}
#[gpui::test]
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
deterministic.forbid_parking();
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
let (_, editor) = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
// Autosave on window change.
editor.update(cx, |editor, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnWindowChange;
});
editor.insert("X", cx);
assert!(editor.is_dirty(cx))
});
// Deactivating the window saves the file.
cx.simulate_window_activation(None);
deterministic.run_until_parked();
assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "X");
editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
// Autosave on focus change.
editor.update(cx, |editor, cx| {
cx.focus_self();
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnFocusChange;
});
editor.insert("X", cx);
assert!(editor.is_dirty(cx))
});
// Blurring the editor saves the file.
editor.update(cx, |_, cx| cx.blur());
deterministic.run_until_parked();
assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XX");
editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
// Autosave after delay.
editor.update(cx, |editor, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
});
editor.insert("X", cx);
assert!(editor.is_dirty(cx))
});
// Delay hasn't fully expired, so the file is still dirty and unsaved.
deterministic.advance_clock(Duration::from_millis(250));
assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XX");
editor.read_with(cx, |editor, cx| assert!(editor.is_dirty(cx)));
// After delay expires, the file is saved.
deterministic.advance_clock(Duration::from_millis(250));
assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XXX");
editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
}
#[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) {
let mut language = Language::new(

View file

@ -445,6 +445,10 @@ impl Item for Editor {
Event::Saved | Event::DirtyChanged | Event::TitleChanged
)
}
fn is_edit_event(event: &Self::Event) -> bool {
matches!(event, Event::BufferEdited)
}
}
impl ProjectItem for Editor {

View file

@ -811,7 +811,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
type FocusObservationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
@ -1305,7 +1305,7 @@ impl MutableAppContext {
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
where
F: 'static + FnMut(ViewHandle<V>, &mut MutableAppContext) -> bool,
F: 'static + FnMut(ViewHandle<V>, bool, &mut MutableAppContext) -> bool,
V: View,
{
let subscription_id = post_inc(&mut self.next_subscription_id);
@ -1314,9 +1314,9 @@ impl MutableAppContext {
self.pending_effects.push_back(Effect::FocusObservation {
view_id,
subscription_id,
callback: Box::new(move |cx| {
callback: Box::new(move |focused, cx| {
if let Some(observed) = observed.upgrade(cx) {
callback(observed, cx)
callback(observed, focused, cx)
} else {
false
}
@ -2525,6 +2525,31 @@ impl MutableAppContext {
if let Some(mut blurred_view) = this.cx.views.remove(&(window_id, blurred_id)) {
blurred_view.on_blur(this, window_id, blurred_id);
this.cx.views.insert((window_id, blurred_id), blurred_view);
let callbacks = this.focus_observations.lock().remove(&blurred_id);
if let Some(callbacks) = callbacks {
for (id, callback) in callbacks {
if let Some(mut callback) = callback {
let alive = callback(false, this);
if alive {
match this
.focus_observations
.lock()
.entry(blurred_id)
.or_default()
.entry(id)
{
btree_map::Entry::Vacant(entry) => {
entry.insert(Some(callback));
}
btree_map::Entry::Occupied(entry) => {
entry.remove();
}
}
}
}
}
}
}
}
@ -2537,7 +2562,7 @@ impl MutableAppContext {
if let Some(callbacks) = callbacks {
for (id, callback) in callbacks {
if let Some(mut callback) = callback {
let alive = callback(this);
let alive = callback(true, this);
if alive {
match this
.focus_observations
@ -3598,20 +3623,21 @@ impl<'a, T: View> ViewContext<'a, T> {
pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
where
F: 'static + FnMut(&mut T, ViewHandle<V>, &mut ViewContext<T>),
F: 'static + FnMut(&mut T, ViewHandle<V>, bool, &mut ViewContext<T>),
V: View,
{
let observer = self.weak_handle();
self.app.observe_focus(handle, move |observed, cx| {
if let Some(observer) = observer.upgrade(cx) {
observer.update(cx, |observer, cx| {
callback(observer, observed, cx);
});
true
} else {
false
}
})
self.app
.observe_focus(handle, move |observed, focused, cx| {
if let Some(observer) = observer.upgrade(cx) {
observer.update(cx, |observer, cx| {
callback(observer, observed, focused, cx);
});
true
} else {
false
}
})
}
pub fn observe_release<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
@ -6448,11 +6474,13 @@ mod tests {
view_1.update(cx, |_, cx| {
cx.observe_focus(&view_2, {
let observed_events = observed_events.clone();
move |this, view, cx| {
move |this, view, focused, cx| {
let label = if focused { "focus" } else { "blur" };
observed_events.lock().push(format!(
"{} observed {}'s focus",
"{} observed {}'s {}",
this.name,
view.read(cx).name
view.read(cx).name,
label
))
}
})
@ -6461,16 +6489,20 @@ mod tests {
view_2.update(cx, |_, cx| {
cx.observe_focus(&view_1, {
let observed_events = observed_events.clone();
move |this, view, cx| {
move |this, view, focused, cx| {
let label = if focused { "focus" } else { "blur" };
observed_events.lock().push(format!(
"{} observed {}'s focus",
"{} observed {}'s {}",
this.name,
view.read(cx).name
view.read(cx).name,
label
))
}
})
.detach();
});
assert_eq!(mem::take(&mut *view_events.lock()), ["view 1 focused"]);
assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
view_1.update(cx, |_, cx| {
// Ensure only the latest focus is honored.
@ -6478,31 +6510,47 @@ mod tests {
cx.focus(&view_1);
cx.focus(&view_2);
});
view_1.update(cx, |_, cx| cx.focus(&view_1));
view_1.update(cx, |_, cx| cx.focus(&view_2));
view_1.update(cx, |_, _| drop(view_2));
assert_eq!(
*view_events.lock(),
[
"view 1 focused".to_string(),
"view 1 blurred".to_string(),
"view 2 focused".to_string(),
"view 2 blurred".to_string(),
"view 1 focused".to_string(),
"view 1 blurred".to_string(),
"view 2 focused".to_string(),
"view 1 focused".to_string(),
],
mem::take(&mut *view_events.lock()),
["view 1 blurred", "view 2 focused"],
);
assert_eq!(
*observed_events.lock(),
mem::take(&mut *observed_events.lock()),
[
"view 1 observed view 2's focus".to_string(),
"view 2 observed view 1's focus".to_string(),
"view 1 observed view 2's focus".to_string(),
"view 2 observed view 1's blur",
"view 1 observed view 2's focus"
]
);
view_1.update(cx, |_, cx| cx.focus(&view_1));
assert_eq!(
mem::take(&mut *view_events.lock()),
["view 2 blurred", "view 1 focused"],
);
assert_eq!(
mem::take(&mut *observed_events.lock()),
[
"view 1 observed view 2's blur",
"view 2 observed view 1's focus"
]
);
view_1.update(cx, |_, cx| cx.focus(&view_2));
assert_eq!(
mem::take(&mut *view_events.lock()),
["view 1 blurred", "view 2 focused"],
);
assert_eq!(
mem::take(&mut *observed_events.lock()),
[
"view 2 observed view 1's blur",
"view 1 observed view 2's focus"
]
);
view_1.update(cx, |_, _| drop(view_2));
assert_eq!(mem::take(&mut *view_events.lock()), ["view 1 focused"]);
assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
}
#[crate::test(self)]

View file

@ -329,6 +329,14 @@ impl Item for ProjectSearchView {
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
matches!(event, ViewEvent::UpdateTab)
}
fn is_edit_event(event: &Self::Event) -> bool {
if let ViewEvent::EditorEvent(editor_event) = event {
Editor::is_edit_event(editor_event)
} else {
false
}
}
}
impl ProjectSearchView {
@ -365,8 +373,10 @@ impl ProjectSearchView {
cx.emit(ViewEvent::EditorEvent(event.clone()))
})
.detach();
cx.observe_focus(&query_editor, |this, _, _| {
this.results_editor_was_focused = false;
cx.observe_focus(&query_editor, |this, _, focused, _| {
if focused {
this.results_editor_was_focused = false;
}
})
.detach();
@ -377,8 +387,10 @@ impl ProjectSearchView {
});
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
.detach();
cx.observe_focus(&results_editor, |this, _, _| {
this.results_editor_was_focused = true;
cx.observe_focus(&results_editor, |this, _, focused, _| {
if focused {
this.results_editor_was_focused = true;
}
})
.detach();
cx.subscribe(&results_editor, |this, _, event, cx| {

View file

@ -14,7 +14,7 @@ use gpui::{
};
use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
use settings::Settings;
use settings::{Autosave, Settings};
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
use util::ResultExt;
@ -677,7 +677,13 @@ impl Pane {
_ => return Ok(false),
}
} else if is_dirty && (can_save || is_singleton) {
let should_save = if should_prompt_for_save {
let will_autosave = cx.read(|cx| {
matches!(
cx.global::<Settings>().autosave,
Autosave::OnFocusChange | Autosave::OnWindowChange
) && Self::can_autosave_item(item.as_ref(), cx)
});
let should_save = if should_prompt_for_save && !will_autosave {
let mut answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
cx.prompt(
@ -718,6 +724,23 @@ impl Pane {
Ok(true)
}
fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
let is_deleted = item.project_entry_ids(cx).is_empty();
item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
}
pub fn autosave_item(
item: &dyn ItemHandle,
project: ModelHandle<Project>,
cx: &mut MutableAppContext,
) -> Task<Result<()>> {
if Self::can_autosave_item(item, cx) {
item.save(project, cx)
} else {
Task::ready(Ok(()))
}
}
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_item) = self.active_item() {
cx.focus(active_item);

View file

@ -11,6 +11,7 @@ use client::{
};
use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::oneshot, FutureExt};
use gpui::{
actions,
color::Color,
@ -30,7 +31,7 @@ pub use pane_group::*;
use postage::prelude::Stream;
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
use serde::Deserialize;
use settings::Settings;
use settings::{Autosave, Settings};
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
use smallvec::SmallVec;
use status_bar::StatusBar;
@ -41,12 +42,14 @@ use std::{
cell::RefCell,
fmt,
future::Future,
mem,
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
time::Duration,
};
use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
@ -296,6 +299,9 @@ pub trait Item: View {
fn should_update_tab_on_event(_: &Self::Event) -> bool {
false
}
fn is_edit_event(_: &Self::Event) -> bool {
false
}
fn act_as_type(
&self,
type_id: TypeId,
@ -510,6 +516,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
}
}
let mut pending_autosave = None;
let mut cancel_pending_autosave = oneshot::channel::<()>().0;
let pending_update = Rc::new(RefCell::new(None));
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
let pane = pane.downgrade();
@ -570,6 +578,40 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
cx.notify();
});
}
if T::is_edit_event(event) {
if let Autosave::AfterDelay { milliseconds } = cx.global::<Settings>().autosave {
let prev_autosave = pending_autosave.take().unwrap_or(Task::ready(Some(())));
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
let prev_cancel_tx = mem::replace(&mut cancel_pending_autosave, cancel_tx);
let project = workspace.project.downgrade();
let _ = prev_cancel_tx.send(());
pending_autosave = Some(cx.spawn_weak(|_, mut cx| async move {
let mut timer = cx
.background()
.timer(Duration::from_millis(milliseconds))
.fuse();
prev_autosave.await;
futures::select_biased! {
_ = cancel_rx => return None,
_ = timer => {}
}
let project = project.upgrade(&cx)?;
cx.update(|cx| Pane::autosave_item(&item, project, cx))
.await
.log_err();
None
}));
}
}
})
.detach();
cx.observe_focus(self, move |workspace, item, focused, cx| {
if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
Pane::autosave_item(&item, workspace.project.clone(), cx).detach_and_log_err(cx);
}
})
.detach();
}
@ -774,6 +816,8 @@ impl Workspace {
cx.notify()
})
.detach();
cx.observe_window_activation(Self::on_window_activation_changed)
.detach();
cx.subscribe(&project, move |this, project, event, cx| {
match event {
@ -2314,6 +2358,19 @@ impl Workspace {
}
None
}
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if !active && cx.global::<Settings>().autosave == Autosave::OnWindowChange {
for pane in &self.panes {
pane.update(cx, |pane, cx| {
for item in pane.items() {
Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
.detach_and_log_err(cx);
}
});
}
}
}
}
impl Entity for Workspace {
@ -2631,7 +2688,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
#[cfg(test)]
mod tests {
use super::*;
use gpui::{ModelHandle, TestAppContext, ViewContext};
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
use project::{FakeFs, Project, ProjectEntryId};
use serde_json::json;
@ -2969,6 +3026,110 @@ mod tests {
});
}
#[gpui::test]
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
deterministic.forbid_parking();
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let item = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
item
});
let item_id = item.id();
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item.clone()), cx);
});
// Autosave on window change.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnWindowChange;
});
item.is_dirty = true;
});
// Deactivating the window saves the file.
cx.simulate_window_activation(None);
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
// Autosave on focus change.
item.update(cx, |item, cx| {
cx.focus_self();
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnFocusChange;
});
item.is_dirty = true;
});
// Blurring the item saves the file.
item.update(cx, |_, cx| cx.blur());
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
// Autosave after delay.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
});
item.is_dirty = true;
cx.emit(TestItemEvent::Edit);
});
// Delay hasn't fully expired, so the file is still dirty and unsaved.
deterministic.advance_clock(Duration::from_millis(250));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
// After delay expires, the file is saved.
deterministic.advance_clock(Duration::from_millis(250));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
// Autosave on focus change, ensuring closing the tab counts as such.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnFocusChange;
});
item.is_dirty = true;
});
workspace
.update(cx, |workspace, cx| {
let pane = workspace.active_pane().clone();
Pane::close_items(workspace, pane, cx, move |id| id == item_id)
})
.await
.unwrap();
assert!(!cx.has_pending_prompt(window_id));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
// Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item.clone()), cx);
});
item.update(cx, |item, cx| {
item.project_entry_ids = Default::default();
item.is_dirty = true;
cx.blur();
});
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
// Ensure autosave is prevented for deleted files also when closing the buffer.
let _close_items = workspace.update(cx, |workspace, cx| {
let pane = workspace.active_pane().clone();
Pane::close_items(workspace, pane, cx, move |id| id == item_id)
});
deterministic.run_until_parked();
assert!(cx.has_pending_prompt(window_id));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
}
#[derive(Clone)]
struct TestItem {
save_count: usize,
@ -2981,6 +3142,10 @@ mod tests {
is_singleton: bool,
}
enum TestItemEvent {
Edit,
}
impl TestItem {
fn new() -> Self {
Self {
@ -2997,7 +3162,7 @@ mod tests {
}
impl Entity for TestItem {
type Event = ();
type Event = TestItemEvent;
}
impl View for TestItem {
@ -3054,6 +3219,7 @@ mod tests {
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.save_count += 1;
self.is_dirty = false;
Task::ready(Ok(()))
}
@ -3064,6 +3230,7 @@ mod tests {
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.save_as_count += 1;
self.is_dirty = false;
Task::ready(Ok(()))
}
@ -3073,11 +3240,16 @@ mod tests {
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.reload_count += 1;
self.is_dirty = false;
Task::ready(Ok(()))
}
fn should_update_tab_on_event(_: &Self::Event) -> bool {
true
}
fn is_edit_event(event: &Self::Event) -> bool {
matches!(event, TestItemEvent::Edit)
}
}
}