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 {...@@ -145,7 +145,8 @@ impl Action {
145pub enum Next {145pub enum Next {
146 /// The open page's last edit, or the last undone.146 /// The open page's last edit, or the last undone.
147 Edit,147 Edit,
148 Action(Action),148 /// An action and its original position until preflight accepts it.
149 Action(Action, usize),
149 /// The page a created page Undo would take out still holds edits made since, so150 /// The page a created page Undo would take out still holds edits made since, so
150 /// Undo shows it, where those go back first.151 /// Undo shows it, where those go back first.
151 Show(ExGuid),152 Show(ExGuid),
...@@ -168,6 +169,12 @@ impl Timeline {...@@ -168,6 +169,12 @@ impl Timeline {
168 }169 }
169 }170 }
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
171 /// Follows page `page`'s history, `depth` steps of Undo and Redo, after an edit or178 /// Follows page `page`'s history, `depth` steps of Undo and Redo, after an edit or
172 /// before one is taken: steps it gained are the newest; `run`, a run of typing that179 /// before one is taken: steps it gained are the newest; `run`, a run of typing that
173 /// went on, makes its last step the newest.180 /// went on, makes its last step the newest.
...@@ -232,7 +239,7 @@ impl Timeline {...@@ -232,7 +239,7 @@ impl Timeline {
232 Step::Action(_) => {}239 Step::Action(_) => {}
233 }240 }
234 match steps.remove(at) {241 match steps.remove(at) {
235 Step::Action(action) => Some(Next::Action(action)),242 Step::Action(action) => Some(Next::Action(action, at)),
236 Step::Edit(_) => unreachable!("an edit is not taken off"),243 Step::Edit(_) => unreachable!("an edit is not taken off"),
237 }244 }
238 }245 }
...@@ -988,7 +995,7 @@ impl State {...@@ -988,7 +995,7 @@ impl State {
988 }995 }
989 }996 }
990 Some(Next::Show(page)) => self.commands.push(Command::OpenPage(page)),997 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)))?,
992 }999 }
993 Ok(())1000 Ok(())
994 }1001 }
...@@ -998,7 +1005,7 @@ impl State {...@@ -998,7 +1005,7 @@ impl State {
998 pub(crate) fn perform(1005 pub(crate) fn perform(
999 &mut self,1006 &mut self,
1000 action: Action,1007 action: Action,
1001 taken: Option<bool>,1008 taken: Option<(bool, usize)>,
1002 ) -> Result<(), Box<dyn Error>> {1009 ) -> Result<(), Box<dyn Error>> {
1003 let Action {1010 let Action {
1004 notebook,1011 notebook,
...@@ -1014,7 +1021,7 @@ impl State {...@@ -1014,7 +1021,7 @@ impl State {
1014 .ok_or("That notebook is closed")?;1021 .ok_or("That notebook is closed")?;
1015 if !holds(&library.themes(), &from) {1022 if !holds(&library.themes(), &from) {
1016 return match taken {1023 return match taken {
1017 Some(redo) => self.take(redo),1024 Some((redo, _)) => self.take(redo),
1018 None => Ok(()),1025 None => Ok(()),
1019 };1026 };
1020 }1027 }
...@@ -1038,7 +1045,7 @@ impl State {...@@ -1038,7 +1045,7 @@ impl State {
1038 section,1045 section,
1039 change: Change::Themes { from: to, to: from },1046 change: Change::Themes { from: to, to: from },
1040 };1047 };
1041 self.undo.note(taken, undo);1048 self.undo.note(taken.map(|(redo, _)| redo), undo);
1042 return self.wear_theme(true);1049 return self.wear_theme(true);
1043 }1050 }
1044 let Some(section) = section else {1051 let Some(section) = section else {
...@@ -1050,11 +1057,11 @@ impl State {...@@ -1050,11 +1057,11 @@ impl State {
1050 .ok_or("That notebook is closed")?;1057 .ok_or("That notebook is closed")?;
1051 let Some((structure, undo)) = restructuring(&library, &change) else {1058 let Some((structure, undo)) = restructuring(&library, &change) else {
1052 return match taken {1059 return match taken {
1053 Some(redo) => self.take(redo),1060 Some((redo, _)) => self.take(redo),
1054 None => Ok(()),1061 None => Ok(()),
1055 };1062 };
1056 };1063 };
1057 self.undo.note(taken, undo);1064 self.undo.note(taken.map(|(redo, _)| redo), undo);
1058 self.restructure(library, structure);1065 self.restructure(library, structure);
1059 return Ok(());1066 return Ok(());
1060 };1067 };
...@@ -1065,13 +1072,20 @@ impl State {...@@ -1065,13 +1072,20 @@ impl State {
1065 Change::Delete { created: false, .. } | Change::Restore { created: false, .. }1072 Change::Delete { created: false, .. } | Change::Restore { created: false, .. }
1066 ) && matches!(session.library.notebook, Ok(Some(_)))1073 ) && matches!(session.library.notebook, Ok(Some(_)))
1067 {1074 {
1068 Some(1075 let Some(changing) = session.library.structure.try_lock_arc() else {
1069 session1076 if let Some(taken) = taken {
1070 .library1077 self.undo.put_back(
1071 .structure1078 taken,
1072 .try_lock_arc()1079 Action {
1073 .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::WouldBlock))?,1080 notebook,
1074 )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)
1075 } else {1089 } else {
1076 None1090 None
1077 };1091 };
...@@ -1125,7 +1139,7 @@ impl State {...@@ -1125,7 +1139,7 @@ impl State {
1125 let Some(applied) = applied else {1139 let Some(applied) = applied else {
1126 // What it would change is gone: Undo goes on to the step before.1140 // What it would change is gone: Undo goes on to the step before.
1127 return match taken {1141 return match taken {
1128 Some(redo) if stale => {1142 Some((redo, _)) if stale => {
1129 state.undo.waiting.push_front(redo);1143 state.undo.waiting.push_front(redo);
1130 Ok(())1144 Ok(())
1131 }1145 }
...@@ -1138,7 +1152,7 @@ impl State {...@@ -1138,7 +1152,7 @@ impl State {
1138 section: Some(section),1152 section: Some(section),
1139 change: applied.undo,1153 change: applied.undo,
1140 };1154 };
1141 state.undo.note(taken, undo);1155 state.undo.note(taken.map(|(redo, _)| redo), undo);
1142 state.edited(Vec::new());1156 state.edited(Vec::new());
1143 Ok(())1157 Ok(())
1144 })));1158 })));
...@@ -1207,7 +1221,7 @@ mod tests {...@@ -1207,7 +1221,7 @@ mod tests {
1207 }1221 }
12081222
1209 fn is_action(next: Option<Next>) -> bool {1223 fn is_action(next: Option<Next>) -> bool {
1210 matches!(next, Some(Next::Action(_)))1224 matches!(next, Some(Next::Action(..)))
1211 }1225 }
12121226
1213 fn is_edit(next: Option<Next>) -> bool {1227 fn is_edit(next: Option<Next>) -> bool {
...@@ -1288,10 +1302,13 @@ mod tests {...@@ -1288,10 +1302,13 @@ mod tests {
1288 assert!(!timeline.reaches(false, &other));1302 assert!(!timeline.reaches(false, &other));
1289 assert!(matches!(1303 assert!(matches!(
1290 timeline.next(false, page(7), [0, 0], &elsewhere),1304 timeline.next(false, page(7), [0, 0], &elsewhere),
1291 Some(Next::Action(Action {1305 Some(Next::Action(
1292 change: Change::Rename { .. },1306 Action {
1293 ..1307 change: Change::Rename { .. },
1294 }))1308 ..
1309 },
1310 _
1311 ))
1295 ));1312 ));
1296 assert!(timeline.next(false, page(7), [0, 0], &elsewhere).is_none());1313 assert!(timeline.next(false, page(7), [0, 0], &elsewhere).is_none());
1297 assert!(is_action(timeline.next(false, page(7), [0, 0], &here())));1314 assert!(is_action(timeline.next(false, page(7), [0, 0], &here())));
...@@ -1306,7 +1323,7 @@ mod tests {...@@ -1306,7 +1323,7 @@ mod tests {
1306 let mut timeline = Timeline::default();1323 let mut timeline = Timeline::default();
1307 let shown = page(1);1324 let shown = page(1);
1308 timeline.record(created(page(2)));1325 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 {
1310 panic!()1327 panic!()
1311 };1328 };
1312 timeline.note(Some(false), action);1329 timeline.note(Some(false), action);
...@@ -1320,6 +1337,52 @@ mod tests {...@@ -1320,6 +1337,52 @@ mod tests {
1320 assert!(!timeline.reaches(true, &here()));1337 assert!(!timeline.reaches(true, &here()));
1321 }1338 }
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
1323 const AUTHOR: &str = "Rust Author";1386 const AUTHOR: &str = "Rust Author";
13241387
1325 fn dated() -> PageCreation {1388 fn dated() -> PageCreation {