1 | import { retreatFrontier } from "../line/highlight.js"
|
2 | import { startWorker } from "../display/highlight_worker.js"
|
3 | import { operation } from "../display/operations.js"
|
4 | import { regChange, regLineChange } from "../display/view_tracking.js"
|
5 | import { clipLine, clipPos, cmp, Pos } from "../line/pos.js"
|
6 | import { sawReadOnlySpans } from "../line/saw_special_spans.js"
|
7 | import { lineLength, removeReadOnlyRanges, stretchSpansOverChange, visualLine } from "../line/spans.js"
|
8 | import { getBetween, getLine, lineNo } from "../line/utils_line.js"
|
9 | import { estimateHeight } from "../measurement/position_measurement.js"
|
10 | import { hasHandler, signal, signalCursorActivity } from "../util/event.js"
|
11 | import { indexOf, lst, map, sel_dontScroll } from "../util/misc.js"
|
12 | import { signalLater } from "../util/operation_group.js"
|
13 |
|
14 | import { changeEnd, computeSelAfterChange } from "./change_measurement.js"
|
15 | import { isWholeLineUpdate, linkedDocs, updateDoc } from "./document_data.js"
|
16 | import { addChangeToHistory, historyChangeFromChange, mergeOldSpans, pushSelectionToHistory } from "./history.js"
|
17 | import { Range, Selection } from "./selection.js"
|
18 | import { setSelection, setSelectionNoUndo, skipAtomic } from "./selection_updates.js"
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | function filterChange(doc, change, update) {
|
24 | let obj = {
|
25 | canceled: false,
|
26 | from: change.from,
|
27 | to: change.to,
|
28 | text: change.text,
|
29 | origin: change.origin,
|
30 | cancel: () => obj.canceled = true
|
31 | }
|
32 | if (update) obj.update = (from, to, text, origin) => {
|
33 | if (from) obj.from = clipPos(doc, from)
|
34 | if (to) obj.to = clipPos(doc, to)
|
35 | if (text) obj.text = text
|
36 | if (origin !== undefined) obj.origin = origin
|
37 | }
|
38 | signal(doc, "beforeChange", doc, obj)
|
39 | if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj)
|
40 |
|
41 | if (obj.canceled) {
|
42 | if (doc.cm) doc.cm.curOp.updateInput = 2
|
43 | return null
|
44 | }
|
45 | return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}
|
46 | }
|
47 |
|
48 |
|
49 |
|
50 | export function makeChange(doc, change, ignoreReadOnly) {
|
51 | if (doc.cm) {
|
52 | if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly)
|
53 | if (doc.cm.state.suppressEdits) return
|
54 | }
|
55 |
|
56 | if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
|
57 | change = filterChange(doc, change, true)
|
58 | if (!change) return
|
59 | }
|
60 |
|
61 |
|
62 |
|
63 | let split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to)
|
64 | if (split) {
|
65 | for (let i = split.length - 1; i >= 0; --i)
|
66 | makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin})
|
67 | } else {
|
68 | makeChangeInner(doc, change)
|
69 | }
|
70 | }
|
71 |
|
72 | function makeChangeInner(doc, change) {
|
73 | if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return
|
74 | let selAfter = computeSelAfterChange(doc, change)
|
75 | addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN)
|
76 |
|
77 | makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change))
|
78 | let rebased = []
|
79 |
|
80 | linkedDocs(doc, (doc, sharedHist) => {
|
81 | if (!sharedHist && indexOf(rebased, doc.history) == -1) {
|
82 | rebaseHist(doc.history, change)
|
83 | rebased.push(doc.history)
|
84 | }
|
85 | makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change))
|
86 | })
|
87 | }
|
88 |
|
89 |
|
90 | export function makeChangeFromHistory(doc, type, allowSelectionOnly) {
|
91 | let suppress = doc.cm && doc.cm.state.suppressEdits
|
92 | if (suppress && !allowSelectionOnly) return
|
93 |
|
94 | let hist = doc.history, event, selAfter = doc.sel
|
95 | let source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done
|
96 |
|
97 |
|
98 |
|
99 | let i = 0
|
100 | for (; i < source.length; i++) {
|
101 | event = source[i]
|
102 | if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)
|
103 | break
|
104 | }
|
105 | if (i == source.length) return
|
106 | hist.lastOrigin = hist.lastSelOrigin = null
|
107 |
|
108 | for (;;) {
|
109 | event = source.pop()
|
110 | if (event.ranges) {
|
111 | pushSelectionToHistory(event, dest)
|
112 | if (allowSelectionOnly && !event.equals(doc.sel)) {
|
113 | setSelection(doc, event, {clearRedo: false})
|
114 | return
|
115 | }
|
116 | selAfter = event
|
117 | } else if (suppress) {
|
118 | source.push(event)
|
119 | return
|
120 | } else break
|
121 | }
|
122 |
|
123 |
|
124 |
|
125 | let antiChanges = []
|
126 | pushSelectionToHistory(selAfter, dest)
|
127 | dest.push({changes: antiChanges, generation: hist.generation})
|
128 | hist.generation = event.generation || ++hist.maxGeneration
|
129 |
|
130 | let filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")
|
131 |
|
132 | for (let i = event.changes.length - 1; i >= 0; --i) {
|
133 | let change = event.changes[i]
|
134 | change.origin = type
|
135 | if (filter && !filterChange(doc, change, false)) {
|
136 | source.length = 0
|
137 | return
|
138 | }
|
139 |
|
140 | antiChanges.push(historyChangeFromChange(doc, change))
|
141 |
|
142 | let after = i ? computeSelAfterChange(doc, change) : lst(source)
|
143 | makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change))
|
144 | if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)})
|
145 | let rebased = []
|
146 |
|
147 |
|
148 | linkedDocs(doc, (doc, sharedHist) => {
|
149 | if (!sharedHist && indexOf(rebased, doc.history) == -1) {
|
150 | rebaseHist(doc.history, change)
|
151 | rebased.push(doc.history)
|
152 | }
|
153 | makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change))
|
154 | })
|
155 | }
|
156 | }
|
157 |
|
158 |
|
159 |
|
160 | function shiftDoc(doc, distance) {
|
161 | if (distance == 0) return
|
162 | doc.first += distance
|
163 | doc.sel = new Selection(map(doc.sel.ranges, range => new Range(
|
164 | Pos(range.anchor.line + distance, range.anchor.ch),
|
165 | Pos(range.head.line + distance, range.head.ch)
|
166 | )), doc.sel.primIndex)
|
167 | if (doc.cm) {
|
168 | regChange(doc.cm, doc.first, doc.first - distance, distance)
|
169 | for (let d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
|
170 | regLineChange(doc.cm, l, "gutter")
|
171 | }
|
172 | }
|
173 |
|
174 |
|
175 |
|
176 | function makeChangeSingleDoc(doc, change, selAfter, spans) {
|
177 | if (doc.cm && !doc.cm.curOp)
|
178 | return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans)
|
179 |
|
180 | if (change.to.line < doc.first) {
|
181 | shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line))
|
182 | return
|
183 | }
|
184 | if (change.from.line > doc.lastLine()) return
|
185 |
|
186 |
|
187 | if (change.from.line < doc.first) {
|
188 | let shift = change.text.length - 1 - (doc.first - change.from.line)
|
189 | shiftDoc(doc, shift)
|
190 | change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
|
191 | text: [lst(change.text)], origin: change.origin}
|
192 | }
|
193 | let last = doc.lastLine()
|
194 | if (change.to.line > last) {
|
195 | change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
|
196 | text: [change.text[0]], origin: change.origin}
|
197 | }
|
198 |
|
199 | change.removed = getBetween(doc, change.from, change.to)
|
200 |
|
201 | if (!selAfter) selAfter = computeSelAfterChange(doc, change)
|
202 | if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans)
|
203 | else updateDoc(doc, change, spans)
|
204 | setSelectionNoUndo(doc, selAfter, sel_dontScroll)
|
205 |
|
206 | if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0)))
|
207 | doc.cantEdit = false
|
208 | }
|
209 |
|
210 |
|
211 |
|
212 | function makeChangeSingleDocInEditor(cm, change, spans) {
|
213 | let doc = cm.doc, display = cm.display, from = change.from, to = change.to
|
214 |
|
215 | let recomputeMaxLength = false, checkWidthStart = from.line
|
216 | if (!cm.options.lineWrapping) {
|
217 | checkWidthStart = lineNo(visualLine(getLine(doc, from.line)))
|
218 | doc.iter(checkWidthStart, to.line + 1, line => {
|
219 | if (line == display.maxLine) {
|
220 | recomputeMaxLength = true
|
221 | return true
|
222 | }
|
223 | })
|
224 | }
|
225 |
|
226 | if (doc.sel.contains(change.from, change.to) > -1)
|
227 | signalCursorActivity(cm)
|
228 |
|
229 | updateDoc(doc, change, spans, estimateHeight(cm))
|
230 |
|
231 | if (!cm.options.lineWrapping) {
|
232 | doc.iter(checkWidthStart, from.line + change.text.length, line => {
|
233 | let len = lineLength(line)
|
234 | if (len > display.maxLineLength) {
|
235 | display.maxLine = line
|
236 | display.maxLineLength = len
|
237 | display.maxLineChanged = true
|
238 | recomputeMaxLength = false
|
239 | }
|
240 | })
|
241 | if (recomputeMaxLength) cm.curOp.updateMaxLine = true
|
242 | }
|
243 |
|
244 | retreatFrontier(doc, from.line)
|
245 | startWorker(cm, 400)
|
246 |
|
247 | let lendiff = change.text.length - (to.line - from.line) - 1
|
248 |
|
249 | if (change.full)
|
250 | regChange(cm)
|
251 | else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))
|
252 | regLineChange(cm, from.line, "text")
|
253 | else
|
254 | regChange(cm, from.line, to.line + 1, lendiff)
|
255 |
|
256 | let changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change")
|
257 | if (changeHandler || changesHandler) {
|
258 | let obj = {
|
259 | from: from, to: to,
|
260 | text: change.text,
|
261 | removed: change.removed,
|
262 | origin: change.origin
|
263 | }
|
264 | if (changeHandler) signalLater(cm, "change", cm, obj)
|
265 | if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj)
|
266 | }
|
267 | cm.display.selForContextMenu = null
|
268 | }
|
269 |
|
270 | export function replaceRange(doc, code, from, to, origin) {
|
271 | if (!to) to = from
|
272 | if (cmp(to, from) < 0) [from, to] = [to, from]
|
273 | if (typeof code == "string") code = doc.splitLines(code)
|
274 | makeChange(doc, {from, to, text: code, origin})
|
275 | }
|
276 |
|
277 |
|
278 |
|
279 | function rebaseHistSelSingle(pos, from, to, diff) {
|
280 | if (to < pos.line) {
|
281 | pos.line += diff
|
282 | } else if (from < pos.line) {
|
283 | pos.line = from
|
284 | pos.ch = 0
|
285 | }
|
286 | }
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 | function rebaseHistArray(array, from, to, diff) {
|
296 | for (let i = 0; i < array.length; ++i) {
|
297 | let sub = array[i], ok = true
|
298 | if (sub.ranges) {
|
299 | if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true }
|
300 | for (let j = 0; j < sub.ranges.length; j++) {
|
301 | rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff)
|
302 | rebaseHistSelSingle(sub.ranges[j].head, from, to, diff)
|
303 | }
|
304 | continue
|
305 | }
|
306 | for (let j = 0; j < sub.changes.length; ++j) {
|
307 | let cur = sub.changes[j]
|
308 | if (to < cur.from.line) {
|
309 | cur.from = Pos(cur.from.line + diff, cur.from.ch)
|
310 | cur.to = Pos(cur.to.line + diff, cur.to.ch)
|
311 | } else if (from <= cur.to.line) {
|
312 | ok = false
|
313 | break
|
314 | }
|
315 | }
|
316 | if (!ok) {
|
317 | array.splice(0, i + 1)
|
318 | i = 0
|
319 | }
|
320 | }
|
321 | }
|
322 |
|
323 | function rebaseHist(hist, change) {
|
324 | let from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1
|
325 | rebaseHistArray(hist.done, from, to, diff)
|
326 | rebaseHistArray(hist.undone, from, to, diff)
|
327 | }
|
328 |
|
329 |
|
330 |
|
331 |
|
332 | export function changeLine(doc, handle, changeType, op) {
|
333 | let no = handle, line = handle
|
334 | if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle))
|
335 | else no = lineNo(handle)
|
336 | if (no == null) return null
|
337 | if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType)
|
338 | return line
|
339 | }
|