lineWrapping: clicking past end of trailing wrapped segment places cursor on previous visual line #1693

Open
opened 2026-04-14 15:16:08 +02:00 by jscaltreto · 6 comments
jscaltreto commented 2026-04-14 15:16:08 +02:00 (Migrated from github.com)

Describe the issue

When EditorView.lineWrapping is enabled and a line wraps across multiple visual lines, clicking in the horizontal whitespace after the last character on the trailing visual line places the cursor at the equivalent x-position on the visual line above, rather than at the end of the logical line.

Expected: the cursor lands at the end of the line (after the last character of the trailing wrapped segment), matching what the user clicked near.

Actual: the cursor lands mid-line on the previous visual row. Typing then inserts in the wrong place, which is jarring.

Browser and platform

Firefox 149.0 on Linux

https://codemirror.net/try/#c=aW1wb3J0IHttaW5pbWFsU2V0dXAsIEVkaXRvclZpZXd9IGZyb20gImNvZGVtaXJyb3IiCgpuZXcgRWRpdG9yVmlldyh7CiAgZG9jOiAiVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy5cblxuQSBOZXcgTGluZS4iLAogIGV4dGVuc2lvbnM6IFsKICAgIEVkaXRvclZpZXcubGluZVdyYXBwaW5nCiAgXSwKICBwYXJlbnQ6IGRvY3VtZW50LmJvZHkKfSkK

### Describe the issue When `EditorView.lineWrapping` is enabled and a line wraps across multiple visual lines, clicking in the horizontal whitespace *after* the last character on the **trailing** visual line places the cursor at the equivalent x-position on the visual line *above*, rather than at the end of the logical line. Expected: the cursor lands at the end of the line (after the last character of the trailing wrapped segment), matching what the user clicked near. Actual: the cursor lands mid-line on the previous visual row. Typing then inserts in the wrong place, which is jarring. ### Browser and platform Firefox 149.0 on Linux ### Reproduction link https://codemirror.net/try/#c=aW1wb3J0IHttaW5pbWFsU2V0dXAsIEVkaXRvclZpZXd9IGZyb20gImNvZGVtaXJyb3IiCgpuZXcgRWRpdG9yVmlldyh7CiAgZG9jOiAiVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy5cblxuQSBOZXcgTGluZS4iLAogIGV4dGVuc2lvbnM6IFsKICAgIEVkaXRvclZpZXcubGluZVdyYXBwaW5nCiAgXSwKICBwYXJlbnQ6IGRvY3VtZW50LmJvZHkKfSkK
marijnh commented 2026-04-14 16:54:55 +02:00 (Migrated from github.com)

I cannot reproduce this. Attached patch adds a test, but it's passing on all browsers (including Firefox Linux) that I have access to. Are you sure this problem still exists in @codemirror/view 6.41.0?

I cannot reproduce this. Attached patch adds a test, but it's passing on all browsers (including Firefox Linux) that I have access to. Are you sure this problem still exists in @codemirror/view 6.41.0?
jscaltreto commented 2026-04-14 21:23:33 +02:00 (Migrated from github.com)

Weird. I'm able to repro it reliably in FF 149.0 on Linux using the link in the description. However Chrome on linux seems to work correctly, as does FF 149 on Windows.

Here's what I see on FF in linux:

https://github.com/user-attachments/assets/017b5b1c-04ea-4212-abbc-31c577e54e27

(My setup probably represents a vanishingly small number of real users, to be fair)

Weird. I'm able to repro it reliably in FF 149.0 on Linux using the link in the description. However Chrome on linux seems to work correctly, as does FF 149 on Windows. Here's what I see on FF in linux: https://github.com/user-attachments/assets/017b5b1c-04ea-4212-abbc-31c577e54e27 (My setup probably represents a vanishingly small number of real users, to be fair)
jscaltreto commented 2026-04-15 02:10:00 +02:00 (Migrated from github.com)

I was able to trace the behavior I'm seeing to this check in cursor.ts: github.com/codemirror/view@59b1fed1f0/src/cursor.ts (L333-L340)

I think there's some weird subpixel rendering artifacts in FF's font rendering (at least in my environment). above.bottom turns out to be some fraction of a pixel lower than closestRect.top, so the scan nudges me into the row above.

One thought might be to gate the check on overlaps >= 1 px (e.g. above && above.bottom - closestRect.top >= 1) to guard against such subpixel weirdness. I just don't know well enough whether this would break the case that the check was designed to catch in the first place.

Edit to add: I also was able to reproduce a similar issue (with I believe the same root cause) in Chrome (Linux; still no repro on Windows), if the line above included a lint diagnostic. It adds paddingBottom: "0.7px" which extends the above row's rects into closestRect by 0.7px, and results in the same issue during cursor scan. Reproduction (just make sure LINT appears on the second-to-last visual line).

