From 25279067663f1ce361d0b83a8c425d4e84b27d35 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Mon, 29 Jul 2024 12:35:17 +0200 Subject: [PATCH] Make sure viewport lines are recomputed when heightmap nodes change type 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 https://github.com/codemirror/dev/issues/1406 --- src/heightmap.ts | 36 +++++++++++++++++++++++------------- src/viewstate.ts | 11 +++++++---- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/heightmap.ts b/src/heightmap.ts index 2d6dd9b..ea8d641 100644 --- a/src/heightmap.ts +++ b/src/heightmap.ts @@ -4,6 +4,11 @@ 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} = {} @@ -11,8 +16,6 @@ export class HeightOracle { 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) {} @@ -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 } } @@ -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) } @@ -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 { @@ -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 } @@ -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 @@ -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 @@ -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 diff --git a/src/viewstate.ts b/src/viewstate.ts index d77edee..2c041f3 100644 --- a/src/viewstate.ts +++ b/src/viewstate.ts @@ -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" @@ -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 @@ -325,7 +328,7 @@ 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 = ( @@ -333,7 +336,7 @@ export class ViewState { [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) ||