Skip to content

Commit

Permalink
Fix a bug where composition DOM gets reused for another view node
Browse files Browse the repository at this point in the history
FIX: Fix a bug that could cause compositions to be disrupted because their surrounding
DOM was repurposed for some other piece of content.

Closes codemirror/dev#1188
  • Loading branch information
marijnh committed Jul 3, 2023
1 parent 2500faf commit 5bd7ac0
Show file tree
Hide file tree
Showing 2 changed files with 13 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/contentview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export abstract class ContentView {
let prev: Node | null = null, next
for (let child of this.children) {
if (child.dirty) {
if (!child.dom && (next = prev ? prev.nextSibling : parent.firstChild)) {
if (!child.dom && (next = prev ? prev.nextSibling : parent.firstChild) && next != view.docView.compositionNode) {
let contentView = ContentView.get(next)
if (!contentView || !contentView.parent && contentView.canReuseDOM(child))
child.reuseDOM(next)
Expand Down
22 changes: 12 additions & 10 deletions src/docview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class DocView extends ContentView {
children!: BlockView[]

compositionDeco = Decoration.none
compositionNode: Node | null = null
decorations: readonly DecorationSet[] = []
dynamicDecorationMap: boolean[] = []

Expand Down Expand Up @@ -66,10 +67,8 @@ export class DocView extends ContentView {
}
}

if (this.view.inputState.composing < 0)
this.compositionDeco = Decoration.none
else if (update.transactions.length || this.dirty)
this.compositionDeco = computeCompositionDeco(this.view, update.changes)
;({deco: this.compositionDeco, node: this.compositionNode} =
this.view.inputState.composing < 0 ? noComp : computeCompositionDeco(this.view, update.changes))

// When the DOM nodes around the selection are moved to another
// parent, Chrome sometimes reports a different selection through
Expand Down Expand Up @@ -456,35 +455,38 @@ export function compositionSurroundingNode(view: EditorView) {
}
}

function computeCompositionDeco(view: EditorView, changes: ChangeSet): DecorationSet {
const noComp = {deco: Decoration.none, node: null}

function computeCompositionDeco(view: EditorView, changes: ChangeSet): {deco: DecorationSet, node: Node | null} {
let surrounding = compositionSurroundingNode(view)
if (!surrounding) return Decoration.none
if (!surrounding) return noComp
let {from, to, node, text: textNode} = surrounding

let newFrom = changes.mapPos(from, 1), newTo = Math.max(newFrom, changes.mapPos(to, -1))
let {state} = view, reader = new DOMReader([], state)
if (node.nodeType == 3) reader.readTextNode(node as Text)
else reader.readRange(node.firstChild, null)
let {text} = reader
if (text.indexOf(LineBreakPlaceholder) > -1) return Decoration.none // Don't try to preserve multi-line compositions
if (text.indexOf(LineBreakPlaceholder) > -1) return noComp // Don't try to preserve multi-line compositions

if (newTo - newFrom < text.length) {
if (state.doc.sliceString(newFrom, Math.min(state.doc.length, newFrom + text.length)) == text)
newTo = newFrom + text.length
else if (state.doc.sliceString(Math.max(0, newTo - text.length), newTo) == text)
newFrom = newTo - text.length
else
return Decoration.none
return noComp
} else if (state.doc.sliceString(newFrom, newTo) != text) {
return Decoration.none
return noComp
}

let topView = ContentView.get(node)
if (topView instanceof CompositionView) topView = topView.widget.topView
else if (topView) topView.parent = null
return Decoration.set(
let deco = Decoration.set(
Decoration.replace({widget: new CompositionWidget(node, textNode, topView), inclusive: true})
.range(newFrom, newTo))
return {deco, node}
}

export class CompositionWidget extends WidgetType {
Expand Down

0 comments on commit 5bd7ac0

Please sign in to comment.