Fix Dragon NaturallySpeaking corrections breaking the editor#1101
Fix Dragon NaturallySpeaking corrections breaking the editor#1101brunoprietog wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds first-class support for Dragon NaturallySpeaking’s web-extension messaging protocol to prevent Dragon corrections/selection changes from crashing the Lexical editor and causing subsequent DOM/state divergence, while also supporting Dragon format commands (e.g. “bold that”). It installs a page-level handler at module load time to reliably intercept and apply Dragon’s makeChanges messages through Lexical APIs.
Changes:
- Add a global
DragonSupportwindowmessagehandler that honors Dragon’stext === -1selection-only sentinel and applies supported format commands. - Install
DragonSupportat module load so it can intercept messages early and stop the extension’s raw DOM mutation handler. - Add Playwright coverage for replacement, selection-only updates, correction flow, supported/unknown format commands, and clamped selections.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
test/browser/tests/dragon.test.js |
Adds Playwright tests covering Dragon’s makeChanges protocol scenarios. |
src/index.js |
Installs Dragon support at module load to intercept Dragon messages early. |
src/editor/dragon_support.js |
Implements a Lexical-safe Dragon message handler (selection-only sentinel + format commands). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
fc25839 to
a50e108
Compare
a50e108 to
8ff4f45
Compare
|
🤖 Suggestion: validate the The PR's goal is to absorb malformed payloads because an exception inside
Suggestion: validate the positional args up front — the four offsets must be numbers, and Related cheap hardening while you're in there: guard |
jorgemanrubia
left a comment
There was a problem hiding this comment.
Great one @brunoprietog
jorgemanrubia
left a comment
There was a problem hiding this comment.
@brunoprietog minor comments and a suggestion via Fable that looks quite legit 👏
There was a problem hiding this comment.
I presume that the existing @lexical/dragon extension does not properly support the sentinel value?
Given that support was extension-shaped and intercepted the event, is there an opportunity to use a Lexxy extension while still preventing the double-handling with a capturing handler?
Dragon's web extension drives editable fields through window messages (protocol "nuanria_messaging"). The @lexical/dragon handler that Lexxy picks up through the RichTextExtension dependency chain crashes on selection-only messages: Dragon sends the number -1 as the text argument to mean "don't change the text" (Select-and-Say corrections, cursor moves by voice), and insertRawText(-1) throws inside editor.update. The editor is left permanently broken, so the extension falls back to raw DOM edits that diverge from the editor state, and content disappears when Lexical reconciles. Install a corrected handler when the Lexxy module loads instead of per editor. The extension's content script registers its message listener at document_end of the initial page load, so per-editor handlers always run after it and their stopImmediatePropagation arrives too late. The module-load listener wins that race and absorbs every makeChanges addressed to a Lexxy editor, validating the payload up front so nothing can throw inside editor.update: both the extension's raw DOM edits and the broken stock handler behind us would corrupt the editor otherwise. The fixes and an installDragonSupport() entrypoint API are also submitted upstream as facebook/lexical#8665. Once that ships in a release Lexxy adopts, this handler can be replaced with a call to that function at the entrypoint.
Voice commands like "bold that" arrive in the sixth makeChanges argument as execCommand names, which is how the Dragon extension's own handler applies them. Map the supported names to Lexical text formats and apply them to the final selection, skipping selections that the protocol's offset clamping left collapsed so they don't toggle the pending format. This matches the upstream @lexical/dragon change submitted alongside this one.
8ff4f45 to
0b2cc32
Compare
|
Right, the stock handler breaks on the sentinel: it passes the -1 straight to About the extension shape, that was the first thing I tried. The problem is that capture doesn't buy any priority here. The message event's target is window itself, and at the target listeners run in plain registration order, so Upstream is heading the same way: in facebook/lexical#8664 the maintainer suggested an exported function apps call synchronously at their entrypoint, without constructing an editor, and that's now part of facebook/lexical#8665. Once that ships in a release we adopt, this file becomes a call to their We could still wrap the dispatch in a Lexxy extension, but since the handler resolves the focused editor from the DOM it wouldn't add much beyond ceremony, and the early call in |
A customer who dictates with Dragon NaturallySpeaking reported that correcting a word by voice in any rich text field destroys everything after the corrected word (Basecamp card). The cause is in the handler that Lexical ships for Dragon's web extension messages, which Lexxy picks up through the RichTextExtension dependency chain: Dragon sends the number -1 as the text argument to mean "change the selection without touching the text", the handler passes it straight to
insertRawText, and the resulting exception insideeditor.updateleaves the editor permanently broken. From then on the extension edits the DOM behind Lexical's back and content disappears when the two diverge. On top of that, the stock handler registers per editor, which is always after the extension's content script, so itsstopImmediatePropagationcan never actually block the extension's raw DOM edits.So this PR:
DragonSupport, installed once when the Lexxy module loads, early enough to win the listener registration race against the extension's content script.Everything here is also submitted upstream in facebook/lexical#8665, including an
installDragonSupport()entrypoint API for the registration race, with the maintainer on board with that direction. Once it ships in a release we adopt, our handler can be replaced with a call to that function at the entrypoint.I validated the handler against the real extension's unpacked content scripts in both registration orders, but final confirmation with Dragon itself will have to come through the customer, since it needs their setup.