fix: cursor drift during vertical arrow navigation (SD-1689)#1918
fix: cursor drift during vertical arrow navigation (SD-1689)#1918luccas-harbour wants to merge 19 commits intomainfrom
Conversation
af96315 to
09421f5
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 09421f5954
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
packages/super-editor/src/extensions/vertical-navigation/vertical-navigation.js
Outdated
Show resolved
Hide resolved
3ded6e3 to
94c3fcf
Compare
afn
left a comment
There was a problem hiding this comment.
Looks good to the best of my understanding! Had a couple of minor suggestions but nothing blocking.
| const pageEl = options.painterHost.querySelector( | ||
| `.superdoc-page[data-page-index="${options.pageIndex}"]`, |
There was a problem hiding this comment.
Consider making this a helper function (and replace several other places in the codebase that are querying the DOM for data-page-index) to encapsulate the implementation details
There was a problem hiding this comment.
It turns out there was already a helper for this here so I am referencing it instead.
| * @param options - Configuration object containing DOM elements and page information. | ||
| * @returns The Y offset in layout-space units, or null if calculation fails. | ||
| */ | ||
| export function getPageOffsetY(options: { |
There was a problem hiding this comment.
This seems to duplicate the contents of getPageOffsets(). Why not use that method (or wrap it like getPageOffsetX) instead?
There was a problem hiding this comment.
Good call! I should have double checked this one
| // Upper-bound search for pmStart <= pos | ||
| let lo = 0; | ||
| let hi = entries.length; | ||
| while (lo < hi) { | ||
| const mid = (lo + hi) >>> 1; | ||
| if (entries[mid].pmStart <= pos) { | ||
| lo = mid + 1; | ||
| } else { | ||
| hi = mid; | ||
| } | ||
| } | ||
|
|
||
| const idx = lo - 1; |
There was a problem hiding this comment.
Binary search is notoriously tricky to get right :) Consider using something like lodash's sortedIndexBy (lodash isn't currently a direct dependency of superdoc-editor, but it's pulled in via naive-ui) to simplify the code and avoid potential pitfalls.
| // Upper-bound search for pmStart <= pos | |
| let lo = 0; | |
| let hi = entries.length; | |
| while (lo < hi) { | |
| const mid = (lo + hi) >>> 1; | |
| if (entries[mid].pmStart <= pos) { | |
| lo = mid + 1; | |
| } else { | |
| hi = mid; | |
| } | |
| } | |
| const idx = lo - 1; | |
| const idx = sortedIndexBy(entries, { pmStart: pos }, 'pmStart') - 1; |
There was a problem hiding this comment.
Sounds good, I changed it
| * @param {boolean} extend | ||
| * @returns {import('prosemirror-state').Selection | null} | ||
| */ | ||
| function buildSelection(state, pos, extend) { |
There was a problem hiding this comment.
Consider moving this function to a place where it can be used by other extensions if needed, perhaps src/core/presentation-editor/selection/SelectionHelpers.ts?
There was a problem hiding this comment.
I had a look at this one and it looks quite simple and specific to what is being done in this plugin so I think I'd rather keep it as a local function. I could not find anywhere else in the codebase that would benefit from this helper.
I don't think it would really fit in src/core/presentation-editor/selection/SelectionHelpers.ts as it modifies the selection from the ProseMirror side of things, whereas the code in the SelectionHelpers file deals with rendering concerns from the layout-engine's side.
4d9c694 to
e710bcc
Compare
| const presentationEditor = editor.presentationEditor; | ||
| const layoutSpaceCoords = presentationEditor.computeCaretLayoutRect(selection.head); | ||
| if (!layoutSpaceCoords) return null; | ||
| const clientCoords = presentationEditor.denormalizeClientPoint(layoutSpaceCoords.x, layoutSpaceCoords.y, layoutSpaceCoords.pageIndex, layoutSpaceCoords.height); |
There was a problem hiding this comment.
denormalizeClientPoint can return null but we're accessing .x without a guard
this could throw if hosts are missing or coords invalid
There was a problem hiding this comment.
That function will only return null if the values of x and y are null. Those values come from the call to presentationEditor.computeCaretLayoutRect(selection.head) which will never return null for these values.
| const pageEl = adjacentLine.closest?.(`.${DOM_CLASS_NAMES.PAGE}`); | ||
| const pageIndex = pageEl ? Number(pageEl.dataset.pageIndex ?? 'NaN') : null; | ||
| const rect = adjacentLine.getBoundingClientRect(); | ||
| const clientY = rect.top + rect.height / 2; |
There was a problem hiding this comment.
clientY is computed here then recomputed in the return below
maybe use a variable?
There was a problem hiding this comment.
oops, absolutely
fixed
| const clamped = Math.max(0, Math.min(pos, doc.content.size)); | ||
| if (extend) { | ||
| const anchor = selection.anchor ?? selection.from; | ||
| return TextSelection.create(doc, clamped, anchor); |
There was a problem hiding this comment.
args appear reversed - TextSelection.create(doc, anchor, head) expects anchor first.
ref: https://prosemirror.net/docs/ref/#state.TextSelection.constructor

There was a problem hiding this comment.
great catch! fixed
| */ | ||
| function getHitFromLayoutCoords(editor, goalX, clientY, coords, pageIndex) { | ||
| const presentationEditor = editor.presentationEditor; | ||
| const clientPoint = presentationEditor.denormalizeClientPoint(goalX, coords.y, pageIndex); |
There was a problem hiding this comment.
does crossing page boundaries affects Y and the X calculation here?
There was a problem hiding this comment.
it does and that's taken into account via the pageIndex parameter (right here)
Shiftis held.