| ... | @@ -145,7 +145,8 @@ impl Action { | ... | @@ -145,7 +145,8 @@ impl Action { |
| 145 | pub enum Next { | 145 | pub 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, so | 150 | /// 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 | } |
| 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 | /// Follows page `page`'s history, `depth` steps of Undo and Redo, after an edit or | 178 | /// 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 that | 179 | /// 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 | session | 1076 | if let Some(taken) = taken { |
| 1070 | .library | 1077 | self.undo.put_back( |
| 1071 | .structure | 1078 | 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 | None | 1090 | 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 | } |
| 1208 | | 1222 | |
| 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 | } |
| 1212 | | 1226 | |
| 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 | } |
| 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 | const AUTHOR: &str = "Rust Author"; | 1386 | const AUTHOR: &str = "Rust Author"; |
| 1324 | | 1387 | |
| 1325 | fn dated() -> PageCreation { | 1388 | fn dated() -> PageCreation { |