ElyMark: keys typed before an outside write survive; same text adopts the file; autosave resumes (M-01)
This commit is contained in:
10 files changed
+215
-11
No files matched your search
@@ -29,6 +29,14 @@ impl Desk {
|
||||
return;
|
||||
}
|
||||
tab.extra.heard = changed;
|
||||
self.save_after_pause(id, cx);
|
||||
}
|
||||
|
||||
/// Counts unsaved text as an edit; saves after the pause.
|
||||
pub fn save_after_pause(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||
let Some(tab) = self.tab_mut(id) else {
|
||||
return;
|
||||
};
|
||||
tab.extra.policy.edit(Instant::now());
|
||||
cx.spawn(async move |desk, cx| {
|
||||
let wait = DEBOUNCE + Duration::from_millis(50);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use ely_files::{Fingerprint, Policy, PolicyEngine};
|
||||
use ely_files::Fingerprint;
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
git::{self as merging, Region, Take, ThreeWayMerge},
|
||||
@@ -102,7 +102,7 @@ impl Desk {
|
||||
Choice::Mine => {
|
||||
if let Some(tab) = self.tab_mut(id) {
|
||||
tab.session.rebase(fingerprint);
|
||||
tab.extra.policy = PolicyEngine::new(Policy::MarkAutoSave, true);
|
||||
tab.extra.restart_autosave();
|
||||
}
|
||||
self.set_outside(id, Outside::Clear, cx);
|
||||
self.save(id, None, window, cx);
|
||||
@@ -120,7 +120,7 @@ impl Desk {
|
||||
return self.set_outside(id, Outside::Unread(why), cx);
|
||||
}
|
||||
tab.extra.base = disk.text.clone();
|
||||
tab.extra.policy = PolicyEngine::new(Policy::MarkAutoSave, true);
|
||||
tab.extra.restart_autosave();
|
||||
tab.doc.lock().expect("the text lock").source = disk;
|
||||
let view = tab.view.clone();
|
||||
view.update(cx, |view, cx| view.adopt(merged, cx));
|
||||
|
||||
@@ -55,6 +55,14 @@ pub struct Extra {
|
||||
pub restored: bool,
|
||||
}
|
||||
|
||||
impl Extra {
|
||||
/// Autosave from scratch; a write in flight no longer counts.
|
||||
pub fn restart_autosave(&mut self) {
|
||||
self.policy = PolicyEngine::new(Policy::MarkAutoSave, true);
|
||||
self.writing = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Extra {
|
||||
fn default() -> Self {
|
||||
Extra {
|
||||
@@ -217,11 +225,26 @@ impl Desk {
|
||||
Ok(read) => read,
|
||||
Err(error) => return self.set_outside(id, Outside::Unread(error.to_string()), cx),
|
||||
};
|
||||
// Keys the page has not sent yet count first.
|
||||
if !self.settled(id, cx) {
|
||||
return self.when_settled(id, move |desk, _, cx| desk.disk_read(id, path, Ok((fingerprint, disk)), cx));
|
||||
}
|
||||
let current = tab.doc.lock().expect("the text lock").current.clone();
|
||||
let kept = disk.text == tab.extra.base;
|
||||
let dirty = tab.dirty(cx);
|
||||
log::info!("mark: {} changed outside; unsaved {dirty}", path.display());
|
||||
match (disk.text == current, dirty) {
|
||||
(true, _) | (false, false) => self.take_disk(id, disk, fingerprint, cx),
|
||||
// The same text: only the file underneath is new.
|
||||
(true, _) => {
|
||||
if let Some(view) = self.adopt_disk(id, &disk, fingerprint, cx) {
|
||||
// What shows is now saved: the tab is clean.
|
||||
let saved = crate::engine::mark(&disk.text);
|
||||
view.update(cx, |view, cx| view.saved(&saved, cx));
|
||||
}
|
||||
}
|
||||
// The text as last saved: the edits stay unsaved.
|
||||
(false, true) if kept => self.keep_edits(id, disk, fingerprint, cx),
|
||||
(false, false) => self.take_disk(id, disk, fingerprint, cx),
|
||||
(false, true) => {
|
||||
let tab = self.tab_mut(id).expect("open");
|
||||
tab.session.external_modified();
|
||||
@@ -247,14 +270,47 @@ impl Desk {
|
||||
fingerprint: Fingerprint,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(view) = self.adopt_disk(id, &disk, fingerprint, cx) {
|
||||
view.update(cx, |view, cx| view.refresh(disk, cx));
|
||||
}
|
||||
}
|
||||
|
||||
/// A new file with the saved text; edits stay.
|
||||
fn keep_edits(&mut self, id: u64, disk: Source, fingerprint: Fingerprint, cx: &mut Context<Self>) {
|
||||
let Some(tab) = self.tab_mut(id) else {
|
||||
return;
|
||||
};
|
||||
tab.session.rebase(fingerprint);
|
||||
tab.doc.lock().expect("the text lock").source = disk;
|
||||
// A conflict stopped autosave, maybe mid-write; it resumes.
|
||||
tab.extra.restart_autosave();
|
||||
let (registry, path) = (tab.registry, tab.session.path().map(Path::to_path_buf));
|
||||
if let (Some(registry), Some(path)) = (registry, path)
|
||||
&& let Err(error) = self.core.sessions.track(registry, &path)
|
||||
{
|
||||
log::warn!("mark: the replaced file did not register: {error}");
|
||||
}
|
||||
self.set_outside(id, Outside::Clear, cx);
|
||||
self.save_after_pause(id, cx);
|
||||
}
|
||||
|
||||
/// The tab takes the file as saved; returns its view.
|
||||
fn adopt_disk(
|
||||
&mut self,
|
||||
id: u64,
|
||||
disk: &Source,
|
||||
fingerprint: Fingerprint,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<gpui::Entity<crate::view::MarkView>> {
|
||||
let tab = self.tab_mut(id)?;
|
||||
if let Err(error) = tab.session.reload_at(fingerprint) {
|
||||
return self.set_outside(id, Outside::Unread(error.to_string()), cx);
|
||||
self.set_outside(id, Outside::Unread(error.to_string()), cx);
|
||||
return None;
|
||||
}
|
||||
tab.extra.base = disk.text.clone();
|
||||
tab.extra.policy = PolicyEngine::new(Policy::MarkAutoSave, true);
|
||||
tab.extra.restart_autosave();
|
||||
// Its bytes (BOM, line ends) are what saves keep now.
|
||||
tab.doc.lock().expect("the text lock").source = disk.clone();
|
||||
let (view, registry) = (tab.view.clone(), tab.registry);
|
||||
let path = tab.session.path().map(Path::to_path_buf);
|
||||
// An atomic replace is a new file underneath.
|
||||
@@ -263,8 +319,8 @@ impl Desk {
|
||||
{
|
||||
log::warn!("mark: the replaced file did not register: {error}");
|
||||
}
|
||||
view.update(cx, |view, cx| view.refresh(disk, cx));
|
||||
self.set_outside(id, Outside::Clear, cx);
|
||||
Some(view)
|
||||
}
|
||||
|
||||
pub fn set_outside(&mut self, id: u64, outside: Outside, cx: &mut Context<Self>) {
|
||||
@@ -345,3 +401,29 @@ fn watch(dirs: &[PathBuf], cx: &mut Context<Desk>) -> notify::Result<Recommended
|
||||
log::info!("mark: watching {} folders", dirs.len());
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ely_files::Action;
|
||||
|
||||
use super::Extra;
|
||||
|
||||
#[test]
|
||||
fn a_restarted_autosave_saves_the_next_edit() {
|
||||
let mut extra = Extra::default();
|
||||
extra.policy.set_has_path();
|
||||
extra.policy.edit(Instant::now());
|
||||
// A write was in flight when the disk changed.
|
||||
extra.writing = Some(1);
|
||||
extra.restart_autosave();
|
||||
// As autosave settles a heard write, then edits.
|
||||
if let Some(revision) = extra.writing.take() {
|
||||
extra.policy.save_finished(revision, true);
|
||||
}
|
||||
let now = Instant::now();
|
||||
extra.policy.edit(now);
|
||||
assert!(matches!(extra.policy.tick(now + Duration::from_secs(5)), Action::SaveDue(_)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user