Detailed changes
@@ -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 {
@@ -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 {
@@ -718,6 +718,18 @@ impl Pane {
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>) {
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 {
@@ -396,9 +396,11 @@ mod tests {
};
use project::{Project, ProjectPath};
use serde_json::json;
+ use settings::Autosave;
use std::{
collections::HashSet,
path::{Path, PathBuf},
+ time::Duration,
};
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
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]
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
let app_state = init(cx);