Skip to content

fix(rich-markdown-editor): eliminate empty-block / boundary-key structural bugs#5517

Merged
waleedlatif1 merged 5 commits into
stagingfrom
fix/list-item-delete-gap
Jul 8, 2026
Merged

fix(rich-markdown-editor): eliminate empty-block / boundary-key structural bugs#5517
waleedlatif1 merged 5 commits into
stagingfrom
fix/list-item-delete-gap

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a class of structural editing + serialization bugs in the rich-markdown (.md) editor, discovered while fixing the reported "deleting a bullet leaves a weird space" bug. A deep audit (parallel subagents + a real-browser ProseMirror harness) found the original fix was not isolated — the same class spanned other boundary keys, node types, and the serializer. This PR eliminates the whole class.

Tier 1 — Empty-wrapped-block boundary keys (keymap.ts)

joinBackward on an empty list item left a stray empty paragraph inside the previous item (flat lists), no-op'd on nested items (leaving them stuck), and never handled blockquotes (which split in two). Replaced with a walk-up-and-delete that removes the whole emptied wrapper, generalized to list items, task items, and blockquotes at every position. Added an Enter handler: pressing Enter on an empty non-trailing list/task item previously split the list around a stranded, non-round-trippable empty paragraph; it now removes the empty item. A trailing empty item still exits the list via the default (unchanged).

Tier 2 — Verbatim node isolation (raw-markdown-snippet.tsx)

footnoteDef and rawHtmlBlock hold exact source text but were neither isolating nor atom, so a single Backspace/Delete at their boundary merged their raw markdown into an adjacent paragraph as HTML-escaped prose — silently destroying the node and corrupting saved markdown. Marked the block variants isolating.

Tier 3 — Paragraph serialization guard (extensions.ts)

