Skip to content

Keep a comment that starts a line from hiding its formatting - #95

Merged
kylemcd merged 15 commits into
mainfrom
94-markers-at-line-start
Oct 8, 2026
Merged

kylemcd merged 15 commits into
mainfrom
94-markers-at-line-start

Conversation

@kylemcd

@kylemcd kylemcd commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Fixes #94.

Markdown reads a line whose text starts with <!-- as a raw HTML block, so Reading view shows the whole line as written. A comment that starts a paragraph puts its <!--c:ID--> marker exactly there. Every ==highlight==, **bold** and link on that line then comes out as plain text. Live Preview has its own parser, which is why editing looked fine. Rendering through Obsidian's own renderer shows the same for:

  • the text of a list item, task, quote, callout title or body, or footnote;
  • a soft-wrapped line of a paragraph, which also split the paragraph into three blocks;
  • the header row of a table without outer pipes, which stopped rendering as a table.

Headings, table body rows and markers mid-line were fine. It has worked this way since the first release.

A second bug has the same cause. Obsidian's triple-click selects a line from its very start, so commenting on a whole list item, quote or heading put the marker in front of the - , > or ##. The line then stopped being a list item, quote or heading, in Live Preview as well as Reading view.

Before: a note written by 0.1.16, opened on this branch. Each card offers the new Repair.

Reading view of a note whose comments start a paragraph, a list item and a quote: ==World==, bold and ==a highlight== show as raw text, and each card says the comment is breaking its line's formatting

After: the same note with a zero-width space in front of each marker, as new comments and Repair now write it.

The same note in Reading view: World and a highlight are highlighted, bold renders, and the cards have no notice

What changes

  • A zero-width space in front of the marker. When a new comment's text starts a line, the plugin writes a U+200B before <!--c:ID-->. The line reads as ordinary text again, and the marker as an inline comment. It renders as nothing in Obsidian, GitHub and other Markdown tools. Headings, table body rows and a marker alone on its line don't need one and don't get one.
  • Markers stay off block markup. A selection that starts in front of a bullet, task box, >, callout [!type], footnote label or heading #s now anchors after them. A selection that ends at the start of the next line, or just after its bullet, ends on the text it covers. It isn't pulled back onto the end of a fence, rule or table row, where it would break that line. In that case it goes after the next line's markup and takes the zero-width space instead. A marker on a line of nothing but whitespace goes at the line's start, where it stays invisible.
  • The editor treats the zero-width space and its marker as one. It hides them together, one arrow press crosses both, and Backspace or Delete can't remove the space on its own. Deleting a comment removes its space, unless another comment's marker still needs it.
  • Existing notes are left alone until you ask.
    • A comment whose markers break their line says "This comment is breaking its line's formatting." on its card, with a one-click Repair.
    • Repair adds the zero-width space, or moves a marker from in front of the markup to after it.
    • Reading view cards now offer Repair too, for tables as well as lines, since that's where a broken line shows.
    • Repair comments in this note replaces Repair table comments in this note. It repairs both kinds in one write, runs from Reading view, and keeps the same command id, so hotkeys keep working.
    • Moving a closing marker back takes two edits. When a table repair touches one of them, the command repairs the table and asks you to run it again rather than make only one.
  • The agent skill documents the zero-width space, and validate_comments.py flags a marker that starts a line without one, or that comes right after a backslash.

Also fixed here

