Repository navigation
PT-4985: Put the caret in a verse inserted at the end of a paragraph - #50
Conversation
irahopkinson
left a comment
There was a problem hiding this comment.
@irahopkinson+AI made 3 comments.
Reviewable status: 0 of 5 files reviewed, 3 unresolved discussions (waiting on captaincrazybro).
a discussion (no related file):
Reviewed the verse-insert caret fix and its tests; 3 findings follow. Each comment shows the severity I settled on; 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
#2 (low · checked and confirmed) packages/platform/CHANGELOG.md:224 - The platform package's changelog has no entry for this fix, so host developers reading the release notes do not learn that the caret now lands in a verse inserted at the end of a paragraph.
What happens: the PR changes visible caret placement at usj-marker-action.utils.ts:538-546 but leaves packages/platform/CHANGELOG.md untouched; its [Unreleased] → ### Fixed section (:224) already lists comparable caret fixes.
Why it matters: documentation only; the release notes for the next platform-editor version will omit a behavior change consumers can see. Recent fix PRs added entries here, though no repo rule requires one.
Fix: add a bullet under ## [Unreleased] → ### Fixed in packages/platform/CHANGELOG.md stating that a verse inserted at the end of a paragraph now gets the caret after its number, so typing starts the new verse; keep Jira IDs out of it.
How this was checked: The PR changes src, tests and dist only (git diff --stat 06cbc96..HEAD); packages/platform/CHANGELOG.md is untouched, and its [Unreleased] ### Fixed section (CHANGELOG.md:224) holds the recent comparable entries. The caret-placement branch added at usj-marker-action.utils.ts:536-546 is user-visible. Recent fix PRs mostly do add entries: da56685 (caret after the number in an empty verse), 6f87a49, 52c9b53, ef60659 all touched the changelog, while 06cbc96 (context-menu close) did not, so the convention is common but not universal. No repo rule requires an entry, so this stays low and documentation-only.
Not attached to a line: this finding is in a file the pull request does not change, so the review has no file to attach a comment to
(AI-assisted, with my guidance)
packages/platform/src/editor/adaptors/usj-marker-action.utils.ts line 536 at r1 (raw file):
} else { const nextNode = nodeToInsert.getNextSibling(); if (nextNode) {
#1 - medium · checked and confirmed
Inserting a verse right after a footnoted word puts the caret inside the footnote: in Standard view the first typed characters land in the \f marker, not the new verse.
What happens: selection.insertNodes (usj-marker-action.utils.ts:506) places the new verse between the word and its NoteNode. nodeToInsert.getNextSibling() (:535) is that note, so nextNode.selectStart() (:537) descends to the note's first leaf: offset 0 of the \f marker text in Standard view, an element point inside the note in the hidden-marker view. The new $isSomeVerseNode branch (:538) is never reached because a next sibling exists.
Why it matters: in Standard view, text typed right after adding a verse after a footnoted word is lost from the saved USJ (getUsj() shows verse 5 followed directly by the note). In the hidden-marker view the typed text still lands in the new verse, but the caret starts inside the collapsed note. The nextNode.selectStart() line predates this PR, but it is the same wrong-caret-after-verse-insert defect this PR fixes, one branch over, so fixing it here is cheap.
Fix: In the plain-insert branch of getUsjMarkerAction (usj-marker-action.utils.ts:535-550), test $isSomeVerseNode(nodeToInsert) first, before nextNode.selectStart(): call nodeToInsert.selectEnd() for an editable verse ($isTextNode), else $placeCaretAtBoundary(parent, nodeToInsert.getIndexWithinParent() + 1); keep nextNode.selectStart() then nodeToInsert.selectStart() as the fall-backs for non-verse nodes. Verified: with a note after the insertion point the caret ends in the new verse's \v 5 text (Standard) or at a paragraph element point outside the note (hidden), and typed text lands in the new verse. Add one test per view to verseInsertCaret.test.tsx (para with a footnote after the word, insert v, read the selection's focus point, type text, assert getUsj() has the text after the new verse and the caret is never inside a NoteNode), chaining .append(...) if Lexical trees are built, then rebuild packages/platform/dist/ with node scripts/rebuild-committed-output.mjs.
How this was checked: Reproduced through mountViewEditor with para [\v 4, "third", note], caret at the end of "third", insertMarker("v"). In Standard view the caret lands at offset 0 of the note's "\f" marker text (usj-marker-action.utils.ts:535-537, nextNode is the NoteNode); typing "abc" was not in the saved USJ at all (getUsj shows verse 5 followed directly by the note). In the hidden-marker view the caret lands as an element point inside the note (offset 0), as claimed, but typing "abc" still ended up in verse 5 before the note in the saved USJ, so the data damage is Standard-only in jsdom. The if (nextNode) nextNode.selectStart() line is unchanged by this PR (the PR only added the $isSomeVerseNode arm after it), so the defect pre-exists and sits next to the new no-next-sibling arm; the anchor lines are not changed lines.
Correction: lines 536-537 do appear in this pull request's diff, but only because the existing next-sibling branch was reformatted with braces; it still calls selectStart on the next sibling exactly as before, so the behavior described here predates this pull request.
Other findings in this file: #4
packages/platform/src/editor/adaptors/usj-marker-action.utils.ts line 544 at r1 (raw file):
// editable marker's own text, or the boundary after an immutable one, where // EmptyVerseCaretGuardPlugin hosts it. const parent = nodeToInsert.getParentOrThrow();
#4 - low · checked and confirmed
The new verse branch looks up the verse's parent before the editable-verse path, which never uses it.
What happens: const parent = nodeToInsert.getParentOrThrow() (usj-marker-action.utils.ts:544) runs before the $isTextNode(nodeToInsert) check at :545; only the $placeCaretAtBoundary call at :546 reads parent.
Why it matters: readability only, with no behavior effect: a reader expects parent to matter on both paths.
Fix: replace :544-546 with if ($isTextNode(nodeToInsert)) nodeToInsert.selectEnd(); else $placeCaretAtBoundary(nodeToInsert.getParentOrThrow(), nodeToInsert.getIndexWithinParent() + 1); and rebuild packages/platform/dist/.
How this was checked: In the verse branch, const parent = nodeToInsert.getParentOrThrow() at usj-marker-action.utils.ts:544 runs before the $isTextNode(nodeToInsert) check at :545. Only the $placeCaretAtBoundary(parent, ...) call at :546 reads parent, so the editable-verse path computes a value it never uses. The line is added by this PR (git diff against the merge base shows it as new). The lookup cannot fail on a node that was just inserted, so there is no behavior effect; it is a readability cost only.
Other findings in this file: #1
…tnote Address Ira's review on #50: - An inserted verse now takes the caret through $placeCaretAtVerseContentStart, the rule verse navigation uses, ahead of the selectStart() fallback. A verse inserted right before a footnote no longer puts the caret inside the note (in Standard view the typed text was lost from the saved USJ). The helper moves from ScriptureReferencePlugin into verseCaret.utils.ts so both share it. - Look up the verse's parent only on the path that uses it. - Add a changelog entry under Unreleased > Fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, Ira. All three are addressed in e2cd1d6: #1: Fixed. An inserted verse now gets its caret from #2: Added a bullet under #4: Fixed. The parent is now looked up only on the path that uses it, and the code is now a single helper call. |
captaincrazybro
left a comment
There was a problem hiding this comment.
@captaincrazybro resolved 3 discussions.
Reviewable status: 0 of 8 files reviewed, all discussions resolved (waiting on irahopkinson+AI).
irahopkinson
left a comment
There was a problem hiding this comment.
@irahopkinson+AI made 1 comment.
Reviewable status: 0 of 8 files reviewed, all discussions resolved.
a discussion (no related file):
Previously, irahopkinson+AI (Agent AI for Ira Hopkinson) wrote…
Reviewed the verse-insert caret fix and its tests; 3 findings follow. Each comment shows the severity I settled on;
checked and confirmedmeans the finding was verified against the code, andneeds a human callmeans it could not be settled either way and is yours to judge.Findings that could not be attached to a line in this diff
#2 (low · checked and confirmed) packages/platform/CHANGELOG.md:224 - The platform package's changelog has no entry for this fix, so host developers reading the release notes do not learn that the caret now lands in a verse inserted at the end of a paragraph.
What happens: the PR changes visible caret placement at
usj-marker-action.utils.ts:538-546but leavespackages/platform/CHANGELOG.mduntouched; its[Unreleased]→### Fixedsection (:224) already lists comparable caret fixes.Why it matters: documentation only; the release notes for the next platform-editor version will omit a behavior change consumers can see. Recent fix PRs added entries here, though no repo rule requires one.
Fix: add a bullet under
## [Unreleased]→### Fixedinpackages/platform/CHANGELOG.mdstating that a verse inserted at the end of a paragraph now gets the caret after its number, so typing starts the new verse; keep Jira IDs out of it.How this was checked: The PR changes src, tests and dist only (git diff --stat 06cbc96..HEAD); packages/platform/CHANGELOG.md is untouched, and its [Unreleased] ### Fixed section (CHANGELOG.md:224) holds the recent comparable entries. The caret-placement branch added at usj-marker-action.utils.ts:536-546 is user-visible. Recent fix PRs mostly do add entries: da56685 (caret after the number in an empty verse), 6f87a49, 52c9b53, ef60659 all touched the changelog, while 06cbc96 (context-menu close) did not, so the convention is common but not universal. No repo rule requires an entry, so this stays low and documentation-only.
Not attached to a line: this finding is in a file the pull request does not change, so the review has no file to attach a comment to
(AI-assisted, with my guidance)
Thanks — checked e2cd1d6: #1, #2 and #4 are fixed, and moving the helper into verseCaret.utils.ts also removes the duplicated caret rule. One small thing: now that $placeCaretAtVerseContentStart is exported, the repo CLAUDE.md asks its TSDoc to state the calling context ("Mutating: call inside editor.update()").
There was a problem hiding this comment.
I'm approving this so it can merge. Assuming you will either fix the TSDoc in this PR or a follow-up.
@irahopkinson made 1 comment.
Reviewable status: 0 of 8 files reviewed, all discussions resolved (waiting on irahopkinson+AI).
Inserting a verse number where nothing follows it in the paragraph (for example at the end of a chapter) left the caret before the new number, so typing extended the previous verse. Place the caret after the verse marker instead; EmptyVerseCaretGuardPlugin hosts it there so typing starts the new verse. Other inserted nodes, including chapter markers, keep their placement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Place the caret explicitly for a verse ending its paragraph, mirroring $placeCaretAtVerseContentStart: the end of an editable marker's text, or the boundary after an immutable one, instead of relying on a later normalization of selectNext()'s element point in Standard view - Pin the caret position in Standard view and the reported reference (verse 2) in both views - Brace the if/else chain; find the para by type; reuse requireViewOptions; read the selection's focus, not its anchor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tnote Address Ira's review on #50: - An inserted verse now takes the caret through $placeCaretAtVerseContentStart, the rule verse navigation uses, ahead of the selectStart() fallback. A verse inserted right before a footnote no longer puts the caret inside the note (in Standard view the typed text was lost from the saved USJ). The helper moves from ScriptureReferencePlugin into verseCaret.utils.ts so both share it. - Look up the verse's parent only on the path that uses it. - Add a changelog entry under Unreleased > Fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ild dist requireViewOptions left settledGetUsj.test-helpers on main, so the tests now take their views from editorView(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e2cd1d6 to
1d62338
Compare
Code Review Summary
Branch: pt-4985-verse-insert-caret
Base: origin/main
Date: 2026-11-03
Review model: Claude Opus 5.5
Files changed: 8
Overview
This fixes PT-4985. In Simple mode, typing
\vand Enter inserts the next verse number, but thecaret did not reliably land in the new verse.
caret was left before the new number, so typing extended the previous verse. This is the
ticket's case.
inside the footnote. In Standard view the typed text was lost from the saved USJ. Ira found
this case in review.
Both came from the plain-insert branch of
getUsjMarkerAction, which placed the caret withnextSibling.selectStart(), or withnodeToInsert.selectStart()when there was no next sibling.For a verse marker, those calls resolve to the previous verse's text or to the first leaf inside a
following note.
The fix sends every inserted verse marker (
$isSomeVerseNode) through$placeCaretAtVerseContentStart, the rule verse navigation already uses for "the start of thisverse's content". That rule covers four cases:
marker's own
\v 2text.EmptyVerseCaretGuardPluginhosts it.It never enters a note. The helper (with its private
$firstCaretHost) moved unchanged fromScriptureReferencePlugin.tsxinto the newpackages/platform/src/editor/verseCaret.utils.ts, sonavigation and insertion share one rule. Other inserted nodes, chapter markers included, keep their
current placement. The "duplicate verse number → select the inserted marker" rule
(
highlightInserted) is deliberately unchanged.The author confirmed both cases in Platform.Bible against a locally staged build. The committed
packages/platform/dist/was rebuilt to match the source.packages/platform/CHANGELOG.mdhas anentry under
[Unreleased]→### Fixed.API Changes
None. The package's public exports, signatures and type declarations are unchanged:
$placeCaretAtVerseContentStartis now exported from the new internal moduleverseCaret.utils.ts,but not from the package entry point.
getUsjMarkerAction()changes only in where the caret lands after it inserts averse marker.
Findings
Combined from
/review-paratext, a/code-review(high) pass, and Ira's review on this PR.Critical — Must address before merge
None.
Important — Should address before merge
the typed text was lost from the saved USJ (Ira Adopt this repository into the paranext organization #1, medium) (fixed during review: any
inserted verse now takes the caret through
$placeCaretAtVerseContentStart, ahead of thenextNode.selectStart()fallback. The helper moved intoverseCaret.utils.tsso navigationand insertion share it. New footnote tests in both views failed before the fix.)
Minor — Consider
bullet added under
[Unreleased]→### Fixed)it (Ira Consider committing built dist/ like platform-bible-react, so consumers stop building at install time #4) (fixed during review: superseded. The branch is now a single helper call.)
selectNext()leftan element point after an editable
VerseNode) (fixed during review: placement is nowexplicit, via the shared helper)
EmptyVerseCaretGuardPluginhosts the caret in both views (fixedduring review: the comment now describes the shared rule)
during review: unit and editor-level tests pin the caret at the end of the new
\v 2text,with no caret host)
onScrRefChangereports verse 2 in both views)requireViewOptionshelper; tests readselection.anchorinstead offocus(fixedduring review)
Consumers of(author: this was always the intended behavior, and the signature isgetUsjMarkerActionsee a behavior change; note it in the consumingparanext-core PR
unchanged)
Other non-char inline nodes (e.g. a milestone) inserted at the end of a paragraph still get(deferred: this predates the change and needs a caret-host rule forthe caret before them
non-verse decorators. It is a candidate for its own ticket.)
Template Propagation
Shared Regions Modified
None.
Extension Config Changes
None.
Positive Observations
of copying part of it, so the two can't drift apart. Its documented cases (text host,
editable-marker end, character span, never entering a note, empty verse) now apply to inserted
verses too.
ScriptureReferencePlugin.test.tsxpassesunchanged.
checks the end of the marker text.
verseInsertCaret.test.tsx), each in Simple's hidden-marker view andStandard view:
editable markers
the new verse
\wspan cases already passed and guard against regressions.Interview Notes
Purpose (author's words): "To fix a small bug when trying to insert a verse without a
duplicate verse number marker after it (like at the end of a chapter) putting the cursor before
the new marker instead of after it."
Verified in the app:
npm run build:editor), both now work as intended.Choosing the footnote fix: the author weighed three options:
$placeCaretAtVerseContentStart.NoteNode.The author chose B, because once something can follow the new verse, all of the helper's
branches apply, and one shared rule can't drift. This supersedes an earlier choice to copy the
two lines.
The intermittent report: inserting between two verses that already exist selects the new
marker. That is the intentional duplicate-number highlight from the PRD's verse-inference rule,
left unchanged here. It likely explains why the ticket reported the bug as intermittent.
Behavior change for consumers: the author considers the new caret placement, and the
reference moving to the new verse, the intended behavior.
Design record: the design is recorded in paranext-core at
.context/designs/2026-11-03-verse-insert-caret-design.md, not yet committed. It describes thefirst version (
selectNext()) and predates the review changes.No unresolved items.
In-Review Quality Check
All checks passed after the review fixes.
scripts/rebuild-committed-output.mjs, runningthe
extract-apitargets forplatform-editorand then forutilitiesthroughnpx, becausepnpmcan't findnodein this shell. Onlypackages/platform/dist/index.jsandindex.js.mapchanged. The utilities output is unchanged.
tsconfig.lib.jsonshows no errors other than TS6305 errors, which come from thestale local build output.
passed and 17 were skipped.
Suggested Review Focus
$placeCaretAtVerseContentStartintoverseCaret.utils.ts. It movedunchanged apart from a doc line that now mentions insertion. Is the shared rule right for a
freshly inserted verse in every case it covers?
goes to the start of the span's text, so typing joins the span. That is consistent with
navigation, but confirm it's wanted for insertion.
selectionchange, so the testdispatches
SELECTION_CHANGE_COMMANDby hand. In the real app the browser provides it, andthe author confirmed the behavior there manually.
the end of a paragraph still get the caret before them.
still wanted?
🤖 Generated with Claude Code
This change is