| author | |
| committer | |
| log | 282b96db3198a95fdb61aa5d1977a7b2c9e05828 |
| tree | e185b1a8f6ab5754b33628ca4682ffaee1635137 |
| parent | 6bc7028096a351aa0348bcbf6d76425c5a611103 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
The settings kept the pages shown lately, but a launch opened the shown
notebook at its first section and page, and so did every section and
notebook opened after. The launch now shows the latest of those pages in the
shown notebook, and sections and notebooks opened later return to theirs.
Assisted-by: claude-opus-5.54 files changed, 81 insertions(+), 17 deletions(-)
crates/snowbound/src/main.rs+16-5| ... | @@ -1039,23 +1039,32 @@ impl State { | ... | @@ -1039,23 +1039,32 @@ impl State { |
| 1039 | library.purge_recycle_bin(); | 1039 | library.purge_recycle_bin(); |
| 1040 | } | 1040 | } |
| 1041 | } | 1041 | } |
| 1042 | // Where the notebook was left, or its first section. | ||
| 1042 | let shown = notebooks | 1043 | let shown = notebooks |
| 1043 | .iter() | 1044 | .iter() |
| 1044 | .filter(|library| Some(&library.location) == current.as_ref()) | 1045 | .filter(|library| Some(&library.location) == current.as_ref()) |
| 1045 | .chain(&notebooks) | 1046 | .chain(&notebooks) |
| 1046 | .find_map(|library| Some((library, library.first_section()?))); | 1047 | .find_map(|library| { |
| 1048 | let left = stored.recent.iter().find(|place| { | ||
| 1049 | place.notebook == library.location && library.contains(&place.section) | ||
| 1050 | }); | ||
| 1051 | match left { | ||
| 1052 | Some(place) => Some((library, place.section.clone(), Some(place.page))), | ||
| 1053 | None => Some((library, library.first_section()?, None)), | ||
| 1054 | } | ||
| 1055 | }); | ||
| 1047 | match shown { | 1056 | match shown { |
| 1048 | Some((library, path)) if !library.locked(&path) => { | 1057 | Some((library, path, left)) if !library.locked(&path) => { |
| 1049 | let section = library.open(&path, notify(proxy.clone()))?; | 1058 | let section = library.open(&path, notify(proxy.clone()))?; |
| 1050 | let (opened, page) = | 1059 | let (opened, page) = |
| 1051 | read_session(section, Arc::clone(library), path, None)?; | 1060 | read_session(section, Arc::clone(library), path, left)?; |
| 1052 | let (scene, editor) = PageScene::from_page(page, &mut engine)?; | 1061 | let (scene, editor) = PageScene::from_page(page, &mut engine)?; |
| 1053 | session = Some(opened); | 1062 | session = Some(opened); |
| 1054 | (editor, Some((scene, [0.0; 2]))) | 1063 | (editor, Some((scene, [0.0; 2]))) |
| 1055 | } | 1064 | } |
| 1056 | shown => { | 1065 | shown => { |
| 1057 | // A first section still locked shows its locked page. | 1066 | // A first section still locked shows its locked page. |
| 1058 | locked = shown.map(|(library, path)| protection::Locked { | 1067 | locked = shown.map(|(library, path, _)| protection::Locked { |
| 1059 | library: Arc::clone(library), | 1068 | library: Arc::clone(library), |
| 1060 | path, | 1069 | path, |
| 1061 | }); | 1070 | }); |
| ... | @@ -1228,7 +1237,9 @@ impl State { | ... | @@ -1228,7 +1237,9 @@ impl State { |
| 1228 | opening: None, | 1237 | opening: None, |
| 1229 | switching: None, | 1238 | switching: None, |
| 1230 | places: HashMap::new(), | 1239 | places: HashMap::new(), |
| 1231 | last_pages: HashMap::new(), | 1240 | last_pages: (stored.recent.iter().rev()) |
| 1241 | .map(|place| (library::key(&place.notebook, &place.section), place.page)) | ||
| 1242 | .collect(), | ||
| 1232 | trail: { | 1243 | trail: { |
| 1233 | let mut trail = navigation::Trail::default(); | 1244 | let mut trail = navigation::Trail::default(); |
| 1234 | trail.recent = stored.recent; | 1245 | trail.recent = stored.recent; |
crates/snowbound/src/settings.rs+1-1| ... | @@ -46,7 +46,7 @@ pub struct Settings { | ... | @@ -46,7 +46,7 @@ pub struct Settings { |
| 46 | pub servers: Vec<String>, | 46 | pub servers: Vec<String>, |
| 47 | /// Chords set in Options' Keyboard, by command, each list in place of its defaults. | 47 | /// Chords set in Options' Keyboard, by command, each list in place of its defaults. |
| 48 | pub keys: std::collections::BTreeMap<String, Vec<String>>, | 48 | pub keys: std::collections::BTreeMap<String, Vec<String>>, |
| 49 | /// Pages shown lately, latest first, which the palette lists first. | 49 | /// Pages shown lately, latest first, which the palette lists first and a launch returns to. |
| 50 | pub recent: Vec<crate::navigation::Place>, | 50 | pub recent: Vec<crate::navigation::Place>, |
| 51 | pub passwords: Passwords, | 51 | pub passwords: Passwords, |
| 52 | } | 52 | } |
crates/snowbound/src/sidebar.rs+15-5| ... | @@ -1100,8 +1100,8 @@ impl crate::State { | ... | @@ -1100,8 +1100,8 @@ impl crate::State { |
| 1100 | self.read_notebook(location, section, open, read); | 1100 | self.read_notebook(location, section, open, read); |
| 1101 | } | 1101 | } |
| 1102 | 1102 | ||
| 1103 | /// Shows `section`, or the first section, of the notebook at `location`, `open` or read | 1103 | /// Shows `section`, or where it was left, or the first section, of the notebook at |
| 1104 | /// with `read`, in place of the notebook listed there. | 1104 | /// `location`, `open` or read with `read`, in place of the notebook listed there. |
| 1105 | pub(crate) fn read_notebook( | 1105 | pub(crate) fn read_notebook( |
| 1106 | &mut self, | 1106 | &mut self, |
| 1107 | location: String, | 1107 | location: String, |
| ... | @@ -1109,6 +1109,15 @@ impl crate::State { | ... | @@ -1109,6 +1109,15 @@ impl crate::State { |
| 1109 | open: Option<Arc<Library>>, | 1109 | open: Option<Arc<Library>>, |
| 1110 | read: impl FnOnce(&str, &std::path::Path) -> Result<Library, String> + Send + 'static, | 1110 | read: impl FnOnce(&str, &std::path::Path) -> Result<Library, String> + Send + 'static, |
| 1111 | ) { | 1111 | ) { |
| 1112 | let section = section.or_else(|| { | ||
| 1113 | (self.trail.recent.iter()) | ||
| 1114 | .find(|place| place.notebook == location) | ||
| 1115 | .map(|place| place.section.clone()) | ||
| 1116 | }); | ||
| 1117 | let left = section | ||
| 1118 | .as_ref() | ||
| 1119 | .and_then(|path| self.last_pages.get(&crate::library::key(&location, path))) | ||
| 1120 | .copied(); | ||
| 1112 | let (cache, notify) = (self.cache.clone(), crate::notify(self.proxy.clone())); | 1121 | let (cache, notify) = (self.cache.clone(), crate::notify(self.proxy.clone())); |
| 1113 | self.load(move || { | 1122 | self.load(move || { |
| 1114 | let library = match open { | 1123 | let library = match open { |
| ... | @@ -1122,9 +1131,10 @@ impl crate::State { | ... | @@ -1122,9 +1131,10 @@ impl crate::State { |
| 1122 | if let Err(error) = &library.notebook { | 1131 | if let Err(error) = &library.notebook { |
| 1123 | return Err(error.clone().into()); | 1132 | return Err(error.clone().into()); |
| 1124 | } | 1133 | } |
| 1125 | let Some(path) = section | 1134 | let Some((path, left)) = section |
| 1126 | .filter(|path| library.contains(path)) | 1135 | .filter(|path| library.contains(path)) |
| 1127 | .or_else(|| library.first_section()) | 1136 | .map(|path| (path, left)) |
| 1137 | .or_else(|| Some((library.first_section()?, None))) | ||
| 1128 | else { | 1138 | else { |
| 1129 | // Shown once its sections arrive. | 1139 | // Shown once its sections arrive. |
| 1130 | if library.downloading() { | 1140 | if library.downloading() { |
| ... | @@ -1133,7 +1143,7 @@ impl crate::State { | ... | @@ -1133,7 +1143,7 @@ impl crate::State { |
| 1133 | return Err("This folder holds no notebook sections.".into()); | 1143 | return Err("This folder holds no notebook sections.".into()); |
| 1134 | }; | 1144 | }; |
| 1135 | let section = library.open(&path, notify)?; | 1145 | let section = library.open(&path, notify)?; |
| 1136 | let (session, page) = crate::read_session(section, library, path, None)?; | 1146 | let (session, page) = crate::read_session(section, library, path, left)?; |
| 1137 | Ok(crate::Loaded::Section(Box::new(session), page)) | 1147 | Ok(crate::Loaded::Section(Box::new(session), page)) |
| 1138 | }); | 1148 | }); |
| 1139 | } | 1149 | } |
crates/snowbound/tests/replay.rs+49-6| ... | @@ -12,6 +12,11 @@ impl Scratch { | ... | @@ -12,6 +12,11 @@ impl Scratch { |
| 12 | let path = std::env::temp_dir().join(format!("snowbound-{name}-{}", std::process::id())); | 12 | let path = std::env::temp_dir().join(format!("snowbound-{name}-{}", std::process::id())); |
| 13 | let _ = std::fs::remove_dir_all(&path); | 13 | let _ = std::fs::remove_dir_all(&path); |
| 14 | std::fs::create_dir_all(path.join("notebook")).unwrap(); | 14 | std::fs::create_dir_all(path.join("notebook")).unwrap(); |
| 15 | std::fs::write( | ||
| 16 | path.join("settings.json"), | ||
| 17 | r#"{"user_name": "Snowbound Test"}"#, | ||
| 18 | ) | ||
| 19 | .unwrap(); | ||
| 15 | Self(path) | 20 | Self(path) |
| 16 | } | 21 | } |
| 17 | } | 22 | } |
| ... | @@ -22,7 +27,8 @@ impl Drop for Scratch { | ... | @@ -22,7 +27,8 @@ impl Drop for Scratch { |
| 22 | } | 27 | } |
| 23 | } | 28 | } |
| 24 | 29 | ||
| 25 | /// Runs `steps` against a copy of `notebook`'s files, then returns the trees each | 30 | /// Runs `steps` against a copy of `notebook`'s files with the scratch's settings, then |
| 31 | /// returns the trees each | ||
| 26 | /// `accessibility` step named by the trailing word wrote. | 32 | /// `accessibility` step named by the trailing word wrote. |
| 27 | fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec<String> { | 33 | fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec<String> { |
| 28 | let dir = &scratch.0; | 34 | let dir = &scratch.0; |
| ... | @@ -46,11 +52,6 @@ fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec<Str | ... | @@ -46,11 +52,6 @@ fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec<Str |
| 46 | } | 52 | } |
| 47 | script += "quit\n"; | 53 | script += "quit\n"; |
| 48 | std::fs::write(dir.join("script"), script).unwrap(); | 54 | std::fs::write(dir.join("script"), script).unwrap(); |
| 49 | std::fs::write( | ||
| 50 | dir.join("settings.json"), | ||
| 51 | r#"{"user_name": "Snowbound Test"}"#, | ||
| 52 | ) | ||
| 53 | .unwrap(); | ||
| 54 | let status = Command::new(env!("CARGO_BIN_EXE_snowbound")) | 55 | let status = Command::new(env!("CARGO_BIN_EXE_snowbound")) |
| 55 | .env("SNOWBOUND_REPLAY", dir.join("script")) | 56 | .env("SNOWBOUND_REPLAY", dir.join("script")) |
| 56 | .arg("--notebook") | 57 | .arg("--notebook") |
| ... | @@ -389,3 +390,45 @@ fn the_find_bar_keeps_room_for_the_query() { | ... | @@ -389,3 +390,45 @@ fn the_find_bar_keeps_room_for_the_query() { |
| 389 | "{found}" | 390 | "{found}" |
| 390 | ); | 391 | ); |
| 391 | } | 392 | } |
| 393 | |||
| 394 | /// A launch shows the page each notebook was left on, as the settings recall them: the | ||
| 395 | /// notebook shown at once, and another as its section is opened. | ||
| 396 | #[test] | ||
| 397 | fn a_launch_returns_to_the_page_each_notebook_was_left_on() { | ||
| 398 | let scratch = Scratch::new("left-on"); | ||
| 399 | let source = | ||
| 400 | Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate"); | ||
| 401 | let other = scratch.0.join("other"); | ||
| 402 | std::fs::create_dir_all(&other).unwrap(); | ||
| 403 | for entry in std::fs::read_dir(&source).unwrap() { | ||
| 404 | let entry = entry.unwrap(); | ||
| 405 | std::fs::copy(entry.path(), other.join(entry.file_name())).unwrap(); | ||
| 406 | } | ||
| 407 | let place = |notebook: &Path, page: &str| serde_json::json!({"notebook": notebook, "section": "Cross.one", "page": page}); | ||
| 408 | let settings = serde_json::json!({ | ||
| 409 | "user_name": "Snowbound Test", | ||
| 410 | "sidebar": true, | ||
| 411 | "notebooks": [other], | ||
| 412 | "recent": [ | ||
| 413 | // Type over a row, then Delete a whole table. | ||
| 414 | place(&scratch.0.join("notebook"), "{F8285B5C-5410-4A62-B7DC-43E408FE82AE},1"), | ||
| 415 | place(&other, "{9F114CE9-F7D5-4234-B7C3-FF0D79615BB2},1"), | ||
| 416 | ], | ||
| 417 | }); | ||
| 418 | std::fs::write(scratch.0.join("settings.json"), settings.to_string()).unwrap(); | ||
| 419 | // The other notebook's section in the sidebar. | ||
| 420 | let mut steps = vec!["accessibility launched", "move 63 104", "press", "release"]; | ||
| 421 | steps.extend(["wait 4000", "accessibility other"]); | ||
| 422 | let shown = |tree: &str| page_tabs(tree).into_iter().find(|tab| tab.starts_with('*')); | ||
| 423 | let [launched, other] = replay(&scratch, Some(&source), &steps).try_into().unwrap(); | ||
| 424 | assert_eq!( | ||
| 425 | shown(&launched).as_deref(), | ||
| 426 | Some("*Type over a row"), | ||
| 427 | "{launched}" | ||
| 428 | ); | ||
| 429 | assert_eq!( | ||
| 430 | shown(&other).as_deref(), | ||
| 431 | Some("*Delete a whole table"), | ||
| 432 | "{other}" | ||
| 433 | ); | ||
| 434 | } |