These are the same kind of problem. I found them by rendering thousands of random comments with markdown-it, then checked each shape in Obsidian.

  • Rules, setext underlines and blank lines. A selection that started or ended on ---, ===, an empty bullet or a blank line put a marker on that line. Anywhere on a rule or underline, a marker stops it rendering as one. These selections now anchor on the text beside them. A selection of nothing but a rule is refused.

    • The move stops at a line that opens a $$ math block, a %% comment block or an HTML block. A marker in front of one of those breaks the block, so a start on a blank line above one stays on the blank line, where it's invisible.
  • Code blocks.

    • A comment on a paragraph followed directly by a code block wrote its thread inside the block whenever the code had a blank line. The thread then showed as code. It now goes after the closing fence.
    • A code block with no closing fence now refuses a comment instead of filling up with comment text.
  • Reading view highlights. These cases had no highlight in Reading view:

    • a comment whose text carried formatting (==hl==, bold, links, inline code);
    • one that ran across a line break or into the next paragraph;
    • one in a syntax-highlighted code block;
    • one in a footnote.

    A repeated phrase also highlighted its first occurrence instead of the comment's. The highlight now finds the comment's rendered text and wraps it across elements. Obsidian rebuilds a syntax-highlighted block after the plugin has wrapped it, so a code comment wraps again once that happens.

    A comment over three or more blocks highlights its first and last. Obsidian keeps any rendered block whose source didn't change, so a highlighted middle block would keep its highlight after the comment was deleted.

    Obsidian renders all of a note's footnotes in one section of their own, after the note's last line, so the plugin found no comments there. Each footnote now maps back to its definition, which holds its comments. Add comment in reading view works on a footnote's text for the same reason. It used to say it couldn't map the selection.

  • Highlights stay in sync with the note. Each Reading view refresh removes highlights whose comment is gone, and updates resolved status and preview on the rest. That covers a block Obsidian kept from before the change, which used to keep a deleted code comment's highlight, for example. The sync checks against the text the pane shows, not the file. A Reading pane beside an editing pane shows the editor's unsaved text, so a just-added comment would otherwise lose its highlight.

  • Repair also moves a marker off a rule or underline it broke.

  • Lines ending in a backslash. A backslash at the end of a line is a hard line break, and \< is an escape. A selection that stopped there, as Obsidian's triple-click does, or a closing marker pulled back there, put the marker right after the backslash. Reading view then showed the marker as text and dropped the line break. A marker now goes in front of the backslash, and Repair moves one that an older version wrote after it.

  • Indented code. A comment on indented code put its markers into the code, where they showed as text. It now takes in the code block's whole lines.

    • For a top-level block with a blank line above it and room below, the markers go on those lines, and the code still renders as code. Reading view highlights it and shows its card.
    • Inside a list item, the markers stay on the code's own line, at the item's text column. That line shows as plain text, as it did on main, but the list around it doesn't change.
    • Telling indented code from a list item's or a footnote's indented text follows Obsidian's own rendering, which I checked in the app. It differs from CommonMark around comment lines, quotes, footnotes, raw HTML blocks and ordered lists.
  • Raw HTML blocks. A marker in front of a block's opening tag (<details>, <div>, </details>) turned the line into a comment block. The lines after it then rendered as something else, and the zero-width space would have turned it into a paragraph. A marker now goes after the opening tag, and Repair moves one that an older version wrote in front of it. A selection of nothing but a lone tag is refused.

  • Deleting a comment gives back the note exactly. Deleting a comment used to remove a line that held only its marker, even when that line was blank before the comment. That could join a code block to the paragraph next to it. Now only a code comment, which adds lines of its own around its block, takes them out again.

Reading view highlights over formatted text, the second of two repeated words, two paragraphs, a list item with bold, a syntax-highlighted code line, and a rule repaired from its card

Reading view with comments highlighted across four footnotes, mid-line, at a footnote's start, over formatted text and in a second paragraph, each with its card

