Click-accurate cursor positioning in the note preview #16

Merged
Lemarkis merged 6 commits from feat/click-accurate-cursor-position into main 2026-09-18 10:50:10 +00:00
Owner

Summary

Clicking into a note's rendered markdown preview now enters edit mode with the cursor placed at the end of the clicked line, instead of always jumping to the end of the note. Two deliberate exceptions keep exact (not line-end) positioning:

  • Table cells: exact cell identification, and the whole cell's text is pre-selected - so clicking a cell then hitting a format-bar button (bold/strikethrough/etc.) immediately wraps just that cell.
  • Images: cursor lands right after the image's markdown (![alt](markit-image:...)).

Both are resolved directly from the clicked DOM element's position (event.target), the same pattern already used for checkboxes and the code-block copy button - not through caretRangeFromPoint, which is unreliable for a void element like <img>.

History

This PR went through a design pivot mid-session: it originally tried to place the cursor at the closest character to the click (proportional interpolation through markdown syntax), but that was abandoned - the interpolation was frequently a few characters off and felt unpredictable. The current design (always end of line, with the two exceptions above) is simpler and fully deterministic.

Testing

  • Automated: npm run check (0 errors/warnings), npm run test (150 tests passing).
  • Manually confirmed on Windows/x86_64 (this session): plain paragraphs, headings, bold/italic/links, list items (including a boundary tie-break bug and a syntax-highlighted-code-block whitespace-counting bug found and fixed during testing), checkboxes, table cells, images.
  • Linux/ARM64: pending retest - tracked in PENDING_CROSS_PLATFORM_TESTS.md. The PR's original Linux confirmation was for the abandoned "closest position" design and no longer applies to the current code.

Two real bugs found and fixed during manual testing:

  1. Clicking the very first character of a list item that starts with a formatted span (bold/italic/link) landed on the previous item's line - the click's numeric position was indistinguishable from "end of the previous item" without also knowing whether the real click landed at offset 0 of its own DOM text node.
  2. domRangeToTextOffset's whitespace filter rejected any whitespace-only text node to skip marked's decorative "\n" padding between block tags, but that same filter also silently dropped meaningful inline spaces - e.g. between two highlight.js <span>s in a syntax-highlighted code block, or a checkbox's donated leading space before a bold-starting label. Each dropped space desynced every offset after it by one character from the token-based model, occasionally landing several lines early.

🤖 Generated with Claude Code

## Summary Clicking into a note's rendered markdown preview now enters edit mode with the cursor placed at the end of the clicked line, instead of always jumping to the end of the note. Two deliberate exceptions keep exact (not line-end) positioning: - **Table cells**: exact cell identification, and the *whole cell's text* is pre-selected - so clicking a cell then hitting a format-bar button (bold/strikethrough/etc.) immediately wraps just that cell. - **Images**: cursor lands right after the image's markdown (`![alt](markit-image:...)`). Both are resolved directly from the clicked DOM element's position (`event.target`), the same pattern already used for checkboxes and the code-block copy button - not through `caretRangeFromPoint`, which is unreliable for a void element like `<img>`. ## History This PR went through a design pivot mid-session: it originally tried to place the cursor at the *closest* character to the click (proportional interpolation through markdown syntax), but that was abandoned - the interpolation was frequently a few characters off and felt unpredictable. The current design (always end of line, with the two exceptions above) is simpler and fully deterministic. ## Testing - Automated: `npm run check` (0 errors/warnings), `npm run test` (150 tests passing). - Manually confirmed on Windows/x86_64 (this session): plain paragraphs, headings, bold/italic/links, list items (including a boundary tie-break bug and a syntax-highlighted-code-block whitespace-counting bug found and fixed during testing), checkboxes, table cells, images. - Linux/ARM64: pending retest - tracked in `PENDING_CROSS_PLATFORM_TESTS.md`. The PR's original Linux confirmation was for the abandoned "closest position" design and no longer applies to the current code. Two real bugs found and fixed during manual testing: 1. Clicking the very first character of a list item that starts with a formatted span (bold/italic/link) landed on the *previous* item's line - the click's numeric position was indistinguishable from "end of the previous item" without also knowing whether the real click landed at offset 0 of its own DOM text node. 2. `domRangeToTextOffset`'s whitespace filter rejected *any* whitespace-only text node to skip marked's decorative "\n" padding between block tags, but that same filter also silently dropped meaningful inline spaces - e.g. between two `highlight.js` `<span>`s in a syntax-highlighted code block, or a checkbox's donated leading space before a bold-starting label. Each dropped space desynced every offset after it by one character from the token-based model, occasionally landing several lines early. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Click-accurate cursor positioning in the note preview (WIP)
All checks were successful
CI / frontend (pull_request) Successful in 25s
CI / backend (pull_request) Successful in 2m41s
3825dd3098
Clicking into a note's rendered markdown preview now enters edit mode
with the cursor placed as close as possible to the click, instead of
always jumping to the end. Maps a click position back to a raw-markdown
offset by walking marked's token tree (see clickToOffset.ts's module
comment and inline rationale for the algorithm and known approximations).

Table cells get exact identification and pre-select their whole text
(ready for a format-bar button to wrap it); clicking near an image lands
the cursor right after its markdown, since there's no visible text to
land "inside".

