Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.

Commit d5c19bb

Browse files
committed
Be more careful about recognizing selection replacements in DOM changes
FIX: Fix an issue where some types of text input over a selection would be read as happening in wrong position. See https://discuss.codemirror.net/t/css-variables-issue-with-multi-cursor-replace/9686
1 parent 780ad2c commit d5c19bb

1 file changed

Lines changed: 15 additions & 23 deletions

File tree

src/domchange.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function domBoundsAround(tile: Tile, from: number, to: number, offset: number):
9898

9999
export function applyDOMChange(view: EditorView, domChange: DOMChange): boolean {
100100
let change: undefined | {from: number, to: number, insert: Text}
101-
let {newSel} = domChange, sel = view.state.selection.main
101+
let {newSel} = domChange, {state} = view, sel = state.selection.main
102102
let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1
103103
if (domChange.bounds) {
104104
let {from, to} = domChange.bounds
@@ -109,44 +109,36 @@ export function applyDOMChange(view: EditorView, domChange: DOMChange): boolean
109109
preferredPos = sel.to
110110
preferredSide = "end"
111111
}
112-
let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text,
113-
preferredPos - from, preferredSide)
114-
if (diff) {
112+
let cmp = state.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff
113+
if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
114+
cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
115+
cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
116+
// This looks like a selection replacement
117+
change = {from: sel.from, to: sel.to,
118+
insert: Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder))}
119+
} else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
115120
// Chrome inserts two newlines when pressing shift-enter at the
116121
// end of a line. DomChange drops one of those.
117122
if (browser.chrome && lastKey == 13 &&
118-
diff.toB == diff.from + 2 && domChange.text.slice(diff.from, diff.toB) == LineBreakPlaceholder + LineBreakPlaceholder)
123+
diff.toB == diff.from + 2 && domChange.text.slice(diff.from, diff.toB) == LineBreakPlaceholder + LineBreakPlaceholder)
119124
diff.toB--
120125

121126
change = {from: from + diff.from, to: from + diff.toA,
122127
insert: Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder))}
123128
}
124-
} else if (newSel && (!view.hasFocus && view.state.facet(editable) || sameSelPos(newSel, sel))) {
129+
} else if (newSel && (!view.hasFocus && state.facet(editable) || sameSelPos(newSel, sel))) {
125130
newSel = null
126131
}
127132

128133
if (!change && !newSel) return false
129134

130-
if (!change && domChange.typeOver && !sel.empty && newSel && newSel.main.empty) {
131-
// Heuristic to notice typing over a selected character
132-
change = {from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to)}
133-
} else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
135+
if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
134136
/^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
135137
// Detect insert-period-on-double-space Mac and Android behavior,
136138
// and transform it into a regular space insert.
137139
if (newSel && change.insert.length == 2) newSel = EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1)
138140
change = {from: change.from, to: change.to, insert: Text.of([change.insert.toString().replace(".", " ")])}
139-
} else if (change && change.from >= sel.from && change.to <= sel.to &&
140-
(change.from != sel.from || change.to != sel.to) &&
141-
(sel.to - sel.from) - (change.to - change.from) <= 4) {
142-
// If the change is inside the selection and covers most of it,
143-
// assume it is a selection replace (with identical characters at
144-
// the start/end not included in the diff)
145-
change = {
146-
from: sel.from, to: sel.to,
147-
insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
148-
}
149-
} else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
141+
} else if (state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
150142
view.inputState.insertingTextAt > Date.now() - 50) {
151143
// For a cross-line insertion, Chrome and Safari will crudely take
152144
// the text of the line after the selection, flattening any
@@ -155,7 +147,7 @@ export function applyDOMChange(view: EditorView, domChange: DOMChange): boolean
155147
// replace of the text provided by the beforeinput event.
156148
change = {
157149
from: sel.from, to: sel.to,
158-
insert: view.state.toText(view.inputState.insertingText)
150+
insert: state.toText(view.inputState.insertingText)
159151
}
160152
} else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
161153
change.insert.toString() == "\n " && view.lineWrapping) {
@@ -174,7 +166,7 @@ export function applyDOMChange(view: EditorView, domChange: DOMChange): boolean
174166
if (view.inputState.lastSelectionOrigin == "select") scrollIntoView = true
175167
userEvent = view.inputState.lastSelectionOrigin!
176168
if (userEvent == "select.pointer")
177-
newSel = skipAtomsForSelection(view.state.facet(atomicRanges).map(f => f(view)), newSel)
169+
newSel = skipAtomsForSelection(state.facet(atomicRanges).map(f => f(view)), newSel)
178170
}
179171
view.dispatch({selection: newSel, scrollIntoView, userEvent})
180172
return true

0 commit comments

Comments
 (0)