Skip to content

Commit

Permalink
Make sure viewport lines are recomputed when heightmap nodes change type
Browse files Browse the repository at this point in the history
FIX: Fix an issue where `EditorView.viewportLineBlocks` (and thus other things like
the gutter) might be out of date after some kinds of decoration changes.

Issue codemirror/dev#1406
  • Loading branch information
marijnh committed Jul 29, 2024
1 parent 439acaf commit 2527906
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 17 deletions.
36 changes: 23 additions & 13 deletions src/heightmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import {ChangedRange} from "./extension"

const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line", "break-spaces"]

// Used to track, during updateHeight, if any actual heights changed
export let heightChangeFlag = false

export function clearHeightChangeFlag() { heightChangeFlag = false }

export class HeightOracle {
doc: Text = Text.empty
heightSamples: {[key: number]: boolean} = {}
lineHeight: number = 14 // The height of an entire line (line-height)
charWidth: number = 7
textHeight: number = 14 // The height of the actual font (font-size)
lineLength: number = 30
// Used to track, during updateHeight, if any actual heights changed
heightChanged: boolean = false

constructor(public lineWrapping: boolean) {}

Expand Down Expand Up @@ -158,9 +161,9 @@ export abstract class HeightMap {
abstract updateHeight(oracle: HeightOracle, offset?: number, force?: boolean, measured?: MeasuredHeights): HeightMap
abstract toString(): void

setHeight(oracle: HeightOracle, height: number) {
setHeight(height: number) {
if (this.height != height) {
if (Math.abs(this.height - height) > Epsilon) oracle.heightChanged = true
if (Math.abs(this.height - height) > Epsilon) heightChangeFlag = true
this.height = height
}
}
Expand Down Expand Up @@ -192,7 +195,7 @@ export abstract class HeightMap {
}
fromB += start.from - fromA; fromA = start.from
let nodes = NodeBuilder.build(oracle.setDoc(doc), decorations, fromB, toB)
me = me.replace(fromA, toA, nodes)
me = replace(me, me.replace(fromA, toA, nodes))
}
return me.updateHeight(oracle, 0)
}
Expand Down Expand Up @@ -240,6 +243,12 @@ export abstract class HeightMap {
}
}

function replace(old: HeightMap, val: HeightMap) {
if (old == val) return old
if (old.constructor != val.constructor) heightChangeFlag = true
return val
}

HeightMap.prototype.size = 1

class HeightMapBlock extends HeightMap {
Expand All @@ -259,7 +268,7 @@ class HeightMapBlock extends HeightMap {

updateHeight(oracle: HeightOracle, offset: number = 0, _force: boolean = false, measured?: MeasuredHeights) {
if (measured && measured.from <= offset && measured.more)
this.setHeight(oracle, measured.heights[measured.index++])
this.setHeight(measured.heights[measured.index++])
this.outdated = false
return this
}
Expand Down Expand Up @@ -293,9 +302,9 @@ class HeightMapText extends HeightMapBlock {

updateHeight(oracle: HeightOracle, offset: number = 0, force: boolean = false, measured?: MeasuredHeights) {
if (measured && measured.from <= offset && measured.more)
this.setHeight(oracle, measured.heights[measured.index++])
this.setHeight(measured.heights[measured.index++])
else if (force || this.outdated)
this.setHeight(oracle, Math.max(this.widgetHeight, oracle.heightForLine(this.length - this.collapsed)) +
this.setHeight(Math.max(this.widgetHeight, oracle.heightForLine(this.length - this.collapsed)) +
this.breaks * oracle.lineHeight)
this.outdated = false
return this
Expand Down Expand Up @@ -418,10 +427,10 @@ class HeightMapGap extends HeightMap {
let result = HeightMap.of(nodes)
if (singleHeight < 0 || Math.abs(result.height - this.height) >= Epsilon ||
Math.abs(singleHeight - this.heightMetrics(oracle, offset).perLine) >= Epsilon)
oracle.heightChanged = true
return result
heightChangeFlag = true
return replace(this, result)
} else if (force || this.outdated) {
this.setHeight(oracle, oracle.heightForGap(offset, offset + this.length))
this.setHeight(oracle.heightForGap(offset, offset + this.length))
this.outdated = false
}
return this
Expand Down Expand Up @@ -514,8 +523,9 @@ class HeightMapBranch extends HeightMap {
balanced(left: HeightMap, right: HeightMap): HeightMap {
if (left.size > 2 * right.size || right.size > 2 * left.size)
return HeightMap.of(this.break ? [left, null, right] : [left, right])
this.left = left; this.right = right
this.height = left.height + right.height
this.left = replace(this.left, left)
this.right = replace(this.right, right)
this.setHeight(left.height + right.height)
this.outdated = left.outdated || right.outdated
this.size = left.size + right.size
this.length = left.length + this.break + right.length
Expand Down
11 changes: 7 additions & 4 deletions src/viewstate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Text, EditorState, ChangeSet, ChangeDesc, RangeSet, EditorSelection} from "@codemirror/state"
import {Rect, isScrolledToBottom, getScale} from "./dom"
import {HeightMap, HeightOracle, BlockInfo, MeasuredHeights, QueryType, heightRelevantDecoChanges} from "./heightmap"
import {HeightMap, HeightOracle, BlockInfo, MeasuredHeights, QueryType, heightRelevantDecoChanges,
clearHeightChangeFlag, heightChangeFlag} from "./heightmap"
import {decorations, ViewUpdate, UpdateFlag, ChangedRange, ScrollTarget, nativeSelectionHidden,
contentAttributes} from "./extension"
import {WidgetType, Decoration, DecorationSet, BlockType} from "./decoration"
Expand Down Expand Up @@ -216,9 +217,11 @@ export class ViewState {
prevDeco, this.stateDeco, update ? update.changes : ChangeSet.empty(this.state.doc.length)))
let prevHeight = this.heightMap.height
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollTop)
clearHeightChangeFlag()
this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc,
this.heightOracle.setDoc(this.state.doc), heightChanges)
if (this.heightMap.height != prevHeight) update.flags |= UpdateFlag.Height
if (this.heightMap.height != prevHeight || heightChangeFlag)
update.flags |= UpdateFlag.Height
if (scrollAnchor) {
this.scrollAnchorPos = update.changes.mapPos(scrollAnchor.from, -1)
this.scrollAnchorHeight = scrollAnchor.top
Expand Down Expand Up @@ -325,15 +328,15 @@ export class ViewState {
if (dTop > 0 && dBottom > 0) bias = Math.max(dTop, dBottom)
else if (dTop < 0 && dBottom < 0) bias = Math.min(dTop, dBottom)

oracle.heightChanged = false
clearHeightChangeFlag()
for (let vp of this.viewports) {
let heights = vp.from == this.viewport.from ? lineHeights : view.docView.measureVisibleLineHeights(vp)
this.heightMap = (
refresh ? HeightMap.empty().applyChanges(this.stateDeco, Text.empty, this.heightOracle,
[new ChangedRange(0, 0, 0, view.state.doc.length)]) : this.heightMap
).updateHeight(oracle, 0, refresh, new MeasuredHeights(vp.from, heights))
}
if (oracle.heightChanged) result |= UpdateFlag.Height
if (heightChangeFlag) result |= UpdateFlag.Height
}

let viewportChange = !this.viewportIsAppropriate(this.viewport, bias) ||
Expand Down

0 comments on commit 2527906

Please sign in to comment.