Skip to content

Investigate live preview caret behavior in empty list items #102

@jsgrrchg

Description

@jsgrrchg

Problem

Live preview has an edge case with empty Markdown list items where the caret and bullet marker become visually misaligned or confusing.

Observed examples:

- Probando
- 
    - eeeee

or nested variants such as:

- eeeee
    - 

In live preview, the empty item can show a preview bullet while the caret appears on a separate/awkward visual position, or the raw - marker can appear if we reveal the Markdown source. The result feels unstable: neither fully polished live preview nor fully honest source editing.

Why this is tricky

The rendered bullet is currently a CSS pseudo-element on the line decoration (.cm-lp-li-line::before). It is not real editable document text. CodeMirror still needs a real document position to place the caret when the user is editing an empty list item.

That creates a three-way tension:

  • Keep the live preview bullet visible and hide raw - .
  • Keep a stable, editable caret position where typing continues the list item.
  • Avoid ghost spacing, duplicated markers, or the caret appearing detached from the bullet.

For non-empty list items this works well because there is real content after the marker. For empty list items, the only editable anchor is the marker/spacing itself.

Approaches Tried

1. Reveal raw Markdown for active empty list items

Behavior:

  • If the item is active and empty, skip live-preview list decoration for that line.
  • Show - / - as raw Markdown.

Pros:

  • Technically stable.
  • Caret position is honest and predictable.
  • Avoids fighting CodeMirror's caret model.

Cons:

  • Visually jarring in live preview.
  • The raw - appears while editing, which is not the desired polished preview behavior.

Outcome: rejected because it fixed caret anchoring but degraded the live preview UX.

2. Hide the marker and replace only the trailing marker space with an invisible zero-width anchor

Behavior:

  • Keep the pseudo-bullet.
  • Hide the raw marker.
  • Replace the final source space with a tiny/zero-width widget so CodeMirror has an anchor.

Pros:

  • Avoided showing raw -.
  • Preserved the preview bullet.

Cons:

  • The caret still appeared awkwardly positioned in practice.
  • The source-space/widget interaction remained too dependent on CodeMirror inline layout details.

Outcome: rejected because the visual edge case still reproduced.

3. Replace the whole empty list prefix with an inline caret shim widget

Behavior:

  • Replace - or - with a widget that draws the bullet and reserves indent + marker + gap width.
  • Let the caret sit immediately after that widget.

Pros:

  • More principled than a zero-width anchor.
  • Tests could verify the decoration model for normal and nested empty list items.

Cons:

  • Still did not feel correct visually in the real editor.
  • The widget is contenteditable=false, so caret placement around it remains delicate.
  • Risks around keyboard navigation, IME composition, accessibility, selection, and screen reader behavior.

Outcome: rejected after manual visual testing.

Related Code

Likely hotspots:

  • apps/desktop/src/features/editor/extensions/livePreviewInline.ts
  • listMarkRule
  • applyLooseListFallback
  • isActiveEmptyListLine
  • apps/desktop/src/features/editor/extensions/livePreviewTheme.ts
  • .cm-lp-li-line::before
  • .cm-lp-list-continuation
  • apps/desktop/src/features/editor/markdownLists.ts
  • parseMarkdownListItem

Current Understanding

This is probably not a simple CSS spacing bug. It is an editing-model problem caused by rendering list markers as pseudo-elements while hiding the actual source marker. Empty list items have no content after the marker, so the caret has no natural visible text anchor.

A robust solution may need a more explicit editing mode for active empty list items, possibly accepting that raw Markdown should appear for that exact line, or introducing a custom CodeMirror widget/decoration strategy with careful browser-level testing.

Acceptance Criteria for a Future Fix

  • Empty unordered list items in live preview do not show duplicated/ghost markers.
  • Empty nested list items behave consistently with top-level empty items.
  • The caret appears visually attached to the intended typing position.
  • Typing into the empty item immediately produces a normal live-preview list item.
  • Backspace/Enter/Tab/Shift-Tab behavior remains correct.
  • Task list items (- [ ]) are not regressed.
  • Multiline selection across list items still reveals raw Markdown where appropriate.
  • IME/composition and accessibility are considered before shipping.

Notes

This came up while hardening live preview caret behavior. A separate inline emphasis boundary issue was fixed independently in PR #101; this issue is specifically about empty list item caret/list marker behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions