Conflict between block widget and folding?

First off, thanks for a great editor Marijn. Assuming I release my project, I will contribute in the future when I am in a position to do so.

I have encountered an issue when using a block widget and code folding. Essentially when I have block widgets inserting at code folding points (positioned above them), the folding arrows get duplicated sometimes. It almost appears non-deterministic, like there’s a timing interaction between the background parser and ui but it occurs even when not making any changes to the document. I was able to cut out my parser and all the other customizations to reproduce the issue I am having here:

Minimal Code Sandbox Reproduction

You’ll likely have to open/close the fold points a few times and/or add text to the document to see the issue (I’m using chrome on a mac in case that is important as well). The issue disappears when line 87 is commented out, removing the block widget extension.

I did some digging, and it looks like line 268 in fold.ts might be the high-level source of the issue where the markers are duplicated:

for (let line of view.viewportLineBlocks) {

viewportLineBlocks appears to return multiple BlockInfo entries for the ranges containing the block widget. That led me to look at HeightMap but I got overwhelmed with the internals at that point. I had to do some experimenting how to use block widgets so I am not confident in my implementation, hence making the post here first rather than submitting a github issue. I suspect I am missing something in my widget implementation that is throwing the line calculations off, like estimated height, but I have not been able to figure it out.

If you could have a look at the sandbox and let me know what I am missing or if I should submit an github issue I would appreciate it. If it is an issue, I’m happy to also take a stab at fixing it if you can identify it and point me in the right direction. Thank you!

I did some digging into the internals of heightmap.ts and while I am not fully confident, I think there is a very subtle corner-case bug somewhere within HeightMapBranch.lineAt() and HeightMapBranch.forEachLine() which results in duplicate BlockInfo objects for some lines. You can see it in this dump of viewportLines (a BlockInfo array)

Entries 4, 5, and 6 contain duplicate ranges. I assume there should be unique entries and no overlaps (excluding 0-length blocks like the widgets). I think the issue is related to the following code in HeightMapBranch.forEachLine() which constructs the mid object first with lineAt, then visits left and right branches, processing the mid object in between the two calls:

let mid = this.lineAt(rightOffset, QueryType.ByPos, doc, top, offset)
if (from < mid.from) this.left.forEachLine(from, mid.from - 1, doc, top, offset, f)
if (mid.to >= from && mid.from <= to) f(mid)
if (to > mid.to) this.right.forEachLine(mid.to + 1, to, doc, rightTop, rightOffset, f)

The call to lineAt() to construct the mid object visits 2 HeightMap nodes and joins them (entry 5 in the array). The calls to lineAt() on the left and right branches here seem to visit and duplicate the ranges (entries 4 and 6) already joined in entry 5. These additional entries then result in the folding extension duplicating the folding controls.

I suspect there is/are some very subtle boundary checks that overlap and/or a 0 length block, like a widget, is throwing off a calculation somewhere (like a ‘<’ should be ‘<=’ or a +1/-1 adjustment is missing). But like I said, I’m not fully confident. The code must have been challenging to write and is subsequently challenging to follow. I hope this is helpful. I will keep looking when I have time.

That was indeed a bug near that logic. The patch below should help.

Fantastic. I was able to verify the fix on my end. Thank you Marijn!