Skip to content

Fix live preview empty list caret anchor#106

Merged
jsgrrchg merged 5 commits into
mainfrom
fix/live-preview-empty-list-caret-anchor
May 18, 2026
Merged

Fix live preview empty list caret anchor#106
jsgrrchg merged 5 commits into
mainfrom
fix/live-preview-empty-list-caret-anchor

Conversation

@jsgrrchg
Copy link
Copy Markdown
Owner

Summary

Fixes the live preview caret behavior for active empty list items.

The issue had two layers: collapsing only the marker left a real trailing source space that pushed the caret about one character past the rendered bullet, while collapsing the full prefix fixed the horizontal position but left drawSelection() without real geometry for the cursor in empty items.

This PR keeps the full source prefix collapsed and adds a zero-width caret anchor only for active empty list items, so CodeMirror has a natural-height geometry target without changing global cursor styling. It also normalizes clicks that land inside the collapsed list prefix so the caret moves to the visual content start instead of staying inside hidden source text.

Changes

  • Collapse the full active empty list prefix, including task-list prefixes.
  • Add a local invisible caret anchor widget for active empty list items.
  • Redirect clicks inside collapsed empty-list prefixes to the visual content start.
  • Add a minimal Playwright harness for measuring the rendered CodeMirror cursor.
  • Add e2e coverage for top-level, nested, middle, task, ordered, inactive, non-empty, and click-on-prefix cases.

Validation

  • npm test -- --run src/features/editor/extensions/livePreviewInline.test.ts
  • npm run test:e2e

Fixes #102.

Copy link
Copy Markdown
Contributor

@spamsch spamsch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent confirmation: pulled the branch and ran the suite. 10/10 Playwright + 44/44 vitest in livePreviewInline.test.ts pass against PR #106.

Two notes after comparing approaches:

The decomposition is the right one. Keeping the hide and the caret anchor as separate decorations at separate positions reads more cleanly than collapsing the whole line as one Decoration.replace. It also leaves taskMarkerRule's existing hide intact, which keeps the responsibilities at "this rule hides this token" instead of having to remember that another rule already covered me.

The click-redirect is the part I missed in my own attempt. Without it, posAtCoords returning a position inside the collapsed prefix dropped the caret into hidden source and made it disappear on click — same artifact as before, different trigger. moveEmptyListPrefixClickToContentStart closes that gap, and the new clicking an active empty list prefix keeps the caret visible e2e case pins it.

One small style suggestion: addEmptyListCaretAnchor calls pushDeco with an inline Decoration.widget({ widget, side: -1 }) — building that wrapper once at module scope (the way emptyListCaretAnchorWidget already is) would avoid allocating a new spec object per anchor. Minor, take it or leave it.

LGTM. Glad to see this land.

@jsgrrchg
Copy link
Copy Markdown
Owner Author

Thank you for checking my work. I added a new commit with your suggestions. I’m really proud of this PR. Thank you for helping me chase it down.

@jsgrrchg jsgrrchg marked this pull request as ready for review May 18, 2026 06:10
@jsgrrchg
Copy link
Copy Markdown
Owner Author

Small follow-up fix: I found one more path where the caret anchor could be missing.

If an empty list item was initially inactive, its decorations were built without the caret anchor. Clicking into that line changed the selection, but the plugin did not always rebuild because the line was only registered as multiline-line sensitive. As a result, the caret could still land inside the collapsed prefix until a real text edit forced a rebuild.

This commit registers empty list lines as normal line-selection-sensitive too, so entering/leaving an empty list item rebuilds the decorations and installs the caret anchor immediately. I also added an e2e case for clicking an initially inactive empty list item.

Validation:

  • 44/44 vitest
  • 11/11 Playwright

@jsgrrchg jsgrrchg merged commit a7cc4f6 into main May 18, 2026
8 checks passed
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.

Investigate live preview caret behavior in empty list items

2 participants