operations.js
7.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import { clipPos } from "../line/pos.js"
import { findMaxLine } from "../line/spans.js"
import { displayWidth, measureChar, scrollGap } from "../measurement/position_measurement.js"
import { signal } from "../util/event.js"
import { activeElt, doc } from "../util/dom.js"
import { finishOperation, pushOperation } from "../util/operation_group.js"
import { ensureFocus } from "./focus.js"
import { measureForScrollbars, updateScrollbars } from "./scrollbars.js"
import { restartBlink } from "./selection.js"
import { maybeScrollWindow, scrollPosIntoView, setScrollLeft, setScrollTop } from "./scrolling.js"
import { DisplayUpdate, maybeClipScrollbars, postUpdateDisplay, setDocumentHeight, updateDisplayIfNeeded } from "./update_display.js"
import { updateHeightsInViewport } from "./update_lines.js"
// Operations are used to wrap a series of changes to the editor
// state in such a way that each change won't have to update the
// cursor and display (which would be awkward, slow, and
// error-prone). Instead, display updates are batched and then all
// combined and executed at once.
let nextOpId = 0
// Start a new operation.
export function startOperation(cm) {
cm.curOp = {
cm: cm,
viewChanged: false, // Flag that indicates that lines might need to be redrawn
startHeight: cm.doc.height, // Used to detect need to update scrollbar
forceUpdate: false, // Used to force a redraw
updateInput: 0, // Whether to reset the input textarea
typing: false, // Whether this reset should be careful to leave existing text (for compositing)
changeObjs: null, // Accumulated changes, for firing change events
cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
selectionChanged: false, // Whether the selection needs to be redrawn
updateMaxLine: false, // Set when the widest line needs to be determined anew
scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
scrollToPos: null, // Used to scroll to a specific position
focus: false,
id: ++nextOpId, // Unique ID
markArrays: null // Used by addMarkedSpan
}
pushOperation(cm.curOp)
}
// Finish an operation, updating the display and signalling delayed events
export function endOperation(cm) {
let op = cm.curOp
if (op) finishOperation(op, group => {
for (let i = 0; i < group.ops.length; i++)
group.ops[i].cm.curOp = null
endOperations(group)
})
}
// The DOM updates done when an operation finishes are batched so
// that the minimum number of relayouts are required.
function endOperations(group) {
let ops = group.ops
for (let i = 0; i < ops.length; i++) // Read DOM
endOperation_R1(ops[i])
for (let i = 0; i < ops.length; i++) // Write DOM (maybe)
endOperation_W1(ops[i])
for (let i = 0; i < ops.length; i++) // Read DOM
endOperation_R2(ops[i])
for (let i = 0; i < ops.length; i++) // Write DOM (maybe)
endOperation_W2(ops[i])
for (let i = 0; i < ops.length; i++) // Read DOM
endOperation_finish(ops[i])
}
function endOperation_R1(op) {
let cm = op.cm, display = cm.display
maybeClipScrollbars(cm)
if (op.updateMaxLine) findMaxLine(cm)
op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
op.scrollToPos.to.line >= display.viewTo) ||
display.maxLineChanged && cm.options.lineWrapping
op.update = op.mustUpdate &&
new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate)
}
function endOperation_W1(op) {
op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update)
}
function endOperation_R2(op) {
let cm = op.cm, display = cm.display
if (op.updatedDisplay) updateHeightsInViewport(cm)
op.barMeasure = measureForScrollbars(cm)
// If the max line changed since it was last measured, measure it,
// and ensure the document's width matches it.
// updateDisplay_W2 will use these properties to do the actual resizing
if (display.maxLineChanged && !cm.options.lineWrapping) {
op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3
cm.display.sizerWidth = op.adjustWidthTo
op.barMeasure.scrollWidth =
Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth)
op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm))
}
if (op.updatedDisplay || op.selectionChanged)
op.preparedSelection = display.input.prepareSelection()
}
function endOperation_W2(op) {
let cm = op.cm
if (op.adjustWidthTo != null) {
cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"
if (op.maxScrollLeft < cm.doc.scrollLeft)
setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true)
cm.display.maxLineChanged = false
}
let takeFocus = op.focus && op.focus == activeElt(doc(cm))
if (op.preparedSelection)
cm.display.input.showSelection(op.preparedSelection, takeFocus)
if (op.updatedDisplay || op.startHeight != cm.doc.height)
updateScrollbars(cm, op.barMeasure)
if (op.updatedDisplay)
setDocumentHeight(cm, op.barMeasure)
if (op.selectionChanged) restartBlink(cm)
if (cm.state.focused && op.updateInput)
cm.display.input.reset(op.typing)
if (takeFocus) ensureFocus(op.cm)
}
function endOperation_finish(op) {
let cm = op.cm, display = cm.display, doc = cm.doc
if (op.updatedDisplay) postUpdateDisplay(cm, op.update)
// Abort mouse wheel delta measurement, when scrolling explicitly
if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
display.wheelStartX = display.wheelStartY = null
// Propagate the scroll position to the actual DOM scroller
if (op.scrollTop != null) setScrollTop(cm, op.scrollTop, op.forceScroll)
if (op.scrollLeft != null) setScrollLeft(cm, op.scrollLeft, true, true)
// If we need to scroll a specific position into view, do so.
if (op.scrollToPos) {
let rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin)
maybeScrollWindow(cm, rect)
}
// Fire events for markers that are hidden/unidden by editing or
// undoing
let hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers
if (hidden) for (let i = 0; i < hidden.length; ++i)
if (!hidden[i].lines.length) signal(hidden[i], "hide")
if (unhidden) for (let i = 0; i < unhidden.length; ++i)
if (unhidden[i].lines.length) signal(unhidden[i], "unhide")
if (display.wrapper.offsetHeight)
doc.scrollTop = cm.display.scroller.scrollTop
// Fire change events, and delayed event handlers
if (op.changeObjs)
signal(cm, "changes", cm, op.changeObjs)
if (op.update)
op.update.finish()
}
// Run the given function in an operation
export function runInOp(cm, f) {
if (cm.curOp) return f()
startOperation(cm)
try { return f() }
finally { endOperation(cm) }
}
// Wraps a function in an operation. Returns the wrapped function.
export function operation(cm, f) {
return function() {
if (cm.curOp) return f.apply(cm, arguments)
startOperation(cm)
try { return f.apply(cm, arguments) }
finally { endOperation(cm) }
}
}
// Used to add methods to editor and doc instances, wrapping them in
// operations.
export function methodOp(f) {
return function() {
if (this.curOp) return f.apply(this, arguments)
startOperation(this)
try { return f.apply(this, arguments) }
finally { endOperation(this) }
}
}
export function docMethodOp(f) {
return function() {
let cm = this.cm
if (!cm || cm.curOp) return f.apply(this, arguments)
startOperation(cm)
try { return f.apply(this, arguments) }
finally { endOperation(cm) }
}
}