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:
or nested variants such as:
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.
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:
or nested variants such as:
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:
-.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:
-/-as raw Markdown.Pros:
Cons:
-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:
Pros:
-.Cons:
Outcome: rejected because the visual edge case still reproduced.
3. Replace the whole empty list prefix with an inline caret shim widget
Behavior:
-or-with a widget that draws the bullet and reserves indent + marker + gap width.Pros:
Cons:
contenteditable=false, so caret placement around it remains delicate.Outcome: rejected after manual visual testing.
Related Code
Likely hotspots:
apps/desktop/src/features/editor/extensions/livePreviewInline.tslistMarkRuleapplyLooseListFallbackisActiveEmptyListLineapps/desktop/src/features/editor/extensions/livePreviewTheme.ts.cm-lp-li-line::before.cm-lp-list-continuationapps/desktop/src/features/editor/markdownLists.tsparseMarkdownListItemCurrent 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
- [ ]) are not regressed.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.