Testing

  • npm run check passes (602 tests).

    • New tests cover each block kind, adding comments, deleting them back to the original text, repair, how the editor handles the zero-width space, and footnotes in Reading view.
    • With the editor's and creation's handling of the space switched off, 12 of the new tests fail.
  • One expectation from Keep a marker outside a table where the selection put it #93 changes. A selection ending just after the bullet of a list item below a table still leaves its closing marker there, since pulling it back would put it after the table's closing pipe. It now gets the zero-width space, so the item keeps its formatting.

  • In Obsidian, with this branch's build in the test vault:

    • Rendered each case above with and without the zero-width space.
    • Added comments through the composer on a paragraph's first word and on a triple-clicked list item. Both wrote the space after any markup.
    • Repaired 0.1.16-style comments from a Live Preview card and from a Reading view card.
    • Ran the command from Reading view. It repaired two lines and a table in one write.
    • The screenshots above show the space leaves nothing visible in either view.
  • Reviews: a fresh agent with no context reviewed each commit, with reproductions and randomized tests.

    • The first pass found three bugs. The second commit fixes each one, with a regression test:

      • the whole-note repair could delete a closing marker when a table repair touched one of the two edits that move it;
      • a closing marker could still land in front of a bullet when a selection ended right after a code fence or --- rule;
      • on files with Windows line endings, --- didn't count as a rule, so the repair could add a marker to its end.
    • The second pass confirmed those fixes.

      • Across about 52,000 damaged notes, repeating the whole-note repair always finished within 4 runs.
      • Across about 66,000 random new comments, the repair flagged none.
      • It found that the second commit put a marker on a whitespace-only line after its indentation. Four or more columns of indentation make that a code block. The third commit fixes it.
    • The third pass confirmed that fix and found nothing new. Across 31,600 random comment creations rendered with markdown-it, this branch breaks rendering in fewer cases than main (8,230 against 24,174), and in none that main renders correctly.

    • A second fresh agent reviewed the "Also fixed here" commit and found two problems:

      • a start on a blank line could land in front of a $$ or %% block below it, which then showed as text;
      • the middle block of a comment over three blocks kept its highlight after the comment was deleted.

      Rechecking each fix, it found three more:

      • the HTML-block stop also caught inline HTML such as <b>Note:</b>;
      • a highlight could outlive a comment over two blocks;
      • syncing against the file removed a new comment's highlight in a Reading pane next to an unsaved editor.

      Each fix came with a test that failed first. Its last pass found nothing new.

    • Five more fresh agents then reviewed the whole branch, one after each round of fixes. They found 15 more problems, most of them in the handling of backslashes, indented code and raw HTML blocks above. Each fix came with a test that failed first. The last full review found one: running Add comment again on the selection that made an empty comment missed it and wrote a second comment over it. The next commit fixes that. A fresh review of that fix found it made Add comment slow on a select-all in a note with hundreds of empty comments: about a second per lookup, blocking the editor. The commit after brings it back to milliseconds. Its own review found that with three or more empty comments around the selection, the speed-up skipped the comment it had just made. The last commit fixes that: a lookup takes 16 ms, against 711 ms, with 250 empty comments in a 47 KB note, and the reviewer's fuzz of nested highlights finds no misses in 2,228 cases, against 440 before.

    • A fresh agent reviewed the footnote fix and found no bugs. Of its side notes, one was worth fixing here: a selected word that a footnote's label shares mapped into the label, so Add comment refused it. Another, a line right after an indented fence or heading in a footnote, renders inside the footnote in Obsidian, as the fix assumes. The third, a thread written between a footnote's paragraphs, is older and listed under "Known".

  • Rendering check: the same markdown-it harness ran over the same kinds of random notes and selections. Where a new comment's markers land now breaks the rendering in 0 of 23,102 comments, against 8,230 before this round and 24,174 on main.

    • Some selections are now refused instead of written: those in code blocks with no closing fence, those holding nothing but a rule or a lone HTML tag, and those of indented code that leave no hidden place for a marker, such as code right before a rule.
    • In about 600 cases a comment's thread still sits between items of a loose list. markdown-it splits the list there, but Obsidian keeps it whole, which I checked in the app.
  • A second harness adds hard line breaks, indented code, footnotes, tables and raw HTML blocks to the random notes, about 40,000 comments in all. It renders every case where markdown-it shows a difference in Obsidian itself, through the running app.

    • Judged by Obsidian, a new comment never renders worse than before this round. Against main, only the one rare layout under "Known" differs.
    • Deleting a comment gives back the original note in every case. On main, 9,464 deletions didn't.
    • Running Add comment again on the same selection always finds the empty comment it made.
    • Repair never loses an anchor, and no new comment is flagged as damaged, apart from 4 selections made inside an existing HTML comment.
  • In Obsidian: I checked the note in the screenshot above. It covers:

    • formatted text and a repeated word;
    • a comment over two paragraphs;
    • a list item and a syntax-highlighted code line;
    • a marker after --- from an older version, repaired from its Reading view card;
    • deleting one paragraph of a comment over two, which cleared the other paragraph's highlight;
    • adding a comment in an editing pane beside a Reading pane, where the Reading pane kept the highlight before and after the autosave.
  • In Obsidian, for this round: I added comments with the app's real triple-click on a line ending in a backslash, on top-level indented code and on indented code in a list item. I also added a code comment over a fenced block holding an HTML comment line. In Reading view:

    • the line break stayed;
    • both code blocks rendered intact;
    • each comment was highlighted and its card shown;
    • the list kept the paragraph after it separate;
    • no marker showed as text.
  • In Obsidian, for footnotes: I made a note with five comments across four footnotes: mid-line, guarded at a footnote's start, over ==formatted== text, in a footnote's second paragraph, and in the footnote defined last. In Reading view each comment was highlighted in its own footnote, with its card, as in the screenshot above. That held:

    • with frontmatter, and in a note that ends without a line break;
    • with two footnotes holding the same word;
    • after edits that added lines above the footnotes and between them.

    Add comment in reading view on a footnote's first word wrote the marker into its definition, behind a zero-width space, and its highlight and card appeared. Deleting it from its card gave back the note byte for byte. On a word that the footnote's label also holds, it commented on the footnote's text, not the label.

  • Known, not fixed here:

    • A comment inside an inline footnote (^[…]) has no highlight in Reading view, as on main.
    • A comment on the first paragraph of a footnote with several paragraphs writes its thread between them. That ends the footnote, so the paragraphs after it show as code, as on main.
    • Adding a comment in Reading view still needs plain text. A selection across formatting can't be mapped back to the Markdown.
    • In a loose list, a comment's thread goes between items. Obsidian keeps the list whole, but GitHub and other CommonMark tools split it there.
    • A thread that an older version wrote inside a code block stays there. The parser skips everything inside code, so Repair can't see it either.
    • A comment on indented code inside a list item, or with no blank line above the block, keeps its markers on a code line. That line shows as plain text rather than code, as it did on main.
    • One rare layout doesn't follow Obsidian's usual rules: an ordered list item, then an HTML comment line, then a line indented six spaces. A comment there can show its closing marker as code. Main hid the marker but showed the line as plain text.
    • A selection made inside an existing HTML comment or inside a tag's own text writes markers that break it, as on main. Repair can't fix comments that an older version wrote inside another HTML comment.

