C14.15: the kept mode is typed, an unknown one does not read; relaunch and crash restore tested in windows
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d9dadb3d0e
commit
e1b889cb7f
4 files changed
+229
-68
No files matched your search
@@ -21,7 +21,7 @@ use crate::view::{Mode, Place};
|
||||
pub struct Kept {
|
||||
path: Option<PathBuf>,
|
||||
name: String,
|
||||
mode: String,
|
||||
mode: Mode,
|
||||
place: Option<Place>,
|
||||
slot: String,
|
||||
}
|
||||
@@ -219,7 +219,7 @@ impl Desk {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mode = Mode::parse(&kept.mode).unwrap_or(Mode::Visual);
|
||||
let mode = kept.mode;
|
||||
log::info!("mark: restores tab {id} as {}", mode.name());
|
||||
let tab = self.tab_mut(id).expect("open");
|
||||
// A recovered copy keeps writing to the same points.
|
||||
@@ -262,7 +262,7 @@ impl Desk {
|
||||
kept.push(Kept {
|
||||
path: tab.session.path().map(Path::to_path_buf),
|
||||
name: tab.name.to_string(),
|
||||
mode: view.mode.name().to_owned(),
|
||||
mode: view.mode,
|
||||
place: Some(view.place(cx)),
|
||||
slot: tab.extra.slot.clone(),
|
||||
});
|
||||
@@ -405,66 +405,4 @@ fn humantime(at: SystemTime) -> String {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn a_place_without_a_column_or_top_reads_zero() {
|
||||
let kept: super::Kept = serde_json::from_str(
|
||||
r#"{"path":null,"name":"a.md","mode":"source","place":{"line":3,"scroll":0},"slot":"s"}"#,
|
||||
)
|
||||
.expect("an older place reads");
|
||||
let place = kept.place.expect("a place");
|
||||
assert_eq!((place.line, place.column, place.top), (3, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_untitled_copy_goes_to_the_tab_made_for_it() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let kept = |name: &str, mode: &str, slot: &str| super::Kept {
|
||||
path: None,
|
||||
name: name.into(),
|
||||
mode: mode.into(),
|
||||
place: Some(super::Place { line: slot.len() as u64, column: 1, top: 0 }),
|
||||
slot: slot.into(),
|
||||
};
|
||||
let tabs = vec![kept("Untitled1", "visual", "a"), kept("Untitled2", "source", "bb")];
|
||||
for (slot, text) in [("a", "first"), ("bb", "second")] {
|
||||
let mut points = super::Recovery::new(super::recovery(dir.path(), slot));
|
||||
points.write_once(text.as_bytes(), None, std::time::SystemTime::now()).expect("a point");
|
||||
}
|
||||
let saved = serde_json::to_vec(&super::Saved { tabs: tabs.clone() }).expect("a list");
|
||||
std::fs::write(dir.path().join("session.json"), saved).expect("the list");
|
||||
// A pid no process has: the last run crashed.
|
||||
std::fs::write(dir.path().join("session.lock"), "open 4294967295").expect("a lock");
|
||||
let (mut keeping, ..) = super::Keeping::start(dir.path()).expect("a session");
|
||||
let made: Vec<_> = keeping.untitled();
|
||||
assert_eq!(made, [(0, "Untitled1".to_string()), (1, "Untitled2".to_string())]);
|
||||
for (at, _) in made {
|
||||
keeping.made(at, 10 + at as u64);
|
||||
}
|
||||
assert!(keeping.take(None, 12).is_none(), "a new tab takes nothing");
|
||||
// The second tab shows first: each still takes its own.
|
||||
for (id, want, text) in [(11, &tabs[1], "second"), (10, &tabs[0], "first")] {
|
||||
let got = keeping.take(None, id).expect("its copy");
|
||||
assert_eq!((&got.kept, got.text.map(|(text, _)| text)), (want, Some(text.to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unasked_end_keeps_the_lock_and_points_past_release() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let point = dir.path().join("recovery/s/point");
|
||||
let (lock, slot) = (dir.path().join("session.lock"), point.parent().expect("a slot"));
|
||||
let (mut held, ..) = super::Keeping::start(dir.path()).expect("a session");
|
||||
std::fs::create_dir_all(slot).expect("a slot");
|
||||
std::fs::write(&point, "unsaved").expect("a point");
|
||||
held.hold();
|
||||
held.close();
|
||||
assert!(lock.exists() && point.exists(), "release after quit wiped them");
|
||||
drop(held);
|
||||
let (mut clean, ..) = super::Keeping::start(dir.path()).expect("a session");
|
||||
std::fs::create_dir_all(slot).expect("a slot");
|
||||
std::fs::write(&point, "unsaved").expect("a point");
|
||||
clean.close();
|
||||
assert!(!lock.exists() && !point.exists(), "a clean end clears");
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
@@ -0,0 +1,222 @@
|
||||
use crate::view::Mode;
|
||||
|
||||
#[test]
|
||||
fn a_place_without_a_column_or_top_reads_zero() {
|
||||
let kept: super::Kept = serde_json::from_str(
|
||||
r#"{"path":null,"name":"a.md","mode":"source","place":{"line":3,"scroll":0},"slot":"s"}"#,
|
||||
)
|
||||
.expect("an older place reads");
|
||||
let place = kept.place.expect("a place");
|
||||
assert_eq!((place.line, place.column, place.top), (3, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_untitled_copy_goes_to_the_tab_made_for_it() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let kept = |name: &str, mode: Mode, slot: &str| super::Kept {
|
||||
path: None,
|
||||
name: name.into(),
|
||||
mode,
|
||||
place: Some(super::Place {
|
||||
line: slot.len() as u64,
|
||||
column: 1,
|
||||
top: 0,
|
||||
}),
|
||||
slot: slot.into(),
|
||||
};
|
||||
let tabs = vec![
|
||||
kept("Untitled1", Mode::Visual, "a"),
|
||||
kept("Untitled2", Mode::Source, "bb"),
|
||||
];
|
||||
for (slot, text) in [("a", "first"), ("bb", "second")] {
|
||||
let mut points = super::Recovery::new(super::recovery(dir.path(), slot));
|
||||
points
|
||||
.write_once(text.as_bytes(), None, std::time::SystemTime::now())
|
||||
.expect("a point");
|
||||
}
|
||||
let saved = serde_json::to_vec(&super::Saved { tabs: tabs.clone() }).expect("a list");
|
||||
std::fs::write(dir.path().join("session.json"), saved).expect("the list");
|
||||
// A pid no process has: the last run crashed.
|
||||
std::fs::write(dir.path().join("session.lock"), "open 4294967295").expect("a lock");
|
||||
let (mut keeping, ..) = super::Keeping::start(dir.path()).expect("a session");
|
||||
let made: Vec<_> = keeping.untitled();
|
||||
assert_eq!(
|
||||
made,
|
||||
[(0, "Untitled1".to_string()), (1, "Untitled2".to_string())]
|
||||
);
|
||||
for (at, _) in made {
|
||||
keeping.made(at, 10 + at as u64);
|
||||
}
|
||||
assert!(keeping.take(None, 12).is_none(), "a new tab takes nothing");
|
||||
// The second tab shows first: each still takes its own.
|
||||
for (id, want, text) in [(11, &tabs[1], "second"), (10, &tabs[0], "first")] {
|
||||
let got = keeping.take(None, id).expect("its copy");
|
||||
assert_eq!(
|
||||
(&got.kept, got.text.map(|(text, _)| text)),
|
||||
(want, Some(text.to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unasked_end_keeps_the_lock_and_points_past_release() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let point = dir.path().join("recovery/s/point");
|
||||
let (lock, slot) = (
|
||||
dir.path().join("session.lock"),
|
||||
point.parent().expect("a slot"),
|
||||
);
|
||||
let (mut held, ..) = super::Keeping::start(dir.path()).expect("a session");
|
||||
std::fs::create_dir_all(slot).expect("a slot");
|
||||
std::fs::write(&point, "unsaved").expect("a point");
|
||||
held.hold();
|
||||
held.close();
|
||||
assert!(
|
||||
lock.exists() && point.exists(),
|
||||
"release after quit wiped them"
|
||||
);
|
||||
drop(held);
|
||||
let (mut clean, ..) = super::Keeping::start(dir.path()).expect("a session");
|
||||
std::fs::create_dir_all(slot).expect("a slot");
|
||||
std::fs::write(&point, "unsaved").expect("a point");
|
||||
clean.close();
|
||||
assert!(!lock.exists() && !point.exists(), "a clean end clears");
|
||||
}
|
||||
|
||||
mod window {
|
||||
use gpui::{Entity, TestAppContext, VisualTestContext};
|
||||
|
||||
use crate::desk::{
|
||||
Desk,
|
||||
tests::{open, start},
|
||||
};
|
||||
use crate::view::{MarkView, Mode};
|
||||
|
||||
/// Shows the tab named `name`; its view.
|
||||
fn show(desk: &Entity<Desk>, name: &str, cx: &mut VisualTestContext) -> Entity<MarkView> {
|
||||
let view = desk.update(cx, |desk, cx| {
|
||||
let tab = desk.core.tabs.iter().find(|tab| tab.name.as_ref() == name);
|
||||
let tab = tab.unwrap_or_else(|| panic!("no tab {name}"));
|
||||
let (id, view) = (tab.id, tab.view.clone());
|
||||
desk.core.active = Some(id);
|
||||
cx.notify();
|
||||
view
|
||||
});
|
||||
cx.run_until_parked();
|
||||
view
|
||||
}
|
||||
|
||||
fn names(desk: &Entity<Desk>, cx: &mut VisualTestContext) -> Vec<String> {
|
||||
desk.read_with(cx, |desk, _| {
|
||||
desk.core
|
||||
.tabs
|
||||
.iter()
|
||||
.map(|tab| tab.name.to_string())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_new_launch_brings_back_tabs_modes_places_and_look(
|
||||
first: &mut TestAppContext,
|
||||
second: &mut TestAppContext,
|
||||
) {
|
||||
let (data, files) = (
|
||||
tempfile::tempdir().expect("data"),
|
||||
tempfile::tempdir().expect("files"),
|
||||
);
|
||||
let long: String = (0..80).map(|n| format!("Line {n}\n\n")).collect();
|
||||
let (a, b) = (files.path().join("a.md"), files.path().join("b.md"));
|
||||
std::fs::write(&a, &long).expect("a");
|
||||
std::fs::write(&b, "# B\n").expect("b");
|
||||
let (desk, cx) = start(first, data.path());
|
||||
open(&desk, &a, cx);
|
||||
open(&desk, &b, cx);
|
||||
let view = show(&desk, "a.md", cx);
|
||||
view.update_in(cx, |view, window, cx| {
|
||||
view.switch(Mode::Source, window, cx);
|
||||
view.document().update(cx, |document, cx| {
|
||||
document.set_caret(40, 2, cx);
|
||||
document.set_top_line(30, window, cx);
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
desk.update(cx, |desk, cx| {
|
||||
desk.relook(|look| look.focus = true, cx);
|
||||
desk.leave(cx);
|
||||
desk.keeping.close();
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let (desk, cx) = start(second, data.path());
|
||||
assert_eq!(names(&desk, cx), ["a.md", "b.md"]);
|
||||
let view = show(&desk, "a.md", cx);
|
||||
let (mode, place) = view.read_with(cx, |view, cx| (view.mode, view.place(cx)));
|
||||
assert_eq!(mode, Mode::Source);
|
||||
assert_eq!((place.line, place.column, place.top), (40, 2, 30));
|
||||
assert!(
|
||||
desk.read_with(cx, |desk, _| desk.look.focus),
|
||||
"the look came back"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_crash_brings_unsaved_text_back_as_a_copy(
|
||||
first: &mut TestAppContext,
|
||||
second: &mut TestAppContext,
|
||||
) {
|
||||
let (data, files) = (
|
||||
tempfile::tempdir().expect("data"),
|
||||
tempfile::tempdir().expect("files"),
|
||||
);
|
||||
let a = files.path().join("a.md");
|
||||
std::fs::write(&a, "Text.\n").expect("a");
|
||||
let (desk, cx) = start(first, data.path());
|
||||
open(&desk, &a, cx);
|
||||
let view = show(&desk, "a.md", cx);
|
||||
view.update(cx, |view, cx| view.adopt("Changed.\n".into(), cx));
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
desk.update(cx, |desk, cx| desk.keep(cx));
|
||||
cx.run_until_parked();
|
||||
// A pid no process has: this run crashed.
|
||||
std::fs::write(data.path().join("session.lock"), "open 4294967295").expect("a lock");
|
||||
|
||||
let (desk, cx) = start(second, data.path());
|
||||
assert_eq!(names(&desk, cx).len(), 2, "{:?}", names(&desk, cx));
|
||||
let copy = desk.read_with(cx, |desk, _| {
|
||||
let tab = desk
|
||||
.core
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|tab| tab.session.path().is_none());
|
||||
tab.expect("an untitled copy").name.to_string()
|
||||
});
|
||||
show(&desk, ©, cx);
|
||||
let (text, dirty) = desk.read_with(cx, |desk, cx| {
|
||||
let tab = desk
|
||||
.core
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|tab| tab.name.as_ref() == copy)
|
||||
.expect("the copy");
|
||||
(desk.text_of(tab.id).expect("text"), tab.dirty(cx))
|
||||
});
|
||||
assert_eq!(text, "Changed.\n");
|
||||
assert!(dirty, "the copy is unsaved");
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&a).expect("reads"),
|
||||
"Text.\n",
|
||||
"the file stays"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_kept_mode_no_build_knows_does_not_read() {
|
||||
let read = serde_json::from_str::<super::super::Kept>(
|
||||
r#"{"path":null,"name":"a.md","mode":"split","place":null,"slot":"s"}"#,
|
||||
);
|
||||
assert!(read.is_err(), "an unknown mode is no default");
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,8 @@ pub struct Reported;
|
||||
pub struct Failed(pub String);
|
||||
|
||||
/// How a document shows (MARK-02).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Mode {
|
||||
Visual,
|
||||
Source,
|
||||
|
||||
Reference in new issue
Block a user