What I was trying to do
When I copy a range of cells in Excel and paste it into a note, Obsidian gives me a Markdown table. That is usually what I want. But I write reports where the picture is the content — conditional formatting, merged headers, colour scales — and a Markdown table throws all of that away.
The picture is already on the clipboard: copying cells in Excel puts text/plain, text/html, text/rtf and image/png there at the same time. Nothing is missing; Obsidian simply picks the HTML. So I wrote a small command for my own vault that picks the image instead:
- read the image with
clipboard.readImage(); - if it has an alpha channel, composite it onto white — a transparent background renders as invisible black text in dark mode;
- save the attachment with
getAvailablePathForAttachment()+vault.createBinary(); - insert the embed at the cursor.
Steps 2 and 3 are asynchronous, so the command awaits before it writes anything to the document. That turns out to matter.
What goes wrong
Run the command from the command palette, in a note with frontmatter, in Live Preview: three blank lines appear above the frontmatter fence, which breaks the note’s properties. The embed also lands a few characters off, on the wrong line.
Those blank lines are not written by my plugin. I wrapped cm.dispatch and logged the stack of every transaction that changed the document:
108 -> 111 chars top before: "---\ntitle:" top after: "\n\n\n---\ntitle"
Fr <- Or <- e.flush <- e.onSelectionChange <- <my async command>
While the command is suspended on its first await, the palette finishes closing and hands focus back to the editor. That selectionchange makes CodeMirror re-sync with the DOM, and the flush inserts \n\n\n at offset 0.
Two details that took a long time to find, and that may point at the cause:
- the flush runs synchronously from inside a sync Electron IPC call —
clipboard.readImage()pumps the message loop, which lets the pendingselectionchangerun in the middle of my function, before I have even looked at the document; - it is applied together with the next transaction, so it lands inside my own
editor.replaceRange(...)call, immediately before my edit is applied.
The same command never corrupts anything when it is bound to a hotkey, or when I invoke it with app.commands.executeCommandById(...). No modal closing, no flush — which is why every test I ran programmatically passed while the real usage kept failing.
What I do today
I could not find a supported way to let the editor settle before measuring, so the command now:
- snapshots the document and the selection synchronously inside the
editorCallback, before anyawait— including beforereadImage(), since that is what pumps the loop; - fires an empty transaction (
replaceRange("", {line:0,ch:0}, {line:0,ch:0})) to materialise the pending flush harmlessly; - compares the leading newlines against the snapshot and deletes whatever appeared, both before and after inserting;
- computes the insertion offset inside the snapshot, never with
posToOffset()on the live state.
It works, and I can reproduce the failure on demand by injecting "\n\n\n" at offset 0 during the awaits. But writing content and then deleting it to undo something the editor did on its own is obviously the wrong shape for a fix.
What I am asking
Is there a supported way to handle this? Something that flushes the DOM observer on demand, or suppresses the re-sync while a command holds the editor, or — simplest of all — a guarantee that the frontmatter fence is never pushed down by a focus-driven flush.
And if plugins are not expected to await inside an editorCallback at all, it would help to say so. Right now the failure is silent and it corrupts the user’s notes.