authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-10 21:08:05-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-10 21:08:05-07:00
logad10b477304448e0310a90215fdfab07b4dd337f
tree5bfa141e286035a81bb9dc5693b8eb4b15b3babb
parentde01c779539ae34b40906112aa468ef3a52979fa
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

refactor: expose active-revision, page and parent accessors

Add Space::active, Document::{active, pages_in}, Revision::parents and RevisionIndex::{active, resolve_active}, replacing the hand-rolled default- context lookups, page filters and parent walks across the core, offline, notebook and SMB crates. Derive Clone and PartialEq for Layout, Format and Tag and read table-cell indents (0x1c001c12), matching the canvas branch. Validation: 298 workspace tests, Clippy, fmt and the fuzz workspace build. Assisted-by: claude-fable-5.1

22 files changed, 161 insertions(+), 269 deletions(-)

crates/onestore-notebook/src/lib.rs+2-14
......@@ -2,7 +2,7 @@
22#![doc = include_str!("../README.md")]
33
44use onestore::{
5 ExGuid, FileType, RevisionIndex, Store,
5 FileType, RevisionIndex, Store,
66 document::{Document, Kind},
77};
88use serde::Serialize;
......@@ -251,19 +251,7 @@ fn scan(
251251 let parsed = (|| {
252252 let index = RevisionIndex::parse(&store)?;
253253 let document = Document::parse(&index)?;
254 let revision = document
255 .spaces
256 .get(&document.root)
257 .and_then(|space| {
258 space
259 .contexts
260 .get(&ExGuid::default())
261 .and_then(|id| space.revisions.get(id))
262 })
263 .ok_or(onestore::Error {
264 offset: 0,
265 message: "Missing active notebook root revision",
266 })?;
254 let revision = document.active(document.root)?;
267255 if expected == FileType::Section {
268256 if revision
269257 .roots
crates/onestore-offline/src/formatting.rs+1-2
......@@ -68,8 +68,7 @@ fn observe(
6868 let store = Store::parse(source)?;
6969 let index = RevisionIndex::parse(&store)?;
7070 let document = Document::parse(&index)?;
71 let space = &document.spaces[&space];
72 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
71 let revision = document.active(space)?;
7372 let mut spans: Vec<Span> = Vec::new();
7473 let mut start = 0;
7574 for run in revision.text_runs(object)? {
crates/onestore-offline/src/lib.rs+4-28
......@@ -305,29 +305,11 @@ fn active_paths(
305305 pages: &[ExGuid],
306306 objects: &[ExGuid],
307307) -> Option<Vec<Vec<ExGuid>>> {
308 use std::collections::{BTreeMap, BTreeSet};
309 let mut pending = pages.to_vec();
310 let mut seen = BTreeSet::new();
311 let mut parents = BTreeMap::<_, Vec<_>>::new();
312 while let Some(id) = pending.pop() {
313 if !seen.insert(id) {
314 continue;
315 }
316 let node = view.nodes.get(&id)?;
317 for child in node
318 .children
319 .iter()
320 .chain(&node.content)
321 .chain(&node.structure)
322 {
323 parents.entry(*child).or_default().push(id);
324 pending.push(*child);
325 }
326 }
308 let parents = view.parents(pages).ok()?;
327309 objects
328310 .iter()
329311 .map(|object| {
330 if !seen.contains(object) {
312 if !parents.contains_key(object) && !pages.contains(object) {
331313 return None;
332314 }
333315 let mut path = Vec::new();
......@@ -352,14 +334,8 @@ fn paragraph(source: &[u8], space: ExGuid, object: ExGuid) -> Result<Option<Stri
352334 let index = RevisionIndex::parse(&store)?;
353335 let document = Document::parse(&index)?;
354336 let node = document
355 .spaces
356 .get(&space)
357 .and_then(|space| {
358 space
359 .contexts
360 .get(&ExGuid::default())
361 .and_then(|revision| space.revisions.get(revision))
362 })
337 .active(space)
338 .ok()
363339 .and_then(|revision| revision.nodes.get(&object));
364340 Ok(match node.map(|node| &node.kind) {
365341 Some(Kind::RichText { text, .. }) => Some(text.clone()),
crates/onestore-offline/src/outline.rs+2-13
......@@ -27,21 +27,10 @@ fn observe(
2727 let store = Store::parse(source)?;
2828 let index = RevisionIndex::parse(&store)?;
2929 let document = Document::parse(&index)?;
30 let Some(space_view) = document.spaces.get(&space) else {
30 let Ok(view) = document.active(space) else {
3131 return Ok(None);
3232 };
33 let Some(view) = space_view
34 .contexts
35 .get(&ExGuid::default())
36 .and_then(|rid| space_view.revisions.get(rid))
37 else {
38 return Ok(None);
39 };
40 let pages: Vec<_> = document
41 .pages()?
42 .into_iter()
43 .filter_map(|(sid, page)| (sid == space).then_some(page))
44 .collect();
33 let pages = document.pages_in(space)?;
4534 if pages.len() != 1 {
4635 return Ok(None);
4736 }
crates/onestore-offline/src/pages.rs+1-2
......@@ -19,8 +19,7 @@ fn observe(source: &[u8]) -> Result<Option<(ExGuid, PageOrder)>> {
1919 let document = Document::parse(&index)?;
2020 let mut pages = Vec::new();
2121 for (sid, _) in document.pages()? {
22 let space = &document.spaces[&sid];
23 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
22 let view = document.active(sid)?;
2423 let Some(metadata) = view.roots.get(&2).and_then(|id| view.nodes.get(id)) else {
2524 return Ok(None);
2625 };
crates/onestore-offline/src/paragraph.rs+2-13
......@@ -30,21 +30,10 @@ fn observe(source: &[u8], space: ExGuid, texts: &[ExGuid]) -> Result<Option<Vec<
3030 let store = Store::parse(source)?;
3131 let index = RevisionIndex::parse(&store)?;
3232 let document = Document::parse(&index)?;
33 let Some(section) = document.spaces.get(&space) else {
33 let Ok(view) = document.active(space) else {
3434 return Ok(None);
3535 };
36 let Some(view) = section
37 .contexts
38 .get(&ExGuid::default())
39 .and_then(|id| section.revisions.get(id))
40 else {
41 return Ok(None);
42 };
43 let pages: Vec<_> = document
44 .pages()?
45 .into_iter()
46 .filter_map(|(sid, page)| (sid == space).then_some(page))
47 .collect();
36 let pages = document.pages_in(space)?;
4837 let Some(paths) = active_paths(view, &pages, texts) else {
4938 return Ok(None);
5039 };
crates/onestore-offline/src/sync.rs+3-3
......@@ -127,7 +127,7 @@ impl Replica {
127127 EditStatus::AwaitingConfirmation { revision },
128128 )));
129129 }
130 revision = index.spaces[&intent.space].labels[&(ExGuid::default(), 1)];
130 revision = index.active(intent.space)?;
131131 }
132132 if let Err(error) = remote.confirm(&snapshot) {
133133 if error.state == CommitState::Committed {
......@@ -182,7 +182,7 @@ impl Replica {
182182 if prepared.as_bytes() == snapshot {
183183 let store = Store::parse(&snapshot)?;
184184 let index = RevisionIndex::parse(&store)?;
185 let revision = index.spaces[&intent.space].labels[&(ExGuid::default(), 1)];
185 let revision = index.active(intent.space)?;
186186 if let Err(error) = remote.confirm(&snapshot) {
187187 if error.state == CommitState::Committed {
188188 self.acknowledge(intent.id, revision, &snapshot)?;
......@@ -194,7 +194,7 @@ impl Replica {
194194 }
195195 let store = Store::parse(prepared.as_bytes())?;
196196 let index = RevisionIndex::parse(&store)?;
197 let revision = index.spaces[&intent.space].labels[&(ExGuid::default(), 1)];
197 let revision = index.active(intent.space)?;
198198 let before_store = Store::parse(&snapshot)?;
199199 let before = RevisionIndex::parse(&before_store)?;
200200 let mut revisions: BTreeMap<_, _> = index
crates/onestore-offline/src/tree.rs+5-14
......@@ -168,18 +168,10 @@ fn observe(
168168 let store = Store::parse(source)?;
169169 let index = RevisionIndex::parse(&store)?;
170170 let document = Document::parse(&index)?;
171 let Some(section) = document.spaces.get(&space) else {
171 let Ok(view) = document.active(space) else {
172172 return Ok(None);
173173 };
174 let Some(rid) = section.contexts.get(&ExGuid::default()) else {
175 return Ok(None);
176 };
177 let view = &section.revisions[rid];
178 let pages: Vec<_> = document
179 .pages()?
180 .into_iter()
181 .filter_map(|(sid, page)| (sid == space).then_some(page))
182 .collect();
174 let pages = document.pages_in(space)?;
183175 if pages.len() != 1 {
184176 return Ok(None);
185177 }
......@@ -217,7 +209,7 @@ fn observe(
217209 .collect(),
218210 destination,
219211 content: if intent.destination().is_none() {
220 let sha256 = fingerprint(&store, &index.resolve(space, *rid)?, object, legacy)?;
212 let sha256 = fingerprint(&store, &index.resolve_active(space)?, object, legacy)?;
221213 Some(if legacy {
222214 Content::Legacy(sha256)
223215 } else {
......@@ -232,7 +224,7 @@ fn observe(
232224fn mutable_objects(source: &[u8], space: ExGuid) -> Result<BTreeSet<ExGuid>> {
233225 let store = Store::parse(source)?;
234226 let index = RevisionIndex::parse(&store)?;
235 let raw = index.resolve(space, index.spaces[&space].labels[&(ExGuid::default(), 1)])?;
227 let raw = index.resolve_active(space)?;
236228 Ok(raw
237229 .reachable()?
238230 .into_iter()
......@@ -468,8 +460,7 @@ mod tests {
468460 let index = RevisionIndex::parse(&store).unwrap();
469461 let document = Document::parse(&index).unwrap();
470462 let (sid, _) = document.pages().unwrap()[0];
471 let space = &document.spaces[&sid];
472 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
463 let view = document.active(sid).unwrap();
473464 let text = *view.nodes.iter().find(|(_, node)| matches!(&node.kind, Kind::RichText { text, .. } if text == "AlphaOmega")).unwrap().0;
474465 let directory = tempfile::tempdir().unwrap();
475466 let path = directory.path().join("legacy.sqlite");
crates/onestore-smb/src/tests.rs+1-1
......@@ -137,7 +137,7 @@ fn text(bytes: &[u8]) -> (ExGuid, ExGuid, String) {
137137 doc.spaces
138138 .iter()
139139 .find_map(|(sid, space)| {
140 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
140 let revision = space.active().unwrap();
141141 revision
142142 .nodes
143143 .iter()
crates/onestore/src/document.rs+64-13
......@@ -30,6 +30,18 @@ pub struct Space<'a> {
3030 pub revisions: BTreeMap<ExGuid, Revision<'a>>,
3131}
3232
33impl<'a> Space<'a> {
34 /// The revision of the default context, which is the current content of the space.
35 pub fn active(&self) -> Option<&Revision<'a>> {
36 self.revisions.get(self.contexts.get(&ExGuid::default())?)
37 }
38
39 pub fn into_active(mut self) -> Option<Revision<'a>> {
40 self.revisions
41 .remove(self.contexts.get(&ExGuid::default())?)
42 }
43}
44
3345#[derive(Debug, Serialize)]
3446pub struct Revision<'a> {
3547 pub roots: BTreeMap<u32, ExGuid>,
......@@ -61,7 +73,7 @@ pub struct Element<'a> {
6173 pub extra: Vec<Vec<Field<'a>>>,
6274}
6375
64#[derive(Debug, Default, Serialize)]
76#[derive(Debug, Default, Clone, PartialEq, Serialize)]
6577pub struct Layout {
6678 pub x: Option<f32>,
6779 pub y: Option<f32>,
......@@ -73,7 +85,7 @@ pub struct Layout {
7385
7486macro_rules! format_fields {
7587 ($($field:ident: $value:ty),* $(,)?) => {
76 #[derive(Debug, Default, Serialize)]
88 #[derive(Debug, Default, Clone, PartialEq, Serialize)]
7789 pub struct Format { $(pub $field: Option<$value>),* }
7890
7991 impl Format {
......@@ -208,7 +220,36 @@ impl Revision<'_> {
208220 }
209221}
210222
211#[derive(Debug, Serialize)]
223impl Revision<'_> {
224 /// Maps every element reachable from `roots` through children, content and structure
225 /// references to its referencing parents, in visiting order.
226 pub fn parents(&self, roots: &[ExGuid]) -> Result<BTreeMap<ExGuid, Vec<ExGuid>>> {
227 let mut pending: Vec<_> = roots.to_vec();
228 let mut seen = BTreeSet::new();
229 let mut parents = BTreeMap::<_, Vec<_>>::new();
230 while let Some(id) = pending.pop() {
231 if !seen.insert(id) {
232 continue;
233 }
234 let element = self
235 .nodes
236 .get(&id)
237 .ok_or_else(|| invalid("Page content is unavailable"))?;
238 for child in element
239 .children
240 .iter()
241 .chain(&element.content)
242 .chain(&element.structure)
243 {
244 parents.entry(*child).or_default().push(id);
245 pending.push(*child);
246 }
247 }
248 Ok(parents)
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Serialize)]
212253/// Tag dates count seconds since 1980-01-01 UTC; status retains the ActionItemStatus bits.
213254pub struct Tag {
214255 pub definition: Option<ExGuid>,
......@@ -308,6 +349,7 @@ pub enum Kind<'a> {
308349 Row,
309350 Cell {
310351 shading: Option<u32>,
352 indents: Vec<f32>,
311353 },
312354 Image {
313355 container: Option<ExGuid>,
......@@ -547,17 +589,25 @@ fn measurements(bytes: Option<&[u8]>, header: usize) -> Result<Vec<f32>> {
547589impl<'a> Document<'a> {
548590 /// Active (object space, page) identities in section order, excluding history and conflicts.
549591 /// Encrypted sections return no visible pages; table-of-contents files return an error.
592 /// The active revision of one object space.
593 pub fn active(&self, space: ExGuid) -> Result<&Revision<'a>> {
594 self.spaces
595 .get(&space)
596 .and_then(Space::active)
597 .ok_or_else(|| invalid("Object space has no active revision"))
598 }
599
600 /// Active page objects declared in one page space, in section order.
601 pub fn pages_in(&self, space: ExGuid) -> Result<Vec<ExGuid>> {
602 Ok(self
603 .pages()?
604 .into_iter()
605 .filter_map(|(sid, page)| (sid == space).then_some(page))
606 .collect())
607 }
608
550609 pub fn pages(&self) -> Result<Vec<(ExGuid, ExGuid)>> {
551 let active = |sid| {
552 self.spaces
553 .get(&sid)
554 .and_then(|s| {
555 s.contexts
556 .get(&ExGuid::default())
557 .and_then(|rid| s.revisions.get(rid))
558 })
559 .ok_or_else(|| invalid("Section page has no active revision"))
560 };
610 let active = |sid| self.active(sid);
561611 let root = active(self.root)?;
562612 let section = root
563613 .roots
......@@ -1084,6 +1134,7 @@ impl<'a> Element<'a> {
10841134 0x60023 => Kind::Row,
10851135 0x60024 => Kind::Cell {
10861136 shading: f.u32(0x14001e26)?,
1137 indents: measurements(f.bytes(0x1c001c12)?, 4)?,
10871138 },
10881139 0x60011 => Kind::Image {
10891140 container: f.one(0x20001c3f)?,
crates/onestore/src/edit.rs+15-50
......@@ -44,11 +44,7 @@ pub fn replace_text(
4444 let store = Store::parse(source)?;
4545 let index = RevisionIndex::parse(&store)?;
4646 index.validate_current()?;
47 let rid = *index
48 .spaces
49 .get(&space)
50 .and_then(|s| s.labels.get(&(ExGuid::default(), 1)))
51 .ok_or_else(|| invalid("Object space has no active default revision"))?;
47 let rid = index.active(space)?;
5248 let raw = index.resolve(space, rid)?;
5349 if !raw.reachable()?.contains(&object) {
5450 return Err(invalid("Object is not reachable in the active revision"));
......@@ -59,11 +55,7 @@ pub fn replace_text(
5955 .get(&space)
6056 .and_then(|s| s.revisions.get(&rid))
6157 .ok_or_else(|| invalid("The active document revision is unavailable"))?;
62 let pages: Vec<_> = document
63 .pages()?
64 .into_iter()
65 .filter_map(|(sid, page)| (sid == space).then_some(page))
66 .collect();
58 let pages = document.pages_in(space)?;
6759 let parents = editable_parents(revision, &pages, object)?;
6860 let node = revision
6961 .nodes
......@@ -317,37 +309,23 @@ pub(crate) fn editable_parents(
317309 object: ExGuid,
318310) -> Result<std::collections::BTreeMap<ExGuid, Vec<ExGuid>>, Error> {
319311 let invalid = |message| Error { offset: 0, message };
320 let mut pending: Vec<_> = pages.iter().map(|page| (*page, false)).collect();
312 let parents = revision.parents(pages)?;
313 if !parents.contains_key(&object) && !pages.contains(&object) {
314 return Err(invalid("Select content on an active editable page"));
315 }
316 let mut pending = vec![object];
321317 let mut seen = std::collections::BTreeSet::new();
322 let mut parents = std::collections::BTreeMap::<_, Vec<_>>::new();
323 let mut editable = false;
324 while let Some((id, read_only)) = pending.pop() {
325 if !seen.insert((id, read_only)) {
318 while let Some(id) = pending.pop() {
319 if !seen.insert(id) {
326320 continue;
327321 }
328 let element = revision
329 .nodes
330 .get(&id)
331 .ok_or_else(|| invalid("Page content is unavailable"))?;
332 let read_only = read_only || element.extra[0].iter().any(|field| field.id == 0x88001cde);
333 if id == object {
334 if read_only {
335 return Err(invalid("This page or its content is read-only"));
336 }
337 editable = true;
338 }
339 for child in element
340 .children
322 if revision.nodes[&id].extra[0]
341323 .iter()
342 .chain(&element.content)
343 .chain(&element.structure)
324 .any(|field| field.id == 0x88001cde)
344325 {
345 parents.entry(*child).or_default().push(id);
346 pending.push((*child, read_only));
326 return Err(invalid("This page or its content is read-only"));
347327 }
348 }
349 if !editable {
350 return Err(invalid("Select content on an active editable page"));
328 pending.extend(parents.get(&id).into_iter().flatten().copied());
351329 }
352330 Ok(parents)
353331}
......@@ -358,21 +336,8 @@ pub(crate) fn page_title(
358336 text_update: Option<(ExGuid, &str)>,
359337) -> Result<Option<(ExGuid, bool, String)>, Error> {
360338 let invalid = |message| Error { offset: 0, message };
361 let mut pending = pages.to_vec();
362 let mut seen = std::collections::BTreeSet::new();
363 while let Some(id) = pending.pop() {
364 if !seen.insert(id) {
365 continue;
366 }
367 let node = &revision.nodes[&id];
368 pending.extend(
369 node.children
370 .iter()
371 .chain(&node.content)
372 .chain(&node.structure)
373 .copied(),
374 );
375 }
339 let parents = revision.parents(pages)?;
340 let seen: std::collections::BTreeSet<_> = parents.keys().chain(pages).copied().collect();
376341 let titles: Vec<_> = revision
377342 .nodes
378343 .iter()
crates/onestore/src/formatting.rs+2-11
......@@ -96,17 +96,8 @@ pub(crate) fn format_text(
9696 let index = RevisionIndex::parse(&store)?;
9797 index.validate_current()?;
9898 let document = Document::parse(&index)?;
99 let semantic = document
100 .spaces
101 .get(&space)
102 .ok_or_else(|| invalid("The active page is unavailable"))?;
103 let rid = semantic.contexts[&ExGuid::default()];
104 let view = &semantic.revisions[&rid];
105 let pages: Vec<_> = document
106 .pages()?
107 .into_iter()
108 .filter_map(|(sid, page)| (sid == space).then_some(page))
109 .collect();
99 let view = document.active(space)?;
100 let pages = document.pages_in(space)?;
110101 let parents = editable_parents(view, &pages, object)?;
111102 let node = &view.nodes[&object];
112103 let Kind::RichText {
crates/onestore/src/insertion.rs+4-13
......@@ -195,24 +195,15 @@ impl Insertion {
195195 let index = RevisionIndex::parse(&store)?;
196196 index.validate_current()?;
197197 let mut document = Document::parse(&index)?;
198 let pages: Vec<_> = document
199 .pages()?
200 .into_iter()
201 .filter_map(|(sid, id)| (sid == space).then_some(id))
202 .collect();
198 let pages = document.pages_in(space)?;
203199 let [page] = pages.as_slice() else {
204200 return Err(invalid("Insertion requires a single active page"));
205201 };
206 let semantic_space = document
202 let mut view = document
207203 .spaces
208204 .remove(&space)
205 .and_then(crate::document::Space::into_active)
209206 .ok_or_else(|| invalid("The active page is unavailable"))?;
210 let rid = semantic_space.contexts[&ExGuid::default()];
211 let mut view = semantic_space
212 .revisions
213 .into_iter()
214 .find_map(|(id, revision)| (id == rid).then_some(revision))
215 .unwrap();
216207 let parents = editable_parents(&view, &pages, self.parent)?;
217208 let parent = &view.nodes[&self.parent];
218209 let position = match self.placement {
......@@ -372,7 +363,7 @@ impl Insertion {
372363 },
373364 );
374365 }
375 let raw = index.resolve(space, rid)?;
366 let raw = index.resolve_active(space)?;
376367 if new.keys().any(|id| raw.objects.contains_key(id)) {
377368 return Err(invalid(
378369 "An insertion identity is already present; reconcile the existing edit",
crates/onestore/src/objects.rs+2-9
......@@ -200,15 +200,8 @@ impl<'a> RevisionIndex<'a> {
200200 message: "Object references its own object space",
201201 });
202202 }
203 let rid = self
204 .spaces
205 .get(&target)
206 .and_then(|space| space.labels.get(&(ExGuid::default(), 1)))
207 .ok_or(Error {
208 offset: 0,
209 message: "Object-space reference has no default revision",
210 })?;
211 targets.insert((target, *rid));
203 let rid = self.active(target)?;
204 targets.insert((target, rid));
212205 }
213206 for context in references.contexts {
214207 if !space.labels.contains_key(&(context, 1)) {
crates/onestore/src/outline.rs+6-11
......@@ -58,18 +58,14 @@ impl OutlineEdit {
5858 let index = RevisionIndex::parse(&store)?;
5959 index.validate_current()?;
6060 let mut document = Document::parse(&index)?;
61 let pages: Vec<_> = document
62 .pages()?
63 .into_iter()
64 .filter_map(|(sid, page)| (sid == space).then_some(page))
65 .collect();
61 let pages = document.pages_in(space)?;
6662 let [page] = pages.as_slice() else {
6763 return Err(invalid("Outline editing requires a single active page"));
6864 };
69 let mut semantic = document.spaces.remove(&space).unwrap();
70 let view = semantic
71 .revisions
72 .remove(&semantic.contexts[&ExGuid::default()])
65 let view = document
66 .spaces
67 .remove(&space)
68 .and_then(crate::document::Space::into_active)
7369 .unwrap();
7470 let parents = editable_parents(&view, &pages, object)?;
7571 let node = &view.nodes[&object];
......@@ -151,8 +147,7 @@ mod tests {
151147 let index = RevisionIndex::parse(&store).unwrap();
152148 let document = Document::parse(&index).unwrap();
153149 let (sid, page) = document.pages().unwrap()[0];
154 let space = &document.spaces[&sid];
155 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
150 let view = document.active(sid).unwrap();
156151 let outline = *view.nodes[&page]
157152 .children
158153 .iter()
crates/onestore/src/paragraph.rs+8-26
......@@ -84,24 +84,15 @@ impl ParagraphSplit {
8484 let index = RevisionIndex::parse(&store)?;
8585 index.validate_current()?;
8686 let mut document = Document::parse(&index)?;
87 let pages: Vec<_> = document
88 .pages()?
89 .into_iter()
90 .filter_map(|(sid, page)| (sid == space).then_some(page))
91 .collect();
87 let pages = document.pages_in(space)?;
9288 let [_] = pages.as_slice() else {
9389 return Err(invalid("Splitting requires a single active page"));
9490 };
95 let semantic = document
91 let view = document
9692 .spaces
9793 .remove(&space)
94 .and_then(crate::document::Space::into_active)
9895 .ok_or_else(|| invalid("The active page is unavailable"))?;
99 let rid = semantic.contexts[&ExGuid::default()];
100 let view = semantic
101 .revisions
102 .into_iter()
103 .find_map(|(id, view)| (id == rid).then_some(view))
104 .unwrap();
10596 let parents = editable_parents(&view, &pages, self.text)?;
10697 let [paragraph] = parents
10798 .get(&self.text)
......@@ -147,7 +138,7 @@ impl ParagraphSplit {
147138 }
148139 pending.extend(parents.get(&id).into_iter().flatten().copied());
149140 }
150 let raw = index.resolve(space, rid)?;
141 let raw = index.resolve_active(space)?;
151142 let (text, runs) = ordinary_text(&view, &raw, self.text)?;
152143 let node = &view.nodes[&self.text];
153144 let length = u32::try_from(text.encode_utf16().count())
......@@ -419,24 +410,15 @@ impl ParagraphJoin {
419410 let index = RevisionIndex::parse(&store)?;
420411 index.validate_current()?;
421412 let mut document = Document::parse(&index)?;
422 let pages: Vec<_> = document
423 .pages()?
424 .into_iter()
425 .filter_map(|(sid, page)| (sid == space).then_some(page))
426 .collect();
413 let pages = document.pages_in(space)?;
427414 if pages.len() != 1 {
428415 return Err(invalid("Joining requires a single active page"));
429416 }
430 let semantic = document
417 let view = document
431418 .spaces
432419 .remove(&space)
420 .and_then(crate::document::Space::into_active)
433421 .ok_or_else(|| invalid("The active page is unavailable"))?;
434 let rid = semantic.contexts[&ExGuid::default()];
435 let view = semantic
436 .revisions
437 .into_iter()
438 .find_map(|(id, view)| (id == rid).then_some(view))
439 .unwrap();
440422 let parents = editable_parents(&view, &pages, self.left)?;
441423 editable_parents(&view, &pages, self.right)?;
442424 let parent = |id| -> Result<ExGuid, Error> {
......@@ -500,7 +482,7 @@ impl ParagraphJoin {
500482 "Joining these child indentation levels requires a hierarchy edit",
501483 ));
502484 }
503 let raw = index.resolve(space, rid)?;
485 let raw = index.resolve_active(space)?;
504486 let (a, a_runs) = ordinary_text(&view, &raw, self.left)?;
505487 let (b, b_runs) = ordinary_text(&view, &raw, self.right)?;
506488 let length = u32::try_from(a.encode_utf16().count())
crates/onestore/src/revisions.rs+18
......@@ -396,3 +396,21 @@ impl<'a> RevisionIndex<'a> {
396396 })
397397 }
398398}
399
400impl<'a> RevisionIndex<'a> {
401 /// The revision labelled current in the default context of one object space.
402 pub fn active(&self, space: ExGuid) -> Result<ExGuid> {
403 self.spaces
404 .get(&space)
405 .and_then(|space| space.labels.get(&(ExGuid::default(), 1)))
406 .copied()
407 .ok_or(Error {
408 offset: 0,
409 message: "Object space has no active default revision",
410 })
411 }
412
413 pub fn resolve_active(&self, space: ExGuid) -> Result<crate::ResolvedRevision<'a>> {
414 self.resolve(space, self.active(space)?)
415 }
416}
crates/onestore/src/tree.rs+11-22
......@@ -100,20 +100,14 @@ impl TreeEdit {
100100 let index = RevisionIndex::parse(&store)?;
101101 index.validate_current()?;
102102 let mut document = Document::parse(&index)?;
103 let pages: Vec<_> = document
104 .pages()?
105 .into_iter()
106 .filter_map(|(sid, page)| (sid == space).then_some(page))
107 .collect();
103 let pages = document.pages_in(space)?;
108104 let [page] = pages.as_slice() else {
109105 return Err(invalid("Tree editing requires a single active page"));
110106 };
111 let semantic = document.spaces.remove(&space).unwrap();
112 let rid = semantic.contexts[&ExGuid::default()];
113 let mut view = semantic
114 .revisions
115 .into_iter()
116 .find_map(|(id, view)| (id == rid).then_some(view))
107 let mut view = document
108 .spaces
109 .remove(&space)
110 .and_then(crate::document::Space::into_active)
117111 .unwrap();
118112 let parents = editable_parents(&view, &pages, self.object)?;
119113 let path = checked_path(&view, &parents, *page, self.object)?;
......@@ -279,7 +273,7 @@ impl TreeEdit {
279273 .copied(),
280274 );
281275 }
282 let raw = index.resolve(space, rid)?;
276 let raw = index.resolve_active(space)?;
283277 let active_parents = editable_parents(&view, &pages, *page)?;
284278 let modified = current_timestamps()?.0.to_le_bytes();
285279 let mut changed = BTreeMap::new();
......@@ -431,8 +425,7 @@ mod tests {
431425 let index = RevisionIndex::parse(&store).unwrap();
432426 let document = Document::parse(&index).unwrap();
433427 let (sid, page) = document.pages().unwrap()[0];
434 let space = &document.spaces[&sid];
435 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
428 let view = document.active(sid).unwrap();
436429 let outline = *view.nodes[&page]
437430 .children
438431 .iter()
......@@ -453,8 +446,7 @@ mod tests {
453446 let store = Store::parse(repeated.as_bytes()).unwrap();
454447 let index = RevisionIndex::parse(&store).unwrap();
455448 let document = Document::parse(&index).unwrap();
456 let space = &document.spaces[&sid];
457 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
449 let view = document.active(sid).unwrap();
458450 assert_eq!(view.nodes[&outline].children, [second.object(), first]);
459451 assert_eq!(
460452 view.nodes[&first].latest_author,
......@@ -472,8 +464,7 @@ mod tests {
472464 let index = RevisionIndex::parse(&store).unwrap();
473465 let document = Document::parse(&index).unwrap();
474466 let (sid, page) = document.pages().unwrap()[0];
475 let space = &document.spaces[&sid];
476 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
467 let view = document.active(sid).unwrap();
477468 let outline = *view.nodes[&page]
478469 .children
479470 .iter()
......@@ -527,8 +518,7 @@ mod tests {
527518 let index = RevisionIndex::parse(&store).unwrap();
528519 index.validate_current().unwrap();
529520 let document = Document::parse(&index).unwrap();
530 let space = &document.spaces[&sid];
531 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
521 let view = document.active(sid).unwrap();
532522 assert_eq!(view.nodes[&outline].children, [a, second.object()]);
533523 assert_eq!(view.nodes[&outline].child_level, Some(2));
534524 assert_eq!(view.nodes[&a].child_level, Some(1));
......@@ -557,8 +547,7 @@ mod tests {
557547 let index = RevisionIndex::parse(&store).unwrap();
558548 let document = Document::parse(&index).unwrap();
559549 let (sid, page) = document.pages().unwrap()[0];
560 let space = &document.spaces[&sid];
561 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
550 let view = document.active(sid).unwrap();
562551 let outline = *view.nodes[&page]
563552 .children
564553 .iter()
crates/onestore/src/write.rs+2-16
......@@ -596,14 +596,7 @@ pub(crate) fn write_revision(
596596 edit: impl FnOnce(&crate::ResolvedRevision<'_>) -> Result<BTreeMap<ExGuid, PropertyObject>>,
597597) -> Result<Vec<u8>> {
598598 write_revisions(source, |index| {
599 let rid = *index
600 .spaces
601 .get(&space)
602 .and_then(|space| space.labels.get(&(ExGuid::default(), 1)))
603 .ok_or(Error {
604 offset: 0,
605 message: "Object space has no active default revision",
606 })?;
599 let rid = index.active(space)?;
607600 let revision = index.resolve(space, rid)?;
608601 Ok(BTreeMap::from([(
609602 space,
......@@ -680,14 +673,7 @@ pub(crate) fn write_revisions(
680673 for (space, change) in changes {
681674 let (rid, mut revision, mut replacements) = match change {
682675 RevisionEdit::Update(objects) => {
683 let rid = *index
684 .spaces
685 .get(&space)
686 .and_then(|space| space.labels.get(&(ExGuid::default(), 1)))
687 .ok_or(Error {
688 offset: 0,
689 message: "Object space has no active default revision",
690 })?;
676 let rid = index.active(space)?;
691677 (Some(rid), index.resolve(space, rid)?, objects)
692678 }
693679 RevisionEdit::Create { roots, objects } => {
crates/onestore/src/write/tests.rs+4-4
......@@ -144,7 +144,7 @@ fn document_insertions_and_formatting_respect_readonly_ancestors() {
144144 .unwrap();
145145 let document = crate::document::Document::parse(&index).unwrap();
146146 let space = &document.spaces[&sid];
147 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
147 let view = space.active().unwrap();
148148 let text = view.nodes[&paragraph].content[0];
149149 let join = crate::ParagraphJoin::new(text, right.text_object(), "Author").unwrap();
150150 assert!(PreparedEdit::join(&protected, sid, &join).is_err());
......@@ -861,7 +861,7 @@ fn character_formatting_preserves_inheritance_and_associated_data() {
861861 let index = RevisionIndex::parse(&store).unwrap();
862862 let document = Document::parse(&index).unwrap();
863863 let space = &document.spaces[&sid];
864 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
864 let view = space.active().unwrap();
865865 let Kind::RichText { runs, .. } = &view.nodes[&text].kind else {
866866 panic!()
867867 };
......@@ -888,7 +888,7 @@ fn character_formatting_preserves_inheritance_and_associated_data() {
888888 let index = RevisionIndex::parse(&store).unwrap();
889889 let doc = Document::parse(&index).unwrap();
890890 let s = &doc.spaces[&sid];
891 let view = &s.revisions[&s.contexts[&ExGuid::default()]];
891 let view = s.active().unwrap();
892892 let runs = view.text_runs(text).unwrap();
893893 assert_eq!(runs.len(), 1);
894894 assert_eq!(runs[0].format.font.as_deref(), Some("Georgia"));
......@@ -909,7 +909,7 @@ fn character_formatting_preserves_inheritance_and_associated_data() {
909909 let previous = RevisionIndex::parse(&previous).unwrap();
910910 let previous_doc = Document::parse(&previous).unwrap();
911911 let s = &previous_doc.spaces[&sid];
912 let previous_view = &s.revisions[&s.contexts[&ExGuid::default()]];
912 let previous_view = s.active().unwrap();
913913 assert_eq!(
914914 format!("{:?}", view.nodes[&text].extra),
915915 format!("{:?}", previous_view.nodes[&text].extra)
crates/onestore/src/write/tests/multi.rs+3-3
......@@ -28,7 +28,7 @@ fn nest(source: &[u8]) -> Vec<u8> {
2828 index.spaces[&index.root].labels[&(ExGuid::default(), 1)],
2929 )?;
3030 let view = &document.spaces[&index.root];
31 let view = &view.revisions[&view.contexts[&ExGuid::default()]];
31 let view = view.active().unwrap();
3232 let root = section.roots[&1];
3333 let parent = *view.nodes[&root]
3434 .children
......@@ -126,7 +126,7 @@ fn nesting_publishes_section_order_and_page_levels_in_one_transaction() {
126126 assert_eq!(changed_spaces, 3);
127127 for (ordinal, (sid, _)) in pages.iter().enumerate() {
128128 let view = &document.spaces[sid];
129 let view = &view.revisions[&view.contexts[&ExGuid::default()]];
129 let view = view.active().unwrap();
130130 let Kind::Metadata { level, .. } = view.nodes[&view.roots[&2]].kind else {
131131 panic!()
132132 };
......@@ -151,7 +151,7 @@ fn nesting_publishes_section_order_and_page_levels_in_one_transaction() {
151151 }
152152 }
153153 let root = &document.spaces[&document.root];
154 let root = &root.revisions[&root.contexts[&ExGuid::default()]];
154 let root = root.active().unwrap();
155155 assert_eq!(
156156 root.nodes[&root.roots[&1]]
157157 .children
crates/onestore/tests/document.rs+1-1
......@@ -17,7 +17,7 @@ fn documented_cell_shading_survives_native_2010_ignoring_its_display() {
1717 .flat_map(|s| s.revisions.values())
1818 .flat_map(|r| r.nodes.values())
1919 .filter_map(|n| match n.kind {
20 Kind::Cell { shading } => shading,
20 Kind::Cell { shading, .. } => shading,
2121 _ => None,
2222 })
2323 .collect();