authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 11:36:42-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 12:59:38-07:00
logbc754925884fa44f7244701711aa28b4b1042437
tree5aea012f54c67df92e720bf9f6bc54755b82cb9b
parent5df173732775356dc2a04025c303d4e6c23890f9
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: undoing a style on an equation stores

Lowering restored a restyled equation's math runs with Format ops, which storage refuses for math, so undoing a paragraph style on an equation failed to save. Such a paragraph now lowers to one Equation op. Assisted-by: claude-opus-5.5

2 files changed, 67 insertions(+), 1 deletions(-)

crates/onestore/src/op/lower.rs+16-1
...@@ -1901,7 +1901,22 @@ impl Lowering {...@@ -1901,7 +1901,22 @@ impl Lowering {
1901 return Err(invalid("Text edits did not converge on the model"));1901 return Err(invalid("Text edits did not converge on the model"));
1902 }1902 }
1903 let fresh = !stored_texts.contains(&text.id);1903 let fresh = !stored_texts.contains(&text.id);
1904 for (range, set, clear) in format_edits(&stored.text, &text.text, fresh)? {1904 let edits = format_edits(&stored.text, &text.text, fresh)?;
1905 // Math runs refuse formatting, so a style that restyled them is undone whole.
1906 let math = |at: u32| {
1907 stored
1908 .text
1909 .format_at(at + 1)
1910 .is_ok_and(|format| format.math == Some(true))
1911 };
1912 if edits.iter().any(|(range, ..)| range.clone().any(math)) {
1913 self.emit(PageOp::Equation {
1914 text: stored.id,
1915 math: text.text.clone(),
1916 })?;
1917 continue;
1918 }
1919 for (range, set, clear) in edits {
1905 self.emit(PageOp::Format {1920 self.emit(PageOp::Format {
1906 text: stored.id,1921 text: stored.id,
1907 range,1922 range,
crates/onestore/src/op/tests.rs+51
...@@ -3320,3 +3320,54 @@ fn restyle_refuses_renames_and_missing_styles() {...@@ -3320,3 +3320,54 @@ fn restyle_refuses_renames_and_missing_styles() {
3320 assert!(section.page(space).unwrap() == page);3320 assert!(section.page(space).unwrap() == page);
3321 }3321 }
3322}3322}
3323
3324/// An equation's paragraph restyled in italics, then lowered back to the page before it, as
3325/// undo does: the math runs a style left italic go back whole, since they refuse formatting.
3326#[test]
3327fn a_restyled_equation_lowers_back() {
3328 let (_, source) = SOURCES.iter().find(|(name, _)| *name == "math").unwrap();
3329 let space = pages(source)[0];
3330 let arena = Arena::default();
3331 let mut section = Section::open(&arena, source.to_vec()).unwrap();
3332 let before = section.page(space).unwrap();
3333 let paragraph = model::lists(&before)
3334 .into_iter()
3335 .flat_map(|(_, _, list)| list.iter())
3336 .find(|paragraph| {
3337 paragraph
3338 .text()
3339 .is_some_and(|text| crate::page::Math::is_equation(&text.text))
3340 })
3341 .unwrap()
3342 .id;
3343 let style = PageOp::Style {
3344 paragraph,
3345 style: id(),
3346 definition: Definition {
3347 kind: Kind::Style {
3348 name: Some("Quote".into()),
3349 next: None,
3350 },
3351 format: Format {
3352 italic: Some(true),
3353 ..Default::default()
3354 },
3355 },
3356 };
3357 let edit = |ops: Vec<PageOp>| Edit {
3358 at: AT,
3359 ops: ops.into_iter().map(|op| Op::Page { space, op }).collect(),
3360 };
3361 section.apply("Author", &edit(vec![style])).unwrap();
3362 let styled = section.page(space).unwrap();
3363 section
3364 .apply("Author", &edit(lower_page(&styled, &before).unwrap()))
3365 .unwrap();
3366 let after = section.page(space).unwrap();
3367 assert_eq!(
3368 normalize(&after),
3369 normalize(&before),
3370 "{}",
3371 first_difference(&normalize(&after), &normalize(&before))
3372 );
3373}