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