Status: tables and images manually confirmed working on Linux/ARM64. Also
just fixed a boundary bug (clicking in blank space past the end of a line
was jumping to the start of the next line/paragraph/list item instead of
staying at the end of the current one - see the <= comment in mapOffset)
but that specific fix hasn't been manually re-confirmed yet, only covered
by the new automated regression test. Windows/x86_64 untested entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Simplify click positioning to always land at end of the clicked line
All checks were successful
CI / frontend (pull_request) Successful in 25s
CI / backend (pull_request) Successful in 2m41s
0cd99020ed
Proportional interpolation through markdown syntax to guess a precise
in-line character position was frequently a few characters off and felt
unpredictable. Replace it with a simpler, deterministic rule: clicking
anywhere in a rendered line always collapses the cursor to that line's
end. This also drops table-cell whole-text selection and the
image-specific "land right after its markdown" case, since both were
part of the same precision-chasing approach and are now superseded by
the uniform end-of-line behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix line-boundary and image click bugs, restore table cell selection
All checks were successful
CI / frontend (pull_request) Successful in 25s
CI / backend (pull_request) Successful in 2m41s
2e745d9f24
Three issues from manual testing:

- A click on the very first character of a list item that starts with
  a formatted span (e.g. a bold word) landed on the *previous* item's
  line. The end-of-line resolver had no way to distinguish "blank space
  trailing the previous item" from "the start of this item's own text"
  - both produce the same renderedOffset number. Fixed by threading
  through whether the real click landed at offset 0 of its DOM text
  node (sourceRangeFromCaretPoint now derives this and passes it as
  preferFollowing), which breaks the tie correctly either way.

- Clicking an image landed on the following line instead of its own.
  <img> is a void element with no caret position of its own, so
  routing it through caretRangeFromPoint was inherently unreliable.
  Images are now resolved directly from the clicked <img>'s DOM index
  via a new dedicated imageSourceRange(), bypassing caretRangeFromPoint
  entirely for this case - the same pattern already used for checkboxes
  and the code-block copy button.

- Table cells lost their whole-text selection (clicking a cell now
  showed a collapsed cursor at the row's end instead). That was a
  deliberate, still-wanted exception to "always end of line" - not part
  of the imprecision the line-end change was meant to fix - so it's
  restored as a dedicated tableCellSourceRange(), resolved from the
  clicked <td>/<th>'s row/column position the same way images are.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Handle element-container caret points, not just text-node ones
All checks were successful
CI / frontend (pull_request) Successful in 25s
CI / backend (pull_request) Successful in 2m40s
99fa4676cb
The list-item-starting-with-bold boundary bug persisted after the
preferFollowing fix in the previous commit. Every simulated repro
using a text-node click point resolved correctly, which pointed at
caretRangeFromPoint itself: at a boundary right at the start of a
<strong>/<em>/<a> span, it can resolve to an ELEMENT container (e.g.
the <strong> tag, or its parent <li>) with startOffset as a child
index, rather than a text node. domRangeToTextOffset's walker only
ever matches text-node containers, so that case silently fell through
to null and the click lost its explicit position entirely.

sourceRangeFromCaretPoint now normalizes any element-container point
to the nearest actual text position first (preferring the first text
descendant at-or-after the boundary, matching how preferFollowing
already reasons about such points), before doing anything else.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix real root cause: whitespace filter dropped meaningful inline spaces
Some checks failed
CI / frontend (pull_request) Successful in 26s
CI / backend (pull_request) Has been cancelled
066a1ba58a
Found via a temporary on-screen debug overlay and the user's exact note
content, since every synthetic repro attempt had resolved correctly -
the actual bug only showed up with syntax-highlighted code blocks
present earlier in the note.

domRangeToTextOffset's text-node walk rejected any whitespace-only text
node ("if trimmed empty, skip") to filter out marked's decorative "\n"
padding between sibling block tags (e.g. between <li>s). But that same
filter also caught two other cases that are NOT decorative:

- highlight.js wraps each recognized keyword of a syntax-highlighted
  code block in its own <span>, leaving the space between adjacent
  keywords ("public", "void") as its own standalone, non-newline
  whitespace-only text node.
- a task-list checkbox's donated leading space, when the item's label
  starts with a formatted span (bold/italic/link) rather than plain
  text - the space can't merge into the same text node as "**bold**"
  the way it merges into plain "toto".

Each dropped space silently undercounted the DOM-measured rendered
text by one character relative to the token-based model in the rest of
this module, desyncing every offset after it. With several such spaces
before a given click, the resulting drift could land several tokens
early - in the reported case, two items back in a following list.

Narrowed the filter to only reject text nodes that are entirely "\n",
matching what marked actually inserts for pretty-printing, and no
longer swallowing a real inline space just because it happens to sit
alone in its own node.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Track PR #16 as pending Linux/ARM64 retest
All checks were successful
CI / frontend (pull_request) Successful in 25s
CI / backend (pull_request) Successful in 2m40s
32e5a68b9c
Now confirmed working on Windows/x86_64 across the full range: plain
text, bold/list items (including the boundary tie-break and
syntax-highlighted-code desync bugs found and fixed this session),
checkboxes, table cells, and images. The PR's original Linux
confirmation was for the earlier "closest click position" design,
since abandoned in favor of always-end-of-line (except table
cells/images, which keep exact positioning) - that earlier testing no
longer applies to the current code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lemarkis changed title from WIP: Click-accurate cursor positioning in the note preview to Click-accurate cursor positioning in the note preview 2026-09-18 10:42:51 +00:00
Lemarkis referenced this pull request from a commit 2026-09-21 07:31:13 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Lemarkis/Mark-it!16
No description provided.