299 lines
13 KiB
TypeScript
299 lines
13 KiB
TypeScript
|
import {Decoration, WidgetType, BlockType, BlockInfo, __test} from "@codemirror/next/view"
|
||
|
import {Text} from "@codemirror/next/text"
|
||
|
import {ChangedRange} from "@codemirror/next/state"
|
||
|
import ist from "ist"
|
||
|
|
||
|
const {HeightMap, HeightOracle, MeasuredHeights, QueryType} = __test
|
||
|
|
||
|
const byH = QueryType.ByHeight, byP = QueryType.ByPos
|
||
|
|
||
|
function o(doc: Text) {
|
||
|
return (new HeightOracle).setDoc(doc)
|
||
|
}
|
||
|
|
||
|
describe("HeightMap", () => {
|
||
|
it("starts empty", () => {
|
||
|
let empty = HeightMap.empty()
|
||
|
ist(empty.length, 0)
|
||
|
ist(empty.size, 1)
|
||
|
})
|
||
|
|
||
|
function mk(text: Text, deco: any = []) {
|
||
|
return HeightMap.empty().applyChanges([Decoration.set(deco)], Text.empty, o(text),
|
||
|
[new ChangedRange(0, 0, 0, text.length)])
|
||
|
}
|
||
|
function doc(... lineLen: number[]) {
|
||
|
let text = lineLen.map(len => "x".repeat(len))
|
||
|
return Text.of(text)
|
||
|
}
|
||
|
|
||
|
it("grows to match the document", () => {
|
||
|
ist(mk(doc(10, 10, 8)).length, 30)
|
||
|
})
|
||
|
|
||
|
class MyWidget extends WidgetType<number> {
|
||
|
toDOM() { return document.body }
|
||
|
get estimatedHeight() { return this.value }
|
||
|
}
|
||
|
class NoHeightWidget extends WidgetType<null> {
|
||
|
toDOM() { return document.body }
|
||
|
}
|
||
|
|
||
|
it("separates lines with decorations on them", () => {
|
||
|
let map = mk(doc(10, 10, 20, 5),
|
||
|
[Decoration.widget({widget: new MyWidget(20)}).range(5),
|
||
|
Decoration.replace({}).range(25, 46)])
|
||
|
ist(map.length, 48)
|
||
|
ist(map.toString(), "line(10:20) gap(10) line(26-21)")
|
||
|
})
|
||
|
|
||
|
it("ignores irrelevant decorations", () => {
|
||
|
let map = mk(doc(10, 10, 20, 5),
|
||
|
[Decoration.widget({widget: new NoHeightWidget(null)}).range(5),
|
||
|
Decoration.mark({class: "ahah"}).range(25, 46)])
|
||
|
ist(map.length, 48)
|
||
|
ist(map.toString(), "gap(48)")
|
||
|
})
|
||
|
|
||
|
it("drops decorations from the tree when they are deleted", () => {
|
||
|
let text = doc(20)
|
||
|
let map = mk(text, [Decoration.widget({widget: new MyWidget(20)}).range(5)])
|
||
|
ist(map.toString(), "line(20:20)")
|
||
|
map = map.applyChanges([], text, o(text), [new ChangedRange(5, 5, 5, 5)])
|
||
|
ist(map.toString(), "line(20)")
|
||
|
})
|
||
|
|
||
|
it("updates the length of replaced decorations for changes", () => {
|
||
|
let text = doc(20)
|
||
|
let map = mk(text, [Decoration.replace({}).range(5, 15)])
|
||
|
map = map.applyChanges([Decoration.set(Decoration.replace({}).range(5, 10))], text, o(text.replace(7, 12, [""])),
|
||
|
[new ChangedRange(7, 12, 7, 7)])
|
||
|
ist(map.toString(), "line(15-5)")
|
||
|
})
|
||
|
|
||
|
it("stores information about block widgets", () => {
|
||
|
let text = doc(3, 3, 3), oracle = o(text)
|
||
|
let map = mk(text, [Decoration.widget({widget: new MyWidget(10), side: -1, block: true}).range(0),
|
||
|
Decoration.widget({widget: new MyWidget(13), side: -1, block: true}).range(0),
|
||
|
Decoration.widget({widget: new MyWidget(5), side: 1, block: true}).range(3)])
|
||
|
ist(map.toString(), "block(0)-block(0)-line(3)-block(0) gap(7)")
|
||
|
ist(map.height, 28 + 3 * oracle.lineHeight)
|
||
|
let {type} = map.lineAt(0, byP, text, 0, 0)
|
||
|
ist((type as BlockInfo[]).map(b => b.height).join(), [10, 13, oracle.lineHeight, 5].join())
|
||
|
ist(map.lineAt(4, byP, text, 0, 0).top, 28 + oracle.lineHeight)
|
||
|
map = map.updateHeight(oracle, 0, false, new MeasuredHeights(0, [8, 12, 10, 20, 40, 20]))
|
||
|
ist(map.toString(), "block(0)-block(0)-line(3)-block(0) line(3) line(3)")
|
||
|
ist(map.height, 110)
|
||
|
})
|
||
|
|
||
|
it("stores information about block ranges", () => {
|
||
|
let text = doc(3, 3, 3, 3, 3, 3)
|
||
|
let map = mk(text, [Decoration.widget({widget: new MyWidget(10), side: -1, block: true}).range(4),
|
||
|
Decoration.replace({widget: new MyWidget(40), block: true}).range(4, 11),
|
||
|
Decoration.widget({widget: new MyWidget(15), side: 1, block: true}).range(11),
|
||
|
// This one covers the block widgets around it (due to being inclusive)
|
||
|
Decoration.replace({widget: new MyWidget(50), block: true, inclusive: true}).range(16, 19),
|
||
|
Decoration.widget({widget: new MyWidget(20), side: -1, block: true}).range(16),
|
||
|
Decoration.widget({widget: new MyWidget(10), side: 1, block: true}).range(19)])
|
||
|
ist(map.toString(), "gap(3) block(0)-block(7)-block(0) gap(3) block(3) gap(3)")
|
||
|
map = map.updateHeight(o(text), 0, false, new MeasuredHeights(4, [5, 5, 5, 10, 5]))
|
||
|
ist(map.height, 2 * o(text).lineHeight + 30)
|
||
|
})
|
||
|
|
||
|
it("handles empty lines correctly", () => {
|
||
|
let text = doc(0, 0, 0, 0, 0)
|
||
|
let map = mk(text, [Decoration.widget({widget: new MyWidget(10), side: -1, block: true}).range(1),
|
||
|
Decoration.replace({widget: new MyWidget(20), block: true}).range(2, 2),
|
||
|
Decoration.widget({widget: new MyWidget(30), side: 1, block: true}).range(3)])
|
||
|
ist(map.toString(), "gap(0) block(0)-line(0) block(0) line(0)-block(0) gap(0)")
|
||
|
map = map.applyChanges([], text, o(text.replace(1, 3, ["y"])), [new ChangedRange(1, 3, 1, 2)])
|
||
|
ist(map.toString(), "gap(3)")
|
||
|
})
|
||
|
|
||
|
it("joins ranges", () => {
|
||
|
let text = doc(10, 10, 10, 10)
|
||
|
let map = mk(text, [Decoration.replace({}).range(16, 27)])
|
||
|
ist(map.toString(), "gap(10) line(21-11) gap(10)")
|
||
|
map = map.applyChanges([], text, o(text.replace(5, 38, ["yyy"])), [new ChangedRange(5, 38, 5, 8)])
|
||
|
ist(map.toString(), "gap(13)")
|
||
|
})
|
||
|
|
||
|
it("joins lines", () => {
|
||
|
let text = doc(10, 10, 10)
|
||
|
let map = mk(text, [Decoration.replace({}).range(2, 5),
|
||
|
Decoration.widget({widget: new MyWidget(20)}).range(24)])
|
||
|
ist(map.toString(), "line(10-3) gap(10) line(10:20)")
|
||
|
map = map.applyChanges([
|
||
|
Decoration.set([Decoration.replace({}).range(2, 5),
|
||
|
Decoration.widget({widget: new MyWidget(20)}).range(12)])
|
||
|
], text, o(text.replace(10, 22, [""])), [new ChangedRange(10, 22, 10, 10)])
|
||
|
ist(map.toString(), "line(20-3:20)")
|
||
|
})
|
||
|
|
||
|
it("materializes lines for measured heights", () => {
|
||
|
let text = doc(10, 10, 10, 10), oracle = o(text)
|
||
|
let map = mk(text, [])
|
||
|
.updateHeight(oracle, 0, false, new MeasuredHeights(11, [28, 14, 5]))
|
||
|
ist(map.toString(), "gap(10) line(10) line(10) line(10)")
|
||
|
ist(map.height, 61)
|
||
|
})
|
||
|
|
||
|
it("can update lines across the tree", () => {
|
||
|
let text = doc(...new Array(100).fill(10)), oracle = o(text)
|
||
|
let map = mk(text).updateHeight(oracle, 0, false, new MeasuredHeights(0, new Array(100).fill(12)))
|
||
|
ist(map.height, 1200)
|
||
|
ist(map.size, 100)
|
||
|
map = map.updateHeight(oracle, 0, false, new MeasuredHeights(55, new Array(90).fill(10)))
|
||
|
ist(map.height, 1020)
|
||
|
ist(map.size, 100)
|
||
|
})
|
||
|
|
||
|
function depth(heightMap: any): number {
|
||
|
let {left, right} = heightMap
|
||
|
return left ? Math.max(depth(left), depth(right)) + 1 : 1
|
||
|
}
|
||
|
|
||
|
it("balances a big tree", () => {
|
||
|
let text = doc(...new Array(100).fill(30)), oracle = o(text)
|
||
|
let map = mk(text).updateHeight(oracle, 0, false, new MeasuredHeights(0, new Array(100).fill(15)))
|
||
|
ist(map.height, 1500)
|
||
|
ist(map.size, 100)
|
||
|
ist(depth(map), 9, "<")
|
||
|
let text2 = text.replace(0, 31 * 80, [""])
|
||
|
map = map.applyChanges([], text, o(text2), [new ChangedRange(0, 31 * 80, 0, 0)])
|
||
|
ist(map.size, 20)
|
||
|
ist(depth(map), 7, "<")
|
||
|
let len = text2.length
|
||
|
let text3 = text2.replace(len, len, "\nfoo".repeat(200).split("\n"))
|
||
|
map = map.applyChanges([], text2, o(text3), [new ChangedRange(len, len, len, len + 800)])
|
||
|
map = map.updateHeight(oracle.setDoc(text3), 0, false, new MeasuredHeights(len + 1, new Array(200).fill(10)))
|
||
|
ist(map.size, 220)
|
||
|
ist(depth(map), 12, "<")
|
||
|
})
|
||
|
|
||
|
it("can handle inserting a line break", () => {
|
||
|
let text = doc(3, 3, 3), oracle = o(text)
|
||
|
let map = mk(text).updateHeight(oracle, 0, false, new MeasuredHeights(0, [10, 10, 10]))
|
||
|
ist(map.size, 3)
|
||
|
let text2 = text.replace(3, 3, ["", ""])
|
||
|
map = map.applyChanges([], text, oracle.setDoc(text2), [new ChangedRange(3, 3, 3, 4)])
|
||
|
.updateHeight(oracle, 0, false, new MeasuredHeights(0, [10, 10, 10, 10]))
|
||
|
ist(map.size, 4)
|
||
|
ist(map.height, 40)
|
||
|
})
|
||
|
|
||
|
it("can handle insertion in the middle of a line", () => {
|
||
|
let text = doc(3, 3, 3), oracle = o(text)
|
||
|
let map = mk(text).updateHeight(oracle, 0, false, new MeasuredHeights(0, [10, 10, 10]))
|
||
|
let text2 = text.replace(5, 5, ["foo", "bar", "baz", "bug"])
|
||
|
map = map.applyChanges([], text, o(text2), [new ChangedRange(5, 5, 5, 20)])
|
||
|
.updateHeight(o(text2), 0, false, new MeasuredHeights(0, [10, 10, 10, 10, 10, 10]))
|
||
|
ist(map.size, 6)
|
||
|
ist(map.height, 60)
|
||
|
})
|
||
|
|
||
|
describe("blockAt", () => {
|
||
|
it("finds blocks in a gap", () => {
|
||
|
let text = doc(3, 3, 3, 3, 3), map = mk(text)
|
||
|
let block1 = map.blockAt(0, text, 0, 0)
|
||
|
ist(block1.from, 0); ist(block1.to, 3)
|
||
|
ist(block1.top, 0); ist(block1.bottom, 0, ">")
|
||
|
ist(block1.type, BlockType.Text)
|
||
|
let block2 = map.blockAt(block1.bottom + 1, text, 0, 0)
|
||
|
ist(block2.from, 4); ist(block2.to, 7)
|
||
|
ist(block2.top, block1.bottom); ist(block2.bottom, block1.bottom, ">")
|
||
|
let block3 = map.blockAt(1e9, text, 0, 0)
|
||
|
ist(block3.from, 16); ist(block3.to, 19)
|
||
|
ist(block3.bottom, map.height)
|
||
|
})
|
||
|
|
||
|
it("finds blocks in lines", () => {
|
||
|
let text = doc(3, 3, 3, 3), map = mk(text).updateHeight(o(text), 0, false, new MeasuredHeights(0, [10, 20, 10, 30]))
|
||
|
let block1 = map.blockAt(-100, text, 0, 0)
|
||
|
ist(block1.from, 0); ist(block1.to, 3)
|
||
|
ist(block1.top, 0); ist(block1.bottom, 10)
|
||
|
ist(block1.type, BlockType.Text)
|
||
|
let block2 = map.blockAt(39, text, 0, 0)
|
||
|
ist(block2.from, 8); ist(block2.to, 11)
|
||
|
ist(block2.top, 30); ist(block2.bottom, 40)
|
||
|
let block3 = map.blockAt(77, text, 0, 0)
|
||
|
ist(block3.from, 12); ist(block3.to, 15)
|
||
|
ist(block3.top, 40); ist(block3.bottom, 70)
|
||
|
})
|
||
|
|
||
|
it("finds widget blocks", () => {
|
||
|
let text = doc(3, 3, 3, 3)
|
||
|
let map = mk(text, [Decoration.widget({widget: new MyWidget(100), block: true, side: -1}).range(4),
|
||
|
Decoration.replace({widget: new MyWidget(30), block: true}).range(8, 11),
|
||
|
Decoration.widget({widget: new MyWidget(0), block: true, side: 1}).range(15)])
|
||
|
let block1 = map.blockAt(0, text, 0, 0)
|
||
|
ist(block1.from, 0); ist(block1.to, 3)
|
||
|
let block2 = map.blockAt(block1.height + 1, text, 0, 0)
|
||
|
ist(block2.from, 4); ist(block2.to, 4)
|
||
|
ist(block2.top, block1.height); ist(block2.height, 100)
|
||
|
ist(block2.type, BlockType.WidgetBefore)
|
||
|
let top3 = block2.bottom + block1.height
|
||
|
let block3 = map.blockAt(top3 + 10, text, 0, 0)
|
||
|
ist(block3.from, 8); ist(block3.to, 11)
|
||
|
ist(block3.top, top3); ist(block3.height, 30)
|
||
|
ist(block3.type, BlockType.WidgetRange)
|
||
|
let block4 = map.blockAt(block3.bottom + block1.height, text, 0, 0)
|
||
|
ist(block4.type, BlockType.WidgetAfter, "!=")
|
||
|
})
|
||
|
})
|
||
|
|
||
|
function eqBlock(a: BlockInfo, b: BlockInfo) {
|
||
|
return a.from == b.from && a.to == b.to && a.top == b.top && a.bottom == b.bottom
|
||
|
}
|
||
|
|
||
|
describe("lineAt", () => {
|
||
|
it("finds lines in gaps", () => {
|
||
|
let text = doc(3, 3, 3, 3), map = mk(text)
|
||
|
let line1 = map.lineAt(0, byP, text, 0, 0)
|
||
|
ist(line1.from, 0); ist(line1.to, 3)
|
||
|
ist(line1.top, 0)
|
||
|
ist(map.lineAt(0, byH, text, 0, 0), line1, eqBlock)
|
||
|
let line2 = map.lineAt(line1.to + 1, byP, text, 0, 0)
|
||
|
ist(line2.from, 4); ist(line2.to, 7)
|
||
|
ist(line2.top, line1.bottom)
|
||
|
ist(map.lineAt(line1.bottom + 1, byH, text, 0, 0), line2, eqBlock)
|
||
|
let line3 = map.lineAt(15, byP, text, 0, 0)
|
||
|
ist(line3.from, 12); ist(line3.to, 15)
|
||
|
ist(line3.bottom, map.height)
|
||
|
ist(map.lineAt(1e9, byH, text, 0, 0), line3, eqBlock)
|
||
|
})
|
||
|
|
||
|
it("finds lines in lines", () => {
|
||
|
let text = doc(3, 3, 3, 3), map = mk(text).updateHeight(o(text), 0, false, new MeasuredHeights(0, [10, 10, 20, 10]))
|
||
|
let line1 = map.lineAt(0, byP, text, 0, 0)
|
||
|
ist(line1.from, 0); ist(line1.to, 3)
|
||
|
ist(line1.top, 0); ist(line1.bottom, 10)
|
||
|
ist(map.lineAt(9, byH, text, 0, 0), line1, eqBlock)
|
||
|
let line2 = map.lineAt(9, byP, text, 0, 0)
|
||
|
ist(line2.from, 8); ist(line2.to, 11)
|
||
|
ist(line2.top, 20); ist(line2.bottom, 40)
|
||
|
ist(map.lineAt(39, byH, text, 0, 0), line2, eqBlock)
|
||
|
})
|
||
|
|
||
|
it("includes adjacent widgets in lines", () => {
|
||
|
let text = doc(3, 3, 3, 3)
|
||
|
let map = mk(text, [Decoration.widget({widget: new MyWidget(100), block: true, side: -1}).range(4),
|
||
|
Decoration.replace({widget: new MyWidget(30), block: true}).range(7, 8),
|
||
|
Decoration.widget({widget: new MyWidget(0), block: true, side: 1}).range(15)])
|
||
|
let line1 = map.lineAt(4, byP, text, 0, 0)
|
||
|
ist(line1.from, 4); ist(line1.to, 11)
|
||
|
ist((line1.type as any[]).length, 4)
|
||
|
ist(map.lineAt(line1.top + 1, byH, text, 0, 0), line1, eqBlock)
|
||
|
ist(map.lineAt(line1.bottom - 1, byH, text, 0, 0), line1, eqBlock)
|
||
|
ist(map.lineAt(line1.top + line1.height / 2, byH, text, 0, 0), line1, eqBlock)
|
||
|
ist(map.lineAt(5, byP, text, 0, 0), line1, eqBlock)
|
||
|
ist(map.lineAt(7, byP, text, 0, 0), line1, eqBlock)
|
||
|
ist(map.lineAt(11, byP, text, 0, 0), line1, eqBlock)
|
||
|
let line2 = map.lineAt(map.height, byH, text, 0, 0)
|
||
|
ist(line2.from, 12); ist(line2.to, 15)
|
||
|
ist((line2.type as any[]).length!, 2)
|
||
|
ist(map.lineAt(line2.top + 1, byH, text, 0, 0), line2, eqBlock)
|
||
|
})
|
||
|
})
|
||
|
})
|