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 {
|
||||
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>) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue