Regression #1717

Closed
opened 2026-06-23 19:37:39 +02:00 by bjones1 · 2 comments

I've created a minimal test case that shows what I believe is a CodeMirror View bug which appeared after v6.38.8 (the big rewrite). The result, copied from the JavaScript console:

Uncaught (in promise) Error: No tile at position 2
    at DocTile.resolveBlock (index.js:1929:19)
    at DocView.coordsAt (index.js:3230:42)
    at _EditorView.coordsAtPos (index.js:8474:33)
    at no_tile_repro (CodeChatEditor.mts:102:9)
    at CodeChatEditor.mts:112:5
    at on_dom_content_loaded (CodeChatEditor.mts:50:9)
    at CodeChatEditor.mts:107:5

The repro has two adjacent block-replace decorations, [0,1] and [1,2], over the doc "\n\n". On the initial render everything is fine. The updateDocBlock dispatch changes only the first widget's contents, so CodeMirror does an incremental rebuild of just the changed decoration range [0,1] and preserves the rest. This update produces the error.

To build the attached files: esbuild src/bug.html src/CodeChatEditor.mts src/css/CodeChatEditor.css --bundle --outdir=./static/bundled --sourcemap --format=esm --loader:.html=copy. From the output of static/bundled, run npx http-server . then open http://127.0.0.1:8080/bug.html.

Here's Claude's analysis:

Root cause

The error is a regression in @codemirror/view 6.39+ (the upgrade ~6.38.8 → ^6.43.1 in commit a6e4abe). It's a bug in the new tile-based content-view rebuilder, not a misuse in the CodeChat code — the decoration set is valid.

What happens

The repro has two adjacent block-replace decorations, [0,1] and [1,2], over the doc "\n\n". On the initial render everything is fine. The updateDocBlock dispatch changes only the first widget's contents, so CodeMirror does an incremental rebuild of just the changed decoration range [0,1] and preserves the rest.

Instrumenting CodeMirror's rebuilder captured the exact sequence:

EMIT 0..1
  point 0..1 block=true  → addBlockWidget len=1   ← widget 1, correct
  point 1..1 block=true  → addBlockWidget len=0   ← widget 2 emitted at the boundary with length 0  Γ£ù
PRESERVE len=1 incStart=false incEnd=true
  skip widget from=0 to=1 tileLen=1               ← the real widget 2 (len 1) from the old tree
  skip line  from=0 to=0

When emit(0,1) runs RangeSet.spans over the changed range, the span iterator fires the point callback a second time for the adjacent block decoration [1,2], reporting it as a zero-length point (from==to==1) because its start side sits exactly on the right boundary of the emitted range. emit builds a block-widget tile with length = to - from = 0.

The resulting tile tree is corrupted:

before (correct) after (broken)
widget pos=0 len=1 widget pos=0 len=1
widget pos=1 len=1 widget pos=1 len=0
line pos=2 len=0 line pos=1 len=0 (phantom)
line pos=1 len=0 (phantom)
end 2 1

The second block widget loses its length, so the tile tree's cumulative length stops at 1. Nothing covers document position 2, so coordsAtPos(2)docView.coordsAt(2)DocTile.resolveBlock(2) finds no tile and throws Error: No tile at position 2 (index.js:1929).

Trigger conditions (all required)

  1. Two adjacent block decorations (the boundary between them is what's mis-iterated).
  2. An incremental rebuild whose changed range ends exactly on that boundary (the updateDocBlock edit to the first block).
  3. A coordsAtPos at the position past the second block, which now has no tile.
I've created a minimal test case that shows what I believe is a CodeMirror View bug which appeared after v6.38.8 (the big rewrite). The result, copied from the JavaScript console: ``` Uncaught (in promise) Error: No tile at position 2 at DocTile.resolveBlock (index.js:1929:19) at DocView.coordsAt (index.js:3230:42) at _EditorView.coordsAtPos (index.js:8474:33) at no_tile_repro (CodeChatEditor.mts:102:9) at CodeChatEditor.mts:112:5 at on_dom_content_loaded (CodeChatEditor.mts:50:9) at CodeChatEditor.mts:107:5 ``` The repro has two **adjacent block-replace decorations**, `[0,1]` and `[1,2]`, over the doc `"\n\n"`. On the initial render everything is fine. The `updateDocBlock` dispatch changes only the *first* widget's contents, so CodeMirror does an **incremental rebuild** of just the changed decoration range `[0,1]` and preserves the rest. This update produces the error. To build the attached files: `esbuild src/bug.html src/CodeChatEditor.mts src/css/CodeChatEditor.css --bundle --outdir=./static/bundled --sourcemap --format=esm --loader:.html=copy`. From the output of `static/bundled`, run `npx http-server .` then open `http://127.0.0.1:8080/bug.html`. Here's Claude's analysis: ## Root cause The error is a **regression in `@codemirror/view` 6.39+** (the upgrade `~6.38.8` ΓåÆ `^6.43.1` in commit `a6e4abe`). It's a bug in the new *tile-based* content-view rebuilder, **not** a misuse in the CodeChat code ΓÇö the decoration set is valid. ### What happens The repro has two **adjacent block-replace decorations**, `[0,1]` and `[1,2]`, over the doc `"\n\n"`. On the initial render everything is fine. The `updateDocBlock` dispatch changes only the *first* widget's contents, so CodeMirror does an **incremental rebuild** of just the changed decoration range `[0,1]` and preserves the rest. Instrumenting CodeMirror's rebuilder captured the exact sequence: ``` EMIT 0..1 point 0..1 block=true → addBlockWidget len=1 ← widget 1, correct point 1..1 block=true → addBlockWidget len=0 ← widget 2 emitted at the boundary with length 0 Γ£ù PRESERVE len=1 incStart=false incEnd=true skip widget from=0 to=1 tileLen=1 ← the real widget 2 (len 1) from the old tree skip line from=0 to=0 ``` When `emit(0,1)` runs `RangeSet.spans` over the changed range, the span iterator fires the `point` callback a **second time** for the adjacent block decoration `[1,2]`, reporting it as a zero-length point (`from==to==1`) because its start side sits exactly on the right boundary of the emitted range. `emit` builds a block-widget tile with `length = to - from = 0`. The resulting tile tree is corrupted: | | before (correct) | after (broken) | |---|---|---| | | widget pos=0 **len=1** | widget pos=0 len=1 | | | widget pos=1 **len=1** | widget pos=1 **len=0** | | | line pos=2 len=0 | line pos=1 len=0 (phantom) | | | | line pos=1 len=0 (phantom) | | **end** | **2** ✓ | **1** ✗ | The second block widget loses its length, so the tile tree's cumulative length stops at 1. **Nothing covers document position 2**, so `coordsAtPos(2)` → `docView.coordsAt(2)` → `DocTile.resolveBlock(2)` finds no tile and throws `Error: No tile at position 2` (`index.js:1929`). ### Trigger conditions (all required) 1. Two **adjacent** block decorations (the boundary between them is what's mis-iterated). 2. An incremental rebuild whose changed range **ends exactly on that boundary** (the `updateDocBlock` edit to the first block). 3. A `coordsAtPos` at the position past the second block, which now has no tile.
6.1 KiB
Owner

I don't think that pile of AI-generated code can really be called a minimal reproduction (I got the actual reproduction down to 30 lines), but sure it does demonstrate the bug. Attached patch should help.

I don't think that pile of AI-generated code can really be called a _minimal_ reproduction (I got the actual reproduction down to 30 lines), but sure it does demonstrate the bug. Attached patch should help.
Author

Thanks for your quick work on this. I really appreciate it!

Thanks for your quick work on this. I really appreciate it!
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 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#1717
No description provided.