diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index 4aeba29be..18262f701 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -123,6 +123,36 @@ pub fn named_items( } } + if let Some(v) = parent.cast::().filter(|v| { + // Check if the node is in the body of the closure. + let body = parent.find(v.body().span()); + body.is_some_and(|n| n.find(node.span()).is_some()) + }) { + for param in v.params().children() { + match param { + ast::Param::Pos(pattern) => { + for ident in pattern.bindings() { + if let Some(t) = recv(NamedItem::Var(ident)) { + return Some(t); + } + } + } + ast::Param::Named(n) => { + if let Some(t) = recv(NamedItem::Var(n.name())) { + return Some(t); + } + } + ast::Param::Spread(s) => { + if let Some(sink_ident) = s.sink_ident() { + if let Some(t) = recv(NamedItem::Var(sink_ident)) { + return Some(t); + } + } + } + } + } + } + ancestor = Some(parent.clone()); continue; } @@ -269,6 +299,17 @@ mod tests { assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b")); } + #[test] + fn test_param_named_items() { + // Has named items + assert!(has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 12, "a")); + assert!(has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "a")); + + // Doesn't have named items + assert!(!has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 19, "a")); + assert!(!has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "b")); + } + #[test] fn test_import_named_items() { // Cannot test much. diff --git a/crates/typst-layout/src/flow/distribute.rs b/crates/typst-layout/src/flow/distribute.rs index 7a1cf4264..f504d22e7 100644 --- a/crates/typst-layout/src/flow/distribute.rs +++ b/crates/typst-layout/src/flow/distribute.rs @@ -17,7 +17,7 @@ pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult { /// A snapshot which can be restored to migrate a suffix of sticky blocks to /// the next region. sticky: Option>, - /// Whether there was at least one proper block. Otherwise, sticky blocks - /// are disabled (or else they'd keep being migrated). - stickable: bool, + /// Whether the current group of consecutive sticky blocks are still sticky + /// and may migrate with the attached frame. This is `None` while we aren't + /// processing sticky blocks. On the first sticky block, this will become + /// `Some(true)` if migrating sticky blocks as usual would make a + /// difference - this is given by `regions.may_progress()`. Otherwise, it + /// is set to `Some(false)`, which is usually the case when the first + /// sticky block in the group is at the very top of the page (then, + /// migrating it would just lead us back to the top of the page, leading + /// to an infinite loop). In that case, all sticky blocks of the group are + /// also disabled, until this is reset to `None` on the first non-sticky + /// frame we find. + /// + /// While this behavior of disabling stickiness of sticky blocks at the + /// very top of the page may seem non-ideal, it is only problematic (that + /// is, may lead to orphaned sticky blocks / headings) if the combination + /// of 'sticky blocks + attached frame' doesn't fit in one page, in which + /// case there is nothing Typst can do to improve the situation, as sticky + /// blocks are supposed to always be in the same page as the subsequent + /// frame, but that is impossible in that case, which is thus pathological. + stickable: Option, } /// A snapshot of the distribution state. @@ -314,13 +331,31 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { // If the frame is sticky and we haven't remembered a preceding // sticky element, make a checkpoint which we can restore should we // end on this sticky element. - if self.stickable && self.sticky.is_none() { + // + // The first sticky block within consecutive sticky blocks + // determines whether this group of sticky blocks has stickiness + // disabled or not. + // + // The criteria used here is: if migrating this group of sticky + // blocks together with the "attached" block can't improve the lack + // of space, since we're at the start of the region, then we don't + // do so, and stickiness is disabled (at least, for this region). + // Otherwise, migration is allowed. + // + // Note that, since the whole region is checked, this ensures sticky + // blocks at the top of a block - but not necessarily of the page - + // can still be migrated. + if self.sticky.is_none() + && *self.stickable.get_or_insert_with(|| self.regions.may_progress()) + { self.sticky = Some(self.snapshot()); } } else if !frame.is_empty() { - // If the frame isn't sticky, we can forget a previous snapshot. - self.stickable = true; + // If the frame isn't sticky, we can forget a previous snapshot. We + // interrupt a group of sticky blocks, if there was one, so we reset + // the saved stickable check for the next group of sticky blocks. self.sticky = None; + self.stickable = None; } // Handle footnotes. diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 08c2a2f45..0d51a1e4e 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -74,6 +74,7 @@ pub fn layout_enum( regions: Regions, ) -> SourceResult { let numbering = elem.numbering(styles); + let reversed = elem.reversed(styles); let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { @@ -86,7 +87,9 @@ pub fn layout_enum( let mut cells = vec![]; let mut locator = locator.split(); - let mut number = elem.start(styles); + let mut number = + elem.start(styles) + .unwrap_or_else(|| if reversed { elem.children.len() } else { 1 }); let mut parents = EnumElem::parents_in(styles); let full = elem.full(styles); @@ -127,7 +130,8 @@ pub fn layout_enum( item.body.clone().styled(EnumElem::set_parents(smallvec![number])), locator.next(&item.body.span()), )); - number = number.saturating_add(1); + number = + if reversed { number.saturating_sub(1) } else { number.saturating_add(1) }; } let grid = CellGrid::new( diff --git a/crates/typst-layout/src/repeat.rs b/crates/typst-layout/src/repeat.rs index b761438c8..bfc7b32ce 100644 --- a/crates/typst-layout/src/repeat.rs +++ b/crates/typst-layout/src/repeat.rs @@ -33,8 +33,17 @@ pub fn layout_repeat( let fill = region.size.x; let width = piece.width(); - // count * width + (count - 1) * gap = fill, but count is an integer so - // we need to round down and get the remainder. + // We need to fit the body N times, but the number of gaps is (N - 1): + // N * w + (N - 1) * g ≤ F + // where N - body count (count) + // w - body width (width) + // g - gap width (gap) + // F - available space to fill (fill) + // + // N * w + N * g - g ≤ F + // N * (w + g) ≤ F + g + // N ≤ (F + g) / (w + g) + // N = ⌊(F + g) / (w + g)⌋ let count = ((fill + gap) / (width + gap)).floor(); let remaining = (fill + gap) % (width + gap); @@ -52,7 +61,7 @@ pub fn layout_repeat( if width > Abs::zero() { for _ in 0..(count as usize).min(1000) { frame.push_frame(Point::with_x(offset), piece.clone()); - offset += piece.width() + gap; + offset += width + gap; } } diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index db9acece3..2044c917e 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -719,11 +719,7 @@ fn segment( false } - let solid = stroke - .dash - .as_ref() - .map(|pattern| pattern.array.is_empty()) - .unwrap_or(true); + let solid = stroke.dash.as_ref().map(|dash| dash.array.is_empty()).unwrap_or(true); let use_fill = solid && fill_corners(start, end, corners); let shape = if use_fill { diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index 28f983186..d960a666c 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -119,7 +119,6 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { global.define_func::(); global.define_func::(); global.define_func::(); - global.define_func::