Skip to content

Commit fdb5042

Browse files
committed
Count block wrapper margin height as if it is a widget
1 parent 31bf5fa commit fdb5042

File tree

5 files changed

+94
-40
lines changed

5 files changed

+94
-40
lines changed

src/docview.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ export class DocView {
9797
let prevDeco = this.decorations, prevWrappers = this.blockWrappers
9898
this.updateDeco()
9999
let decoDiff = findChangedDeco(prevDeco, this.decorations, update.changes)
100-
if (decoDiff.length) changedRanges = ChangedRange.extendWithRanges(changedRanges, decoDiff, composition?.range)
100+
if (decoDiff.length) changedRanges = ChangedRange.extendWithRanges(changedRanges, decoDiff)
101101
let blockDiff = findChangedWrappers(prevWrappers, this.blockWrappers, update.changes)
102-
if (blockDiff.length) changedRanges = ChangedRange.extendWithRanges(changedRanges, blockDiff, composition?.range)
102+
if (blockDiff.length) changedRanges = ChangedRange.extendWithRanges(changedRanges, blockDiff)
103103
if (composition && !changedRanges.some(r => r.fromA <= composition!.range.fromA && r.toA >= composition!.range.toA))
104104
changedRanges = composition.range.addToSet(changedRanges.slice())
105105

@@ -386,6 +386,7 @@ export class DocView {
386386
let contentWidth = this.view.contentDOM.clientWidth
387387
let isWider = contentWidth > Math.max(this.view.scrollDOM.clientWidth, this.minWidth) + 1
388388
let widest = -1, ltr = this.view.textDirection == Direction.LTR
389+
let spaceAbove = 0
389390
let scan = (tile: DocTile | BlockWrapperTile, pos: number, measureBounds: DOMRect | null) => {
390391
for (let i = 0; i < tile.children.length; i++) {
391392
if (pos > to) break
@@ -394,11 +395,11 @@ export class DocView {
394395
if (end > from) scan(child, pos, child.dom.getBoundingClientRect())
395396
} else if (pos >= from) {
396397
let childRect = (child.dom as HTMLElement).getBoundingClientRect(), {height} = childRect
397-
if (measureBounds) {
398-
if (i == 0) height += childRect.top - measureBounds.top
399-
if (i == tile.children.length - 1) height += measureBounds.bottom - childRect.bottom
400-
}
401-
result.push(height)
398+
if (measureBounds && !i) spaceAbove += childRect.top - measureBounds.top
399+
if (spaceAbove > 0) result.push(-spaceAbove)
400+
result.push(height + spaceAbove)
401+
spaceAbove = 0
402+
if (measureBounds && i == tile.children.length - 1) spaceAbove += measureBounds.bottom - childRect.bottom
402403
if (isWider) {
403404
let last = child.dom.lastChild
404405
let rects = last ? clientRectsFor(last) : []

src/domreader.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ export class DOMReader {
2727
this.findPointBefore(parent, cur)
2828
let oldLen = this.text.length
2929
this.readNode(cur)
30-
let next: Node | null = cur.nextSibling
31-
if (next == end) break
32-
let tile = Tile.get(cur), nextTile = Tile.get(next!)
30+
let tile = Tile.get(cur), next: Node | null = cur.nextSibling
31+
if (next == end) {
32+
if (tile?.breakAfter) this.lineBreak()
33+
break
34+
}
35+
let nextTile = Tile.get(next!)
3336
if ((tile && nextTile ? tile.breakAfter :
3437
(tile ? tile.breakAfter : isBlockElement(cur)) ||
3538
(isBlockElement(next!) && (cur.nodeName != "BR" || tile?.isWidget()) && this.text.length > oldLen)) &&

src/extension.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -371,10 +371,10 @@ export class ChangedRange {
371371
// positions. These pairs are generated in unchanged ranges, so the
372372
// offset between doc A and doc B is the same for their start and
373373
// end points.
374-
static extendWithRanges(diff: readonly ChangedRange[], ranges: number[], separate?: ChangedRange): readonly ChangedRange[] {
374+
static extendWithRanges(diff: readonly ChangedRange[], ranges: number[]): readonly ChangedRange[] {
375375
if (ranges.length == 0) return diff
376376
let result: ChangedRange[] = []
377-
outer: for (let dI = 0, rI = 0, off = 0; rI < ranges.length;) {
377+
outer: for (let dI = 0, rI = 0, off = 0;;) {
378378
let nextD = dI < diff.length ? diff[dI].fromB : 1e9
379379
let nextR = rI < ranges.length ? ranges[rI] : 1e9
380380
let fromB = Math.min(nextD, nextR)
@@ -388,12 +388,6 @@ export class ChangedRange {
388388
toA = Math.max(toA, end + off)
389389
} else if (dI < diff.length && diff[dI].fromB <= toB) {
390390
let next = diff[dI++]
391-
if (next == separate) {
392-
if (fromB < next.fromB) result.push(new ChangedRange(fromA, next.fromA, fromB, next.fromB))
393-
result.push(next)
394-
while (rI < ranges.length && ranges[rI + 1] <= next.toB) rI += 2
395-
continue outer
396-
}
397391
toB = Math.max(toB, next.toB)
398392
toA = Math.max(toA, next.toA)
399393
off = next.toA - next.toB

src/heightmap.ts

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class HeightOracle {
7474
}
7575

7676
// This object is used by `updateHeight` to make DOM measurements
77-
// arrive at the right nides. The `heights` array is a sequence of
77+
// arrive at the right nodes. The `heights` array is a sequence of
7878
// block heights, starting from position `from`.
7979
export class MeasuredHeights {
8080
public index = 0
@@ -98,7 +98,7 @@ export class BlockInfo {
9898
readonly height: number,
9999
/// @internal Weird packed field that holds an array of children
100100
/// for composite blocks, a decoration for block widgets, and a
101-
/// number indicating the amount of widget-create line breaks for
101+
/// number indicating the amount of widget-created line breaks for
102102
/// text blocks.
103103
readonly _content: readonly BlockInfo[] | PointDecoration | number
104104
) {}
@@ -200,7 +200,7 @@ export abstract class HeightMap {
200200
return me.updateHeight(oracle, 0)
201201
}
202202

203-
static empty(): HeightMap { return new HeightMapText(0, 0) }
203+
static empty(): HeightMap { return new HeightMapText(0, 0, 0) }
204204

205205
// nodes uses null values to indicate the position of line breaks.
206206
// There are never line breaks at the start or end of the array, or
@@ -251,24 +251,45 @@ function replace(old: HeightMap, val: HeightMap) {
251251

252252
HeightMap.prototype.size = 1
253253

254+
const SpaceDeco = Decoration.replace({}) as PointDecoration
255+
254256
class HeightMapBlock extends HeightMap {
257+
spaceAbove = 0
258+
255259
constructor(length: number, height: number, readonly deco: PointDecoration | null) { super(length, height) }
256260

257-
blockAt(_height: number, _oracle: HeightOracle, top: number, offset: number) {
258-
return new BlockInfo(offset, this.length, top, this.height, this.deco || 0)
261+
mainBlock(top: number, offset: number) {
262+
return new BlockInfo(offset, this.length, top + this.spaceAbove, this.height - this.spaceAbove, this.deco || 0)
263+
}
264+
265+
blockAt(height: number, _oracle: HeightOracle, top: number, offset: number) {
266+
return this.spaceAbove && height < top + this.spaceAbove ? new BlockInfo(offset, 0, top, this.spaceAbove, SpaceDeco)
267+
: this.mainBlock(top, offset)
259268
}
260269

261270
lineAt(_value: number, _type: QueryType, oracle: HeightOracle, top: number, offset: number) {
262-
return this.blockAt(0, oracle, top, offset)
271+
let main = this.mainBlock(top, offset)
272+
return this.spaceAbove ? this.blockAt(0, oracle, top, offset).join(main) : main
263273
}
264274

265275
forEachLine(from: number, to: number, oracle: HeightOracle, top: number, offset: number, f: (line: BlockInfo) => void) {
266-
if (from <= offset + this.length && to >= offset) f(this.blockAt(0, oracle, top, offset))
276+
if (from <= offset + this.length && to >= offset) f(this.lineAt(0, QueryType.ByPos, oracle, top, offset))
277+
}
278+
279+
setMeasuredHeight(measured: MeasuredHeights) {
280+
let next = measured.heights[measured.index++]
281+
if (next < 0) {
282+
this.spaceAbove = -next
283+
next = measured.heights[measured.index++]
284+
} else {
285+
this.spaceAbove = 0
286+
}
287+
this.setHeight(next)
267288
}
268289

269290
updateHeight(oracle: HeightOracle, offset: number = 0, _force: boolean = false, measured?: MeasuredHeights) {
270291
if (measured && measured.from <= offset && measured.more)
271-
this.setHeight(measured.heights[measured.index++])
292+
this.setMeasuredHeight(measured)
272293
this.outdated = false
273294
return this
274295
}
@@ -281,17 +302,20 @@ class HeightMapText extends HeightMapBlock {
281302
public widgetHeight = 0 // Maximum inline widget height
282303
public breaks = 0 // Number of widget-introduced line breaks on the line
283304

284-
constructor(length: number, height: number) { super(length, height, null) }
305+
constructor(length: number, height: number, above: number) {
306+
super(length, height, null)
307+
this.spaceAbove = above
308+
}
285309

286-
blockAt(_height: number, _oracle: HeightOracle, top: number, offset: number) {
287-
return new BlockInfo(offset, this.length, top, this.height, this.breaks)
310+
mainBlock(top: number, offset: number) {
311+
return new BlockInfo(offset, this.length, top + this.spaceAbove, this.height - this.spaceAbove, this.breaks)
288312
}
289313

290314
replace(_from: number, _to: number, nodes: (HeightMap | null)[]): HeightMap {
291315
let node = nodes[0]
292316
if (nodes.length == 1 && (node instanceof HeightMapText || node instanceof HeightMapGap && (node.flags & Flag.SingleLine)) &&
293317
Math.abs(this.length - node.length) < 10) {
294-
if (node instanceof HeightMapGap) node = new HeightMapText(node.length, this.height)
318+
if (node instanceof HeightMapGap) node = new HeightMapText(node.length, this.height, this.spaceAbove)
295319
else node.height = this.height
296320
if (!this.outdated) node.outdated = false
297321
return node
@@ -301,11 +325,13 @@ class HeightMapText extends HeightMapBlock {
301325
}
302326

303327
updateHeight(oracle: HeightOracle, offset: number = 0, force: boolean = false, measured?: MeasuredHeights) {
304-
if (measured && measured.from <= offset && measured.more)
305-
this.setHeight(measured.heights[measured.index++])
306-
else if (force || this.outdated)
328+
if (measured && measured.from <= offset && measured.more) {
329+
this.setMeasuredHeight(measured)
330+
} else if (force || this.outdated) {
331+
this.spaceAbove = 0
307332
this.setHeight(Math.max(this.widgetHeight, oracle.heightForLine(this.length - this.collapsed)) +
308333
this.breaks * oracle.lineHeight)
334+
}
309335
this.outdated = false
310336
return this
311337
}
@@ -415,10 +441,14 @@ class HeightMapGap extends HeightMap {
415441
while (pos <= end && measured.more) {
416442
let len = oracle.doc.lineAt(pos).length
417443
if (nodes.length) nodes.push(null)
418-
let height = measured.heights[measured.index++]
444+
let height = measured.heights[measured.index++], above = 0
445+
if (height < 0) {
446+
above = -height
447+
height = measured.heights[measured.index++]
448+
}
419449
if (singleHeight == -1) singleHeight = height
420450
else if (Math.abs(height - singleHeight) >= Epsilon) singleHeight = -2
421-
let line = new HeightMapText(len, height)
451+
let line = new HeightMapText(len, height, above)
422452
line.outdated = false
423453
nodes.push(line)
424454
pos += len + 1
@@ -582,7 +612,7 @@ class NodeBuilder implements SpanIterator<Decoration> {
582612
if (last instanceof HeightMapText)
583613
last.length += end - this.pos
584614
else if (end > this.pos || !this.isCovered)
585-
this.nodes.push(new HeightMapText(end - this.pos, -1))
615+
this.nodes.push(new HeightMapText(end - this.pos, -1, 0))
586616
this.writtenTo = end
587617
if (to > end) {
588618
this.nodes.push(null)
@@ -621,7 +651,7 @@ class NodeBuilder implements SpanIterator<Decoration> {
621651
this.nodes.push(null)
622652
}
623653
if (this.pos > from)
624-
this.nodes.push(new HeightMapText(this.pos - from, -1))
654+
this.nodes.push(new HeightMapText(this.pos - from, -1, 0))
625655
this.writtenTo = this.pos
626656
}
627657

@@ -635,7 +665,7 @@ class NodeBuilder implements SpanIterator<Decoration> {
635665
this.enterLine()
636666
let last = this.nodes.length ? this.nodes[this.nodes.length - 1] : null
637667
if (last instanceof HeightMapText) return last
638-
let line = new HeightMapText(0, -1)
668+
let line = new HeightMapText(0, -1, 0)
639669
this.nodes.push(line)
640670
return line
641671
}
@@ -661,7 +691,7 @@ class NodeBuilder implements SpanIterator<Decoration> {
661691
finish(from: number) {
662692
let last = this.nodes.length == 0 ? null : this.nodes[this.nodes.length - 1]
663693
if (this.lineStart > -1 && !(last instanceof HeightMapText) && !this.isCovered)
664-
this.nodes.push(new HeightMapText(0, -1))
694+
this.nodes.push(new HeightMapText(0, -1, 0))
665695
else if (this.writtenTo < this.pos || last == null)
666696
this.nodes.push(this.blankContent(this.writtenTo, this.pos))
667697
let pos = from

test/webtest-draw-decoration.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {EditorView, Decoration, BlockWrapper, DecorationSet, WidgetType, ViewPlugin} from "@codemirror/view"
1+
import {EditorView, Decoration, BlockWrapper, DecorationSet, WidgetType, ViewPlugin, BlockType} from "@codemirror/view"
22
import {tempView, requireFocus} from "./tempview.js"
33
import {EditorSelection, StateEffect, StateField, Range, RangeSet} from "@codemirror/state"
44
import ist from "ist"
@@ -845,12 +845,38 @@ describe("EditorView decoration", () => {
845845
ist(html(cm), `<navigation><div>b</div><section><div>.cf</div></section></navigation>`)
846846
})
847847

848+
it("can handle replacements at end of wrappers", () => {
849+
let cm = wrapEditor("ab\ncd\nef", [section.range(0, 5)])
850+
cm.dispatch({changes: [{from: 4, to: 7}]})
851+
ist(html(cm), `<section><div>ab</div><div>cf</div></section>`)
852+
})
853+
848854
it("can skip large distances correctly", () => {
849855
let cm = tempView("-\n".repeat(12000), [
850856
EditorView.blockWrappers.of(RangeSet.of(section.range(0, 24000))),
851857
EditorView.decorations.of(RangeSet.of(Decoration.replace({}).range(1, 24000 - 1))),
852858
])
853859
ist(html(cm), "<section><div>-<span></span></div><div><br></div></section>")
854860
})
861+
862+
it("represents wrapper padding and borders as ghost widgets", () => {
863+
let cm = wrapEditor("a\nb\nc\nd", [
864+
BlockWrapper.create({tagName: "div", attributes: {style: "border: 2px solid blue; padding: 1px"}}).range(2, 5)
865+
])
866+
let eltTop = cm.elementAtHeight(cm.coordsAtPos(1)!.bottom + 2 - cm.documentTop)
867+
ist(eltTop.type, BlockType.WidgetRange)
868+
ist(eltTop.from, 2)
869+
ist(Math.abs(eltTop.height - 3), 0.1, "<")
870+
let elt2 = cm.elementAtHeight(cm.coordsAtPos(2)!.top + 1 - cm.documentTop)
871+
ist(elt2.type, BlockType.Text)
872+
ist(elt2.from, 2)
873+
ist(Math.abs(elt2.top - eltTop.bottom), 0.1, "<")
874+
let eltBot = cm.elementAtHeight(cm.coordsAtPos(5)!.bottom + 2 - cm.documentTop)
875+
ist(eltBot.type, BlockType.WidgetRange)
876+
let blocks = cm.viewportLineBlocks
877+
ist(blocks.length, 4)
878+
ist(Array.isArray(blocks[1].type))
879+
ist(Array.isArray(blocks[3].type))
880+
})
855881
})
856882
})

0 commit comments

Comments
 (0)