I was able to trace the behavior I'm seeing to this check in `cursor.ts`: https://github.com/codemirror/view/blob/59b1fed1f03bdb1384ad053556ec17d66ef8ed4b/src/cursor.ts#L333-L340 I think there's some weird subpixel rendering artifacts in FF's font rendering (at least in my environment). `above.bottom` turns out to be some fraction of a pixel lower than `closestRect.top`, so the scan nudges me into the row above. One thought might be to gate the check on overlaps >= 1 px (e.g. `above && above.bottom - closestRect.top >= 1`) to guard against such subpixel weirdness. I just don't know well enough whether this would break the case that the check was designed to catch in the first place. Edit to add: I also was able to reproduce a similar issue (with I believe the same root cause) in Chrome (Linux; still no repro on Windows), if the line above included a lint diagnostic. It [adds](https://github.com/codemirror/lint/blob/336707d79c68fc3ce4e7c2f4cce91e20562dd6c6/src/lint.ts#L716) `paddingBottom: "0.7px"` which extends the `above` row's rects into closestRect by 0.7px, and results in the same issue during cursor scan. [Reproduction](https://codemirror.net/try/#c=aW1wb3J0IHttaW5pbWFsU2V0dXAsIEVkaXRvclZpZXd9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7bGludGVyfSBmcm9tICJAY29kZW1pcnJvci9saW50IgoKY29uc3QgZG9jID0gIlRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuIExJTlQuIFRISVMgTElORSBJUyBMT05HLiBUSElTIExJTkUgSVMgTE9ORy4gVEhJUyBMSU5FIElTIExPTkcuXG5cbkEgTmV3IExpbmUuIgoKbmV3IEVkaXRvclZpZXcoewogIGRvYywKICBleHRlbnNpb25zOiBbCiAgICBtaW5pbWFsU2V0dXAsCiAgICBFZGl0b3JWaWV3LmxpbmVXcmFwcGluZywKICAgIGxpbnRlcigoKSA9PiB7CiAgICAgIGNvbnN0IGZyb20gPSBkb2MuaW5kZXhPZigiTElOVCIpCiAgICAgIHJldHVybiBbewogICAgICAgIGZyb20sCiAgICAgICAgdG86IGZyb20gKyA0LAogICAgICAgIHNldmVyaXR5OiAiZXJyb3IiLAogICAgICAgIG1lc3NhZ2U6ICJMaW50IgogICAgICB9XQogICAgfSkKICBdLAogIHBhcmVudDogZG9jdW1lbnQuYm9keQp9KQo=) (just make sure LINT appears on the second-to-last visual line).
marijnh commented 2026-04-15 11:13:31 +02:00 (Migrated from github.com)

Very odd. I guess you're getting a different font or font library than I am getting, leading to different rectangles being returned by getClientRects, which somehow throws off that code. I tried installing nightly, but still cannot reproduce this. Could you maybe see what closestI and closestRect are when that condition you linked is triggered? Maybe that would make the problem obvious enough to try to fix it blindly.

Very odd. I guess you're getting a different font or font library than I am getting, leading to different rectangles being returned by `getClientRects`, which somehow throws off that code. I tried installing nightly, but still cannot reproduce this. Could you maybe see what `closestI` and `closestRect` are when that condition you linked is triggered? Maybe that would make the problem obvious enough to try to fix it blindly.

I added some debugging to cursor.ts. Here's what I see in firefox:

above.bottom=108.77 > closestRect.top=108.57. this.y=123.00
closestRect={"x":382.20001220703125,"y":108.56666564941406,"width":7.1999969482421875,"height":17,"top":108.56666564941406,"right":389.40000915527344,"bottom":125.56666564941406,"left":382.20001220703125}
closestI=284

If I add a lint diagnostic to something on the line above it's more exaggerated.

above.bottom=109.47 > closestRect.top=108.57. this.y=118.00
closestRect={"x":15,"y":108.56666564941406,"width":453.6000061035156,"height":17,"top":108.56666564941406,"right":468.6000061035156,"bottom":125.56666564941406,"left":15}
closestI=2

In chrome, with the lint example I get:

above.bottom=103.56 > closestRect.top=103.06. this.y=117.00
closestRect={"x":15,"y":103.0625,"width":592.8125,"height":18,"top":103.0625,"right":607.8125,"bottom":121.0625,"left":15}
closestI=2
I added some debugging to cursor.ts. Here's what I see in firefox: ``` above.bottom=108.77 > closestRect.top=108.57. this.y=123.00 closestRect={"x":382.20001220703125,"y":108.56666564941406,"width":7.1999969482421875,"height":17,"top":108.56666564941406,"right":389.40000915527344,"bottom":125.56666564941406,"left":382.20001220703125} closestI=284 ``` If I add a lint diagnostic to something on the line above it's more exaggerated. ``` above.bottom=109.47 > closestRect.top=108.57. this.y=118.00 closestRect={"x":15,"y":108.56666564941406,"width":453.6000061035156,"height":17,"top":108.56666564941406,"right":468.6000061035156,"bottom":125.56666564941406,"left":15} closestI=2 ``` In chrome, with the lint example I get: ``` above.bottom=103.56 > closestRect.top=103.06. this.y=117.00 closestRect={"x":15,"y":103.0625,"width":592.8125,"height":18,"top":103.0625,"right":607.8125,"bottom":121.0625,"left":15} closestI=2 ```
Owner

closestRect seems reasonable though. That's the end of the line. I failed to ask for the value of above, which is actually the relevant rectangle determining the decision to recurse into the line above. Any chance you can repeat the experiment and include that too?

`closestRect` seems reasonable though. That's the end of the line. I failed to ask for the value of `above`, which is actually the relevant rectangle determining the decision to recurse into the line above. Any chance you can repeat the experiment and include that too?
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
3 participants
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
codemirror/dev#1693
No description provided.