Move autosave logic up into Workspace and Pane

This commit is contained in:
Antonio Scandurra 2022-07-06 16:11:06 +02:00
parent 980730a4e1
commit b937c1acec
7 changed files with 166 additions and 132 deletions

View file

@ -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>) {

View file

@ -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(

View file

@ -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 {

View file

@ -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 {

View file

@ -718,6 +718,18 @@ impl Pane {
Ok(true) Ok(true)
} }
pub fn autosave_item(
item: &dyn ItemHandle,
project: ModelHandle<Project>,
cx: &mut MutableAppContext,
) -> Task<Result<()>> {
if item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(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);

View file

@ -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 {

View file

@ -396,9 +396,11 @@ mod tests {
}; };
use project::{Project, ProjectPath}; use project::{Project, ProjectPath};
use serde_json::json; use serde_json::json;
use settings::Autosave;
use std::{ use std::{
collections::HashSet, collections::HashSet,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Duration,
}; };
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME}; use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
use workspace::{ use workspace::{
@ -977,6 +979,79 @@ mod tests {
}) })
} }
#[gpui::test]
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
let app_state = init(cx);
let fs = app_state.fs.clone();
fs.as_fake()
.insert_tree("/root", json!({ "a.txt": "" }))
.await;
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
cx.update(|cx| {
workspace.update(cx, |view, cx| {
view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
})
})
.await;
let editor = cx.read(|cx| {
let pane = workspace.read(cx).active_pane().read(cx);
let item = pane.active_item().unwrap();
item.downcast::<Editor>().unwrap()
});
// 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("/root/a.txt")).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("/root/a.txt")).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("/root/a.txt")).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("/root/a.txt")).await.unwrap(), "XXX");
editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
}
#[gpui::test] #[gpui::test]
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
let app_state = init(cx); let app_state = init(cx);