Mercurial
comparison .cms/lib/codemirror/src/model/changes.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78edf6b517a0 |
---|---|
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 // UPDATING | |
21 | |
22 // Allow "beforeChange" event handlers to influence a change | |
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 // Apply a change to a document, and add it to the document's | |
49 // history, and propagating it to all linked documents. | |
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 // Possibly split or suppress the update based on the presence | |
62 // of read-only spans in its range. | |
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 // Revert a change stored in a document's history. | |
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 // Verify that there is a useable event (so that ctrl-z won't | |
98 // needlessly clear selection events) | |
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 // Build up a reverse change object to add to the opposite history | |
124 // stack (redo when undoing, and vice versa). | |
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 // Propagate to the linked documents | |
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 // Sub-views need their line numbers shifted when text is added | |
159 // above or below them in the parent document. | |
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 // More lower-level change function, handling only a single document | |
175 // (not linked ones). | |
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 // Clip the change to the size of this doc | |
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 // Handle the interaction of a change to a document with the editor | |
211 // that this document is part of. | |
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 // Remember that these lines changed, for updating the display | |
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 // Rebasing/resetting history to deal with externally-sourced changes | |
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 // Tries to rebase an array of history events given a change in the | |
289 // document. If the change touches the same lines as the event, the | |
290 // event, and everything 'behind' it, is discarded. If the change is | |
291 // before the event, the event's positions are updated. Uses a | |
292 // copy-on-write scheme for the positions, to avoid having to | |
293 // reallocate them all on every rebase, but also avoid problems with | |
294 // shared position objects being unsafely updated. | |
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 // Utility for applying a change to a line by handle or number, | |
330 // returning the number and optionally registering the line as | |
331 // changed. | |
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 } |