198 lines
6.6 KiB
TypeScript
198 lines
6.6 KiB
TypeScript
|
import {Text} from "@codemirror/next/text"
|
||
|
import ist from "ist"
|
||
|
|
||
|
function depth(node: Text): number {
|
||
|
return !node.children ? 0 : 1 + Math.max(...node.children.map(depth))
|
||
|
}
|
||
|
|
||
|
const line = "1234567890".repeat(10)
|
||
|
const lines = new Array(200).fill(line), text0 = lines.join("\n")
|
||
|
const doc0 = Text.of(lines)
|
||
|
|
||
|
describe("doc", () => {
|
||
|
it("creates a balanced tree when loading a document", () => {
|
||
|
let doc = Text.of(new Array(2000).fill(line)), d = depth(doc)
|
||
|
ist(d, 3, "<=")
|
||
|
ist(d, 1, ">")
|
||
|
})
|
||
|
|
||
|
it("rebalances on insert", () => {
|
||
|
let doc = doc0
|
||
|
let insert = "abc".repeat(200), at = Math.floor(doc.length / 2)
|
||
|
for (let i = 0; i < 10; i++) doc = doc.replace(at, at, [insert])
|
||
|
ist(depth(doc), 3, "<=")
|
||
|
ist(doc.toString(), text0.slice(0, at) + "abc".repeat(2000) + text0.slice(at))
|
||
|
})
|
||
|
|
||
|
it("collapses on delete", () => {
|
||
|
let doc = doc0.replace(10, text0.length - 10, [""])
|
||
|
ist(depth(doc), 0)
|
||
|
ist(doc.length, 20)
|
||
|
ist(doc.toString(), line.slice(0, 20))
|
||
|
})
|
||
|
|
||
|
it("handles deleting at start", () => {
|
||
|
ist(Text.of(lines.slice(0, -1).concat([line + "!"])).replace(0, 9500, [""]).toString(), text0.slice(9500) + "!")
|
||
|
})
|
||
|
|
||
|
it("handles deleting at end", () => {
|
||
|
ist(Text.of(["?" + line].concat(lines.slice(1))).replace(9500, text0.length + 1, [""]).toString(), "?" + text0.slice(0, 9499))
|
||
|
})
|
||
|
|
||
|
it("can insert on node boundaries", () => {
|
||
|
let doc = doc0, pos = doc.children![0].length
|
||
|
ist(doc.replace(pos, pos, ["abc"]).slice(pos, pos + 3), "abc")
|
||
|
})
|
||
|
|
||
|
it("can build up a doc by repeated appending", () => {
|
||
|
let doc = Text.of([""]), text = ""
|
||
|
for (let i = 1; i < 1000; ++i) {
|
||
|
let add = "newtext" + i + " "
|
||
|
doc = doc.replace(doc.length, doc.length, [add])
|
||
|
text += add
|
||
|
}
|
||
|
ist(doc.toString(), text)
|
||
|
})
|
||
|
|
||
|
it("properly maintains content during editing", () => {
|
||
|
let str = text0, doc = doc0
|
||
|
for (let i = 0; i < 200; i++) {
|
||
|
let insPos = Math.floor(Math.random() * doc.length)
|
||
|
let insChar = String.fromCharCode("A".charCodeAt(0) + Math.floor(Math.random() * 26))
|
||
|
str = str.slice(0, insPos) + insChar + str.slice(insPos)
|
||
|
doc = doc.replace(insPos, insPos, [insChar])
|
||
|
let delFrom = Math.floor(Math.random() * doc.length)
|
||
|
let delTo = Math.min(doc.length, delFrom + Math.floor(Math.random() * 20))
|
||
|
str = str.slice(0, delFrom) + str.slice(delTo)
|
||
|
doc = doc.replace(delFrom, delTo, [""])
|
||
|
}
|
||
|
ist(doc.toString(), str)
|
||
|
})
|
||
|
|
||
|
it("returns the correct strings for slice", () => {
|
||
|
let text = []
|
||
|
for (let i = 0; i < 1000; i++) text.push("0".repeat(4 - String(i).length) + i)
|
||
|
let doc = Text.of(text)
|
||
|
let str = text.join("\n")
|
||
|
for (let i = 0; i < 400; i++) {
|
||
|
let start = i == 0 ? 0 : Math.floor(Math.random() * doc.length)
|
||
|
let end = i == 399 ? doc.length : start + Math.floor(Math.random() * (doc.length - start))
|
||
|
ist(doc.slice(start, end, "\n"), str.slice(start, end))
|
||
|
}
|
||
|
})
|
||
|
|
||
|
it("can be compared", () => {
|
||
|
let doc = doc0, doc2 = Text.of(lines)
|
||
|
ist(doc.eq(doc))
|
||
|
ist(doc.eq(doc2))
|
||
|
ist(doc2.eq(doc))
|
||
|
ist(!doc.eq(doc2.replace(5000, 5000, ["y"])))
|
||
|
ist(!doc.eq(doc2.replace(5000, 5001, ["y"])))
|
||
|
})
|
||
|
|
||
|
it("can be compared despite different tree shape", () => {
|
||
|
ist(doc0.replace(100, 201, ["abc"]).eq(Text.of([line + "abc"].concat(lines.slice(2)))))
|
||
|
})
|
||
|
|
||
|
it("can compare small documents", () => {
|
||
|
ist(Text.of(["foo", "bar"]).eq(Text.of(["foo", "bar"])))
|
||
|
ist(!Text.of(["foo", "bar"]).eq(Text.of(["foo", "baz"])))
|
||
|
})
|
||
|
|
||
|
it("is iterable", () => {
|
||
|
for (let iter = doc0.iter(), build = "";;) {
|
||
|
let {value, lineBreak, done} = iter.next()
|
||
|
if (done) {
|
||
|
ist(build, text0)
|
||
|
break
|
||
|
}
|
||
|
if (lineBreak) {
|
||
|
build += "\n"
|
||
|
} else {
|
||
|
ist(value.indexOf("\n"), -1)
|
||
|
build += value
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
it("is iterable in reverse", () => {
|
||
|
let found = ""
|
||
|
for (let iter = doc0.iter(-1); !iter.next().done;) found = iter.value + found
|
||
|
ist(found, text0)
|
||
|
})
|
||
|
|
||
|
it("is partially iterable", () => {
|
||
|
let found = ""
|
||
|
for (let iter = doc0.iterRange(500, doc0.length - 500); !iter.next().done;) found += iter.value
|
||
|
ist(JSON.stringify(found), JSON.stringify(text0.slice(500, text0.length - 500)))
|
||
|
})
|
||
|
|
||
|
it("is partially iterable in reverse", () => {
|
||
|
let found = ""
|
||
|
for (let iter = doc0.iterRange(doc0.length - 500, 500); !iter.next().done;) found = iter.value + found
|
||
|
ist(found, text0.slice(500, text0.length - 500))
|
||
|
})
|
||
|
|
||
|
it("can partially iter over subsections at the start and end", () => {
|
||
|
ist(doc0.iterRange(0, 1).next().value, "1")
|
||
|
ist(doc0.iterRange(1, 2).next().value, "2")
|
||
|
ist(doc0.iterRange(doc0.length - 1, doc0.length).next().value, "0")
|
||
|
ist(doc0.iterRange(doc0.length - 2, doc0.length - 1).next().value, "9")
|
||
|
})
|
||
|
|
||
|
it("can iterate over document lines", () => {
|
||
|
let lines = []
|
||
|
for (let i = 0; i < 200; i++) lines.push("line " + i)
|
||
|
for (let iter = Text.of(lines).iterLines(), i = 0; !iter.next().done; i++) {
|
||
|
ist(iter.value, "line " + i)
|
||
|
ist(i < 200)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
it("iterates lines in empty documents", () => {
|
||
|
let result = []
|
||
|
for (let iter = Text.of([""]).iterLines(); !iter.next().done;) result.push(iter.value)
|
||
|
ist(JSON.stringify(result), JSON.stringify([""]))
|
||
|
})
|
||
|
|
||
|
it("iterates over empty lines", () => {
|
||
|
let lines = ["", "foo", "", "", "bar", "", ""], result = []
|
||
|
for (let iter = Text.of(lines).iterLines(); !iter.next().done;) result.push(iter.value)
|
||
|
ist(JSON.stringify(result), JSON.stringify(lines))
|
||
|
})
|
||
|
|
||
|
it("iterates over long lines", () => {
|
||
|
let long = line.repeat(100), result = []
|
||
|
for (let iter = Text.of([long]).iterLines(); !iter.next().done;) result.push(iter.value)
|
||
|
ist(JSON.stringify(result), JSON.stringify([long]))
|
||
|
})
|
||
|
|
||
|
it("can get line info by line number", () => {
|
||
|
ist.throws(() => doc0.line(0), /Invalid line/)
|
||
|
ist.throws(() => doc0.line(doc0.lines + 1), /Invalid line/)
|
||
|
for (let i = 1; i < doc0.lines; i += 5) {
|
||
|
let l = doc0.line(i)
|
||
|
ist(l.start, (i - 1) * 101)
|
||
|
ist(l.end, i * 101 - 1)
|
||
|
ist(l.number, i)
|
||
|
ist(l.slice(), line)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
it("can get line info by position", () => {
|
||
|
ist.throws(() => doc0.lineAt(-10), /Invalid position/)
|
||
|
ist.throws(() => doc0.lineAt(doc0.length + 1), /Invalid position/)
|
||
|
for (let i = 0; i < doc0.length; i += 5) {
|
||
|
let l = doc0.lineAt(i)
|
||
|
ist(l.start, i - (i % 101))
|
||
|
ist(l.end, i - (i % 101) + 100)
|
||
|
ist(l.number, Math.floor(i / 101) + 1)
|
||
|
ist(l.slice(), line)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
it("can delete a range at the start of a child node", () => {
|
||
|
ist(doc0.replace(0, 100, ["x"]).toString(), "x" + text0.slice(100))
|
||
|
})
|
||
|
})
|