Merge pull request #1291 from zed-industries/fix-autosave-on-close
Fix autosave when closing a tab
This commit is contained in:
commit
00d1c2e56f
7 changed files with 318 additions and 181 deletions
|
@ -568,10 +568,11 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_update_tab_on_event(event: &Event) -> bool {
|
fn should_update_tab_on_event(event: &Event) -> bool {
|
||||||
matches!(
|
Editor::should_update_tab_on_event(event)
|
||||||
event,
|
}
|
||||||
Event::Saved | Event::DirtyChanged | Event::TitleChanged
|
|
||||||
)
|
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>) {
|
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||||
|
|
|
@ -18,7 +18,6 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
pub use display_map::DisplayPoint;
|
pub use display_map::DisplayPoint;
|
||||||
use display_map::*;
|
use display_map::*;
|
||||||
pub use element::*;
|
pub use element::*;
|
||||||
use futures::{channel::oneshot, FutureExt};
|
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
|
@ -51,7 +50,7 @@ use ordered_float::OrderedFloat;
|
||||||
use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
|
use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
|
||||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Autosave, Settings};
|
use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use snippet::Snippet;
|
use snippet::Snippet;
|
||||||
|
@ -439,8 +438,6 @@ pub struct Editor {
|
||||||
leader_replica_id: Option<u16>,
|
leader_replica_id: Option<u16>,
|
||||||
hover_state: HoverState,
|
hover_state: HoverState,
|
||||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||||
pending_autosave: Option<Task<Option<()>>>,
|
|
||||||
cancel_pending_autosave: Option<oneshot::Sender<()>>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1028,13 +1025,10 @@ impl Editor {
|
||||||
leader_replica_id: None,
|
leader_replica_id: None,
|
||||||
hover_state: Default::default(),
|
hover_state: Default::default(),
|
||||||
link_go_to_definition_state: Default::default(),
|
link_go_to_definition_state: Default::default(),
|
||||||
pending_autosave: Default::default(),
|
|
||||||
cancel_pending_autosave: Default::default(),
|
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
cx.observe(&buffer, Self::on_buffer_changed),
|
cx.observe(&buffer, Self::on_buffer_changed),
|
||||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||||
cx.observe(&display_map, Self::on_display_map_changed),
|
cx.observe(&display_map, Self::on_display_map_changed),
|
||||||
cx.observe_window_activation(Self::on_window_activation_changed),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
this.end_selection(cx);
|
this.end_selection(cx);
|
||||||
|
@ -5584,33 +5578,6 @@ impl Editor {
|
||||||
self.refresh_active_diagnostics(cx);
|
self.refresh_active_diagnostics(cx);
|
||||||
self.refresh_code_actions(cx);
|
self.refresh_code_actions(cx);
|
||||||
cx.emit(Event::BufferEdited);
|
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::Reparsed => cx.emit(Event::Reparsed),
|
||||||
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
|
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
|
||||||
|
@ -5629,25 +5596,6 @@ impl Editor {
|
||||||
cx.notify();
|
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) {
|
pub fn set_searchable(&mut self, searchable: bool) {
|
||||||
self.searchable = searchable;
|
self.searchable = searchable;
|
||||||
}
|
}
|
||||||
|
@ -5865,10 +5813,6 @@ impl View for Editor {
|
||||||
hide_hover(self, cx);
|
hide_hover(self, cx);
|
||||||
cx.emit(Event::Blurred);
|
cx.emit(Event::Blurred);
|
||||||
cx.notify();
|
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 {
|
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
||||||
|
@ -6282,23 +6226,22 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
executor::Deterministic,
|
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
platform::{WindowBounds, WindowOptions},
|
platform::{WindowBounds, WindowOptions},
|
||||||
};
|
};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{FakeLspAdapter, LanguageConfig};
|
use language::{FakeLspAdapter, LanguageConfig};
|
||||||
use lsp::FakeLanguageServer;
|
use lsp::FakeLanguageServer;
|
||||||
use project::{FakeFs, Fs};
|
use project::FakeFs;
|
||||||
use settings::LanguageSettings;
|
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 text::Point;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::{
|
use util::{
|
||||||
assert_set_eq,
|
assert_set_eq,
|
||||||
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
|
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
|
||||||
};
|
};
|
||||||
use workspace::{FollowableItem, Item, ItemHandle};
|
use workspace::{FollowableItem, ItemHandle};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_edit_events(cx: &mut MutableAppContext) {
|
fn test_edit_events(cx: &mut MutableAppContext) {
|
||||||
|
@ -9562,72 +9505,6 @@ mod tests {
|
||||||
save.await.unwrap();
|
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]
|
#[gpui::test]
|
||||||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||||
let mut language = Language::new(
|
let mut language = Language::new(
|
||||||
|
|
|
@ -445,6 +445,10 @@ impl Item for Editor {
|
||||||
Event::Saved | Event::DirtyChanged | Event::TitleChanged
|
Event::Saved | Event::DirtyChanged | Event::TitleChanged
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_edit_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, Event::BufferEdited)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectItem for Editor {
|
impl ProjectItem for Editor {
|
||||||
|
|
|
@ -811,7 +811,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
|
||||||
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
|
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
|
||||||
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
|
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
|
||||||
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
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 GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
|
||||||
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
|
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
|
||||||
type ActionObservationCallback = Box<dyn FnMut(TypeId, &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
|
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(ViewHandle<V>, &mut MutableAppContext) -> bool,
|
F: 'static + FnMut(ViewHandle<V>, bool, &mut MutableAppContext) -> bool,
|
||||||
V: View,
|
V: View,
|
||||||
{
|
{
|
||||||
let subscription_id = post_inc(&mut self.next_subscription_id);
|
let subscription_id = post_inc(&mut self.next_subscription_id);
|
||||||
|
@ -1314,9 +1314,9 @@ impl MutableAppContext {
|
||||||
self.pending_effects.push_back(Effect::FocusObservation {
|
self.pending_effects.push_back(Effect::FocusObservation {
|
||||||
view_id,
|
view_id,
|
||||||
subscription_id,
|
subscription_id,
|
||||||
callback: Box::new(move |cx| {
|
callback: Box::new(move |focused, cx| {
|
||||||
if let Some(observed) = observed.upgrade(cx) {
|
if let Some(observed) = observed.upgrade(cx) {
|
||||||
callback(observed, cx)
|
callback(observed, focused, cx)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -2525,6 +2525,31 @@ impl MutableAppContext {
|
||||||
if let Some(mut blurred_view) = this.cx.views.remove(&(window_id, blurred_id)) {
|
if let Some(mut blurred_view) = this.cx.views.remove(&(window_id, blurred_id)) {
|
||||||
blurred_view.on_blur(this, window_id, blurred_id);
|
blurred_view.on_blur(this, window_id, blurred_id);
|
||||||
this.cx.views.insert((window_id, blurred_id), blurred_view);
|
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 {
|
if let Some(callbacks) = callbacks {
|
||||||
for (id, callback) in callbacks {
|
for (id, callback) in callbacks {
|
||||||
if let Some(mut callback) = callback {
|
if let Some(mut callback) = callback {
|
||||||
let alive = callback(this);
|
let alive = callback(true, this);
|
||||||
if alive {
|
if alive {
|
||||||
match this
|
match this
|
||||||
.focus_observations
|
.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
|
pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(&mut T, ViewHandle<V>, &mut ViewContext<T>),
|
F: 'static + FnMut(&mut T, ViewHandle<V>, bool, &mut ViewContext<T>),
|
||||||
V: View,
|
V: View,
|
||||||
{
|
{
|
||||||
let observer = self.weak_handle();
|
let observer = self.weak_handle();
|
||||||
self.app.observe_focus(handle, move |observed, cx| {
|
self.app
|
||||||
if let Some(observer) = observer.upgrade(cx) {
|
.observe_focus(handle, move |observed, focused, cx| {
|
||||||
observer.update(cx, |observer, cx| {
|
if let Some(observer) = observer.upgrade(cx) {
|
||||||
callback(observer, observed, cx);
|
observer.update(cx, |observer, cx| {
|
||||||
});
|
callback(observer, observed, focused, cx);
|
||||||
true
|
});
|
||||||
} else {
|
true
|
||||||
false
|
} else {
|
||||||
}
|
false
|
||||||
})
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe_release<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
|
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| {
|
view_1.update(cx, |_, cx| {
|
||||||
cx.observe_focus(&view_2, {
|
cx.observe_focus(&view_2, {
|
||||||
let observed_events = observed_events.clone();
|
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_events.lock().push(format!(
|
||||||
"{} observed {}'s focus",
|
"{} observed {}'s {}",
|
||||||
this.name,
|
this.name,
|
||||||
view.read(cx).name
|
view.read(cx).name,
|
||||||
|
label
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -6461,16 +6489,20 @@ mod tests {
|
||||||
view_2.update(cx, |_, cx| {
|
view_2.update(cx, |_, cx| {
|
||||||
cx.observe_focus(&view_1, {
|
cx.observe_focus(&view_1, {
|
||||||
let observed_events = observed_events.clone();
|
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_events.lock().push(format!(
|
||||||
"{} observed {}'s focus",
|
"{} observed {}'s {}",
|
||||||
this.name,
|
this.name,
|
||||||
view.read(cx).name
|
view.read(cx).name,
|
||||||
|
label
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.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| {
|
view_1.update(cx, |_, cx| {
|
||||||
// Ensure only the latest focus is honored.
|
// Ensure only the latest focus is honored.
|
||||||
|
@ -6478,31 +6510,47 @@ mod tests {
|
||||||
cx.focus(&view_1);
|
cx.focus(&view_1);
|
||||||
cx.focus(&view_2);
|
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!(
|
assert_eq!(
|
||||||
*view_events.lock(),
|
mem::take(&mut *view_events.lock()),
|
||||||
[
|
["view 1 blurred", "view 2 focused"],
|
||||||
"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(),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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 blur",
|
||||||
"view 2 observed view 1's focus".to_string(),
|
"view 1 observed view 2's focus"
|
||||||
"view 1 observed view 2's focus".to_string(),
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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)]
|
#[crate::test(self)]
|
||||||
|
|
|
@ -329,6 +329,14 @@ impl Item for ProjectSearchView {
|
||||||
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
|
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
|
||||||
matches!(event, ViewEvent::UpdateTab)
|
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 {
|
impl ProjectSearchView {
|
||||||
|
@ -365,8 +373,10 @@ impl ProjectSearchView {
|
||||||
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.observe_focus(&query_editor, |this, _, _| {
|
cx.observe_focus(&query_editor, |this, _, focused, _| {
|
||||||
this.results_editor_was_focused = false;
|
if focused {
|
||||||
|
this.results_editor_was_focused = false;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -377,8 +387,10 @@ impl ProjectSearchView {
|
||||||
});
|
});
|
||||||
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
||||||
.detach();
|
.detach();
|
||||||
cx.observe_focus(&results_editor, |this, _, _| {
|
cx.observe_focus(&results_editor, |this, _, focused, _| {
|
||||||
this.results_editor_was_focused = true;
|
if focused {
|
||||||
|
this.results_editor_was_focused = true;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.subscribe(&results_editor, |this, _, event, cx| {
|
cx.subscribe(&results_editor, |this, _, event, cx| {
|
||||||
|
|
|
@ -14,7 +14,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use project::{Project, ProjectEntryId, ProjectPath};
|
use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::{Autosave, Settings};
|
||||||
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
|
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
@ -677,7 +677,13 @@ impl Pane {
|
||||||
_ => return Ok(false),
|
_ => return Ok(false),
|
||||||
}
|
}
|
||||||
} else if is_dirty && (can_save || is_singleton) {
|
} 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| {
|
let mut answer = pane.update(cx, |pane, cx| {
|
||||||
pane.activate_item(item_ix, true, true, cx);
|
pane.activate_item(item_ix, true, true, cx);
|
||||||
cx.prompt(
|
cx.prompt(
|
||||||
|
@ -718,6 +724,23 @@ impl Pane {
|
||||||
Ok(true)
|
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>) {
|
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(active_item) = self.active_item() {
|
if let Some(active_item) = self.active_item() {
|
||||||
cx.focus(active_item);
|
cx.focus(active_item);
|
||||||
|
|
|
@ -11,6 +11,7 @@ use client::{
|
||||||
};
|
};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
|
use futures::{channel::oneshot, FutureExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
color::Color,
|
color::Color,
|
||||||
|
@ -30,7 +31,7 @@ pub use pane_group::*;
|
||||||
use postage::prelude::Stream;
|
use postage::prelude::Stream;
|
||||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::{Autosave, Settings};
|
||||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use status_bar::StatusBar;
|
use status_bar::StatusBar;
|
||||||
|
@ -41,12 +42,14 @@ use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
mem,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering::SeqCst},
|
atomic::{AtomicBool, Ordering::SeqCst},
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use theme::{Theme, ThemeRegistry};
|
use theme::{Theme, ThemeRegistry};
|
||||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||||
|
@ -296,6 +299,9 @@ pub trait Item: View {
|
||||||
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
fn is_edit_event(_: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
fn act_as_type(
|
fn act_as_type(
|
||||||
&self,
|
&self,
|
||||||
type_id: TypeId,
|
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 = Rc::new(RefCell::new(None));
|
||||||
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
||||||
let pane = pane.downgrade();
|
let pane = pane.downgrade();
|
||||||
|
@ -570,6 +578,40 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
cx.notify();
|
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();
|
.detach();
|
||||||
}
|
}
|
||||||
|
@ -774,6 +816,8 @@ impl Workspace {
|
||||||
cx.notify()
|
cx.notify()
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
cx.observe_window_activation(Self::on_window_activation_changed)
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.subscribe(&project, move |this, project, event, cx| {
|
cx.subscribe(&project, move |this, project, event, cx| {
|
||||||
match event {
|
match event {
|
||||||
|
@ -2314,6 +2358,19 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
None
|
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 {
|
impl Entity for Workspace {
|
||||||
|
@ -2631,7 +2688,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{ModelHandle, TestAppContext, ViewContext};
|
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||||
use project::{FakeFs, Project, ProjectEntryId};
|
use project::{FakeFs, Project, ProjectEntryId};
|
||||||
use serde_json::json;
|
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)]
|
#[derive(Clone)]
|
||||||
struct TestItem {
|
struct TestItem {
|
||||||
save_count: usize,
|
save_count: usize,
|
||||||
|
@ -2981,6 +3142,10 @@ mod tests {
|
||||||
is_singleton: bool,
|
is_singleton: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TestItemEvent {
|
||||||
|
Edit,
|
||||||
|
}
|
||||||
|
|
||||||
impl TestItem {
|
impl TestItem {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -2997,7 +3162,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for TestItem {
|
impl Entity for TestItem {
|
||||||
type Event = ();
|
type Event = TestItemEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for TestItem {
|
impl View for TestItem {
|
||||||
|
@ -3054,6 +3219,7 @@ mod tests {
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
self.save_count += 1;
|
self.save_count += 1;
|
||||||
|
self.is_dirty = false;
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3064,6 +3230,7 @@ mod tests {
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
self.save_as_count += 1;
|
self.save_as_count += 1;
|
||||||
|
self.is_dirty = false;
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3073,11 +3240,16 @@ mod tests {
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
self.reload_count += 1;
|
self.reload_count += 1;
|
||||||
|
self.is_dirty = false;
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_edit_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, TestItemEvent::Edit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue