Don't comment on a stale range when the selection is in a widget - #3
Open
petergaultney wants to merge 2 commits into
Open
Don't comment on a stale range when the selection is in a widget#3petergaultney wants to merge 2 commits into
petergaultney wants to merge 2 commits into
Conversation
Obsidian renders tables, and other embedded blocks, as CodeMirror widgets marked `contenteditable="false"`. A selection made inside one is a real document selection that produces no CodeMirror transaction, because CodeMirror's own state never changes. `state.selection` therefore keeps describing the *previous* selection and still reports itself non-empty. The inline comment button trusted that range twice over. It stayed visible, because placement is only recomputed from a ViewUpdate and no update arrives, and its click handler read the same stale range. So selecting text in a table and clicking the button wrote a comment around whatever had been selected before - typically the first characters of the document, since that is where the cursor starts. The mark existed but pointed nowhere near the table, which is why the selection did not highlight and the sidebar reported no comments on the note. `selectionTrust` compares CodeMirror's idea of the selection against the document's: an uneditable ancestor at either end, or text that disagrees once whitespace is squeezed, means the range cannot be trusted. Containment in `contentDOM` cannot answer this on its own, because widgets live inside the content element too. The button now withdraws instead of going stale, and the click handler declines a range it cannot trust. The keyboard command and context-menu paths go through `editor.getSelection()`, which already reports empty in a widget, so they insert at the cursor rather than at a distant range and needed no change.
Editing a table cell gets its own CodeMirror view, mounted inside the host editor, whose whole document is that cell's text. Editor extensions are installed in it too, so the floating comment button appeared a second time *inside the table*, and the state field built decorations against offsets that are the cell's rather than the note's - on text Obsidian's own renderer has already marked up. A fragment editor now gets no button and reports itself as not-live-preview, which is how a view plugin tells the state field to build nothing. The check is nesting (an editor whose ancestor is an editor) rather than a table-specific class, so callouts and embeds are covered by the same test. It is recomputed per use rather than cached in the constructor: Obsidian attaches the fragment editor's DOM into the host after the view plugin is built, so a constructor-time check sees an unparented element and reports false. The button is therefore created on first placement instead of at construction.
petergaultney
marked this pull request as ready for review
August 18, 2026 13:21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🍋:
Selecting text inside a table and clicking the inline comment button writes the comment around the wrong text — usually the first few characters of the document. The mark is created, but nowhere near the selection, so the highlight never appears and the sidebar reports no comments on the note. Reported by two people on our team today; here is what one of their files looked like afterward, where the intended target was a cell far below:
Obsidian renders tables, and other embedded blocks, as CodeMirror widgets marked
contenteditable="false". A selection made inside one is a real document selection, but it produces no CodeMirror transaction, because CodeMirror's own state never changes.state.selectionkeeps describing the previous selection and still reports itself non-empty.I measured this in a live vault over the debug protocol. With
1.8.26selected in a table cell:The comment button trusts that range twice over. Placement is only recomputed from a
ViewUpdate, and none arrives, so the button stays visible from the previous selection; then its click handler reads that same stale range. Bothselection.emptyandeditor.somethingSelected()are satisfied, because there genuinely is a selection — just not the one on screen.The change
selectionTrustinsrc/editor/selection-trust.tscompares CodeMirror's idea of the selection against the document's. Acontenteditable="false"ancestor at either end means the range cannot be trusted; as a backstop, text that disagrees once whitespace is squeezed means the same. Containment incontentDOMcannot answer this on its own, since widgets live inside the content element too — that was my first attempt and it accepts every broken case.The button now withdraws rather than going stale, and the click handler declines a range it cannot trust.
Details
readDomSelectionFactsdoing the DOM reading separately, so the interesting part is testable in the existingtestEnvironment: "node"suite. Seven unit tests cover each verdict, including a widget with one end outside it and the whitespace-insensitivity of the text comparison.editor.getSelection(), which already reports empty inside a widget, so they insert at the cursor instead of at a distant range. I left them alone.readDomSelectionFactstoo, but the suite is node-only and adding jsdom seemed like your call rather than mine — happy to add them if you'd like it.Verified with
npm run check,npm run check:no-node,npm run lint, and the full unit suite. I also loaded the build into a real vault and confirmed the trust facts come backtrustedfor an ordinary prose selection made by a real drag, so the button is not being suppressed in the normal case.Second commit: per-fragment editors
Testing the fix in a real vault turned up a related problem in the same area. Editing a table cell gets its own CodeMirror view, mounted inside the host editor, whose entire document is that cell's text (
"1.8.26"for one cell). Extensions are installed in it too, so:A fragment editor now gets no button, and reports itself as not-live-preview so the state field builds nothing there. The check is nesting — an editor whose ancestor is an editor — rather than a table-specific class, so callouts and embeds are covered too.
One detail worth flagging if you review the shape: the check has to be recomputed per use rather than cached in the constructor. Obsidian attaches the fragment editor's DOM into the host after the view plugin is built, so a constructor-time check sees an unparented element and reports false. I had it cached first and it silently did nothing. The button is therefore created on first placement instead of at construction.
Confirmed in a live vault: editing a table cell now yields two editors but one button, and that button is not inside the table.
Not in scope here
Obsidian's own table renderer treats
{==text==}as its native==highlight==syntax, consuming the==and leaving the braces visible — a cell showsfolder-{notes}. I confirmed this happens with Relay Comments fully disabled, so it is upstream of this plugin rather than something these commits cause or can fix. Mentioning it because it makes a comment on table text look broken even once anchoring is correct; happy to open a separate issue if it is useful to track.