You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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, setDocumentdefers 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.
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
feat(search): support regex queries #9 — --- directly under a text line is a setext heading underline, not a thematic break. Inserting a divider silently turns the line above it into an <h2>.
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:
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.
Summary
Every inline formatting command in
plugin-toolbardecides 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,toggleHeadingand six commands informatting.tsapply their edit withsetDocument(<whole document>)followed bysetSelection(). Per the contract inpackages/core/src/types.ts,setDocumentdefers the replace untilcompositionendwhile 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 witheditor.getDocument()— no direct API calls, no mocks.A. Inline formatting (
toolbar-commands.ts)⟦…⟧marks the selection,│marks a collapsed cursor.mainhe│llohe****llo**⟦hello⟧***hello*— the bold is gone***hello***⟦**hello**⟧****hello****hello**hel│lo****hel****lo**⟦hello ⟧world**hello **world**hello** world**⟦hello⟧ world******hello** world**hello **world**⟦a`b⟧`a`b```a`b``***⟦hello⟧****hello**hello*(already correct)Cases 1, 3, 4, 5, 6 and 7 do not round-trip through a CommonMark parser at all:
**run cannot be preceded by whitespace, so**hello **worldrenders literally.****hello****and**hel****lo**are not strong nodes.B. Transaction shape (
setDocumentvsreplaceRange)The two cases below run under an identical, active IME composition. The only difference is which API the command uses internally:
setDocument+setSelectionreplaceRange- hello world— lands normallyThe list toggle was already migrated to
replaceRange(roadmap item 1). The commands that still usesetDocumentare: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.tsalready spells this out onreplaceRange:C. Block commands
mainTitle│Title\n---\nTitle\n\n---\n⟦alpha\nbeta\ngamma⟧> alpha\nbeta\ngamma> alpha\n> beta\n> gamma---directly under a text line is a setext heading underline, not a thematic break. Inserting a divider silently turns the line above it into an<h2>.toggleBlockquoteonly ever looks at the anchor line, while the list toggles next to it are already range-aware viagetLinesInRange/applyLines.Environment
main@ af58999pnpm build, consumed frompackages/*/distNotes
I have a fix ready and will open a PR against
mainshortly. It stays insidepackages/plugin-toolbar/, adds no dependencies, and resolves the enclosingstrong/emphasis/delete/inlineCodenode througheditor.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:
hello **world**) rather than unwrapping it whole (hello world).Happy to change either one if you would prefer different semantics.