168 lines
5.2 KiB
JavaScript
168 lines
5.2 KiB
JavaScript
|
import { cleanUpLine } from "../line/line_data.js"
|
||
|
import { indexOf } from "../util/misc.js"
|
||
|
import { signalLater } from "../util/operation_group.js"
|
||
|
|
||
|
// The document is represented as a BTree consisting of leaves, with
|
||
|
// chunk of lines in them, and branches, with up to ten leaves or
|
||
|
// other branch nodes below them. The top node is always a branch
|
||
|
// node, and is the document object itself (meaning it has
|
||
|
// additional methods and properties).
|
||
|
//
|
||
|
// All nodes have parent links. The tree is used both to go from
|
||
|
// line numbers to line objects, and to go from objects to numbers.
|
||
|
// It also indexes by height, and is used to convert between height
|
||
|
// and line object, and to find the total height of the document.
|
||
|
//
|
||
|
// See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html
|
||
|
|
||
|
export function LeafChunk(lines) {
|
||
|
this.lines = lines
|
||
|
this.parent = null
|
||
|
let height = 0
|
||
|
for (let i = 0; i < lines.length; ++i) {
|
||
|
lines[i].parent = this
|
||
|
height += lines[i].height
|
||
|
}
|
||
|
this.height = height
|
||
|
}
|
||
|
|
||
|
LeafChunk.prototype = {
|
||
|
chunkSize() { return this.lines.length },
|
||
|
|
||
|
// Remove the n lines at offset 'at'.
|
||
|
removeInner(at, n) {
|
||
|
for (let i = at, e = at + n; i < e; ++i) {
|
||
|
let line = this.lines[i]
|
||
|
this.height -= line.height
|
||
|
cleanUpLine(line)
|
||
|
signalLater(line, "delete")
|
||
|
}
|
||
|
this.lines.splice(at, n)
|
||
|
},
|
||
|
|
||
|
// Helper used to collapse a small branch into a single leaf.
|
||
|
collapse(lines) {
|
||
|
lines.push.apply(lines, this.lines)
|
||
|
},
|
||
|
|
||
|
// Insert the given array of lines at offset 'at', count them as
|
||
|
// having the given height.
|
||
|
insertInner(at, lines, height) {
|
||
|
this.height += height
|
||
|
this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at))
|
||
|
for (let i = 0; i < lines.length; ++i) lines[i].parent = this
|
||
|
},
|
||
|
|
||
|
// Used to iterate over a part of the tree.
|
||
|
iterN(at, n, op) {
|
||
|
for (let e = at + n; at < e; ++at)
|
||
|
if (op(this.lines[at])) return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function BranchChunk(children) {
|
||
|
this.children = children
|
||
|
let size = 0, height = 0
|
||
|
for (let i = 0; i < children.length; ++i) {
|
||
|
let ch = children[i]
|
||
|
size += ch.chunkSize(); height += ch.height
|
||
|
ch.parent = this
|
||
|
}
|
||
|
this.size = size
|
||
|
this.height = height
|
||
|
this.parent = null
|
||
|
}
|
||
|
|
||
|
BranchChunk.prototype = {
|
||
|
chunkSize() { return this.size },
|
||
|
|
||
|
removeInner(at, n) {
|
||
|
this.size -= n
|
||
|
for (let i = 0; i < this.children.length; ++i) {
|
||
|
let child = this.children[i], sz = child.chunkSize()
|
||
|
if (at < sz) {
|
||
|
let rm = Math.min(n, sz - at), oldHeight = child.height
|
||
|
child.removeInner(at, rm)
|
||
|
this.height -= oldHeight - child.height
|
||
|
if (sz == rm) { this.children.splice(i--, 1); child.parent = null }
|
||
|
if ((n -= rm) == 0) break
|
||
|
at = 0
|
||
|
} else at -= sz
|
||
|
}
|
||
|
// If the result is smaller than 25 lines, ensure that it is a
|
||
|
// single leaf node.
|
||
|
if (this.size - n < 25 &&
|
||
|
(this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) {
|
||
|
let lines = []
|
||
|
this.collapse(lines)
|
||
|
this.children = [new LeafChunk(lines)]
|
||
|
this.children[0].parent = this
|
||
|
}
|
||
|
},
|
||
|
|
||
|
collapse(lines) {
|
||
|
for (let i = 0; i < this.children.length; ++i) this.children[i].collapse(lines)
|
||
|
},
|
||
|
|
||
|
insertInner(at, lines, height) {
|
||
|
this.size += lines.length
|
||
|
this.height += height
|
||
|
for (let i = 0; i < this.children.length; ++i) {
|
||
|
let child = this.children[i], sz = child.chunkSize()
|
||
|
if (at <= sz) {
|
||
|
child.insertInner(at, lines, height)
|
||
|
if (child.lines && child.lines.length > 50) {
|
||
|
// To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced.
|
||
|
// Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest.
|
||
|
let remaining = child.lines.length % 25 + 25
|
||
|
for (let pos = remaining; pos < child.lines.length;) {
|
||
|
let leaf = new LeafChunk(child.lines.slice(pos, pos += 25))
|
||
|
child.height -= leaf.height
|
||
|
this.children.splice(++i, 0, leaf)
|
||
|
leaf.parent = this
|
||
|
}
|
||
|
child.lines = child.lines.slice(0, remaining)
|
||
|
this.maybeSpill()
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
at -= sz
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// When a node has grown, check whether it should be split.
|
||
|
maybeSpill() {
|
||
|
if (this.children.length <= 10) return
|
||
|
let me = this
|
||
|
do {
|
||
|
let spilled = me.children.splice(me.children.length - 5, 5)
|
||
|
let sibling = new BranchChunk(spilled)
|
||
|
if (!me.parent) { // Become the parent node
|
||
|
let copy = new BranchChunk(me.children)
|
||
|
copy.parent = me
|
||
|
me.children = [copy, sibling]
|
||
|
me = copy
|
||
|
} else {
|
||
|
me.size -= sibling.size
|
||
|
me.height -= sibling.height
|
||
|
let myIndex = indexOf(me.parent.children, me)
|
||
|
me.parent.children.splice(myIndex + 1, 0, sibling)
|
||
|
}
|
||
|
sibling.parent = me.parent
|
||
|
} while (me.children.length > 10)
|
||
|
me.parent.maybeSpill()
|
||
|
},
|
||
|
|
||
|
iterN(at, n, op) {
|
||
|
for (let i = 0; i < this.children.length; ++i) {
|
||
|
let child = this.children[i], sz = child.chunkSize()
|
||
|
if (at < sz) {
|
||
|
let used = Math.min(n, sz - at)
|
||
|
if (child.iterN(at, used, op)) return true
|
||
|
if ((n -= used) == 0) break
|
||
|
at = 0
|
||
|
} else at -= sz
|
||
|
}
|
||
|
}
|
||
|
}
|