authorclover caruso <git@paperclover.net> 2026-10-10 21:32:55-07:00
committerclover caruso <git@paperclover.net> 2026-10-10 22:56:06-07:00
log96172e13a7a316a5fc038b61aa836d3f91d8a837
tree4c9b8891cb0a35749d0ecc26a603796398983fc6
parentb28c7cbe0b6bd7420e717e2005265ecbea3a37de
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

fix: keep Undo and Redo history intact when recycling is busy

Carry the removed action position through preflight and restore the original action to its original stack when structural ownership is unavailable. A retry must remain behind newer edits on other pages. Restoration happens only before work starts; uncertain asynchronous publication is not replayed. Cover contention and exact history ordering for both Undo and Redo. Fixes #119 Assisted-by: gpt-6.1-sol

1 files changed, 86 insertions(+), 23 deletions(-)

crates/snowbound/src/undo.rs+86-23
......@@ -145,7 +145,8 @@ impl Action {
145145pub enum Next {
146146 /// The open page's last edit, or the last undone.
147147 Edit,
148 Action(Action),
148 /// An action and its original position until preflight accepts it.
149 Action(Action, usize),
149150 /// The page a created page Undo would take out still holds edits made since, so
150151 /// Undo shows it, where those go back first.
151152 Show(ExGuid),
......@@ -168,6 +169,12 @@ impl Timeline {
168169 }
169170 }
170171
172 /// Restores a refused action before newer edits on other pages, in its original stack.
173 fn put_back(&mut self, (redo, at): (bool, usize), action: Action) {
174 let steps = if redo { &mut self.redo } else { &mut self.undo };
175 steps.insert(at, Step::Action(action));
176 }
177
171178 /// Follows page `page`'s history, `depth` steps of Undo and Redo, after an edit or
172179 /// before one is taken: steps it gained are the newest; `run`, a run of typing that
173180 /// went on, makes its last step the newest.
......@@ -232,7 +239,7 @@ impl Timeline {
232239 Step::Action(_) => {}
233240 }
234241 match steps.remove(at) {
235 Step::Action(action) => Some(Next::Action(action)),
242 Step::Action(action) => Some(Next::Action(action, at)),
236243 Step::Edit(_) => unreachable!("an edit is not taken off"),
237244 }
238245 }
......@@ -988,7 +995,7 @@ impl State {
988995 }
989996 }
990997 Some(Next::Show(page)) => self.commands.push(Command::OpenPage(page)),
991 Some(Next::Action(action)) => self.perform(action, Some(redo))?,
998 Some(Next::Action(action, at)) => self.perform(action, Some((redo, at)))?,
992999 }
9931000 Ok(())
9941001 }
......@@ -998,7 +1005,7 @@ impl State {
9981005 pub(crate) fn perform(
9991006 &mut self,
10001007 action: Action,
1001 taken: Option<bool>,
1008 taken: Option<(bool, usize)>,
10021009 ) -> Result<(), Box<dyn Error>> {
10031010 let Action {
10041011 notebook,
......@@ -1014,7 +1021,7 @@ impl State {
10141021 .ok_or("That notebook is closed")?;
10151022 if !holds(&library.themes(), &from) {
10161023 return match taken {
1017 Some(redo) => self.take(redo),
1024 Some((redo, _)) => self.take(redo),
10181025 None => Ok(()),
10191026 };
10201027 }
......@@ -1038,7 +1045,7 @@ impl State {
10381045 section,
10391046 change: Change::Themes { from: to, to: from },
10401047 };
1041 self.undo.note(taken, undo);
1048 self.undo.note(taken.map(|(redo, _)| redo), undo);
10421049 return self.wear_theme(true);
10431050 }
10441051 let Some(section) = section else {
......@@ -1050,11 +1057,11 @@ impl State {
10501057 .ok_or("That notebook is closed")?;
10511058 let Some((structure, undo)) = restructuring(&library, &change) else {
10521059 return match taken {
1053 Some(redo) => self.take(redo),
1060 Some((redo, _)) => self.take(redo),
10541061 None => Ok(()),
10551062 };
10561063 };
1057 self.undo.note(taken, undo);
1064 self.undo.note(taken.map(|(redo, _)| redo), undo);
10581065 self.restructure(library, structure);
10591066 return Ok(());
10601067 };
......@@ -1065,13 +1072,20 @@ impl State {
10651072 Change::Delete { created: false, .. } | Change::Restore { created: false, .. }
10661073 ) && matches!(session.library.notebook, Ok(Some(_)))
10671074 {
1068 Some(
1069 session
1070 .library
1071 .structure
1072 .try_lock_arc()
1073 .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::WouldBlock))?,
1074 )
1075 let Some(changing) = session.library.structure.try_lock_arc() else {
1076 if let Some(taken) = taken {
1077 self.undo.put_back(
1078 taken,
1079 Action {
1080 notebook,
1081 section: Some(section),
1082 change,
1083 },
1084 );
1085 }
1086 return Err(std::io::Error::from(std::io::ErrorKind::WouldBlock).into());
1087 };
1088 Some(changing)
10751089 } else {
10761090 None
10771091 };
......@@ -1125,7 +1139,7 @@ impl State {
11251139 let Some(applied) = applied else {
11261140 // What it would change is gone: Undo goes on to the step before.
11271141 return match taken {
1128 Some(redo) if stale => {
1142 Some((redo, _)) if stale => {
11291143 state.undo.waiting.push_front(redo);
11301144 Ok(())
11311145 }
......@@ -1138,7 +1152,7 @@ impl State {
11381152 section: Some(section),
11391153 change: applied.undo,
11401154 };
1141 state.undo.note(taken, undo);
1155 state.undo.note(taken.map(|(redo, _)| redo), undo);
11421156 state.edited(Vec::new());
11431157 Ok(())
11441158 })));
......@@ -1207,7 +1221,7 @@ mod tests {
12071221 }
12081222
12091223 fn is_action(next: Option<Next>) -> bool {
1210 matches!(next, Some(Next::Action(_)))
1224 matches!(next, Some(Next::Action(..)))
12111225 }
12121226
12131227 fn is_edit(next: Option<Next>) -> bool {
......@@ -1288,10 +1302,13 @@ mod tests {
12881302 assert!(!timeline.reaches(false, &other));
12891303 assert!(matches!(
12901304 timeline.next(false, page(7), [0, 0], &elsewhere),
1291 Some(Next::Action(Action {
1292 change: Change::Rename { .. },
1293 ..
1294 }))
1305 Some(Next::Action(
1306 Action {
1307 change: Change::Rename { .. },
1308 ..
1309 },
1310 _
1311 ))
12951312 ));
12961313 assert!(timeline.next(false, page(7), [0, 0], &elsewhere).is_none());
12971314 assert!(is_action(timeline.next(false, page(7), [0, 0], &here())));
......@@ -1306,7 +1323,7 @@ mod tests {
13061323 let mut timeline = Timeline::default();
13071324 let shown = page(1);
13081325 timeline.record(created(page(2)));
1309 let Some(Next::Action(action)) = timeline.next(false, shown, [0, 0], &here()) else {
1326 let Some(Next::Action(action, _)) = timeline.next(false, shown, [0, 0], &here()) else {
13101327 panic!()
13111328 };
13121329 timeline.note(Some(false), action);
......@@ -1320,6 +1337,52 @@ mod tests {
13201337 assert!(!timeline.reaches(true, &here()));
13211338 }
13221339
1340 #[test]
1341 fn contended_page_actions_keep_their_position_in_undo_and_redo() {
1342 for redo in [false, true] {
1343 let mut timeline = Timeline::default();
1344 let action = Action {
1345 change: Change::Delete {
1346 pages: vec![page(2)],
1347 show: None,
1348 created: false,
1349 },
1350 ..created(page(2))
1351 };
1352 let steps = if redo {
1353 &mut timeline.redo
1354 } else {
1355 &mut timeline.undo
1356 };
1357 steps.push(Step::Action(action));
1358 steps.push(Step::Edit(page(3)));
1359 let Some(Next::Action(action, at)) = timeline.next(redo, page(1), [0, 0], &here())
1360 else {
1361 panic!("the current page has a pending action");
1362 };
1363 let structure = Arc::new(async_lock::Mutex::new(()));
1364 let changing = structure.try_lock_arc().unwrap();
1365 assert!(structure.try_lock_arc().is_none());
1366 timeline.put_back((redo, at), action);
1367 let depth = if redo { [0, 1] } else { [1, 0] };
1368 assert!(
1369 is_edit(timeline.next(redo, page(3), depth, &here())),
1370 "newer edit stays first"
1371 );
1372 timeline.stepped(page(3), redo);
1373 drop(changing);
1374 let _changing = structure.try_lock_arc().unwrap();
1375 let Some(Next::Action(action, at)) = timeline.next(redo, page(1), [0, 0], &here())
1376 else {
1377 panic!("the refused action must remain in its original stack");
1378 };
1379 assert_eq!(at, 0);
1380 assert!(
1381 matches!(action.change, Change::Delete { pages, created: false, .. } if pages == [page(2)])
1382 );
1383 }
1384 }
1385
13231386 const AUTHOR: &str = "Rust Author";
13241387
13251388 fn dated() -> PageCreation {