Repository navigation
Rest the caret at an empty verse's editable marker, not between markers - #24
Conversation
lyonsil
left a comment
There was a problem hiding this comment.
I reviewed the empty-verse caret placement change and its tests, and have 6 findings, all at low severity. Each comment shows the severity it settled at and a status: checked and confirmed means the finding was verified against the code, and needs a human call means it could not be settled either way and is yours to judge.
Findings that could not be attached to a line in this diff
#1 (low · checked and confirmed) libs/shared/src/nodes/usj/caretBoundaries.utils.ts:69 - Calling $placeCaretAtBoundary at the boundary after an editable empty verse still puts the caret at offset 0 of the next verse's marker, a position that reports the next verse.
What happens: $caretHostAtBoundary (caretBoundaries.utils.ts:69-72) returns whatever text node follows the boundary. Between \v 5 and \v 6 in Standard view that is verse 6's editable VerseNode, so $placeCaretAtBoundary (:81-85) selects its offset 0. This PR sidesteps that only inside $placeCaretAtVerseContentStart (ScriptureReferencePlugin.tsx:596-600). EmptyVerseCaretGuardPlugin.tsx:33 asks the same helper, gets verse 6's marker back as a host, and leaves the caret alone.
Why it matters: maintenance only: no other current caller parks a caret after an editable empty verse, but the next one that does so through $placeCaretAtBoundary reintroduces the wrong-verse report this PR fixes.
Fix: state in the TSDoc of $caretHostAtBoundary and $placeCaretAtBoundary that a caller placing a caret directly after a verse marker must handle an editable next verse marker itself, since the module's forward-looking convention is deliberate. While there, add each function's calling context per CLAUDE.md ("Mutating: call inside editor.update()" on $placeCaretAtBoundary, the safe read form on $caretHostAtBoundary). The platform bundle's source map embeds this file's source, so run pnpm rebuild-committed-output and commit dist/ in the same PR. The alternative - making $caretHostAtBoundary reject a following SomeVerseNode - changes EmptyVerseCaretGuardPlugin's behavior and needs a unit test in libs/shared.
How this was checked: $caretHostAtBoundary (caretBoundaries.utils.ts:69-72) returns any following TextNode, and an editable VerseNode is one, so $placeCaretAtBoundary (:81-85) selects offset 0 of the next verse marker. The PR sidesteps this only in $placeCaretAtVerseContentStart (ScriptureReferencePlugin.tsx:596-600), whose own comment (:585-590) says the next verse's marker would otherwise be borrowed. I checked every other caller (transientCaretHost.ts:118-132, ParaMarkerPrefixCursorGuardPlugin.tsx:108/141, ArrowNavigationPlugin.tsx:348, NoteShellCaretGuardPlugin.tsx:133/163, markerEditDeletion.utils.ts:75/458, ScriptureReferencePlugin.tsx:547/620): none parks a caret directly after an editable empty verse marker (the guard only repairs the decorator case, where no text host exists). So the hazard is real but has no reachable caller today; the forward-looking convention is deliberate and documented, and the file is unchanged by this PR. Maintenance-only, already low. I did not trace the reported-verse resolution for a caret at offset 0 of verse 6's marker; the PR's own comment treats it as the next verse.
Not attached to a line: not on a changed line; cannot be posted inline
#3 (low · checked and confirmed) packages/platform/src/editor/ScriptureReferencePlugin.test.tsx:385 - The title "stops at the next verse marker for an empty verse with an editable marker" names a different position than the test asserts, the end of verse 5's own marker.
What happens: the title at ScriptureReferencePlugin.test.tsx:385 was kept as is. The updated comment at :396-398 says the caret goes to the end of this verse's own marker, "not to the start of verse 6's marker", and :399 asserts $expectSelectionToBe(emptyVerseMarker, emptyVerseMarker.getTextContentSize()).
Why it matters: test-only, with no behavior effect. This test has failed intermittently in CI, and whoever triages its next failure reads a title that names the position the comment now rules out.
Fix: rename the test at :385 to match its sibling at :367: "rests at the end of the editable verse marker for an empty verse".
How this was checked: The title at ScriptureReferencePlugin.test.tsx:385 ("stops at the next verse marker for an empty verse with an editable marker") is unchanged by this PR, while the comment at :396-398 was rewritten to say the caret goes to the end of verse 5's own marker and not to the start of verse 6's marker, and :399 asserts $expectSelectionToBe(emptyVerseMarker, emptyVerseMarker.getTextContentSize()). "Stops at the next verse marker" can be read loosely as "does not run past it", which the test does guard, but it names the neighbour rather than the asserted position, and the sibling at :367 already uses the "rests at the end of the editable verse marker" wording. Test-only, cosmetic; the intermittent-CI-failure motivation was not checked.
Not attached to a line: not on a changed line; cannot be posted inline
(AI-assisted, with my guidance)
|
Thanks — all six addressed or answered (16fb69e):
|
Thanks. One correction, and it's my mistake: the (AI-assisted, with my guidance) |
Moving to an empty verse in Standard view (one followed only by the next verse marker) left an element point between the two editable marker text nodes. Lexical resolved its own selection to the end of this verse's marker but drew the DOM caret at the start of the next verse's marker, so whichever read of the DOM selection came next moved the caret into the next verse's marker. That race is what made "stops at the next verse marker for an empty verse with an editable marker" fail intermittently in CI (macOS, on two unrelated PRs). The caret now goes to the end of the verse's own marker, as it already does for a verse that opens with a note, milestone or char span; the immutable-marker case keeps its element point for EmptyVerseCaretGuardPlugin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds tests that a caret a user puts in an empty verse reports that verse, not the next one: at the end of an editable marker (Standard view) and at the element point after an immutable marker (Formatted view). Also corrects the account of the race: the previous placement selected offset 0 of the next verse's marker, which Lexical sometimes normalized back to the end of this one before the selection was read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Use TextNode.selectEnd() for the marker-end caret in $placeCaretAtVerseContentStart. - Explain both empty-verse shapes (next marker follows / paragraph-final) in its TSDoc. - Document the editable-next-marker hazard and calling context on the caret boundary helpers. - Rename the editable empty-verse placement test to match its assertion. - Pin the caret for an editable empty verse that ends its paragraph. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FwTrLB6VPyyU9JLg3qoqJ
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FwTrLB6VPyyU9JLg3qoqJ
…, not the next verse Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FwTrLB6VPyyU9JLg3qoqJ
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FwTrLB6VPyyU9JLg3qoqJ
|
Thanks, you're right: the clause was wrong. The Also rebased onto the latest |
16fb69e to
73b1f2f
Compare
Brings in #23 (copy limit), #14 (context menu, \id-line editing and the book settle scope), #24 (empty-verse caret) and #15 (note index, key and highlight, unfocused applyUpdate, and the note-editing fixes). Conflicts, resolved to keep both sides' intent: - Settle engine (tier2Rebuild, virtualSettle): this branch's caret and annotation carry ($captureCaret/$restoreCaret through pairRuns, mark carry) replaces main's byte anchor with preserved-key counting, which covers the same typed-undeclared-marker case through the literal alignment. Main's book scope ($rebuildBook, $settledBookLine), note close at a typed closer, undeclared-marker-in-note rule and region caret restore are kept and ported onto this branch's capture, char opener renames, carried runs and tokenizeFragment (tokenizedBookLine now takes the fragment, so placeholders keep their kinds). One fragment join: $buildParaScopeFragment uses main's $appendParaFragments. $charNeedsSentinelAs asks main's edited-glyph question of a span as renamed. - applyUpdate: main's applyOps (focus rule, extra tag, report from onUpdate so a call from an update listener works) with this branch's applying-update signal and settled-USJ report. The signal is now set inside the update and cleared in onUpdate, so a nested call is applied and announced once with its ops; the "inside an update" warning and its test expectation go, since the call now behaves. - Note shell guard: this branch's rule that a caret never rests outside a protected note (front and past-closer carets go into the content, ranges are confined, select-all handled in its own update, moves announced with APP_PLACED_CARET_COMMAND) on main's command handling (deletes refused or stopped at the shell and closer edges, edits confine the range first). The past-closer caret lands at main's content-end text point. - Caller: this branch's caller transform, regex and caret handling are kept; main's caller-slot predicate replaces the duplicate scan, and $createWholeNote builds the slot for an empty caller (unmergeable). The editor->USJ adaptor keeps the identity-based caller slot and adds main's Standard-view space collapse after the caller. - Separator prefix: both helpers kept ($charSeparatorPrefixLength is mark-transparent); both use main's $ownsOpenerSeparator. - Literal-only blocks: the shared $isInLiteralOnlyBlock drops the book, which main gave a settle scope. - Tags: this branch's "tag only a commit that moved a node" and APP_PLACED_CARET_COMMAND are kept over main's release-after-commit in the caret repairs and scripture-reference placement. - Committed dist: this branch's; rebuilt separately. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvcPjD9t3ojhfixqiz43us
Summary
Fixes the intermittent CI failure of
ScriptureReferencePlugin.test.tsx› stops at the next verse marker for an empty verse with an editable marker (seen on macOS in #15 and #19:expected Point{ key: '282', offset: 0 } to deeply equal { key: '280', offset: 5 }).Cause. Navigating to an empty verse (one followed only by the next verse marker) placed the caret at the boundary after its marker with
$placeCaretAtBoundary, which takes the text node that follows as the caret's host. In Standard and Unformatted views the next verse's marker is itself text (\v 6), so the caret was put at offset 0 of the NEXT verse's marker. Lexical sometimes normalizes that point back to the end of\v 5before the selection is next read and sometimes doesn't, which is the race the test lost (1 run in 12 locally on #19's branch, never onmainin 54 runs, all measured beforemainaddedflushQueuedEvents()calls to the test). Those calls wait for the pendingselectionchangeand so hide the race in this test; the placement change removes the race itself, so the caret position no longer depends on when Lexical normalizes.Fix. For an empty verse whose marker is editable text, the caret goes to the end of the verse's own marker — the rule
$placeCaretAtVerseContentStartalready applies when a verse opens with a note, milestone or char span.Behavior change
EmptyVerseCaretGuardPluginrepairs with a caret host.\v 6(the old race outcome) also reports verse 5, so the race only ever affected where the caret sat, not the verse sent to the host.Testing
\v 5) and an immutable marker (the element point after it), the plugin reports verse 5 and never 6. These pin the empty-verse verse-reporting behavior Formatted view depends on; they pass before and after the fix, since reporting was unaffected.main'sflushQueuedEvents()mitigation, the flaky test is deterministic with the fix: 30/30 in a loop and 24/24 under six-way parallel load. Fullpackages/platformsuite passes; typecheck and lint clean;rebuild-committed-outputthenverify-committed-distreports the committed build output current.Review follow-up
Addressed in the commits after the first:
selectEnd()for the marker-end caret, TSDoc on the editable empty-verse shapes and on the caret-boundary helpers, test rename, and rebuilt committed output (built on Linux).AI Involvement
AI-assisted. Claude traced the CI failure to the caret placement race, wrote the fix and tests, and ran the checks.
🤖 Generated with Claude Code
This change is