Markdown reads a line whose text starts with `<!--` as a raw HTML block, so
Reading view showed the whole line unformatted whenever a comment started a
paragraph, list item, quote, callout, or footnote. New comments now put a
zero-width space in front of the opening marker there, and open after the
bullet, `>`, or `#`s a triple-click selects instead of in front of them.

The editor hides the guard with its marker as one unit, and deleting a
comment removes it. Comments written before this offer Repair on their card,
in Live Preview and Reading view, and the repair command now covers lines as
well as tables.
kylemcd added 14 commits October 7, 2026 11:00
A closer moved back is two edits. The whole-note repair dropped whichever one
touched a table repair, which could delete the closer outright, so a run's
edits are now kept or held back together. An end that can't move back onto a
fence, rule, or table row goes past the next line's markup instead of in front
of it, and so does a start on an empty list item. Lines are read without a
trailing CR, so a rule in a file with Windows line endings is still a rule.
Past four columns of indentation, a marker after a whitespace-only line's
spaces or tab starts an indented code block, which shows it as text, or joins
the paragraphs that line kept apart. At the line's start it stays an invisible
HTML block, as it always was when a selection started there.
A selection that starts or ends on a rule, setext underline, empty bullet, or
blank line now anchors on the text beside it; a marker anywhere on a rule or
underline stopped it rendering as one. A paragraph's comment no longer writes
its thread inside a code block that follows it, and a block with no closing
fence refuses a comment instead of filling with comment text. Repair moves a
marker off a rule or underline it broke.

Reading view now finds a comment's rendered text, not its raw source, and
highlights it across elements: through highlights, bold, links and inline code,
across line breaks and paragraphs, in syntax-highlighted code blocks (wrapped
again once highlighting replaces the block), and where a repeated phrase is,
not where it first appears.
A start on a blank line moved on to the next line's text even when that line
opened a $$ math block, a %% comment block, or raw HTML, and a marker in front
of one stops it opening its block. It now stays on the blank line there, and
Repair leaves a rule's marker in place rather than move it onto such a line.

Obsidian keeps a rendered block whose source hasn't changed, so the middle
block of a comment over three or more blocks kept its highlight after the
comment was deleted. Reading view now highlights only the blocks that hold one
of the comment's markers.
The stop for HTML blocks took any line starting with `<`, so a blank-line start
above inline HTML followed by text (`<b>Note:</b> …`) stayed on the blank line.
It now takes only the lines Markdown reads as HTML blocks: block tags, a tag
alone on its line, and a comment left open. A line of nothing but a comment is
looked past.

Obsidian keeps a rendered block whose own source didn't change, so a highlight
there outlived its comment when the change happened elsewhere: one end of a
multi-block comment deleted, or a code comment removed. Reading view now syncs
its highlights with the note on every refresh, unwrapping those whose comment
is gone and updating the rest's resolved status and preview.
The sync read the file, but a Reading pane beside an editing pane on the same
note shows the editor's text, which is ahead of the file until it saves. A
comment added there had its new highlight unwrapped before the save, and
nothing wrapped it again. The sync now uses the text the pane shows.
A closer pulled back to the end of a line that ends in a backslash, or a
selection that stops there as Obsidian's triple-click does, put the marker right
after the backslash. `\<` is an escape, so the marker showed as text and the hard
break was lost. Both ends now go in front of a backslash that isn't escaped
itself, and Repair moves markers an older version wrote after one.

A comment starting or ending on indented code put its markers after the
indentation, in the code, where they showed as text, and Repair moved older
openers there too. A selection touching indented code now takes in its whole
lines, with the markers on the blank lines around the block where there's room,
or at a line's start, where they're hidden, where there isn't. Telling code from
an indented list item's text follows Obsidian's own rendering, checked in the
app: a line of nothing but comments still counts in a list, a quote right after
a list item stays in it, and a bullet four columns past its holder's text
carries on a paragraph.

