Skip to content

Fix dropped and duplicated lines for nested collapse ranges - #470

Open
MaxFreedomPollard wants to merge 1 commit into
expressive-code:mainfrom
MaxFreedomPollard:fix-nested-collapse-ranges
Open

MaxFreedomPollard wants to merge 1 commit into
expressive-code:mainfrom
MaxFreedomPollard:fix-nested-collapse-ranges

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown

Summary

collapse={3-5, 1-8} renders code that no longer matches the source. On a 10-line block, line 10 disappears and lines 3 to 5 show up a second time at the bottom. Writing the same two ranges the other way round, collapse={1-8, 3-5}, renders correctly, so whether you lose a line depends on the order you list the ranges in.

Cause

parseSections in packages/@expressive-code/plugin-collapsible-sections/src/utils.ts skips any range that overlaps a section it already collected, because the <details>-based rendering cannot handle overlapping sections. The check (lines 23 to 26 before this change) only tested whether the new range's from or to falls inside an existing section:

if (from >= existingFrom && from <= existingTo) return
if (to >= existingFrom && to <= existingTo) return

A range that fully contains an existing section satisfies neither test, so { from: 1, to: 8 } was collected alongside { from: 3, to: 5 } instead of being skipped.

sectionizeAst in src/ast.ts then splices both sections into the output array from last to first (line 57), while reading their contents from the unmodified lines array. Splicing the 1-8 section replaces eight entries with one, so the next outp.splice(from - 1, to - from + 1, outerElement) for 3-5 lands on entries that have shifted: it removes line 10 and appends a second copy of lines 3 to 5.

Fix

The two partial checks become the standard interval overlap test, which covers containment in both directions:

if (from <= existingTo && existingFrom <= to) return

Every range combination that already worked keeps its previous result. collapse={2-5, 3-6}, collapse={2-5, 5-6} and collapse={2-5, 1-2} still drop the second range, and collapse={2-5, 6-10} still keeps both.

Tests

test/preprocess-meta.test.ts gets the two orderings of a containing range, and test/rendering.test.ts gets a rendering test that asserts the section boundaries and that the rendered code lines are exactly the source lines, so a dropped or duplicated line fails the test. Both fail on main and pass with the fix.

Verification

pnpm --filter @expressive-code/plugin-collapsible-sections test-short passes, 14 tests in 2 files. With src/utils.ts reverted to main and the new tests in place, the same command fails 2 of 14, reporting sections at lines 1-9 and 10-12 for a 9-line block.

pnpm --filter @expressive-code/core --filter @expressive-code/plugin-collapsible-sections --filter @expressive-code/plugin-frames --filter @expressive-code/plugin-line-numbers --filter @expressive-code/plugin-text-markers test-short passes, 459 tests across the five packages.

pnpm exec eslint packages/@expressive-code/plugin-collapsible-sections and pnpm exec prettier --check on the changed files are both clean, and pnpm --filter @expressive-code/plugin-collapsible-sections build succeeds.

Added a patch changeset for @expressive-code/plugin-collapsible-sections.

`parseSections` in packages/@expressive-code/plugin-collapsible-sections/src/utils.ts skips any `collapse` range that overlaps a section it already collected, because the `<details>`-based rendering cannot handle overlapping sections. The check only tested whether the new range's `from` or `to` falls inside an existing section, so a range that fully contains an existing one, e.g. `collapse={3-5, 1-8}`, was not recognized as overlapping and got added anyway.

Both sections then reached `sectionizeAst` in src/ast.ts, which splices sections into the line array from last to first while reading their content from the unmodified `lines` array. With `collapse={3-5, 1-8}` on a 10-line block, splicing the 1-8 section shrinks the array, so the following splice for 3-5 removes line 10 and appends copies of lines 3 to 5: rendered code that no longer matches the source.

Replaces the two partial checks with the standard interval overlap test `from <= existingTo && existingFrom <= to`, which covers containment in both directions and keeps the previous results for every range combination that was already handled.
@netlify

netlify Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for expressive-code ready!

Name Link
🔨 Latest commit 34ba418
🔍 Latest deploy log https://app.netlify.com/projects/expressive-code/deploys/6a9ea3db15a1fb0008014242
😎 Deploy Preview https://deploy-preview-470--expressive-code.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant