virt2/api/soft/CodeMirror/view/test/test-composition.ts

268 lines
9.3 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {tempEditor, requireFocus} from "./temp-editor"
import {EditorView, ViewPlugin, ViewUpdate, Decoration, DecorationSet, WidgetType} from "@codemirror/next/view"
import {EditorState} from "@codemirror/next/state"
import ist from "ist"
function event(cm: EditorView, type: string) {
cm.contentDOM.dispatchEvent(new CompositionEvent(type))
}
function up(node: Text, text: string = "", from = node.nodeValue!.length, to = from) {
let val = node.nodeValue!
node.nodeValue = val.slice(0, from) + text + val.slice(to)
document.getSelection()!.collapse(node, from + text.length)
return node
}
function hasCompositionDeco(cm: EditorView) {
return cm.docView.compositionDeco.size > 0
}
function compose(cm: EditorView, start: () => Text,
update: ((node: Text) => void)[],
options: {end?: (node: Text) => void, cancel?: boolean} = {}) {
event(cm, "compositionstart")
let node!: Text, sel = document.getSelection()!
for (let i = -1; i < update.length; i++) {
if (i < 0) node = start()
else update[i](node)
let {focusNode, focusOffset} = sel
cm.observer.flush()
if (options.cancel && i == update.length - 1) {
ist(!hasCompositionDeco(cm))
} else {
ist(node.parentNode && cm.contentDOM.contains(node.parentNode))
ist(sel.focusNode, focusNode)
ist(sel.focusOffset, focusOffset)
ist(hasCompositionDeco(cm))
}
}
event(cm, "compositionend")
if (options.end) options.end(node)
cm.observer.flush()
cm.update([])
ist(!cm.inputState.composing)
ist(!hasCompositionDeco(cm))
}
function wordDeco(state: EditorState): DecorationSet {
let re = /\w+/g, m, deco = [], text = state.doc.toString()
while (m = re.exec(text))
deco.push(Decoration.mark({class: "word"}).range(m.index, m.index + m[0].length))
return Decoration.set(deco)
}
const wordHighlighter = EditorView.decorations.compute(["doc"], wordDeco)
function widgets(positions: number[], sides: number[]): ViewPlugin<any> {
let xWidget = new class extends WidgetType<null> {
toDOM() { let s = document.createElement("var"); s.textContent = "×"; return s }
}(null)
let startDeco = Decoration.set(positions.map((p, i) => Decoration.widget({widget: xWidget, side: sides[i]}).range(p)))
return ViewPlugin.define(() => ({
decorations: startDeco,
update(update: ViewUpdate) { this.decorations = this.decorations.map(update.changes) }
})).decorations()
}
describe("Composition", () => {
it("supports composition on an empty line", () => {
let cm = requireFocus(tempEditor("foo\n\nbar"))
compose(cm, () => up(cm.domAtPos(4).node.appendChild(document.createTextNode("a"))), [
n => up(n, "b"),
n => up(n, "c")
])
ist(cm.state.doc.toString(), "foo\nabc\nbar")
})
it("supports composition at end of line in existing node", () => {
let cm = requireFocus(tempEditor("foo"))
compose(cm, () => up(cm.domAtPos(2).node as Text), [
n => up(n, "!"),
n => up(n, "?")
])
ist(cm.state.doc.toString(), "foo!?")
})
it("supports composition at end of line in a new node", () => {
let cm = requireFocus(tempEditor("foo"))
compose(cm, () => up(cm.domAtPos(0).node.appendChild(document.createTextNode("!"))), [
n => up(n, "?")
])
ist(cm.state.doc.toString(), "foo!?")
})
it("supports composition at start of line in a new node", () => {
let cm = requireFocus(tempEditor("foo"))
compose(cm, () => {
let l0 = cm.domAtPos(0).node
return up(l0.insertBefore(document.createTextNode("!"), l0.firstChild))
}, [
n => up(n, "?")
])
ist(cm.state.doc.toString(), "!?foo")
})
it("supports composition inside existing text", () => {
let cm = requireFocus(tempEditor("foo"))
compose(cm, () => up(cm.domAtPos(2).node as Text), [
n => up(n, "x", 1),
n => up(n, "y", 2),
n => up(n, "z", 3)
])
ist(cm.state.doc.toString(), "fxyzoo")
})
it("can deal with Android-style newline-after-composition", () => {
let cm = requireFocus(tempEditor("abcdef"))
compose(cm, () => up(cm.domAtPos(2).node as Text), [
n => up(n, "x", 3),
n => up(n, "y", 4)
], {end: n => {
let line = n.parentNode!.appendChild(document.createElement("div"))
line.textContent = "def"
n.nodeValue = "abcxy"
document.getSelection()!.collapse(line, 0)
}})
ist(cm.state.doc.toString(), "abcxy\ndef")
})
it("handles replacement of existing words", () => {
let cm = requireFocus(tempEditor("one two three"))
compose(cm, () => up(cm.domAtPos(1).node as Text, "five", 4, 7), [
n => up(n, "seven", 4, 8),
n => up(n, "zero", 4, 9)
])
ist(cm.state.doc.toString(), "one zero three")
})
it("doesn't get interrupted by changes in decorations", () => {
let cm = requireFocus(tempEditor("foo ...", [wordHighlighter]))
compose(cm, () => up(cm.domAtPos(5).node as Text), [
n => up(n, "hi", 1, 4)
])
ist(cm.state.doc.toString(), "foo hi")
})
it("works inside highlighted text", () => {
let cm = requireFocus(tempEditor("one two", [wordHighlighter]))
compose(cm, () => up(cm.domAtPos(1).node as Text, "x"), [
n => up(n, "y"),
n => up(n, ".")
])
ist(cm.state.doc.toString(), "onexy. two")
})
it("can handle compositions spanning multiple tokens", () => {
let cm = requireFocus(tempEditor("one two", [wordHighlighter]))
compose(cm, () => up(cm.domAtPos(5).node as Text, "a"), [
n => up(n, "b"),
n => up(n, "c")
], {end: n => {
;(n.parentNode!.previousSibling! as ChildNode).remove()
;(n.parentNode!.previousSibling! as ChildNode).remove()
return up(n, "xyzone ", 0)
}})
ist(cm.state.doc.toString(), "xyzone twoabc")
})
it("doesn't overwrite widgets next to the composition", () => {
let cm = requireFocus(tempEditor("", [widgets([0, 0], [-1, 1])]))
compose(cm, () => {
let l0 = cm.domAtPos(0).node
return up(l0.insertBefore(document.createTextNode("a"), l0.lastChild))
}, [n => up(n, "b", 0, 1)], {end: () => {
ist(cm.contentDOM.querySelectorAll("var").length, 2)
}})
ist(cm.state.doc.toString(), "b")
})
it("cancels composition when a change fully overlaps with it", () => {
let cm = requireFocus(tempEditor("one\ntwo\nthree"))
compose(cm, () => up(cm.domAtPos(5).node as Text, "x"), [
() => cm.dispatch(cm.state.t().replace(2, 10, "---"))
], {cancel: true})
ist(cm.state.doc.toString(), "on---hree")
})
it("cancels composition when a change partially overlaps with it", () => {
let cm = requireFocus(tempEditor("one\ntwo\nthree"))
compose(cm, () => up(cm.domAtPos(5).node as Text, "x", 0), [
() => cm.dispatch(cm.state.t().replace(5, 12, "---"))
], {cancel: true})
ist(cm.state.doc.toString(), "one\nx---ee")
})
it("cancels composition when a change happens inside of it", () => {
let cm = requireFocus(tempEditor("one\ntwo\nthree"))
compose(cm, () => up(cm.domAtPos(5).node as Text, "x", 0), [
() => cm.dispatch(cm.state.t().replace(5, 6, "!"))
], {cancel: true})
ist(cm.state.doc.toString(), "one\nx!wo\nthree")
})
it("doesn't cancel composition when a change happens elsewhere", () => {
let cm = requireFocus(tempEditor("one\ntwo\nthree"))
compose(cm, () => up(cm.domAtPos(5).node as Text, "x", 0), [
n => up(n, "y", 1),
() => cm.dispatch(cm.state.t().replace(1, 2, "!")),
n => up(n, "z", 2)
])
ist(cm.state.doc.toString(), "o!e\nxyztwo\nthree")
})
it("doesn't cancel composition when the composition is moved into a new line", () => {
let cm = requireFocus(tempEditor("one\ntwo three", [wordHighlighter]))
compose(cm, () => up(cm.domAtPos(9).node as Text, "x"), [
n => up(n, "y"),
() => cm.dispatch(cm.state.t().replace(4, 4, "\n")),
n => up(n, "z")
])
ist(cm.state.doc.toString(), "one\n\ntwo threexyz")
})
it("doesn't cancel composition when a line break is inserted in front of it", () => {
let cm = requireFocus(tempEditor("one two three", [wordHighlighter]))
compose(cm, () => up(cm.domAtPos(9).node as Text, "x"), [
n => up(n, "y"),
() => cm.dispatch(cm.state.t().replace(8, 8, "\n")),
n => up(n, "z")
])
ist(cm.state.doc.toString(), "one two \nthreexyz")
})
it("doesn't cancel composition when a newline is added immediately in front", () => {
let cm = requireFocus(tempEditor("one\ntwo three", [wordHighlighter]))
compose(cm, () => up(cm.domAtPos(9).node as Text, "x"), [
n => up(n, "y"),
() => cm.dispatch(cm.state.t().replace(7, 8, "\n")),
n => up(n, "z")
])
ist(cm.state.doc.toString(), "one\ntwo\nthreexyz")
})
it("handles compositions rapidly following each other", () => {
let cm = requireFocus(tempEditor("one\ntwo"))
event(cm, "compositionstart")
let one = cm.domAtPos(1).node as Text
up(one, "!")
cm.observer.flush()
event(cm, "compositionend")
one.nodeValue = "one!!"
let L2 = cm.contentDOM.lastChild
event(cm, "compositionstart")
let two = cm.domAtPos(7).node as Text
ist(cm.contentDOM.lastChild, L2)
up(two, ".")
cm.observer.flush()
ist(hasCompositionDeco(cm))
ist(getSelection()!.focusNode, two)
ist(getSelection()!.focusOffset, 4)
ist(cm.inputState.composing)
event(cm, "compositionend")
cm.observer.flush()
ist(cm.state.doc.toString(), "one!!\ntwo.")
})
})