Reading view dropped HTML and %% comments inside fenced code when matching a
code comment's text, so a code comment over such a line highlighted only the
rest. Comments that start in code are now kept as the code's text.

A marker followed by nothing but comments is no longer guarded, which made an
invisible line an empty paragraph.
A comment on part of an indented code block with a blank line in it put a
marker on that blank line, splitting the block in two. A comment touching
indented code now takes in the whole block, blank lines and all, with its
markers on the lines around it.

Deleting a comment whose marker sat alone on a line removed the line too. Only
a code comment adds lines of its own around its block; any other comment's
marker was written onto a line that was already blank, and that line now
stays, so adding and deleting a comment gives back the note exactly. With the
markers on the lines around the code, selecting the same code again finds the
comment, so an empty one toggles off as before.

A footnote's indented second paragraph read as code, so a comment there ended
the footnote. Its text now counts as four columns in, like a list item's.
Checked in the app, a footnote only opens after a blank line, and a comment or
quote ends it.
A comment on indented code has its markers on the lines around the block, each
a section of its own in Reading view, so the code's section held neither and got
no highlight, and the card, placed by the highlight, was hidden. A section right
between its comment's marker lines is now highlighted.

In a list item, a marker line where the blank line around the code had been let
the list run on, pulling the next paragraph into the item. A comment on
indented code in a list item now keeps its markers on the code's own line, at
the item's text column, where they're hidden and the line shows as text (checked
in the app), or ends on a comment line right after the code. An ordered item's
text counts as at least four columns in, as Obsidian reads it.

A guard in front of an old marker run that sits before a bullet, `>`, or `#`s
made Repair read the run as text, so the line stayed broken and its card
stopped offering Repair. Repair now looks through the guard.
In front of a raw HTML block's opening tag, a marker made the line a comment
block, which ends right there and left the lines after it to render as
something else, and a guard made it a paragraph instead. A marker now goes
after the tag, and Repair moves one an older version wrote in front of it. A
selection of nothing but a lone tag is refused, as it holds no text. The lines
of a raw HTML block, up to the blank line ending it, no longer read as code or
list items.

Indented code's markers stay off a blank line next to a table, which the table
repair read as a marker breaking the table, so a new comment was flagged and
its repair moved the marker into the code.

A selection that would leave an anchor of nothing but line breaks once kept out
of indented code is refused. A selection of exactly a highlight's text finds
that highlight again, wherever moving its ends would take them now that it's
written. Each end of a selection between two code blocks goes by its own
block's list.
Writing a comment can move its markers off the ends of the selection that made
it, past a bullet, `>`, `#`s, or a guard, or back to the text it ended on, and
can put its thread inside that selection. Mapped onto the new text, the
selection then takes those in, so running Add comment on it again missed the
empty comment it had just made and wrote a second one over it. The lookup now
also takes a selection as the comment's when, with the comment's own markers
and thread back out, it anchors that comment again.
The check that a selection anchors an empty comment again reads the whole note,
and it ran for every empty comment the selection overlapped. Select all and Add
comment in a note with hundreds of them took seconds, blocking the editor. The
comment a selection made covers nearly all of it, since anchoring only trims an
end or widens it to whole code lines, so the check now runs only on the three
that cover the most: 11 ms rather than 711 ms per lookup with 250 empty
comments in a 47 KB note.
…ion's

The lookup ran its full check on the three empty comments that left the least
of the selection uncovered. A comment enclosing the selection leaves none of it
uncovered, so with three or more around it, the comment the selection had just
made never got checked, and Add comment wrote a second one over it. Candidates
now rank by how far each of their ends is from the selection's, in the text
that shows, with markers and threads left out. The comment a selection made sits
right at its ends, and one reaching past it ranks behind.
Obsidian renders every footnote in one section, on the line after the note's
last, so the highlighter found no comments there, and Add comment in Reading
view couldn't map a selection in a footnote. Each footnote's data-line counts
from that line to its definition, so each footnote now highlights against its
own definition's text, past its label. Where a definition ends follows
Obsidian's rendering: lines indented four columns past the label, and lines
right after its text that don't start a block of their own.
@kylemcd
kylemcd marked this pull request as ready for review October 8, 2026 18:16
@kylemcd
kylemcd merged commit bff692c into main Oct 8, 2026
1 check passed
@kylemcd
kylemcd deleted the 94-markers-at-line-start branch October 8, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Having a comment that start the beginning of a paragraph "disables" any highlighted text through the rest of the paragraph, in Read mode.

1 participant