Skip to content

feat(mdviewer): MD editor should stick to max line length of the editor#2938

Open
abose wants to merge 2 commits into
mainfrom
mde
Open

feat(mdviewer): MD editor should stick to max line length of the editor#2938
abose wants to merge 2 commits into
mainfrom
mde

Conversation

@abose
Copy link
Copy Markdown
Member

@abose abose commented May 23, 2026

Turndown emits each block as one physical line regardless of width, so
typing or pasting a long paragraph in the md viewer wrote one very long
line to CM on every save. Adds a line-scoped reflow that wraps only the
lines the user actually edited, behind a new opt-out preference
mdViewerWrapEditedLines (default true) using EditorOptionHandlers
.getMaxLineLength() as the width source.

New module markdown-line-wrap.js (with 26 unit specs):

  • Prefix/suffix line-range diff so unchanged lines stay byte-identical.
  • Indent-aware continuation: bullets (- * +), ordered lists (N. / N)),
    and blockquotes drive the continuation indent on wrapped lines.
  • Inline atoms (image-only links like a b, plain
    image links, inline links, inline code, inline HTML tags) are kept
    whole by tokenization. Single oversize tokens are left intact.
  • Conservative skip rules: fenced code, tables, ATX headings, setext
    underlines, link reference definitions, hr, HTML blocks, frontmatter.
  • Balanced packer: binary-searches for the smallest width that still
    produces the greedy line count, so the two physical lines come out
    e.g. 95+90 instead of 118+67 for a 186-char paragraph. Critical for
    the cursor-sync precision in the rendered viewer (see below).
  • Fast-path early exit when no changed line exceeds printWidth, so
    typical typing keystrokes never run the regex-heavy state scan.

Per-source-line cursor sync in the markdown viewer (bridge.js + editor.js):

  • The custom paragraph renderer in bridge.js now wraps each source line
    of a multi-line paragraph in . Without
    these the whole

    shared a single data-source-line and cursor sync
    always resolved to the block's first line, so moving the caret
    through a wrapped paragraph never updated the CM highlight.

  • editor.js _updateSourceLineAttrs keeps those spans in sync as the
    user edits. _refreshParagraphSourceSpans no-ops when the layout is
    unchanged, updates attributes in place when only the start line
    shifted, and rebuilds inner HTML (preserving caret by character
    offset) when the wrap line count actually changed.
  • Fixes a pre-existing _updateSourceLineAttrs filter bug: it was
    skipping any element with the cursor-sync-highlight class, but that
    class is added to real content blocks (

    ,

    ) for highlighting
    purposes. Skipping them threw mdLineIdx off-by-one for every block
    after the highlighted one. Now we only skip the standalone overlay
    variants (cursor-sync-br-line, cursor-sync-code-line).

Tests:

  • 26 unit specs in unit:markdown-line-wrap cover bullets, nested lists,
    ordered lists, blockquotes, fences, tables, headings, link refs,
    inline atoms, frontmatter, and the badge-row regression.
  • The existing 58-spec livepreview:Markdown Editor 1 suite still passes.

abose added 2 commits May 23, 2026 11:54
Turndown emits each block as one physical line regardless of width, so
typing or pasting a long paragraph in the md viewer wrote one very long
line to CM on every save. Adds a line-scoped reflow that wraps only the
lines the user actually edited, behind a new opt-out preference
mdViewerWrapEditedLines (default true) using EditorOptionHandlers
.getMaxLineLength() as the width source.

New module markdown-line-wrap.js (with 26 unit specs):
- Prefix/suffix line-range diff so unchanged lines stay byte-identical.
- Indent-aware continuation: bullets (- * +), ordered lists (N. / N)),
  and blockquotes drive the continuation indent on wrapped lines.
- Inline atoms (image-only links like [![a](u) ![b](u)](href), plain
  image links, inline links, inline code, inline HTML tags) are kept
  whole by tokenization. Single oversize tokens are left intact.
- Conservative skip rules: fenced code, tables, ATX headings, setext
  underlines, link reference definitions, hr, HTML blocks, frontmatter.
- Balanced packer: binary-searches for the smallest width that still
  produces the greedy line count, so the two physical lines come out
  e.g. 95+90 instead of 118+67 for a 186-char paragraph. Critical for
  the cursor-sync precision in the rendered viewer (see below).
- Fast-path early exit when no changed line exceeds printWidth, so
  typical typing keystrokes never run the regex-heavy state scan.

Per-source-line cursor sync in the markdown viewer (bridge.js + editor.js):
- The custom paragraph renderer in bridge.js now wraps each source line
  of a multi-line paragraph in <span data-source-line="N">. Without
  these the whole <p> shared a single data-source-line and cursor sync
  always resolved to the block's first line, so moving the caret
  through a wrapped paragraph never updated the CM highlight.
- editor.js _updateSourceLineAttrs keeps those spans in sync as the
  user edits. _refreshParagraphSourceSpans no-ops when the layout is
  unchanged, updates attributes in place when only the start line
  shifted, and rebuilds inner HTML (preserving caret by character
  offset) when the wrap line count actually changed.
- Fixes a pre-existing _updateSourceLineAttrs filter bug: it was
  skipping any element with the cursor-sync-highlight class, but that
  class is added to real content blocks (<p>, <h1>) for highlighting
  purposes. Skipping them threw mdLineIdx off-by-one for every block
  after the highlighted one. Now we only skip the standalone overlay
  variants (cursor-sync-br-line, cursor-sync-code-line).

Tests:
- 26 unit specs in unit:markdown-line-wrap cover bullets, nested lists,
  ordered lists, blockquotes, fences, tables, headings, link refs,
  inline atoms, frontmatter, and the badge-row regression.
- The existing 58-spec livepreview:Markdown Editor 1 suite still passes.
Hot path in _balancedPack: >>1 is faster than Math.floor for the
midpoint of two non-negative ints, and the values here stay well under
2^31. Adds a local no-bitwise eslint disable with rationale.
@abose abose changed the title feat(mdviewer): line-scoped reflow with per-source-line cursor sync feat(mdviewer): MD editor should stick to max line length of the editor May 23, 2026
@sonarqubecloud
Copy link
Copy Markdown

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