BlockSafeParagraph guards a paragraph's leading characters so it can't re-parse into a different block on the next load:

  • Leading block markers (#, -, +, 1., 1), bare ---) are backslash-escaped (the upstream serializer escaped inline delimiters — so * bullets and > quotes already round-tripped — but not these).
  • Leading indent (4-space/tab) is stripped — it never renders in a paragraph and would otherwise re-parse as an indented code block. (Real indented/fenced code blocks are codeBlock nodes, untouched.)
  • Replacing StarterKit's Paragraph also drops the upstream   empty-paragraph marker: consecutive empty paragraphs now round-trip via blank lines — count-preserved, idempotent, and no longer tripping the read-only safety gate (which flags  ).

All escaping/stripping is idempotent (parsing consumes it) and never over-escapes non-markers (#hashtag, -5).

Testing

Real-browser ProseMirror harness + committed Vitest (CI):

  • Tier 1: bullet/ordered/task/blockquote/nested, Backspace + Enter, all positions, regressions.
  • Tier 2: boundary corruption (footnote + raw HTML) + in-place editing + whole-node deletion + round-trip.
  • Tier 3: marker escaping, indent stripping, no over-escape, and empty-paragraph count preservation (1–4).
  • Regression: round-trip fixtures stay byte-identical idempotent (headings, marks, all list types, quotes, code blocks, mermaid, tables, images, footnotes, raw/inline HTML, mentions, real indented code, full mixed docs), verbatim nodes still edit/delete, input rules (# /- ) still autoformat — confirming the paragraph replacement and isolating changes have no side effects.

Note

An empty blockquote (a > with no content) normalizes away to blank lines on reload. This is a benign one-time normalization (idempotent, no data loss, not read-only) — preserving an empty quote has no clean CommonMark representation and would reintroduce the  →read-only problem, so normalizing is the correct behavior.

@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 8, 2026 10:00pm

Request Review

@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches document structure on every save (paragraph markdown) and keyboard editing in lists/quotes; risk is mitigated by broad round-trip and keymap tests, but wrong guard or keymap logic could still alter user content subtly.

Overview
Fixes structural editing and markdown round-trip bugs in the rich markdown editor (lists, quotes, verbatim blocks, and paragraph serialization).

Backspace / Enter on empty wrapped blocks (keymap.ts): When the caret is at the start of an empty paragraph inside a list item, task item, or blockquote, Backspace now deletes the whole emptied wrapper via removeEmptyWrappedBlock instead of ProseMirror’s lift/join (which left stray paragraphs, split lists, or stuck nested items). Enter on an empty non-trailing list/task item removes that item the same way; a trailing empty item still uses the default to exit the list.

Verbatim block boundaries (raw-markdown-snippet.tsx): Block footnoteDef and rawHtmlBlock nodes are marked isolating so boundary Backspace/Delete cannot merge their raw source into adjacent paragraphs (which would destroy the node on save).

Paragraph serialization (extensions.ts): StarterKit’s paragraph is replaced with BlockSafeParagraph, which on serialize strips leading indent and backslash-escapes leading block markers (#, -, +, ordered list prefixes, ---) so literal paragraph text does not re-parse as headings, lists, thematic breaks, or indented code blocks. That also drops the upstream   empty-paragraph hack so multiple consecutive empty paragraphs round-trip as blank lines without tripping read-only safety.

Vitest coverage was added for keymap behavior, paragraph guards, empty paragraphs, and existing round-trip fixtures.

Reviewed by Cursor Bugbot for commit 3b3cd1a. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a class of structural editing and serialization bugs in the rich-markdown editor across three tiers: empty-wrapper boundary keys (Backspace/Enter on empty list items and blockquotes), verbatim-node boundary isolation, and paragraph leading-character escaping.

  • Keymap (keymap.ts): removeEmptyWrappedBlock walks up through sole-child ancestors and deletes the entire emptied wrapper; the Backspace handler covers listItem, taskItem, and blockquote; a new Enter handler removes non-trailing empty list items instead of splitting the list around a stranded paragraph.
  • Verbatim isolation (raw-markdown-snippet.tsx): isolating: !inline prevents boundary Backspace from merging raw source text into adjacent paragraphs.
  • Paragraph serializer (extensions.ts): BlockSafeParagraph backslash-escapes leading block markers and strips leading indent, replacing the upstream   empty-paragraph marker so consecutive empty paragraphs round-trip via blank lines.

Confidence Score: 5/5

All three tiers of fixes are narrowly scoped to the rich-markdown editor with no schema-breaking changes and comprehensive round-trip and keymap tests.

The handlers are gated by tight pre-conditions and the walk-up delete is bounded to the same-parent chain. The isolating addition and BlockSafeParagraph serializer are confirmed non-destructive by the round-trip fixture suite. Both flagged observations are speculative with plausible explanations in existing code.

No files require special attention; all changed files have direct test coverage in the accompanying test files.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/keymap.ts Adds removeEmptyWrappedBlock (walk-up delete), extends Backspace to cover listItem/taskItem/blockquote, adds Enter handler for non-trailing empty list items
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts Introduces BlockSafeParagraph with guardParagraphLeading to prevent round-trip corruption of paragraphs starting with block markers or leading indent
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/raw-markdown-snippet.tsx Adds isolating: !inline to verbatim block nodes, preventing boundary merges from destroying raw source text
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/keymap.test.ts Adds comprehensive Backspace/Enter/isolating tests across bullet, ordered, task, blockquote, and nested list cases with round-trip idempotency checks
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip.test.ts Adds paragraph serialization guard tests and empty-paragraph count-preservation tests covering the idempotency property

Reviews (2): Last reviewed commit: "fix(rich-markdown-editor): strip leading..." | Re-trigger Greptile

…item

Backspacing an emptied list item in the middle of a list used ProseMirror's default lift, which pulls
the item out into a top-level paragraph — splitting one list into two and stranding an empty paragraph
(a visible gap). Backspace at the start of an empty list item now joins into the previous block instead,
removing the item and keeping a single list. Covers bullet, ordered, and task lists.
Replace the joinBackward empty-list-item Backspace branch with a walk-up-and-delete
that removes the whole emptied wrapper, and extend it to task items and blockquotes.
joinBackward left a stray empty paragraph inside the previous item (flat lists) or
no-op'd entirely on nested items (leaving them stuck); blockquotes were never handled
and split in two. Add an Enter handler so an empty non-trailing list/task item is
removed rather than split into two lists around a stranded, non-round-trippable empty
paragraph; a trailing empty item still exits the list via the default.

Covers bullet/ordered/task/blockquote at every position; non-empty items and the
double-Enter list-exit are unaffected.
… joins

footnoteDef and rawHtmlBlock hold exact source text but were neither isolating nor
atom, so a single Backspace/Delete at their boundary let ProseMirror's default join
merge their raw markdown into an adjacent paragraph as HTML-escaped prose — silently
destroying the node and corrupting saved markdown. Mark the block variants isolating
so boundary keys can't cross their edge. Round-trip and in-place editing are unchanged.
…serialization

A paragraph beginning with #, -, +, 1., 1), or a bare --- serialized unescaped, so it
silently re-parsed into a heading / list / thematic break on the next load (reachable
via type-a-marker then undo). The upstream serializer escapes inline delimiters (* _ `
[ ] ~, so * bullets and > quotes already round-trip) but not these block-starting
markers. BlockSafeParagraph wraps the paragraph renderer with a leading-marker guard;
escaping is idempotent (parsing consumes the backslash) and never over-escapes non-markers
like #hashtag or -5.
@waleedlatif1 waleedlatif1 force-pushed the fix/list-item-delete-gap branch from 3dbfdfc to 70bfe19 Compare July 8, 2026 21:49
@waleedlatif1 waleedlatif1 changed the title fix(rich-markdown-editor): don't split a list when deleting an empty item fix(rich-markdown-editor): eliminate empty-block / boundary-key structural bugs Jul 8, 2026
… paragraphs  -free

A paragraph beginning with a 4-space/tab indent re-parsed as an indented code block on the
next load. Leading whitespace never renders in a paragraph (CommonMark strips up to three
leading spaces; four or more become code), so BlockSafeParagraph now strips it — lossless
and idempotent. Composes with the existing leading-marker escaping (e.g. '   # x' → '\# x').

Also locks in that consecutive empty paragraphs serialize via blank lines rather than the
upstream ' ' marker: replacing StarterKit's Paragraph already dropped the   path,
and the round-trip preserves the empty-paragraph count without tripping the read-only
safety gate (which flags   as a stable-loss pattern). Added tests for both.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 3b3cd1a. Configure here.

@waleedlatif1 waleedlatif1 merged commit 1236474 into staging Jul 8, 2026
18 checks passed
@waleedlatif1 waleedlatif1 deleted the fix/list-item-delete-gap branch July 8, 2026 22:14
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.

1 participant