Repository navigation
feat: foldable headings in the WYSIWYG editor (#1869) - #5276
Conversation
…tor + lint CodeRabbit review of marktext#5276/#4 caught a real nested-fold bug and lint failures. - Fix nested fold state (functional bug): unfolding an ancestor heading no longer reveals content owned by a still-folded descendant heading. _applyFold now consults a pure hiddenByFoldedDescendant() helper so a block stays hidden when it belongs to a nested heading that is itself folded. Regression test added (fold inner → fold ancestor → unfold ancestor). - Replace the three `as unknown as AtxHeading` double casts with a shared structural type guard isFoldableHeading() (foldSection.ts) — removes the lint-suppressed casts flagged by no-restricted-syntax, and _siblingHeadings / both _ownerHeading now narrow safely. - Lint fixes flagged by the muya CI (which was not covered by the desktop lint-staged hook): type-before-value import order, function-declaration fixtures, no explicit any in the test tree walkers. Verified: tsc --noEmit clean; muya lint clean; full muya suite 1508 tests pass (fold suites 55, incl. the new nested-fold regression + hiddenByFoldedDescendant unit tests).
|
Please remove all design docs before I can take a review. |
|
You can put design docs in PR comments. |
|
Thanks for putting this together, @rkristelijn. I've only driven the UI so far — I haven't read the code yet — so this is first-impressions feedback on the interaction design. 1. The copy-anchor is no longer visibleMoving Measured against this PR's CSS (headless Chromium, So below roughly 828px of editor width — any normal window with the sidebar open — only a 4px sliver of the anchor is left. The chevron took over the one gutter slot that is actually inside the column ( The right gutter is free and just as wide. I measured 2. The front-menu fold items feel out of placeThe block front menu operates on this block — change its type, duplicate it, delete it. "Fold all" / "Unfold all" / "Fold to this level" are document-wide view commands, so they read as out of place there, and they make the menu longer for something that will be reached for rarely. I'd drop them from this PR. If we want document-wide folding it belongs with the other view commands (app menu / command palette) and deserves its own PR and its own discussion. 3. The trailing "…" marker feels redundantI understand the argument in the description — a section that just vanishes could be mistaken for an empty one. But the chevron already carries that signal: unlike the copy-anchor it stays visible while the section is folded, and it points right instead of down. A second indicator placed inside the text line means a folded heading no longer looks like the heading the user typed, and it adds a surface that has to keep behaving around wrapping, themes, selection, the TOC and export. I'd drop the marker and the What I'd like to seeScope this down to the core of #1869: fold/unfold one section via the chevron plus Cmd/Ctrl+Shift+[. Leave One merge-forward note for whatever survives: |
Implements Option A from the fold design doc (marktext#1869): fold the section owned by an ATX heading in the new muya engine. A section is every block following a heading up to the next heading of equal-or-higher level, so folding an h1 also hides nested h2/h3. - foldSection.ts: pure, DOM-free section-resolution helpers (unit-tested) - HeadingFoldToggle: chevron affordance (attachment on AtxHeading), operable by click and keyboard (Enter/Space), with aria-expanded and accessible labels - AtxHeading.toggleFold()/_applyFold(): walk siblings and toggle the MU_FOLDED_CONTENT class; fold is runtime-only, never serialized - content.ts: beforeKeydown hook so headings can consume Cmd/Ctrl+Shift+[ (VS Code fold shortcut) before default key routing - CSS chevron + folded-content hiding; locales for all 11 languages - docs/design: design doc, PR marktext#4331 review note, and testing notes Verified: tsc --noEmit clean; 14 unit tests pass (foldSection + headingFold). Known open items (not yet resolved): - Affordance placement is provisional (copy-link -64px, chevron -40px, both in the left gutter). 'Copy-link directly after heading text' was reverted because width:fit-content on headings froze the renderer (see docs/design/fold-heading-testing-notes.md). - Hang fix reverted in source but not yet verified via a dev-server reload. - Collapse/expand children + document-wide fold (Option D) not started.
- Extract isFoldShortcut() as a pure, DOM-free helper in foldSection.ts so the
modifier chord logic is unit-testable without a real KeyboardEvent.
- Extract toFoldSibling() to name the sibling→shape projection used by
collectSectionIndices, with a comment explaining why non-heading blocks
resolve to undefined level.
- Extract _ownerHeading() in HeadingFoldToggle to name and document the
defensive guard against stray clicks during mount/unmount.
- Extract _setFoldedClass() in AtxHeading to make fold CSS toggling read as
intent rather than DOM bookkeeping.
- Add collapsed-section marker: a CSS ::after pill ("…") on folded headings so
hidden content is discoverable. Uses a pseudo-element that does NOT set any
intrinsic width on the heading — avoids the documented width:fit-content
renderer freeze (see fold-heading-testing-notes.md).
- 8 new tests (7 isFoldShortcut chord tests + 1 marker-class integration test);
total fold tests: 22 passed, tsc --noEmit clean.
- Updated testing notes with marker re-test checklist and verification status.
Verified: tsc --noEmit clean; 22 unit+integration tests pass.
Shortcut conflict: confirmed Cmd/Ctrl+Shift+[ is not bound elsewhere.
Hang regression: no fit-content/max-content rule remains on headings (static);
runtime freeze absence confirmed via dev-server (editor responsive, marker
renders, main.log clean).
…marker Builds on the per-heading fold with the actions users need to structure a document, and makes hidden content both visible and directly recoverable. - Clickable "…" collapsed-section marker (HeadingFoldMarker attachment): a real DOM element (a pseudo-element cannot receive a click) shown only while folded, keyboard + pointer operable, that unfolds its section. Positioned without any intrinsic heading width to avoid the documented fit-content renderer freeze. - Document-wide fold on AtxHeading: foldAll (every heading), unfoldAll, and foldToLevel(n) (fold headings deeper than n) driven by the pure, unit-tested foldPlanForLevel helper. foldAll is deliberately NOT foldToLevel(1). - Heading-only fold actions in the paragraph front menu (the block handle): Fold all / Unfold all / Fold to this level; shown only for atx-heading. - isFoldShortcut extracted to a pure helper; attachment lookup deduped into _attachmentByName; fold class toggle named _setFoldedClass (complexity hiding). - Locales for all 11 languages; CSS for the marker; 28 new/updated tests. Verified: tsc --noEmit clean; 50 fold unit+integration tests pass; front menu shows fold items only on headings; runtime dev-server check (fold/unfold, marker click, menu) OK, main.log clean.
…tor + lint CodeRabbit review of marktext#5276/#4 caught a real nested-fold bug and lint failures. - Fix nested fold state (functional bug): unfolding an ancestor heading no longer reveals content owned by a still-folded descendant heading. _applyFold now consults a pure hiddenByFoldedDescendant() helper so a block stays hidden when it belongs to a nested heading that is itself folded. Regression test added (fold inner → fold ancestor → unfold ancestor). - Replace the three `as unknown as AtxHeading` double casts with a shared structural type guard isFoldableHeading() (foldSection.ts) — removes the lint-suppressed casts flagged by no-restricted-syntax, and _siblingHeadings / both _ownerHeading now narrow safely. - Lint fixes flagged by the muya CI (which was not covered by the desktop lint-staged hook): type-before-value import order, function-declaration fixtures, no explicit any in the test tree walkers. Verified: tsc --noEmit clean; muya lint clean; full muya suite 1508 tests pass (fold suites 55, incl. the new nested-fold regression + hiddenByFoldedDescendant unit tests).
SonarCloud quality gate flagged new-code duplicated-lines at 5.4% (threshold 3%), concentrated in the fold test files that each re-declared the same bootMuya + MUYA_VERSION setup/teardown. Extract that boilerplate into src/__tests__/muyaHarness.ts (useMuyaHarness + bootMuya) and consume it from headingFold.spec.ts and foldActions.spec.ts. This removes the duplicated setup blocks (the real duplication) rather than gaming the metric, and future block/UI specs can reuse it. Verified: muya tsc clean; lint clean; full muya suite 1508 tests pass (fold suites 55).
The desktop E2E TOC tests (toc-scroll, toc-panel-content) read a heading via `heading.textContent` and match it against the heading text. The collapsed marker set `domNode.textContent = "…"`, so a folded heading read as "…Heading" and headingIndexByText() returned -1 — failing the TOC scroll/outline suites. This was a real regression from the marker, not pre-existing (thanks to the e2e log that pinpointed it). - Render the "…" glyph via CSS `.mu-fold-marker::after` instead of element text. The element stays a clickable, focusable button, but contributes no text to the heading, so `textContent` stays equal to the heading text. - Add a muya regression test asserting a folded heading textContent (normalized like the desktop consumer) is unchanged — the coverage the unit suite lacked. Verified: muya tsc clean; lint clean; full suite 1509 tests pass.
Address review feedback: drop the document-wide fold actions (Fold all / Unfold all / Fold to this level) and the clickable "..." collapsed-section marker. Keep the core per-heading chevron fold/unfold. Removes the now-dead production code (foldAll/unfoldAll/foldToLevel, the HeadingFoldMarker block + its CSS, the foldPlanForLevel helper, the front-menu fold items), plus the tests and locale strings that only covered the removed surface.
ru was missing 'Fold this section' / 'Unfold this section' since the fold feature first landed, which fails localeCompleteness.spec.ts (every locale must carry the same keys as en). Unrelated to the scope reduction, but required to keep the suite green.
96b9f7f to
69a6255
Compare
|
I took a quick look at the code, and there are a few issues that need to be fixed: 1) Keep the code style consistent with the existing code. 2) Don't write too many comments; you need to judge for yourself where comments are needed(AI always write too much comments). 3) If a function has only one line, don't wrap it in a function, as this makes the code harder to maintain. |
|
Deep review of current head I reproduced the following against the current implementation (including real Chrome interaction checks):
Maintainability/style follow-ups: remove the unused Validation: all 2,362 existing Muya unit tests pass; five added regression scenarios fail (heading conversion, insertion, keyboard navigation, Setext boundaries, search visibility). ESLint has no errors and circular dependency checks pass. Type checking fails identically on the merge base and PR at the pre-existing |
|
Fixed the review findings in e935e2f.
Validation passed:
Local verification used the installed workspace dependencies, which are newer than this PR's lockfile, and temporary configs to allow the isolated worktree to access those dependencies. The root lint command remains blocked by the existing ESLint/@Stylistic configuration incompatibility; the changed Vue file passes using the current workspace ESLint config. Default Muya typechecking retains the existing test |
Preserve column-aware arrow navigation alongside fold-aware traversal and update folding search integration for the renamed query parameter.
Use Home/End on Windows and Linux instead of word-navigation shortcuts. Assert the destination caret before typing and compare stale table picks against the post-undo scroll position.
There was a problem hiding this comment.
Thanks @rkristelijn for contributing heading folding and for getting this feature started.



Closes #1869
Summary
Adds section folding for ATX H1–H6 headings in the TypeScript Muya WYSIWYG engine. Folding hides subsequent blocks up to the next heading of equal or higher level in the same container. Setext headings participate in section boundaries; their own fold controls are outside this PR's delivery scope.
The heading controls are ordered copy heading link → block front-menu handle → fold/unfold. A folded heading keeps its right-pointing chevron visible. An expanded heading’s fold button appears on heading hover or button focus. Activate the fold button with a click, Enter, or Space, or use Cmd/Ctrl+Shift+[ on the heading line.
Ordinary arrow navigation skips folded content. Selection and programmatic cursor placement reveal the necessary sections before editing. Enter, Backspace, Delete, clipboard operations, and undo/redo preserve the hidden document content. Find-in-document expands all sections; closing search leaves them expanded. Revealing a TOC target preserves unrelated and nested fold states.
Fold state is transient and stays out of Markdown and serialized document state. Fold visibility is derived from heading state and section boundaries; DOM classes only present the result. The existing previous/next content traversal methods accept an optional skipFolded parameter for caret navigation.
Scope
This PR delivers individual ATX heading folding and its editor interactions. It does not deliver a trailing ellipsis control, Setext heading fold controls, Fold all / Unfold all / Fold to this level menu actions, or persistence across editor recreation. These features were incorrectly described as already implemented in the previous PR description.
Validation
By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.