Skip to content

Toolbar inline formatting produces invalid markdown, and formatting commands silently no-op during IME composition #283

Description

@jaredchao

Summary

Every inline formatting command in plugin-toolbar decides whether a format is already applied by comparing the raw characters immediately around the selection. That probe cannot see markdown structure, so toggling bold / italic / inline code produces broken or unparseable markdown in most cases that are not "plain text, exactly selected".

Separately, toggleWrap, insertLink, toggleHeading and six commands in formatting.ts apply their edit with setDocument(<whole document>) followed by setSelection(). Per the contract in packages/core/src/types.ts, setDocument defers the replace until compositionend while an IME is composing — so these commands silently do nothing at the exact moment a CJK user reaches for the toolbar.

Reproduction

I built a small React + Vite harness against locally built packages/*/dist. Each case drives a real editor through the real toolbar DOM button ([data-toolbar-action="…"]) and reads the document back with editor.getDocument() — no direct API calls, no mocks.

repro on main

A. Inline formatting (toolbar-commands.ts)

⟦…⟧ marks the selection, │ marks a collapsed cursor.

# Input Action Actual on main Expected
1 he│llo bold he****llo no-op (nothing selected)
2 **⟦hello⟧** italic *hello* — the bold is gone ***hello***
3 ⟦**hello**⟧ bold ****hello**** hello
4 **hel│lo** bold **hel****lo** no-op (nothing selected)
5 ⟦hello ⟧world bold **hello **world **hello** world
6 **⟦hello⟧ world** bold ****hello** world** hello **world**
7 ⟦a`b⟧ inline code `a`b` ``a`b``
8 ***⟦hello⟧*** bold *hello* *hello* (already correct)

Cases 1, 3, 4, 5, 6 and 7 do not round-trip through a CommonMark parser at all:

B. Transaction shape (setDocument vs replaceRange)

The two cases below run under an identical, active IME composition. The only difference is which API the command uses internally:

Command Path Result during composition
bold setDocument + setSelection document unchanged — silent no-op
bulleted list replaceRange - hello world — lands normally

The list toggle was already migrated to replaceRange (roadmap item 1). The commands that still use setDocument are: toggleWrap, insertLink, toggleHeading (toolbar-commands.ts), toggleBlockquote, insertCodeBlock, insertImage, applyTextColor, applyHighlight, insertHorizontalRule (formatting.ts), and the "remove heading" branch of the heading dropdown (toolbar-ui.ts).

types.ts already spells this out on replaceRange:

One transaction = one undo entry. […] Do NOT emulate this with a separate setDocument/setSelection pair […]
Use this instead of setDocument when you are editing a range, not replacing the whole document.

C. Block commands

# Input Action Actual on main Expected
9 Title│ horizontal rule Title\n---\n Title\n\n---\n
10 ⟦alpha\nbeta\ngamma⟧ blockquote > alpha\nbeta\ngamma > alpha\n> beta\n> gamma

Environment

  • main @ af58999
  • Node 22.14.0, pnpm 9.15.4
  • Packages built with pnpm build, consumed from packages/*/dist

Notes

I have a fix ready and will open a PR against main shortly. It stays inside packages/plugin-toolbar/, adds no dependencies, and resolves the enclosing strong / emphasis / delete / inlineCode node through editor.getAst() instead of probing characters.

Two behaviours in the table above are product decisions rather than clear-cut bugs, and I would rather have them confirmed than assumed:

  1. Collapsed selection (Feat/shaoyongqiang demo #1, feat(slash): add command sorting by match priority and limit parameter #4) — I made it a no-op, matching what comparable editors do. The alternative is expanding to the word under the cursor, which needs a word-boundary rule that does not degrade for CJK text.
  2. Partial selection inside a node (feat(core): add getSelectedText API #6) — I split the node (hello **world**) rather than unwrapping it whole (hello world).

Happy to change either one if you would prefer different semantics.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions