feat(editor): collapsible heading sections (Obsidian parity)#187
Merged
Conversation
Add fold/unfold for markdown heading sections in the CodeMirror live-preview editor, matching Obsidian: a chevron in the fold gutter next to every ATX heading collapses everything under it until the next heading of the same or higher level. - foldableHeadings.ts: a foldService computing the section range from the lezer markdown tree (ATXHeading1..6), not a regex. Range = end of heading line to just before the next heading whose level <= this one (or EOF). Per-Tree WeakMap cache keeps the fold-gutter's per-line queries cheap. - codeFolding() + foldGutter() with dark-theme chevrons + a collapsed-section placeholder; touch-friendly gutter sizing under (pointer: coarse). - Keybindings: Mod-. toggles the heading fold at the cursor; Mod-Alt-[ / Mod-Alt-] fold / unfold all. No collisions with existing editor or app shortcuts. - View-only: the markdown text is never modified, so collab (yCollab), save, and the live-preview decorations all keep working. Unit + integration tests cover the range logic and confirm no content corruption with folding + live preview mounted together. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…utter The new fold gutter shifts the editor content right by the gutter column and renders a fold chevron next to the heading in the selection-visibility note, so the committed baselines for editor-selection-themes.spec.ts no longer match. Regenerated all four theme snapshots (default/light/sepia/solarized-dark) in the pinned Playwright v1.60.0 container so the pixels match the CI runner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thetechjon
pushed a commit
that referenced
this pull request
Jun 15, 2026
…beta Temporary, revertable: comments out the foldableHeadings extension (#187) to isolate whether the fold gutter / codeFolding / foldService is the cause of the slow note switch Jon sees on beta (dev-only; prod/main is instant). If switching becomes instant with this off, re-enable and fix its per-switch cost; otherwise revert this commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds Obsidian-style collapsible / foldable heading sections to the CodeMirror live-preview editor. A chevron in the fold gutter next to every markdown heading collapses everything under it until the next heading of the same or higher level; click again (or use the keybinding) to expand.
Fold-range logic
Computed from the lezer markdown syntax tree (
ATXHeading1..ATXHeading6), not a regex (src/components/editor/foldableHeadings.ts):<=this heading's level (smaller-or-equal level number), or to end-of-document for the last heading.##folds until the next##or#; a nested###folds independently inside its parent##; the last heading folds to EOF.null, so the gutter does not offer a no-op fold.Treein aWeakMap, so the fold gutter's repeated per-line queries stay cheap and the cache is GC'd when the tree is superseded.Wired via
@codemirror/language'sfoldService+codeFolding()+foldGutter(), added to the editor extension list inCodeMirrorEditor.tsx. The collapsed section shows a styled⋯placeholder; gutter chevrons (▾/▸) and the placeholder are themed to match the obsidian-dark palette.Keybindings
Picked to avoid collisions with the editor's existing bindings and the app-level shortcuts in
src/utils/shortcuts.ts(verified Ctrl+. and the Alt-bracket chords are unused):toggleFold)foldAll)unfoldAll)The fold gutter chevron is also clickable, and the placeholder is clickable to unfold.
View-only — no content corruption
Folding is a pure view-layer collapse: CodeMirror hides lines with a replace-decoration layer and the underlying markdown text is never modified. So collab (yCollab), save, and the live-preview decorations all keep working. No mark decoration spans the fold boundary (the boundary sits at a line end), and line decorations on hidden lines simply don't render — the two layers coexist cleanly. None of the existing editor extensions (hanging indent, live preview, tasks, images, ordered-list renumber, smart-Enter, table Tab) are touched.
Mobile
The fold gutter widens and the chevron grows under
@media (pointer: coarse)so the tap target is comfortable on touch.Tests (
src/__tests__/foldableHeadings.test.ts)headingFoldRange:##folds to next##/#; nested###folds independently; last heading folds to EOF; empty-body heading folds nothing; non-heading line folds nothing.EditorViewwith the folding extension, fold a heading, assert the body lines are folded (foldedRanges) and the doc text is byte-for-byte unchanged; unfold restores. A further case mounts folding + the markdown live-preview StateField together and confirms no crash and intact content.Verification
npx tsc --noEmit— zero errorsnpx eslint(changed files) — cleannpx jest --ci— 3017 passed, 0 failed (no flake)Rendered-preview folding — deferred
Folding is shipped for the CodeMirror editor only. The read-only ReactMarkdown preview (
EditorContent.tsx) renders a heading and its following content as flat sibling nodes, not a nested tree, so a<details>-style per-section collapse needs a rehype transform that groups siblings under each heading. That transform would also have to coexist with the preview's existing document-order DOM walks (cursor-block tracking, task-checkbox line matching, tag styling), making it a meaningfully separate effort. Deferred as a follow-up rather than rushed in here.🤖 Generated with Claude Code